implemented PKCE auth (#921)
* implemented PKCE auth * removed node-jose * added PKCE tests
This commit is contained in:
parent
20528eb0d1
commit
7ccf462bf8
|
@ -8,7 +8,7 @@ export default function initialState(state, emitter) {
|
||||||
|
|
||||||
Object.assign(state, {
|
Object.assign(state, {
|
||||||
prefix: '/android_asset',
|
prefix: '/android_asset',
|
||||||
user: new User(undefined, storage),
|
user: new User(storage),
|
||||||
getAsset(name) {
|
getAsset(name) {
|
||||||
return `${state.prefix}/${name}`;
|
return `${state.prefix}/${name}`;
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
|
||||||
import * as metrics from './metrics';
|
import * as metrics from './metrics';
|
||||||
import Archive from './archive';
|
import Archive from './archive';
|
||||||
import { bytes } from './utils';
|
import { bytes } from './utils';
|
||||||
import { prepareWrapKey } from './fxa';
|
|
||||||
|
|
||||||
export default function(state, emitter) {
|
export default function(state, emitter) {
|
||||||
let lastRender = 0;
|
let lastRender = 0;
|
||||||
|
@ -45,9 +44,8 @@ export default function(state, emitter) {
|
||||||
lastRender = Date.now();
|
lastRender = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('login', async () => {
|
emitter.on('login', () => {
|
||||||
const k = await prepareWrapKey(state.storage);
|
state.user.login();
|
||||||
location.assign(`/api/fxa/login?keys_jwk=${k}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on('logout', () => {
|
emitter.on('logout', () => {
|
||||||
|
|
154
app/fxa.js
154
app/fxa.js
|
@ -1,21 +1,153 @@
|
||||||
import jose from 'node-jose';
|
|
||||||
import { arrayToB64, b64ToArray } from './utils';
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
export async function prepareWrapKey(storage) {
|
function getOtherInfo(enc) {
|
||||||
const keystore = jose.JWK.createKeyStore();
|
const name = encoder.encode(enc);
|
||||||
const keypair = await keystore.generate('EC', 'P-256');
|
const length = 256;
|
||||||
storage.set('fxaWrapKey', JSON.stringify(keystore.toJSON(true)));
|
const buffer = new ArrayBuffer(name.length + 16);
|
||||||
return jose.util.base64url.encode(JSON.stringify(keypair.toJSON()));
|
const dv = new DataView(buffer);
|
||||||
|
const result = new Uint8Array(buffer);
|
||||||
|
let i = 0;
|
||||||
|
dv.setUint32(i, name.length);
|
||||||
|
i += 4;
|
||||||
|
result.set(name, i);
|
||||||
|
i += name.length;
|
||||||
|
dv.setUint32(i, 0);
|
||||||
|
i += 4;
|
||||||
|
dv.setUint32(i, 0);
|
||||||
|
i += 4;
|
||||||
|
dv.setUint32(i, length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function concat(b1, b2) {
|
||||||
|
const result = new Uint8Array(b1.length + b2.length);
|
||||||
|
result.set(b1, 0);
|
||||||
|
result.set(b2, b1.length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function concatKdf(key, enc) {
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error('unsupported key length');
|
||||||
|
}
|
||||||
|
const otherInfo = getOtherInfo(enc);
|
||||||
|
const buffer = new ArrayBuffer(4 + key.length + otherInfo.length);
|
||||||
|
const dv = new DataView(buffer);
|
||||||
|
const concat = new Uint8Array(buffer);
|
||||||
|
dv.setUint32(0, 1);
|
||||||
|
concat.set(key, 4);
|
||||||
|
concat.set(otherInfo, key.length + 4);
|
||||||
|
const result = await crypto.subtle.digest('SHA-256', concat);
|
||||||
|
return new Uint8Array(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareScopedBundleKey(storage) {
|
||||||
|
const keys = await crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
namedCurve: 'P-256'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['deriveBits']
|
||||||
|
);
|
||||||
|
const privateJwk = await crypto.subtle.exportKey('jwk', keys.privateKey);
|
||||||
|
const publicJwk = await crypto.subtle.exportKey('jwk', keys.publicKey);
|
||||||
|
const kid = await crypto.subtle.digest(
|
||||||
|
'SHA-256',
|
||||||
|
encoder.encode(JSON.stringify(publicJwk))
|
||||||
|
);
|
||||||
|
privateJwk.kid = kid;
|
||||||
|
publicJwk.kid = kid;
|
||||||
|
storage.set('scopedBundlePrivateKey', JSON.stringify(privateJwk));
|
||||||
|
return arrayToB64(encoder.encode(JSON.stringify(publicJwk)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptBundle(storage, bundle) {
|
||||||
|
const privateJwk = JSON.parse(storage.get('scopedBundlePrivateKey'));
|
||||||
|
storage.remove('scopedBundlePrivateKey');
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
privateJwk,
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
namedCurve: 'P-256'
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['deriveBits']
|
||||||
|
);
|
||||||
|
const jweParts = bundle.split('.');
|
||||||
|
if (jweParts.length !== 5) {
|
||||||
|
throw new Error('invalid jwe');
|
||||||
|
}
|
||||||
|
const header = JSON.parse(decoder.decode(b64ToArray(jweParts[0])));
|
||||||
|
const additionalData = encoder.encode(jweParts[0]);
|
||||||
|
const iv = b64ToArray(jweParts[2]);
|
||||||
|
const ciphertext = b64ToArray(jweParts[3]);
|
||||||
|
const tag = b64ToArray(jweParts[4]);
|
||||||
|
|
||||||
|
if (header.alg !== 'ECDH-ES' || header.enc !== 'A256GCM') {
|
||||||
|
throw new Error('unsupported jwe type');
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = await crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
header.epk,
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
namedCurve: 'P-256'
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const sharedBits = await crypto.subtle.deriveBits(
|
||||||
|
{
|
||||||
|
name: 'ECDH',
|
||||||
|
public: publicKey
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
256
|
||||||
|
);
|
||||||
|
|
||||||
|
const rawSharedKey = await concatKdf(new Uint8Array(sharedBits), header.enc);
|
||||||
|
const sharedKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
rawSharedKey,
|
||||||
|
{
|
||||||
|
name: 'AES-GCM'
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
const plaintext = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv: iv,
|
||||||
|
additionalData: additionalData,
|
||||||
|
tagLength: tag.length * 8
|
||||||
|
},
|
||||||
|
sharedKey,
|
||||||
|
concat(ciphertext, tag)
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSON.parse(decoder.decode(plaintext));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preparePkce(storage) {
|
||||||
|
const verifier = arrayToB64(crypto.getRandomValues(new Uint8Array(64)));
|
||||||
|
storage.set('pkceVerifier', verifier);
|
||||||
|
const challenge = await crypto.subtle.digest(
|
||||||
|
'SHA-256',
|
||||||
|
encoder.encode(verifier)
|
||||||
|
);
|
||||||
|
return arrayToB64(new Uint8Array(challenge));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileListKey(storage, bundle) {
|
export async function getFileListKey(storage, bundle) {
|
||||||
const keystore = await jose.JWK.asKeyStore(
|
const jwks = await decryptBundle(storage, bundle);
|
||||||
JSON.parse(storage.get('fxaWrapKey'))
|
|
||||||
);
|
|
||||||
const result = await jose.JWE.createDecrypt(keystore).decrypt(bundle);
|
|
||||||
const jwks = JSON.parse(jose.util.utf8.encode(result.plaintext));
|
|
||||||
const jwk = jwks['https://identity.mozilla.com/apps/send'];
|
const jwk = jwks['https://identity.mozilla.com/apps/send'];
|
||||||
const baseKey = await crypto.subtle.importKey(
|
const baseKey = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* global userInfo */
|
|
||||||
import 'fast-text-encoding'; // MS Edge support
|
import 'fast-text-encoding'; // MS Edge support
|
||||||
import 'fluent-intl-polyfill';
|
import 'fluent-intl-polyfill';
|
||||||
import app from './routes';
|
import app from './routes';
|
||||||
|
@ -13,7 +12,6 @@ import experiments from './experiments';
|
||||||
import Raven from 'raven-js';
|
import Raven from 'raven-js';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import User from './user';
|
import User from './user';
|
||||||
import { getFileListKey } from './fxa';
|
|
||||||
|
|
||||||
(async function start() {
|
(async function start() {
|
||||||
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
if (navigator.doNotTrack !== '1' && window.RAVEN_CONFIG) {
|
||||||
|
@ -23,9 +21,7 @@ import { getFileListKey } from './fxa';
|
||||||
if (capa.streamDownload) {
|
if (capa.streamDownload) {
|
||||||
navigator.serviceWorker.register('/serviceWorker.js');
|
navigator.serviceWorker.register('/serviceWorker.js');
|
||||||
}
|
}
|
||||||
if (userInfo && userInfo.keys_jwe) {
|
|
||||||
userInfo.fileListKey = await getFileListKey(storage, userInfo.keys_jwe);
|
|
||||||
}
|
|
||||||
app.use((state, emitter) => {
|
app.use((state, emitter) => {
|
||||||
state.capabilities = capa;
|
state.capabilities = capa;
|
||||||
state.transfer = null;
|
state.transfer = null;
|
||||||
|
@ -33,7 +29,7 @@ import { getFileListKey } from './fxa';
|
||||||
state.translate = locale.getTranslator();
|
state.translate = locale.getTranslator();
|
||||||
state.storage = storage;
|
state.storage = storage;
|
||||||
state.raven = Raven;
|
state.raven = Raven;
|
||||||
state.user = new User(userInfo, storage);
|
state.user = new User(storage);
|
||||||
window.appState = state;
|
window.appState = state;
|
||||||
let unsupportedReason = null;
|
let unsupportedReason = null;
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -68,9 +68,14 @@ app.route('/legal', body(require('../pages/legal')));
|
||||||
app.route('/error', body(require('../pages/error')));
|
app.route('/error', body(require('../pages/error')));
|
||||||
app.route('/blank', body(require('../pages/blank')));
|
app.route('/blank', body(require('../pages/blank')));
|
||||||
app.route('/signin', body(require('../pages/signin')));
|
app.route('/signin', body(require('../pages/signin')));
|
||||||
app.route('/api/fxa/oauth', function(state, emit) {
|
app.route('/api/fxa/oauth', async function(state, emit) {
|
||||||
|
try {
|
||||||
|
await state.user.finishLogin(state.query.code);
|
||||||
emit('replaceState', '/');
|
emit('replaceState', '/');
|
||||||
|
} catch (e) {
|
||||||
|
emit('replaceState', '/error');
|
||||||
setTimeout(() => emit('render'));
|
setTimeout(() => emit('render'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
app.route('*', body(require('../pages/notFound')));
|
app.route('*', body(require('../pages/notFound')));
|
||||||
|
|
||||||
|
|
55
app/user.js
55
app/user.js
|
@ -1,20 +1,18 @@
|
||||||
/* global LIMITS */
|
/* global LIMITS AUTH_CONFIG */
|
||||||
import assets from '../common/assets';
|
import assets from '../common/assets';
|
||||||
import { getFileList, setFileList } from './api';
|
import { getFileList, setFileList } from './api';
|
||||||
import { encryptStream, decryptStream } from './ece';
|
import { encryptStream, decryptStream } from './ece';
|
||||||
import { b64ToArray, streamToArrayBuffer } from './utils';
|
import { b64ToArray, streamToArrayBuffer } from './utils';
|
||||||
import { blobStream } from './streams';
|
import { blobStream } from './streams';
|
||||||
|
import { getFileListKey, prepareScopedBundleKey, preparePkce } from './fxa';
|
||||||
|
|
||||||
const textEncoder = new TextEncoder();
|
const textEncoder = new TextEncoder();
|
||||||
const textDecoder = new TextDecoder();
|
const textDecoder = new TextDecoder();
|
||||||
|
|
||||||
export default class User {
|
export default class User {
|
||||||
constructor(info, storage) {
|
constructor(storage) {
|
||||||
if (info && storage) {
|
|
||||||
storage.user = info;
|
|
||||||
}
|
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.data = info || storage.user || {};
|
this.data = storage.user || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get avatar() {
|
get avatar() {
|
||||||
|
@ -55,7 +53,50 @@ export default class User {
|
||||||
return this.loggedIn ? LIMITS.MAX_DOWNLOADS : LIMITS.ANON.MAX_DOWNLOADS;
|
return this.loggedIn ? LIMITS.MAX_DOWNLOADS : LIMITS.ANON.MAX_DOWNLOADS;
|
||||||
}
|
}
|
||||||
|
|
||||||
login() {}
|
async login() {
|
||||||
|
const keys_jwk = await prepareScopedBundleKey(this.storage);
|
||||||
|
const code_challenge = await preparePkce(this.storage);
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: AUTH_CONFIG.client_id,
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
response_type: 'code',
|
||||||
|
scope: 'profile https://identity.mozilla.com/apps/send', //TODO param
|
||||||
|
state: 'todo',
|
||||||
|
keys_jwk
|
||||||
|
});
|
||||||
|
location.assign(
|
||||||
|
`${AUTH_CONFIG.authorization_endpoint}?${params.toString()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async finishLogin(code) {
|
||||||
|
const tokenResponse = await fetch(AUTH_CONFIG.token_endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code,
|
||||||
|
client_id: AUTH_CONFIG.client_id,
|
||||||
|
code_verifier: this.storage.get('pkceVerifier')
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const auth = await tokenResponse.json();
|
||||||
|
const infoResponse = await fetch(AUTH_CONFIG.userinfo_endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${auth.access_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const userInfo = await infoResponse.json();
|
||||||
|
userInfo.keys_jwe = auth.keys_jwe;
|
||||||
|
userInfo.access_token = auth.access_token;
|
||||||
|
userInfo.fileListKey = await getFileListKey(this.storage, auth.keys_jwe);
|
||||||
|
this.storage.user = userInfo;
|
||||||
|
this.data = userInfo;
|
||||||
|
this.storage.remove('pkceVerifier');
|
||||||
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.storage.user = null;
|
this.storage.user = null;
|
||||||
|
|
|
@ -3921,12 +3921,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
|
||||||
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
|
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
|
||||||
},
|
},
|
||||||
"base64url": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-LIVmqIrIWuiqTvn4RzcrwCOuHo2DD6tKmKBPXXlr4p4n4l6BZBkwFTIa3zu1XkX5MbZgro4a6BvPi+n2Mns5Gg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"basic-auth": {
|
"basic-auth": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz",
|
||||||
|
@ -9587,24 +9581,12 @@
|
||||||
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
|
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash.assign": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
|
|
||||||
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.camelcase": {
|
"lodash.camelcase": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
|
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash.clone": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz",
|
|
||||||
"integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.clonedeep": {
|
"lodash.clonedeep": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||||
|
@ -9616,60 +9598,18 @@
|
||||||
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
|
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash.fill": {
|
|
||||||
"version": "3.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.fill/-/lodash.fill-3.4.0.tgz",
|
|
||||||
"integrity": "sha1-o8dK5kDQU63w3CB5+HIHiOi/74U=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.flatten": {
|
|
||||||
"version": "4.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
|
||||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.get": {
|
"lodash.get": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
|
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash.intersection": {
|
|
||||||
"version": "4.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz",
|
|
||||||
"integrity": "sha1-ChG6Yx0OlcI8fy9Mu5ppLtF45wU=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.memoize": {
|
"lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
|
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash.merge": {
|
|
||||||
"version": "4.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
|
|
||||||
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.omit": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
|
|
||||||
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.partialright": {
|
|
||||||
"version": "4.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.partialright/-/lodash.partialright-4.2.1.tgz",
|
|
||||||
"integrity": "sha1-ATDYDoM2MmTUAHTzKbij56ihzEs=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.pick": {
|
|
||||||
"version": "4.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
|
|
||||||
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"lodash.template": {
|
"lodash.template": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz",
|
||||||
|
@ -10560,37 +10500,6 @@
|
||||||
"integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==",
|
"integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node-jose": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-jose/-/node-jose-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-RE3P8l60Rj9ELrpPmvw6sOQ1hSyYfmQdNUMCa4EN7nCE1ux5JVX+GfXv+mfUTEMhZwNMwxBtI0+X1CKKeukSVQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"base64url": "^3.0.0",
|
|
||||||
"es6-promise": "^4.0.5",
|
|
||||||
"lodash.assign": "^4.0.8",
|
|
||||||
"lodash.clone": "^4.3.2",
|
|
||||||
"lodash.fill": "^3.2.2",
|
|
||||||
"lodash.flatten": "^4.2.0",
|
|
||||||
"lodash.intersection": "^4.1.2",
|
|
||||||
"lodash.merge": "^4.3.5",
|
|
||||||
"lodash.omit": "^4.2.1",
|
|
||||||
"lodash.partialright": "^4.1.3",
|
|
||||||
"lodash.pick": "^4.2.0",
|
|
||||||
"lodash.uniq": "^4.2.1",
|
|
||||||
"long": "^4.0.0",
|
|
||||||
"node-forge": "^0.7.1",
|
|
||||||
"uuid": "^3.0.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"long": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node-libs-browser": {
|
"node-libs-browser": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
|
||||||
|
|
|
@ -89,7 +89,6 @@
|
||||||
"morgan": "^1.9.0",
|
"morgan": "^1.9.0",
|
||||||
"nanobus": "^4.3.2",
|
"nanobus": "^4.3.2",
|
||||||
"nanotiming": "^7.3.1",
|
"nanotiming": "^7.3.1",
|
||||||
"node-jose": "^1.0.0",
|
|
||||||
"npm-run-all": "^4.1.3",
|
"npm-run-all": "^4.1.3",
|
||||||
"nyc": "^13.0.1",
|
"nyc": "^13.0.1",
|
||||||
"postcss-cssnext": "^3.1.0",
|
"postcss-cssnext": "^3.1.0",
|
||||||
|
|
|
@ -134,11 +134,6 @@ const conf = convict({
|
||||||
format: String,
|
format: String,
|
||||||
default: 'b50ec33d3c9beb6d', // localhost
|
default: 'b50ec33d3c9beb6d', // localhost
|
||||||
env: 'FXA_CLIENT_ID'
|
env: 'FXA_CLIENT_ID'
|
||||||
},
|
|
||||||
fxa_client_secret: {
|
|
||||||
format: String,
|
|
||||||
default: '05ac76fbe3e739c9effbaea439bc07d265c613c5e0da9070590a2378377c09d8', // localhost
|
|
||||||
env: 'FXA_CLIENT_SECRET'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
|
const KEY_SCOPE = 'https://identity.mozilla.com/apps/send';
|
||||||
|
let fxaConfig = null;
|
||||||
|
let lastConfigRefresh = 0;
|
||||||
|
|
||||||
|
async function getFxaConfig() {
|
||||||
|
if (fxaConfig && Date.now() - lastConfigRefresh < 1000 * 60 * 5) {
|
||||||
|
return fxaConfig;
|
||||||
|
}
|
||||||
|
const res = await fetch(`${config.fxa_url}/.well-known/openid-configuration`);
|
||||||
|
fxaConfig = await res.json();
|
||||||
|
lastConfigRefresh = Date.now();
|
||||||
|
return fxaConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getFxaConfig,
|
||||||
|
verify: async function(token) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = await getFxaConfig();
|
||||||
|
try {
|
||||||
|
const verifyUrl = c.jwks_uri.replace('jwks', 'verify'); //HACK
|
||||||
|
const result = await fetch(verifyUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ token })
|
||||||
|
});
|
||||||
|
const info = await result.json();
|
||||||
|
if (
|
||||||
|
info.scope &&
|
||||||
|
Array.isArray(info.scope) &&
|
||||||
|
info.scope.includes(KEY_SCOPE)
|
||||||
|
) {
|
||||||
|
return info.user;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// gulp
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
|
@ -6,9 +6,6 @@ module.exports = function(state) {
|
||||||
return state.cspNonce
|
return state.cspNonce
|
||||||
? html`
|
? html`
|
||||||
<script nonce="${state.cspNonce}">
|
<script nonce="${state.cspNonce}">
|
||||||
const userInfo = ${
|
|
||||||
state.user.loggedIn ? raw(JSON.stringify(state.user)) : 'null'
|
|
||||||
};
|
|
||||||
const downloadMetadata = ${
|
const downloadMetadata = ${
|
||||||
state.downloadMetadata ? raw(JSON.stringify(state.downloadMetadata)) : '{}'
|
state.downloadMetadata ? raw(JSON.stringify(state.downloadMetadata)) : '{}'
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const storage = require('../storage');
|
const storage = require('../storage');
|
||||||
const fxa = require('../routes/fxa');
|
const fxa = require('../fxa');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hmac: async function(req, res, next) {
|
hmac: async function(req, res, next) {
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
const { URLSearchParams } = require('url');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
const config = require('../config');
|
|
||||||
const pages = require('./pages');
|
|
||||||
|
|
||||||
const KEY_SCOPE = 'https://identity.mozilla.com/apps/send';
|
|
||||||
let fxaConfig = null;
|
|
||||||
let lastConfigRefresh = 0;
|
|
||||||
|
|
||||||
async function getFxaConfig() {
|
|
||||||
if (fxaConfig && Date.now() - lastConfigRefresh < 1000 * 60 * 5) {
|
|
||||||
return fxaConfig;
|
|
||||||
}
|
|
||||||
const res = await fetch(`${config.fxa_url}/.well-known/openid-configuration`);
|
|
||||||
fxaConfig = await res.json();
|
|
||||||
lastConfigRefresh = Date.now();
|
|
||||||
return fxaConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
login: async function(req, res) {
|
|
||||||
const query = req.query;
|
|
||||||
if (!query || !query.keys_jwk) {
|
|
||||||
return res.sendStatus(400);
|
|
||||||
}
|
|
||||||
const c = await getFxaConfig();
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
client_id: config.fxa_client_id,
|
|
||||||
redirect_uri: `${config.base_url}/api/fxa/oauth`,
|
|
||||||
state: 'todo',
|
|
||||||
scope: `profile ${KEY_SCOPE}`,
|
|
||||||
action: 'email',
|
|
||||||
keys_jwk: query.keys_jwk
|
|
||||||
});
|
|
||||||
res.redirect(`${c.authorization_endpoint}?${params.toString()}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
oauth: async function(req, res) {
|
|
||||||
const query = req.query;
|
|
||||||
if (!query || !query.code || !query.state || !query.action) {
|
|
||||||
return res.sendStatus(400);
|
|
||||||
}
|
|
||||||
const c = await getFxaConfig();
|
|
||||||
const x = await fetch(c.token_endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
code: query.code,
|
|
||||||
client_id: config.fxa_client_id,
|
|
||||||
client_secret: config.fxa_client_secret
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const zzz = await x.json();
|
|
||||||
console.error(zzz);
|
|
||||||
const p = await fetch(c.userinfo_endpoint, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
authorization: `Bearer ${zzz.access_token}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const userInfo = await p.json();
|
|
||||||
userInfo.keys_jwe = zzz.keys_jwe;
|
|
||||||
userInfo.access_token = zzz.access_token;
|
|
||||||
req.userInfo = userInfo;
|
|
||||||
pages.index(req, res);
|
|
||||||
},
|
|
||||||
|
|
||||||
verify: async function(token) {
|
|
||||||
if (!token) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const c = await getFxaConfig();
|
|
||||||
try {
|
|
||||||
const verifyUrl = c.jwks_uri.replace('jwks', 'verify');
|
|
||||||
const result = await fetch(verifyUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ token })
|
|
||||||
});
|
|
||||||
const info = await result.json();
|
|
||||||
if (
|
|
||||||
info.scope &&
|
|
||||||
Array.isArray(info.scope) &&
|
|
||||||
info.scope.includes(KEY_SCOPE)
|
|
||||||
) {
|
|
||||||
return info.user;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// gulp
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -6,7 +6,6 @@ const config = require('../config');
|
||||||
const auth = require('../middleware/auth');
|
const auth = require('../middleware/auth');
|
||||||
const language = require('../middleware/language');
|
const language = require('../middleware/language');
|
||||||
const pages = require('./pages');
|
const pages = require('./pages');
|
||||||
const fxa = require('./fxa');
|
|
||||||
const filelist = require('./filelist');
|
const filelist = require('./filelist');
|
||||||
|
|
||||||
const IS_DEV = config.env === 'development';
|
const IS_DEV = config.env === 'development';
|
||||||
|
@ -34,6 +33,8 @@ module.exports = function(app) {
|
||||||
'wss://*.dev.lcip.org',
|
'wss://*.dev.lcip.org',
|
||||||
'wss://*.mozaws.net',
|
'wss://*.mozaws.net',
|
||||||
'wss://send.firefox.com',
|
'wss://send.firefox.com',
|
||||||
|
'https://*.dev.lcip.org',
|
||||||
|
'https://*.accounts.firefox.com',
|
||||||
'https://sentry.prod.mozaws.net',
|
'https://sentry.prod.mozaws.net',
|
||||||
'https://www.google-analytics.com'
|
'https://www.google-analytics.com'
|
||||||
],
|
],
|
||||||
|
@ -80,8 +81,7 @@ module.exports = function(app) {
|
||||||
);
|
);
|
||||||
app.get(`/api/exists/:id${ID_REGEX}`, require('./exists'));
|
app.get(`/api/exists/:id${ID_REGEX}`, require('./exists'));
|
||||||
app.get(`/api/metadata/:id${ID_REGEX}`, auth.hmac, require('./metadata'));
|
app.get(`/api/metadata/:id${ID_REGEX}`, auth.hmac, require('./metadata'));
|
||||||
app.get('/api/fxa/login', fxa.login);
|
app.get('/api/fxa/oauth', pages.blank);
|
||||||
app.get('/api/fxa/oauth', fxa.oauth);
|
|
||||||
app.get('/api/filelist', auth.fxa, filelist.get);
|
app.get('/api/filelist', auth.fxa, filelist.get);
|
||||||
app.post('/api/filelist', auth.fxa, filelist.post);
|
app.post('/api/filelist', auth.fxa, filelist.post);
|
||||||
app.post('/api/upload', auth.fxa, require('./upload'));
|
app.post('/api/upload', auth.fxa, require('./upload'));
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
const { getFxaConfig } = require('../fxa');
|
||||||
|
|
||||||
let sentry = '';
|
let sentry = '';
|
||||||
if (config.sentry_id) {
|
if (config.sentry_id) {
|
||||||
|
@ -27,14 +28,17 @@ if (config.analytics_id) {
|
||||||
ga = `var GOOGLE_ANALYTICS_ID = '${config.analytics_id}';`;
|
ga = `var GOOGLE_ANALYTICS_ID = '${config.analytics_id}';`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable no-useless-escape */
|
module.exports = async function(req, res) {
|
||||||
const jsconfig = `
|
const fxaConfig = await getFxaConfig();
|
||||||
var isIE = /trident\\\/7\.|msie/i.test(navigator.userAgent);
|
fxaConfig.client_id = config.fxa_client_id;
|
||||||
var isUnsupportedPage = /\\\/unsupported/.test(location.pathname);
|
/* eslint-disable no-useless-escape */
|
||||||
if (isIE && !isUnsupportedPage) {
|
const jsconfig = `
|
||||||
|
var isIE = /trident\\\/7\.|msie/i.test(navigator.userAgent);
|
||||||
|
var isUnsupportedPage = /\\\/unsupported/.test(location.pathname);
|
||||||
|
if (isIE && !isUnsupportedPage) {
|
||||||
window.location.replace('/unsupported/ie');
|
window.location.replace('/unsupported/ie');
|
||||||
}
|
}
|
||||||
var LIMITS = {
|
var LIMITS = {
|
||||||
ANON: {
|
ANON: {
|
||||||
MAX_FILE_SIZE: ${config.anon_max_file_size},
|
MAX_FILE_SIZE: ${config.anon_max_file_size},
|
||||||
MAX_DOWNLOADS: ${config.anon_max_downloads},
|
MAX_DOWNLOADS: ${config.anon_max_downloads},
|
||||||
|
@ -45,15 +49,14 @@ var LIMITS = {
|
||||||
MAX_EXPIRE_SECONDS: ${config.max_expire_seconds},
|
MAX_EXPIRE_SECONDS: ${config.max_expire_seconds},
|
||||||
MAX_FILES_PER_ARCHIVE: ${config.max_files_per_archive},
|
MAX_FILES_PER_ARCHIVE: ${config.max_files_per_archive},
|
||||||
MAX_ARCHIVES_PER_USER: ${config.max_archives_per_user}
|
MAX_ARCHIVES_PER_USER: ${config.max_archives_per_user}
|
||||||
};
|
};
|
||||||
var DEFAULTS = {
|
var DEFAULTS = {
|
||||||
EXPIRE_SECONDS: ${config.default_expire_seconds}
|
EXPIRE_SECONDS: ${config.default_expire_seconds}
|
||||||
};
|
};
|
||||||
${ga}
|
var AUTH_CONFIG = ${JSON.stringify(fxaConfig)};
|
||||||
${sentry}
|
${ga}
|
||||||
`;
|
${sentry}
|
||||||
|
`;
|
||||||
module.exports = function(req, res) {
|
|
||||||
res.set('Content-Type', 'application/javascript');
|
res.set('Content-Type', 'application/javascript');
|
||||||
res.send(jsconfig);
|
res.send(jsconfig);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ const mozlog = require('../log');
|
||||||
const Limiter = require('../limiter');
|
const Limiter = require('../limiter');
|
||||||
const Parser = require('../streamparser');
|
const Parser = require('../streamparser');
|
||||||
const wsStream = require('websocket-stream/stream');
|
const wsStream = require('websocket-stream/stream');
|
||||||
const fxa = require('./fxa');
|
const fxa = require('../fxa');
|
||||||
|
|
||||||
const log = mozlog('send.upload');
|
const log = mozlog('send.upload');
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,6 @@ const assets = require('../common/assets');
|
||||||
|
|
||||||
module.exports = function(req) {
|
module.exports = function(req) {
|
||||||
const locale = req.language || 'en-US';
|
const locale = req.language || 'en-US';
|
||||||
const userInfo = req.userInfo || { avatar: assets.get('user.svg') };
|
|
||||||
userInfo.loggedIn = !!userInfo.access_token;
|
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
translate: locales.getTranslator(locale),
|
translate: locales.getTranslator(locale),
|
||||||
|
@ -21,7 +19,7 @@ module.exports = function(req) {
|
||||||
fira: false,
|
fira: false,
|
||||||
fileInfo: {},
|
fileInfo: {},
|
||||||
cspNonce: req.cspNonce,
|
cspNonce: req.cspNonce,
|
||||||
user: userInfo,
|
user: { avatar: assets.get('user.svg'), loggedIn: false },
|
||||||
layout
|
layout
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import assert from 'assert';
|
||||||
|
import storage from '../../../app/storage';
|
||||||
|
import { decryptBundle, prepareScopedBundleKey } from '../../../app/fxa';
|
||||||
|
import { b64ToArray } from '../../../app/utils';
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
describe('user auth', function() {
|
||||||
|
it('prepares ECDH keys for PKCE auth', async function() {
|
||||||
|
const empty = storage.get('scopedBundlePrivateKey');
|
||||||
|
assert.equal(empty, undefined);
|
||||||
|
const publicKeyB64 = await prepareScopedBundleKey(storage);
|
||||||
|
const publicKey = JSON.parse(decoder.decode(b64ToArray(publicKeyB64)));
|
||||||
|
assert(!publicKey.d, 'not a public key');
|
||||||
|
assert(publicKey.x);
|
||||||
|
assert(publicKey.y);
|
||||||
|
assert.equal(publicKey.kty, 'EC');
|
||||||
|
assert.equal(publicKey.crv, 'P-256');
|
||||||
|
|
||||||
|
const privateKey = JSON.parse(storage.get('scopedBundlePrivateKey'));
|
||||||
|
storage.remove('scopedBundlePrivateKey');
|
||||||
|
assert.equal(privateKey.kty, 'EC');
|
||||||
|
assert.equal(privateKey.crv, 'P-256');
|
||||||
|
assert(privateKey.d, 'not a private key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrypts the PKCE auth bundle', async function() {
|
||||||
|
storage.set(
|
||||||
|
'scopedBundlePrivateKey',
|
||||||
|
'{"kty":"EC","kid":"cV9_thVX9XRa-R2nVZF9rFdwrcR_eST4UZuUCx03ebI","crv":"P-256","x":"-0OOb6SPdYBz0CkQLWRu8ojDUhRe-VoKnwLEBi97KAk","y":"U3fXgj1LV7KhiO5O60niMjPpDqToh15-R6C22NnmNXY","d":"KfIQCxZrqSI6j69rAC6fEiGIYKwYv2buQG9NTcKOiGc"}'
|
||||||
|
);
|
||||||
|
const jwks = await decryptBundle(
|
||||||
|
storage,
|
||||||
|
'eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiRUNESC1FUyIsImtpZCI6ImNWOV90aFZYOVhSYS1SMm5WWkY5ckZkd3JjUl9lU1Q0VVp1VUN4MDNlYkkiLCJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJqckcwajNFODNodDZJcDE1YmtuZWRUV3kwZmR1WnR0V3NtMkFybUNoQU5rIiwieSI6Ijl3SmNQUDRrQmQ5amtCbEJJcWRhclQ2NjVIQU00SndUX0FSSFc0aTN4QUUifX0..Dkf-FXtakCiPuXjW.-KfVQEntYjUe3f5OxslSQwjLFauc50RurLQHDV75sUixNTlsjTIldCZVb6WUKpQkpOdFHOUYFX9_Cvk2ENKdfcVm2eTuyomlKklHF3q5209KwJz8lDK3gOQuAlz79eDou0k_Z3JNGu-qZ8IiDhZZ9iNSgBrsq0BZwVXZ9ViSFEW-YzJBQlKmildscXhp_-Lf6-qiJJrPbZCXFD3PZmzcule3kyBOarg_fjjHLFlIpdjP1lI5wBETqdjk7iBKeO2isSQO7-8.q5EzqP6OPg9yb5BcJH2oFg'
|
||||||
|
);
|
||||||
|
assert.deepEqual(jwks, {
|
||||||
|
'https://identity.mozilla.com/apps/send': {
|
||||||
|
kty: 'oct',
|
||||||
|
scope: 'https://identity.mozilla.com/apps/send',
|
||||||
|
k: '5_jrbS76RzJ4EwlKSl527vqz3BDqf5DM4sNsoEK_hoA',
|
||||||
|
kid: '1414456160-n6yE-eL-ADvnsJo_huq3DA'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue