зеркало из https://github.com/mozilla/fxa.git
feat: vendor fxa-jwtool
Because: * We want to reduce the dependencies in fxa-jwtool and bring more of the functionality into the monorepo. This commit: * Moves and updates the fxa-jwtool code to function the same except as TypeScript with classes and no additional dependencies.
This commit is contained in:
Родитель
477c091d36
Коммит
9a649c6441
|
@ -4,11 +4,17 @@
|
|||
|
||||
import { createPrivateKey, createPublicKey, JsonWebKey } from 'crypto';
|
||||
|
||||
export function pem2jwk(pem: string): JsonWebKey {
|
||||
export function pem2jwk(pem: string, extras?: Record<string, any>): JsonWebKey {
|
||||
if (pem.includes('PUBLIC')) {
|
||||
return createPublicKey({ key: pem }).export({ format: 'jwk' });
|
||||
return Object.assign(
|
||||
createPublicKey({ key: pem }).export({ format: 'jwk' }),
|
||||
extras ?? {}
|
||||
);
|
||||
}
|
||||
return createPrivateKey({ key: pem }).export({ format: 'jwk' });
|
||||
return Object.assign(
|
||||
createPrivateKey({ key: pem }).export({ format: 'jwk' }),
|
||||
extras ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
export function jwk2pem(jwk: JsonWebKey): string {
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
export { encode } from './lib/encoder';
|
||||
export { decode } from './lib/decoder';
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.json"],
|
||||
"parser": "jsonc-eslint-parser",
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
# jwtool
|
||||
|
||||
Port of [fxa-jwtool](https://github.com/mozilla/fxa-jwtool).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build jwtool` to build the library.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test jwtool` to execute the unit tests via [Jest](https://jestjs.io).
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'jwtool',
|
||||
preset: '../../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../../coverage/libs/vendored/jwtool',
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "@fxa/vendored/jwtool",
|
||||
"version": "0.0.1"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "jwtool",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/vendored/jwtool/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/vendored/jwtool",
|
||||
"tsConfig": "libs/vendored/jwtool/tsconfig.lib.json",
|
||||
"packageJson": "libs/vendored/jwtool/package.json",
|
||||
"main": "libs/vendored/jwtool/src/index.ts",
|
||||
"assets": ["libs/vendored/jwtool/*.md"]
|
||||
}
|
||||
},
|
||||
"test-unit": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/vendored/jwtool/jest.config.ts",
|
||||
"testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
export * from './lib/jwtool';
|
|
@ -0,0 +1,96 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import * as http from 'http';
|
||||
import { JWTool, PrivateJWK } from '../index';
|
||||
|
||||
describe('JWTool', () => {
|
||||
let server: http.Server;
|
||||
let trustedJKUs: any;
|
||||
let secretJWK: PrivateJWK;
|
||||
const msg = { bar: 'baz' };
|
||||
|
||||
beforeAll(async () => {
|
||||
server = http.createServer(function (req, res) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ keys: [publicJWK] }));
|
||||
});
|
||||
const isListening = new Promise((resolve) =>
|
||||
server.once('listening', resolve)
|
||||
);
|
||||
server.listen(0, '127.0.0.1');
|
||||
await isListening;
|
||||
const serverAddress = server.address();
|
||||
if (!serverAddress) throw new Error('Server address not available');
|
||||
const port =
|
||||
typeof serverAddress === 'string' ? serverAddress : serverAddress.port;
|
||||
trustedJKUs = ['http://127.0.0.1:' + port + '/'];
|
||||
secretJWK = JWTool.JWK.fromPEM(secretKey, {
|
||||
jku: trustedJKUs[0],
|
||||
kid: 'test1',
|
||||
}) as PrivateJWK;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
});
|
||||
|
||||
it('should sign and verify a public key', async () => {
|
||||
const str = JWTool.sign({ header: { foo: 'x' }, payload: msg }, secretKey);
|
||||
const jwt = JWTool.verify(str, publicKey);
|
||||
expect(jwt).toStrictEqual(msg);
|
||||
});
|
||||
|
||||
it('should verify a JWK', async () => {
|
||||
const jwtool = new JWTool(trustedJKUs);
|
||||
const payload = await jwtool.verify(secretJWK.signSync(msg));
|
||||
expect(payload.bar).toBe('baz');
|
||||
});
|
||||
});
|
||||
|
||||
const secretKey = `
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAsV7Zn+AEa+8+eAwH32PAAIinwiUgWh7ootTt7AGtbeaDz163
|
||||
qSmycqPXpHyDiM7tB1KMVK/+yJpgFqllmqjwG/1N1jnM9+8lIG1XA283sUV+aF9o
|
||||
VyuzVdbaBnO6frRIfIyvmuGGDWI9U6jO1zk/2oPs2kYtHdEQSsM1PA2MY10I45jK
|
||||
YpQuWJNZxQdiR2A+c0iS5kcOjse9+9xuUR2tHlN96afceXlrbXcW+DRTIg/wL/B/
|
||||
tvS3c5Ri4v4ehq0P5jXT4HhHnJgIabyFfKfTo5/bicPu+pOksWTF/pjUOSMFHWuy
|
||||
I3Y1PwW6A3fgPWj0x2Rl9S1Nic7JbQ/S9tvqBwIDAQABAoIBAQCoKRr+vm6yvjJl
|
||||
slJMcs/4MZeLM5PGnYNFzcZ8eOKqTWAuXMiXsxaiJcAvDHXQYQ7MYHD3YZyXJ/Vt
|
||||
xtCznvN2NeNz9Xzkm3CBm+hhMzKD+TTtU3cjHiV6fqZac6IeumH245MhritfyQIH
|
||||
rQXde0OUsnr+PoZLvIhLuWNhOh8dm+SlruYEvd33S1EuWaboM8FKgTvxcKqwJ2WC
|
||||
anZKp/su5h30nE3cGYcquICNNFFZDhkZ2wc5+nV2Wde9qobRr7zCfcppq6ZNf6+U
|
||||
OFrj2L5bcjYnObFEJy89Py1alNgJxvh0WJzK7GY6YTIeHi+uVLzZ8IK2ecDh5dnW
|
||||
fYHp163xAoGBANx3SUuXC9hQ/Ae2rN5mlEhQOVL6TPoc9lgiFdjCZKi46Badr0Ep
|
||||
m+Oyz9bFsGWscDr4sdxqJ0ofa4bFtJtf4ntCjKkQjuPoU0law43yX/lqNRyuYO/T
|
||||
5sOw0XkcQ3pXJSIu21A30bNX60O6JieijMRSpXwkc6EexqKPevMnqtgFAoGBAM31
|
||||
Y5gxaEUywapNZXYVo4U4PB/QohkfJsB2F+3Y5GLkTDGy+0ITm10s5+Pslb+8pAI2
|
||||
gsFwT53+CydARjCCVpjh0JW7T498995Y3PhhPMsNniALfFd4hAJ/QmUgwKUY5pDJ
|
||||
oHRdNHOiumUmRay4aG79ESHFVdhwYHJ+Qs7SE9ObAoGAMbkhqc/GVyJkxWSY9owS
|
||||
M4EMfL+BLwPrN5Nwc/Pb+gXCKp+j0EGPLDq/D4SEtVm/8jz2+Gxksh4GBV5/zm9A
|
||||
yGYJDXRzlclnR2sWIeShasJeejqGGHElYct2YydRvLz83gnNYvlD7XwNzrekNVo+
|
||||
/2RYeHhML/GeATn1E/RFXvUCgYAv4MKlR58IrxLsRw+2ErOvrXH0p2h3VJGKnilT
|
||||
5l65Sn8X8paMNsigMWc6ye3J481wokFlPHmVrc/j8QIgFryQz7XQiPmmzpNEgf3k
|
||||
U55xSZofsuvV3bM6bWD+501BU/eNYwHE60HdO8/+4ZXC4B+O5Y+M/TXGmeEh3I4l
|
||||
TBrFzwKBgDXC9zDwYyt/2na3sFTL13IHqgiMnOUytSZ8caD1464IzX4hZcJbQaS3
|
||||
uUaGvnI81NNl3YkNwYvZIJcfuZIjjS6itG6DQsTeq9xzxZHoPNXgCtdq0GTb6D6x
|
||||
liaw3qdreLkZe9ra8HS6bAhUp8h8fVIuhDHsu0gkVyj2xpN7SZkS
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`.trim();
|
||||
|
||||
const publicKey = `
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsV7Zn+AEa+8+eAwH32PA
|
||||
AIinwiUgWh7ootTt7AGtbeaDz163qSmycqPXpHyDiM7tB1KMVK/+yJpgFqllmqjw
|
||||
G/1N1jnM9+8lIG1XA283sUV+aF9oVyuzVdbaBnO6frRIfIyvmuGGDWI9U6jO1zk/
|
||||
2oPs2kYtHdEQSsM1PA2MY10I45jKYpQuWJNZxQdiR2A+c0iS5kcOjse9+9xuUR2t
|
||||
HlN96afceXlrbXcW+DRTIg/wL/B/tvS3c5Ri4v4ehq0P5jXT4HhHnJgIabyFfKfT
|
||||
o5/bicPu+pOksWTF/pjUOSMFHWuyI3Y1PwW6A3fgPWj0x2Rl9S1Nic7JbQ/S9tvq
|
||||
BwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
`.trim();
|
||||
|
||||
const publicJWK = JWTool.JWK.fromPEM(publicKey, {
|
||||
kid: 'test1',
|
||||
});
|
|
@ -0,0 +1,207 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { jwk2pem, pem2jwk } from '@fxa/shared/pem-jwk';
|
||||
|
||||
const JWT_STRING = /^([a-zA-Z0-9\-_]+)\.([a-zA-Z0-9\-_]+)\.([a-zA-Z0-9\-_]+)$/;
|
||||
|
||||
function base64url(str: string | Buffer): string {
|
||||
const base64 = Buffer.from(str).toString('base64');
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
function sign(jwt: crypto.JsonWebKey, pem: string): string {
|
||||
const header = base64url(JSON.stringify(jwt.header));
|
||||
const payload = base64url(JSON.stringify(jwt.payload));
|
||||
const signed = header + '.' + payload;
|
||||
const s = crypto.createSign('RSA-SHA256');
|
||||
s.update(signed);
|
||||
const sig = base64url(s.sign(pem));
|
||||
return signed + '.' + sig;
|
||||
}
|
||||
|
||||
function decode(str: string) {
|
||||
const match = JWT_STRING.exec(str);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return {
|
||||
header: JSON.parse(Buffer.from(match[1], 'base64').toString()),
|
||||
payload: JSON.parse(Buffer.from(match[2], 'base64').toString()),
|
||||
signature: Buffer.from(match[3], 'base64'),
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function verify(str: string, pem: string) {
|
||||
const jwt = decode(str);
|
||||
if (!jwt) {
|
||||
return false;
|
||||
}
|
||||
const signed = str.split('.', 2).join('.');
|
||||
const v = crypto.createVerify('RSA-SHA256');
|
||||
v.update(signed);
|
||||
return v.verify(pem, jwt.signature) ? jwt.payload : false;
|
||||
}
|
||||
|
||||
function addExtras(obj: any, extras: any) {
|
||||
extras = extras || {};
|
||||
Object.keys(extras).forEach((key) => {
|
||||
obj[key] = extras[key];
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
class JWK {
|
||||
protected jwk: crypto.JsonWebKey;
|
||||
protected pem: string;
|
||||
|
||||
constructor(jwk: crypto.JsonWebKey, pem?: string) {
|
||||
this.jwk = jwk || (pem && pem2jwk(pem));
|
||||
this.pem = pem || jwk2pem(jwk);
|
||||
}
|
||||
|
||||
static fromPEM(pem: string, extras?: any): PublicJWK | PrivateJWK {
|
||||
const obj = pem2jwk(pem, extras);
|
||||
if (obj.d) {
|
||||
return new PrivateJWK(obj, pem);
|
||||
}
|
||||
return new PublicJWK(obj, pem);
|
||||
}
|
||||
|
||||
static fromObject(obj: any, extras?: any): PublicJWK | PrivateJWK {
|
||||
obj = addExtras(obj, extras);
|
||||
if (obj.d) {
|
||||
return new PrivateJWK(obj);
|
||||
}
|
||||
return new PublicJWK(obj);
|
||||
}
|
||||
|
||||
static fromFile(filename: string, extras?: any): PublicJWK | PrivateJWK {
|
||||
const file = fs.readFileSync(filename, 'utf8');
|
||||
if (file[0] === '{') {
|
||||
return JWK.fromObject(JSON.parse(file), extras);
|
||||
}
|
||||
return JWK.fromPEM(file, extras);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.jwk;
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivateJWK extends JWK {
|
||||
signSync(data?: any): string {
|
||||
const payload = data || {};
|
||||
payload.iss = this.jwk.iss;
|
||||
return sign(
|
||||
{
|
||||
header: {
|
||||
alg: 'RS256',
|
||||
jku: this.jwk.jku,
|
||||
kid: this.jwk.kid,
|
||||
},
|
||||
payload: payload,
|
||||
},
|
||||
this.pem
|
||||
);
|
||||
}
|
||||
|
||||
async sign(data?: any): Promise<string> {
|
||||
return Promise.resolve(this.signSync(data));
|
||||
}
|
||||
}
|
||||
|
||||
export class PublicJWK extends JWK {
|
||||
verifySync(str: string): any {
|
||||
return verify(str, this.pem);
|
||||
}
|
||||
|
||||
async verify(str: string): Promise<any> {
|
||||
return Promise.resolve(this.verifySync(str));
|
||||
}
|
||||
}
|
||||
|
||||
export class JWTVerificationError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
this.name = 'JWTVerificationError';
|
||||
this.message = msg;
|
||||
}
|
||||
}
|
||||
|
||||
async function getJwkSet(jku: string) {
|
||||
try {
|
||||
const res = await fetch(jku, {
|
||||
redirect: 'error',
|
||||
});
|
||||
const body = await res.text();
|
||||
const json = JSON.parse(body);
|
||||
const set: Record<string, PublicJWK> = {};
|
||||
json.keys.forEach((key: any) => {
|
||||
set[key.kid] = new PublicJWK(key);
|
||||
});
|
||||
return set;
|
||||
} catch (e) {
|
||||
throw new JWTVerificationError('bad jku');
|
||||
}
|
||||
}
|
||||
|
||||
export class JWTool {
|
||||
trusted: string[];
|
||||
jwkSets: Record<string, Record<string, PublicJWK>>;
|
||||
|
||||
constructor(trusted: string[]) {
|
||||
this.trusted = trusted;
|
||||
this.jwkSets = {};
|
||||
}
|
||||
|
||||
static JWK = JWK;
|
||||
static PublicJWK = PublicJWK;
|
||||
static PrivateJWK = PrivateJWK;
|
||||
static JWTVerificationError = JWTVerificationError;
|
||||
static unverify = decode;
|
||||
static verify = verify;
|
||||
static sign = sign;
|
||||
|
||||
async fetch(jku: string, kid: string): Promise<PublicJWK> {
|
||||
let set = this.jwkSets[jku];
|
||||
if (set && set[kid]) {
|
||||
return set[kid];
|
||||
}
|
||||
set = await getJwkSet(jku);
|
||||
this.jwkSets[jku] = set;
|
||||
if (!set[kid]) {
|
||||
throw new JWTVerificationError('unknown kid');
|
||||
}
|
||||
return set[kid];
|
||||
}
|
||||
|
||||
async verify(str: string, defaults?: any): Promise<any> {
|
||||
defaults = defaults || {};
|
||||
let jwt = decode(str);
|
||||
if (!jwt) {
|
||||
throw new JWTVerificationError('malformed');
|
||||
}
|
||||
|
||||
const jku = jwt.header.jku || defaults.jku;
|
||||
const kid = jwt.header.kid || defaults.kid;
|
||||
if (!this.trusted.includes(jku)) {
|
||||
throw new JWTVerificationError('untrusted');
|
||||
}
|
||||
const jwk = await this.fetch(jku, kid);
|
||||
jwt = await jwk.verify(str);
|
||||
if (!jwt) {
|
||||
throw new JWTVerificationError('invalid');
|
||||
}
|
||||
return jwt;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
10
package.json
10
package.json
|
@ -78,6 +78,7 @@
|
|||
"@swc/helpers": "~0.5.2",
|
||||
"@type-cacheable/core": "^14.0.1",
|
||||
"@type-cacheable/ioredis-adapter": "^10.0.4",
|
||||
"bn.js": "^5.2.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"diffparser": "^2.0.1",
|
||||
|
@ -89,6 +90,7 @@
|
|||
"knex": "^3.1.0",
|
||||
"kysely": "^0.27.2",
|
||||
"lint-staged": "^15.2.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^3.9.7",
|
||||
"nest-typed-config": "^2.9.2",
|
||||
|
@ -176,7 +178,9 @@
|
|||
"@swc/core": "~1.4.6",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@types/babel__core": "^7",
|
||||
"@types/bn.js": "^5",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/module-alias": "^2",
|
||||
"@types/mysql": "^2",
|
||||
"@types/node": "^20.11.1",
|
||||
"@types/passport": "^1.0.6",
|
||||
|
@ -257,5 +261,9 @@
|
|||
"terser:>5 <6": ">=5.14.2",
|
||||
"underscore": ">=1.13.2"
|
||||
},
|
||||
"packageManager": "yarn@3.3.0"
|
||||
"packageManager": "yarn@3.3.0",
|
||||
"_moduleAliases": {
|
||||
"@fxa/vendored/jwtool": "./dist/libs/vendored/jwtool/src/index.js",
|
||||
"@fxa/shared/pem-jwk": "./dist/libs/shared/pem-jwk/src/index.js"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ var config = require('./config').getProperties();
|
|||
var crypto = require('crypto');
|
||||
var request = require('request');
|
||||
var querystring = require('querystring');
|
||||
var JWTool = require('fxa-jwtool');
|
||||
var { JWTool } = require('@fxa/vendored/jwtool');
|
||||
|
||||
// oauth flows are stored in memory
|
||||
var oauthFlows = {};
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
"convict": "^6.2.4",
|
||||
"convict-format-with-validator": "^6.2.0",
|
||||
"express": "^4.19.2",
|
||||
"fxa-jwtool": "^0.7.2",
|
||||
"ioredis": "^5.0.6",
|
||||
"morgan": "^1.10.0",
|
||||
"node-rsa": "1.1.1",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require('module-alias/register');
|
||||
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
|
|
|
@ -18,7 +18,7 @@ const {
|
|||
const TracingProvider = require('fxa-shared/tracing/node-tracing');
|
||||
|
||||
const error = require('../lib/error');
|
||||
const jwtool = require('fxa-jwtool');
|
||||
const { JWTool } = require('@fxa/vendored/jwtool');
|
||||
const { StatsD } = require('hot-shots');
|
||||
const { Container } = require('typedi');
|
||||
const { StripeHelper } = require('../lib/payments/stripe');
|
||||
|
@ -202,13 +202,13 @@ async function run(config) {
|
|||
const senders = await require('../lib/senders')(log, config, bounces, statsd);
|
||||
|
||||
const serverPublicKeys = {
|
||||
primary: jwtool.JWK.fromFile(config.publicKeyFile, {
|
||||
primary: JWTool.JWK.fromFile(config.publicKeyFile, {
|
||||
algorithm: 'RS',
|
||||
use: 'sig',
|
||||
kty: 'RSA',
|
||||
}),
|
||||
secondary: config.oldPublicKeyFile
|
||||
? jwtool.JWK.fromFile(config.oldPublicKeyFile, {
|
||||
? JWTool.JWK.fromFile(config.oldPublicKeyFile, {
|
||||
algorithm: 'RS',
|
||||
use: 'sig',
|
||||
kty: 'RSA',
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const jwtool = require('fxa-jwtool');
|
||||
const BN = require('bn.js');
|
||||
|
||||
const MISC_DOCS = require('../../docs/swagger/misc-api').default;
|
||||
|
||||
function b64toDec(str) {
|
||||
const n = new jwtool.BN(Buffer.from(str, 'base64'));
|
||||
const n = new BN(Buffer.from(str, 'base64'));
|
||||
return n.toString(10);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const jwtool = require('fxa-jwtool');
|
||||
const { JWTool } = require('@fxa/vendored/jwtool');
|
||||
|
||||
module.exports = function (secretKeyFile, domain) {
|
||||
const key = jwtool.JWK.fromFile(secretKeyFile, { iss: domain });
|
||||
const key = JWTool.JWK.fromFile(secretKeyFile, { iss: domain });
|
||||
|
||||
return {
|
||||
sign: async function (data) {
|
||||
|
|
|
@ -92,7 +92,6 @@
|
|||
"emittery": "^0.13.1",
|
||||
"fxa-customs-server": "workspace:*",
|
||||
"fxa-geodb": "workspace:*",
|
||||
"fxa-jwtool": "^0.7.2",
|
||||
"fxa-shared": "workspace:*",
|
||||
"google-auth-library": "^9.8.0",
|
||||
"googleapis": "^135.1.0",
|
||||
|
|
|
@ -7,7 +7,7 @@ const { assert } = require('chai');
|
|||
const nock = require('nock');
|
||||
const buf = require('buf').hex;
|
||||
const generateRSAKeypair = require('keypair');
|
||||
const JWTool = require('fxa-jwtool');
|
||||
const { JWTool } = require('@fxa/vendored/jwtool');
|
||||
const testServer = require('../lib/server');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
const { decodeJWT } = require('../lib/util');
|
||||
|
|
|
@ -8,248 +8,260 @@ const { assert } = require('chai');
|
|||
const url = require('url');
|
||||
const Client = require('../client')();
|
||||
const TestServer = require('../test_server');
|
||||
const jwtool = require('fxa-jwtool');
|
||||
const { JWTool } = require('@fxa/vendored/jwtool');
|
||||
|
||||
const config = require('../../config').default.getProperties();
|
||||
|
||||
[{version:""}, {version:"V2"}].forEach((testOptions) => {
|
||||
|
||||
describe(`#integration${testOptions.version} - remote account reset`, function () {
|
||||
|
||||
this.timeout(15000);
|
||||
let server;
|
||||
config.signinConfirmation.skipForNewAccounts.enabled = true;
|
||||
before(() => {
|
||||
return TestServer.start(config).then((s) => {
|
||||
server = s;
|
||||
[{ version: '' }, { version: 'V2' }].forEach((testOptions) => {
|
||||
describe(`#integration${testOptions.version} - remote account reset`, function () {
|
||||
this.timeout(15000);
|
||||
let server;
|
||||
config.signinConfirmation.skipForNewAccounts.enabled = true;
|
||||
before(() => {
|
||||
return TestServer.start(config).then((s) => {
|
||||
server = s;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('account reset w/o sessionToken', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
it('account reset w/o sessionToken', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
|
||||
let client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true
|
||||
let client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
);
|
||||
const keys1 = await client.keys();
|
||||
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
assert.isRejected(client.resetPassword(newPassword));
|
||||
const response = await resetPassword(client, code, newPassword, {
|
||||
sessionToken: false,
|
||||
});
|
||||
assert(!response.sessionToken, 'session token is not in response');
|
||||
assert(!response.keyFetchToken, 'keyFetchToken token is not in response');
|
||||
assert(!response.verified, 'verified is not in response');
|
||||
|
||||
const emailData = await server.mailbox.waitForEmail(email);
|
||||
const link = emailData.headers['x-link'];
|
||||
const query = url.parse(link, true).query;
|
||||
assert.ok(query.email, 'email is in the link');
|
||||
|
||||
if (testOptions.version === 'V2') {
|
||||
// Reset password only acts on V1 accounts, so we need to create a v1 client
|
||||
// run a password upgrade.
|
||||
const newClient = await Client.login(
|
||||
config.publicUrl,
|
||||
email,
|
||||
newPassword,
|
||||
{
|
||||
version: '',
|
||||
keys: true,
|
||||
}
|
||||
);
|
||||
await newClient.upgradeCredentials(newPassword);
|
||||
}
|
||||
);
|
||||
const keys1 = await client.keys();
|
||||
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
assert.isRejected(client.resetPassword(newPassword));
|
||||
const response = await resetPassword(client, code, newPassword, {
|
||||
sessionToken: false,
|
||||
// make sure we can still login after password reset
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
client = await Client.login(config.publicUrl, email, newPassword, {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
});
|
||||
const keys2 = await client.keys();
|
||||
assert.notEqual(keys1.wrapKb, keys2.wrapKb, 'wrapKb was reset');
|
||||
assert.equal(keys1.kA, keys2.kA, 'kA was not reset');
|
||||
assert.equal(typeof client.getState().kB, 'string');
|
||||
assert.equal(
|
||||
client.getState().kB.length,
|
||||
64,
|
||||
'kB exists, has the right length'
|
||||
);
|
||||
});
|
||||
assert(!response.sessionToken, 'session token is not in response');
|
||||
assert(!response.keyFetchToken, 'keyFetchToken token is not in response');
|
||||
assert(!response.verified, 'verified is not in response');
|
||||
|
||||
const emailData = await server.mailbox.waitForEmail(email);
|
||||
const link = emailData.headers['x-link'];
|
||||
const query = url.parse(link, true).query;
|
||||
assert.ok(query.email, 'email is in the link');
|
||||
it('account reset with keys', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
|
||||
if (testOptions.version === "V2") {
|
||||
// Reset password only acts on V1 accounts, so we need to create a v1 client
|
||||
// run a password upgrade.
|
||||
const newClient = await Client.login(
|
||||
let client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
newPassword,
|
||||
{
|
||||
version: '',
|
||||
keys:true
|
||||
}
|
||||
password,
|
||||
server.mailbox,
|
||||
{ ...testOptions, keys: true }
|
||||
);
|
||||
await newClient.upgradeCredentials(newPassword);
|
||||
}
|
||||
const keys1 = await client.keys();
|
||||
|
||||
// make sure we can still login after password reset
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
client = await Client.login(config.publicUrl, email, newPassword, {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
assert.isRejected(client.resetPassword(newPassword));
|
||||
const response = await resetPassword(client, code, newPassword, {
|
||||
keys: true,
|
||||
});
|
||||
assert.ok(response.sessionToken, 'session token is in response');
|
||||
assert.ok(response.keyFetchToken, 'keyFetchToken token is in response');
|
||||
assert.equal(response.verified, true, 'verified is true');
|
||||
|
||||
const emailData = await server.mailbox.waitForEmail(email);
|
||||
const link = emailData.headers['x-link'];
|
||||
const query = url.parse(link, true).query;
|
||||
assert.ok(query.email, 'email is in the link');
|
||||
|
||||
if (testOptions.version === 'V2') {
|
||||
// Reset password only acts on V1 accounts, so we need to create a v1 client
|
||||
// run a password upgrade.
|
||||
const newClient = await Client.login(
|
||||
config.publicUrl,
|
||||
email,
|
||||
newPassword,
|
||||
{
|
||||
version: '',
|
||||
keys: true,
|
||||
}
|
||||
);
|
||||
const status = await newClient.getCredentialsStatus(email);
|
||||
assert(status.upgradeNeeded);
|
||||
await newClient.upgradeCredentials(newPassword);
|
||||
}
|
||||
|
||||
// make sure we can still login after password reset
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
client = await Client.login(config.publicUrl, email, newPassword, {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
});
|
||||
const keys2 = await client.keys();
|
||||
assert.notEqual(keys1.wrapKb, keys2.wrapKb, 'wrapKb was reset');
|
||||
assert.equal(keys1.kA, keys2.kA, 'kA was not reset');
|
||||
assert.equal(typeof client.getState().kB, 'string');
|
||||
assert.equal(
|
||||
client.getState().kB.length,
|
||||
64,
|
||||
'kB exists, has the right length'
|
||||
);
|
||||
});
|
||||
const keys2 = await client.keys();
|
||||
assert.notEqual(keys1.wrapKb, keys2.wrapKb, 'wrapKb was reset');
|
||||
assert.equal(keys1.kA, keys2.kA, 'kA was not reset');
|
||||
assert.equal(typeof client.getState().kB, 'string');
|
||||
assert.equal(client.getState().kB.length, 64, 'kB exists, has the right length');
|
||||
});
|
||||
|
||||
it('account reset with keys', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
it('account reset w/o keys, with sessionToken', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
|
||||
let client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{ ...testOptions, keys: true }
|
||||
);
|
||||
const keys1 = await client.keys();
|
||||
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
assert.isRejected(client.resetPassword(newPassword));
|
||||
const response = await resetPassword(client, code, newPassword, {
|
||||
keys: true,
|
||||
});
|
||||
assert.ok(response.sessionToken, 'session token is in response');
|
||||
assert.ok(response.keyFetchToken, 'keyFetchToken token is in response');
|
||||
assert.equal(response.verified, true, 'verified is true');
|
||||
|
||||
const emailData = await server.mailbox.waitForEmail(email);
|
||||
const link = emailData.headers['x-link'];
|
||||
const query = url.parse(link, true).query;
|
||||
assert.ok(query.email, 'email is in the link');
|
||||
|
||||
if (testOptions.version === "V2") {
|
||||
// Reset password only acts on V1 accounts, so we need to create a v1 client
|
||||
// run a password upgrade.
|
||||
const newClient = await Client.login(
|
||||
const client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
newPassword,
|
||||
{
|
||||
version: '',
|
||||
keys:true
|
||||
}
|
||||
password,
|
||||
server.mailbox,
|
||||
testOptions
|
||||
);
|
||||
const status = await newClient.getCredentialsStatus(email);
|
||||
assert(status.upgradeNeeded);
|
||||
await newClient.upgradeCredentials(newPassword);
|
||||
}
|
||||
|
||||
// make sure we can still login after password reset
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
client = await Client.login(config.publicUrl, email, newPassword, {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
assert.isRejected(client.resetPassword(newPassword));
|
||||
const response = await resetPassword(client, code, newPassword);
|
||||
assert.ok(response.sessionToken, 'session token is in response');
|
||||
assert(!response.keyFetchToken, 'keyFetchToken token is not in response');
|
||||
assert.equal(response.verified, true, 'verified is true');
|
||||
});
|
||||
const keys2 = await client.keys();
|
||||
assert.notEqual(keys1.wrapKb, keys2.wrapKb, 'wrapKb was reset');
|
||||
assert.equal(keys1.kA, keys2.kA, 'kA was not reset');
|
||||
assert.equal(typeof client.getState().kB, 'string');
|
||||
assert.equal(client.getState().kB.length, 64, 'kB exists, has the right length');
|
||||
});
|
||||
|
||||
it('account reset w/o keys, with sessionToken', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
it('account reset deletes tokens', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
const options = {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
};
|
||||
|
||||
const client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
testOptions
|
||||
);
|
||||
const client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
options
|
||||
);
|
||||
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
assert.isRejected(client.resetPassword(newPassword));
|
||||
const response = await resetPassword(client, code, newPassword);
|
||||
assert.ok(response.sessionToken, 'session token is in response');
|
||||
assert(!response.keyFetchToken, 'keyFetchToken token is not in response');
|
||||
assert.equal(response.verified, true, 'verified is true');
|
||||
});
|
||||
await client.forgotPassword();
|
||||
// Stash original reset code then attempt to use it after another reset
|
||||
const originalCode = await server.mailbox.waitForCode(email);
|
||||
|
||||
it('account reset deletes tokens', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
const options = {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
};
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
assert.isRejected(client.resetPassword(newPassword));
|
||||
await resetPassword(client, code, newPassword, undefined, options);
|
||||
|
||||
const client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
options
|
||||
);
|
||||
const emailData = await server.mailbox.waitForEmail(email);
|
||||
const templateName = emailData.headers['x-template-name'];
|
||||
assert.equal(templateName, 'passwordReset');
|
||||
|
||||
await client.forgotPassword();
|
||||
// Stash original reset code then attempt to use it after another reset
|
||||
const originalCode = await server.mailbox.waitForCode(email);
|
||||
try {
|
||||
await resetPassword(
|
||||
client,
|
||||
originalCode,
|
||||
newPassword,
|
||||
undefined,
|
||||
options
|
||||
);
|
||||
assert.fail('Should not have succeeded password reset');
|
||||
} catch (err) {
|
||||
// Ensure that password reset fails with unknown token error codes
|
||||
assert.equal(err.code, 401);
|
||||
assert.equal(err.errno, 110);
|
||||
}
|
||||
});
|
||||
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
assert.isRejected(client.resetPassword(newPassword));
|
||||
await resetPassword(client, code, newPassword, undefined, options);
|
||||
it('account reset updates keysChangedAt', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123',
|
||||
e: '65537',
|
||||
};
|
||||
|
||||
const emailData = await server.mailbox.waitForEmail(email);
|
||||
const templateName = emailData.headers['x-template-name'];
|
||||
assert.equal(templateName, 'passwordReset');
|
||||
const client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{ ...testOptions, keys: true }
|
||||
);
|
||||
|
||||
try {
|
||||
await resetPassword(client, originalCode, newPassword, undefined, options);
|
||||
assert.fail('Should not have succeeded password reset');
|
||||
} catch (err) {
|
||||
// Ensure that password reset fails with unknown token error codes
|
||||
assert.equal(err.code, 401);
|
||||
assert.equal(err.errno, 110);
|
||||
const cert1 = JWTool.unverify(
|
||||
await client.sign(publicKey, duration)
|
||||
).payload;
|
||||
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
await resetPassword(client, code, newPassword);
|
||||
await server.mailbox.waitForEmail(email);
|
||||
|
||||
const cert2 = JWTool.unverify(
|
||||
await client.sign(publicKey, duration)
|
||||
).payload;
|
||||
|
||||
assert.equal(cert1['fxa-uid'], cert2['fxa-uid']);
|
||||
assert.ok(cert1['fxa-generation'] < cert2['fxa-generation']);
|
||||
assert.ok(cert1['fxa-keysChangedAt'] < cert2['fxa-keysChangedAt']);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return TestServer.stop(server);
|
||||
});
|
||||
|
||||
async function resetPassword(client, code, newPassword, options) {
|
||||
await client.verifyPasswordResetCode(code);
|
||||
return await client.resetPassword(newPassword, {}, options);
|
||||
}
|
||||
});
|
||||
|
||||
it('account reset updates keysChangedAt', async () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const newPassword = 'ez';
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123',
|
||||
e: '65537',
|
||||
};
|
||||
|
||||
const client = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{ ...testOptions, keys: true }
|
||||
);
|
||||
|
||||
const cert1 = jwtool.unverify(
|
||||
await client.sign(publicKey, duration)
|
||||
).payload;
|
||||
|
||||
await client.forgotPassword();
|
||||
const code = await server.mailbox.waitForCode(email);
|
||||
await resetPassword(client, code, newPassword);
|
||||
await server.mailbox.waitForEmail(email);
|
||||
|
||||
const cert2 = jwtool.unverify(
|
||||
await client.sign(publicKey, duration)
|
||||
).payload;
|
||||
|
||||
assert.equal(cert1['fxa-uid'], cert2['fxa-uid']);
|
||||
assert.ok(cert1['fxa-generation'] < cert2['fxa-generation']);
|
||||
assert.ok(cert1['fxa-keysChangedAt'] < cert2['fxa-keysChangedAt']);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return TestServer.stop(server);
|
||||
});
|
||||
|
||||
async function resetPassword(client, code, newPassword, options) {
|
||||
await client.verifyPasswordResetCode(code);
|
||||
return await client.resetPassword(newPassword, {}, options);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -7,11 +7,11 @@
|
|||
const { assert } = require('chai');
|
||||
const TestServer = require('../test_server');
|
||||
const Client = require('../client')();
|
||||
const jwtool = require('fxa-jwtool');
|
||||
const { JWTool } = require('@fxa/vendored/jwtool');
|
||||
|
||||
const config = require('../../config').default.getProperties();
|
||||
config.redis.sessionTokens.enabled = false;
|
||||
const pubSigKey = jwtool.JWK.fromFile(config.publicKeyFile);
|
||||
const pubSigKey = JWTool.JWK.fromFile(config.publicKeyFile);
|
||||
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
|
@ -19,268 +19,284 @@ const publicKey = {
|
|||
e: '65537',
|
||||
};
|
||||
|
||||
[{version:""},{version:"V2"}].forEach((testOptions) => {
|
||||
|
||||
describe(`#integration${testOptions.version} - remote certificate sign`, function () {
|
||||
this.timeout(15000);
|
||||
let server;
|
||||
before(() => {
|
||||
return TestServer.start(config).then((s) => {
|
||||
server = s;
|
||||
[{ version: '' }, { version: 'V2' }].forEach((testOptions) => {
|
||||
describe(`#integration${testOptions.version} - remote certificate sign`, function () {
|
||||
this.timeout(15000);
|
||||
let server;
|
||||
before(() => {
|
||||
return TestServer.start(config).then((s) => {
|
||||
server = s;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('certificate sign', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
let client = null;
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{ ...testOptions, keys: true }
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then((cert) => {
|
||||
assert.equal(typeof cert, 'string', 'cert exists');
|
||||
const payload = jwtool.verify(cert, pubSigKey.pem);
|
||||
assert.equal(payload.iss, config.domain, 'issuer is correct');
|
||||
assert.equal(
|
||||
payload.principal.email.split('@')[0],
|
||||
client.uid,
|
||||
'cert has correct uid'
|
||||
);
|
||||
assert.ok(
|
||||
payload['fxa-generation'] > 0,
|
||||
'cert has non-zero generation number'
|
||||
);
|
||||
assert.ok(
|
||||
new Date() - new Date(payload['fxa-lastAuthAt'] * 1000) <
|
||||
1000 * 60 * 60,
|
||||
'lastAuthAt is plausible'
|
||||
);
|
||||
assert.equal(
|
||||
payload['fxa-verifiedEmail'],
|
||||
email,
|
||||
'verifiedEmail is correct'
|
||||
);
|
||||
assert.equal(
|
||||
payload['fxa-tokenVerified'],
|
||||
true,
|
||||
'tokenVerified is correct'
|
||||
);
|
||||
assert.deepEqual(
|
||||
payload['fxa-amr'].sort(),
|
||||
['email', 'pwd'],
|
||||
'amr values are correct'
|
||||
);
|
||||
assert.equal(payload['fxa-aal'], 1, 'aal value is correct');
|
||||
assert.ok(
|
||||
new Date() - new Date(payload['fxa-profileChangedAt'] * 1000) <
|
||||
1000 * 60 * 60,
|
||||
'profileChangedAt is plausible'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('certificate sign with TOTP', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
let client = null;
|
||||
return Client.createAndVerifyAndTOTP(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{ ...testOptions, keys: true }
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then((cert) => {
|
||||
assert.equal(typeof cert, 'string', 'cert exists');
|
||||
const payload = jwtool.verify(cert, pubSigKey.pem);
|
||||
assert.equal(payload.iss, config.domain, 'issuer is correct');
|
||||
assert.equal(
|
||||
payload.principal.email.split('@')[0],
|
||||
client.uid,
|
||||
'cert has correct uid'
|
||||
);
|
||||
assert.ok(
|
||||
payload['fxa-generation'] > 0,
|
||||
'cert has non-zero generation number'
|
||||
);
|
||||
assert.ok(
|
||||
new Date() - new Date(payload['fxa-lastAuthAt'] * 1000) <
|
||||
1000 * 60 * 60,
|
||||
'lastAuthAt is plausible'
|
||||
);
|
||||
assert.equal(
|
||||
payload['fxa-verifiedEmail'],
|
||||
email,
|
||||
'verifiedEmail is correct'
|
||||
);
|
||||
assert.equal(
|
||||
payload['fxa-tokenVerified'],
|
||||
true,
|
||||
'tokenVerified is correct'
|
||||
);
|
||||
assert.deepEqual(
|
||||
payload['fxa-amr'].sort(),
|
||||
['otp', 'pwd'],
|
||||
'amr values are correct'
|
||||
);
|
||||
assert.equal(payload['fxa-aal'], 2, 'aal value is correct');
|
||||
assert.ok(
|
||||
new Date() - new Date(payload['fxa-profileChangedAt'] * 1000) <
|
||||
1000 * 60 * 60,
|
||||
'profileChangedAt is plausible'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('certificate sign requires a verified account', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
let client = null;
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
return Client.create(config.publicUrl, email, password, testOptions)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then(
|
||||
(cert) => {
|
||||
assert(false, 'should not be able to sign with unverified account');
|
||||
},
|
||||
(err) => {
|
||||
assert.equal(err.errno, 104, 'should get an unverifiedAccount error');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('/certificate/sign inputs', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = '123456';
|
||||
let client = null;
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
testOptions
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
// string as publicKey
|
||||
return client.sign('tada', 1000);
|
||||
})
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'string as publicKey');
|
||||
// empty object as publicKey
|
||||
return client.sign({}, 1000);
|
||||
}
|
||||
it('certificate sign', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
let client = null;
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{ ...testOptions, keys: true }
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'empty object as publicKey');
|
||||
// undefined duration
|
||||
return client.sign({ algorithm: 'RS', n: 'x', e: 'y' }, undefined);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'undefined duration');
|
||||
// missing publicKey arguments (e)
|
||||
return client.sign({ algorithm: 'RS', n: 'x' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (e)');
|
||||
// missing publicKey arguments (n)
|
||||
return client.sign({ algorithm: 'RS', e: 'x' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (n)');
|
||||
// missing publicKey arguments (y)
|
||||
return client.sign({ algorithm: 'DS', p: 'p', q: 'q', g: 'g' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (y)');
|
||||
// missing publicKey arguments (p)
|
||||
return client.sign({ algorithm: 'DS', y: 'y', q: 'q', g: 'g' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (p)');
|
||||
// missing publicKey arguments (q)
|
||||
return client.sign({ algorithm: 'DS', y: 'y', p: 'p', g: 'g' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (q)');
|
||||
// missing publicKey arguments (g)
|
||||
return client.sign({ algorithm: 'DS', y: 'y', p: 'p', q: 'q' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (g)');
|
||||
// invalid algorithm
|
||||
return client.sign({ algorithm: 'NSA' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'invalid algorithm');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('no payload', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
testOptions
|
||||
)
|
||||
.then((client) => {
|
||||
client.api.once('startRequest', (options) => {
|
||||
// we want the payload hash in the auth header
|
||||
// but no payload in the request body
|
||||
options.json = true;
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then((cert) => {
|
||||
assert.equal(typeof cert, 'string', 'cert exists');
|
||||
const payload = JWTool.verify(cert, pubSigKey.pem);
|
||||
assert.equal(payload.iss, config.domain, 'issuer is correct');
|
||||
assert.equal(
|
||||
payload.principal.email.split('@')[0],
|
||||
client.uid,
|
||||
'cert has correct uid'
|
||||
);
|
||||
assert.ok(
|
||||
payload['fxa-generation'] > 0,
|
||||
'cert has non-zero generation number'
|
||||
);
|
||||
assert.ok(
|
||||
new Date() - new Date(payload['fxa-lastAuthAt'] * 1000) <
|
||||
1000 * 60 * 60,
|
||||
'lastAuthAt is plausible'
|
||||
);
|
||||
assert.equal(
|
||||
payload['fxa-verifiedEmail'],
|
||||
email,
|
||||
'verifiedEmail is correct'
|
||||
);
|
||||
assert.equal(
|
||||
payload['fxa-tokenVerified'],
|
||||
true,
|
||||
'tokenVerified is correct'
|
||||
);
|
||||
assert.deepEqual(
|
||||
payload['fxa-amr'].sort(),
|
||||
['email', 'pwd'],
|
||||
'amr values are correct'
|
||||
);
|
||||
assert.equal(payload['fxa-aal'], 1, 'aal value is correct');
|
||||
assert.ok(
|
||||
new Date() - new Date(payload['fxa-profileChangedAt'] * 1000) <
|
||||
1000 * 60 * 60,
|
||||
'profileChangedAt is plausible'
|
||||
);
|
||||
});
|
||||
return client.api.Token.SessionToken.fromHex(client.sessionToken).then(
|
||||
(token) => {
|
||||
});
|
||||
|
||||
it('certificate sign with TOTP', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
let client = null;
|
||||
return Client.createAndVerifyAndTOTP(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{ ...testOptions, keys: true }
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then((cert) => {
|
||||
assert.equal(typeof cert, 'string', 'cert exists');
|
||||
const payload = JWTool.verify(cert, pubSigKey.pem);
|
||||
assert.equal(payload.iss, config.domain, 'issuer is correct');
|
||||
assert.equal(
|
||||
payload.principal.email.split('@')[0],
|
||||
client.uid,
|
||||
'cert has correct uid'
|
||||
);
|
||||
assert.ok(
|
||||
payload['fxa-generation'] > 0,
|
||||
'cert has non-zero generation number'
|
||||
);
|
||||
assert.ok(
|
||||
new Date() - new Date(payload['fxa-lastAuthAt'] * 1000) <
|
||||
1000 * 60 * 60,
|
||||
'lastAuthAt is plausible'
|
||||
);
|
||||
assert.equal(
|
||||
payload['fxa-verifiedEmail'],
|
||||
email,
|
||||
'verifiedEmail is correct'
|
||||
);
|
||||
assert.equal(
|
||||
payload['fxa-tokenVerified'],
|
||||
true,
|
||||
'tokenVerified is correct'
|
||||
);
|
||||
assert.deepEqual(
|
||||
payload['fxa-amr'].sort(),
|
||||
['otp', 'pwd'],
|
||||
'amr values are correct'
|
||||
);
|
||||
assert.equal(payload['fxa-aal'], 2, 'aal value is correct');
|
||||
assert.ok(
|
||||
new Date() - new Date(payload['fxa-profileChangedAt'] * 1000) <
|
||||
1000 * 60 * 60,
|
||||
'profileChangedAt is plausible'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('certificate sign requires a verified account', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
let client = null;
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
return Client.create(config.publicUrl, email, password, testOptions)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then(
|
||||
(cert) => {
|
||||
assert(false, 'should not be able to sign with unverified account');
|
||||
},
|
||||
(err) => {
|
||||
assert.equal(
|
||||
err.errno,
|
||||
104,
|
||||
'should get an unverifiedAccount error'
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('/certificate/sign inputs', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = '123456';
|
||||
let client = null;
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
testOptions
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
// string as publicKey
|
||||
return client.sign('tada', 1000);
|
||||
})
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'string as publicKey');
|
||||
// empty object as publicKey
|
||||
return client.sign({}, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'empty object as publicKey');
|
||||
// undefined duration
|
||||
return client.sign({ algorithm: 'RS', n: 'x', e: 'y' }, undefined);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'undefined duration');
|
||||
// missing publicKey arguments (e)
|
||||
return client.sign({ algorithm: 'RS', n: 'x' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (e)');
|
||||
// missing publicKey arguments (n)
|
||||
return client.sign({ algorithm: 'RS', e: 'x' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (n)');
|
||||
// missing publicKey arguments (y)
|
||||
return client.sign(
|
||||
{ algorithm: 'DS', p: 'p', q: 'q', g: 'g' },
|
||||
1000
|
||||
);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (y)');
|
||||
// missing publicKey arguments (p)
|
||||
return client.sign(
|
||||
{ algorithm: 'DS', y: 'y', q: 'q', g: 'g' },
|
||||
1000
|
||||
);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (p)');
|
||||
// missing publicKey arguments (q)
|
||||
return client.sign(
|
||||
{ algorithm: 'DS', y: 'y', p: 'p', g: 'g' },
|
||||
1000
|
||||
);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (q)');
|
||||
// missing publicKey arguments (g)
|
||||
return client.sign(
|
||||
{ algorithm: 'DS', y: 'y', p: 'p', q: 'q' },
|
||||
1000
|
||||
);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'missing publicKey arguments (g)');
|
||||
// invalid algorithm
|
||||
return client.sign({ algorithm: 'NSA' }, 1000);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.code, 400, 'invalid algorithm');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('no payload', () => {
|
||||
const email = server.uniqueEmail();
|
||||
const password = 'allyourbasearebelongtous';
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
testOptions
|
||||
)
|
||||
.then((client) => {
|
||||
client.api.once('startRequest', (options) => {
|
||||
// we want the payload hash in the auth header
|
||||
// but no payload in the request body
|
||||
options.json = true;
|
||||
});
|
||||
return client.api.Token.SessionToken.fromHex(
|
||||
client.sessionToken
|
||||
).then((token) => {
|
||||
return client.api.doRequest(
|
||||
'POST',
|
||||
`${client.api.baseURL}/certificate/sign`,
|
||||
|
@ -290,20 +306,18 @@ describe(`#integration${testOptions.version} - remote certificate sign`, functio
|
|||
duration: duration,
|
||||
}
|
||||
);
|
||||
});
|
||||
})
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.errno, 109, 'Missing payload authentication');
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(
|
||||
() => assert(false),
|
||||
(err) => {
|
||||
assert.equal(err.errno, 109, 'Missing payload authentication');
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return TestServer.stop(server);
|
||||
after(() => {
|
||||
return TestServer.stop(server);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -7,107 +7,113 @@
|
|||
const { assert } = require('chai');
|
||||
const Client = require('../client')();
|
||||
const TestServer = require('../test_server');
|
||||
const jwtool = require('fxa-jwtool');
|
||||
const { JWTool } = require('@fxa/vendored/jwtool');
|
||||
|
||||
const config = require('../../config').default.getProperties();
|
||||
|
||||
const pubSigKey = jwtool.JWK.fromFile(config.publicKeyFile);
|
||||
const pubSigKey = JWTool.JWK.fromFile(config.publicKeyFile);
|
||||
|
||||
[{version:""},{version:"V2"}].forEach((testOptions) => {
|
||||
[{ version: '' }, { version: 'V2' }].forEach((testOptions) => {
|
||||
describe(`#integration${testOptions.version} - remote flow`, function () {
|
||||
this.timeout(15000);
|
||||
let server;
|
||||
let email1;
|
||||
config.signinConfirmation.skipForNewAccounts.enabled = true;
|
||||
before(() => {
|
||||
return TestServer.start(config).then((s) => {
|
||||
server = s;
|
||||
email1 = server.uniqueEmail();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`#integration${testOptions.version} - remote flow`, function () {
|
||||
this.timeout(15000);
|
||||
let server;
|
||||
let email1;
|
||||
config.signinConfirmation.skipForNewAccounts.enabled = true;
|
||||
before(() => {
|
||||
return TestServer.start(config).then((s) => {
|
||||
server = s;
|
||||
email1 = server.uniqueEmail();
|
||||
it('Create account flow', () => {
|
||||
const email = email1;
|
||||
const password = 'allyourbasearebelongtous';
|
||||
let client = null;
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123',
|
||||
e: '65537',
|
||||
};
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
)
|
||||
.then((x) => {
|
||||
client = x;
|
||||
return client.keys();
|
||||
})
|
||||
.then((keys) => {
|
||||
assert.equal(typeof keys.kA, 'string', 'kA exists');
|
||||
assert.equal(typeof keys.wrapKb, 'string', 'wrapKb exists');
|
||||
assert.equal(typeof keys.kB, 'string', 'kB exists');
|
||||
assert.equal(
|
||||
client.getState().kB.length,
|
||||
64,
|
||||
'kB exists, has the right length'
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then((cert) => {
|
||||
assert.equal(typeof cert, 'string', 'cert exists');
|
||||
const payload = JWTool.verify(cert, pubSigKey.pem);
|
||||
assert.equal(
|
||||
payload.principal.email.split('@')[0],
|
||||
client.uid,
|
||||
'cert has correct uid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Login flow', () => {
|
||||
const email = email1;
|
||||
const password = 'allyourbasearebelongtous';
|
||||
let client = null;
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123',
|
||||
e: '65537',
|
||||
};
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
return Client.login(config.publicUrl, email, password, {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
})
|
||||
.then((x) => {
|
||||
client = x;
|
||||
assert.ok(client.authAt, 'authAt was set');
|
||||
assert.ok(client.uid, 'got a uid');
|
||||
return client.keys();
|
||||
})
|
||||
.then((keys) => {
|
||||
assert.equal(typeof keys.kA, 'string', 'kA exists');
|
||||
assert.equal(typeof keys.wrapKb, 'string', 'wrapKb exists');
|
||||
assert.equal(typeof keys.kB, 'string', 'kB exists');
|
||||
assert.equal(
|
||||
client.getState().kB.length,
|
||||
64,
|
||||
'kB exists, has the right length'
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then((cert) => {
|
||||
assert.equal(typeof cert, 'string', 'cert exists');
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return TestServer.stop(server);
|
||||
});
|
||||
});
|
||||
|
||||
it('Create account flow', () => {
|
||||
const email = email1;
|
||||
const password = 'allyourbasearebelongtous';
|
||||
let client = null;
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123',
|
||||
e: '65537',
|
||||
};
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true
|
||||
}
|
||||
)
|
||||
.then((x) => {
|
||||
client = x;
|
||||
return client.keys();
|
||||
})
|
||||
.then((keys) => {
|
||||
assert.equal(typeof keys.kA, 'string', 'kA exists');
|
||||
assert.equal(typeof keys.wrapKb, 'string', 'wrapKb exists');
|
||||
assert.equal(typeof keys.kB, 'string', 'kB exists');
|
||||
assert.equal(client.getState().kB.length, 64, 'kB exists, has the right length');
|
||||
})
|
||||
.then(() => {
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then((cert) => {
|
||||
assert.equal(typeof cert, 'string', 'cert exists');
|
||||
const payload = jwtool.verify(cert, pubSigKey.pem);
|
||||
assert.equal(
|
||||
payload.principal.email.split('@')[0],
|
||||
client.uid,
|
||||
'cert has correct uid'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Login flow', () => {
|
||||
const email = email1;
|
||||
const password = 'allyourbasearebelongtous';
|
||||
let client = null;
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123',
|
||||
e: '65537',
|
||||
};
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
return Client.login(config.publicUrl, email, password, {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
})
|
||||
.then((x) => {
|
||||
client = x;
|
||||
assert.ok(client.authAt, 'authAt was set');
|
||||
assert.ok(client.uid, 'got a uid');
|
||||
return client.keys();
|
||||
})
|
||||
.then((keys) => {
|
||||
assert.equal(typeof keys.kA, 'string', 'kA exists');
|
||||
assert.equal(typeof keys.wrapKb, 'string', 'wrapKb exists');
|
||||
assert.equal(typeof keys.kB, 'string', 'kB exists');
|
||||
assert.equal(client.getState().kB.length, 64, 'kB exists, has the right length');
|
||||
})
|
||||
.then(() => {
|
||||
return client.sign(publicKey, duration);
|
||||
})
|
||||
.then((cert) => {
|
||||
assert.equal(typeof cert, 'string', 'cert exists');
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return TestServer.stop(server);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -9,107 +9,103 @@ const config = require('../../config').default.getProperties();
|
|||
const crypto = require('crypto');
|
||||
const TestServer = require('../test_server');
|
||||
const Client = require('../client')();
|
||||
const jwtool = require('fxa-jwtool');
|
||||
const { JWTool } = require('@fxa/vendored/jwtool');
|
||||
|
||||
[{ version: '' }, { version: 'V2' }].forEach((testOptions) => {
|
||||
describe(`#integration${testOptions.version} - remote recovery keys`, function () {
|
||||
this.timeout(10000);
|
||||
|
||||
[{version:""},{version:"V2"}].forEach((testOptions) => {
|
||||
let server, client, email;
|
||||
const password = '(-.-)Zzz...';
|
||||
|
||||
describe(`#integration${testOptions.version} - remote recovery keys`, function () {
|
||||
this.timeout(10000);
|
||||
let recoveryKeyId;
|
||||
let recoveryData;
|
||||
let keys;
|
||||
|
||||
let server, client, email;
|
||||
const password = '(-.-)Zzz...';
|
||||
function createMockRecoveryKey() {
|
||||
// The auth-server does not care about the encryption details of the recovery data.
|
||||
// To simplify things, we can mock out some random bits to be stored. Check out
|
||||
// /docs/recovery_keys.md for more details on the encryption that a client
|
||||
// could perform.
|
||||
const recoveryCode = crypto.randomBytes(16).toString('hex');
|
||||
const recoveryKeyId = crypto.randomBytes(16).toString('hex');
|
||||
const recoveryKey = crypto.randomBytes(16).toString('hex');
|
||||
const recoveryData = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
let recoveryKeyId;
|
||||
let recoveryData;
|
||||
let keys;
|
||||
return Promise.resolve({
|
||||
recoveryCode,
|
||||
recoveryData,
|
||||
recoveryKeyId,
|
||||
recoveryKey,
|
||||
});
|
||||
}
|
||||
|
||||
function createMockRecoveryKey() {
|
||||
// The auth-server does not care about the encryption details of the recovery data.
|
||||
// To simplify things, we can mock out some random bits to be stored. Check out
|
||||
// /docs/recovery_keys.md for more details on the encryption that a client
|
||||
// could perform.
|
||||
const recoveryCode = crypto.randomBytes(16).toString('hex');
|
||||
const recoveryKeyId = crypto.randomBytes(16).toString('hex');
|
||||
const recoveryKey = crypto.randomBytes(16).toString('hex');
|
||||
const recoveryData = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
return Promise.resolve({
|
||||
recoveryCode,
|
||||
recoveryData,
|
||||
recoveryKeyId,
|
||||
recoveryKey,
|
||||
before(() => {
|
||||
return TestServer.start(config).then((s) => (server = s));
|
||||
});
|
||||
}
|
||||
|
||||
before(() => {
|
||||
return TestServer.start(config).then((s) => (server = s));
|
||||
});
|
||||
beforeEach(() => {
|
||||
email = server.uniqueEmail();
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
)
|
||||
.then((x) => {
|
||||
client = x;
|
||||
assert.ok(client.authAt, 'authAt was set');
|
||||
|
||||
beforeEach(() => {
|
||||
email = server.uniqueEmail();
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
)
|
||||
.then((x) => {
|
||||
client = x;
|
||||
assert.ok(client.authAt, 'authAt was set');
|
||||
return client.keys();
|
||||
})
|
||||
.then((result) => {
|
||||
keys = result;
|
||||
|
||||
return client.keys();
|
||||
})
|
||||
.then((result) => {
|
||||
keys = result;
|
||||
|
||||
return createMockRecoveryKey(client.uid, keys.kB).then((result) => {
|
||||
recoveryKeyId = result.recoveryKeyId;
|
||||
recoveryData = result.recoveryData;
|
||||
// Should create account recovery key
|
||||
return client
|
||||
.createRecoveryKey(result.recoveryKeyId, result.recoveryData)
|
||||
.then((res) => assert.ok(res, 'empty response'))
|
||||
.then(() => server.mailbox.waitForEmail(email))
|
||||
.then((emailData) => {
|
||||
assert.equal(
|
||||
emailData.headers['x-template-name'],
|
||||
'postAddAccountRecovery'
|
||||
);
|
||||
});
|
||||
return createMockRecoveryKey(client.uid, keys.kB).then((result) => {
|
||||
recoveryKeyId = result.recoveryKeyId;
|
||||
recoveryData = result.recoveryData;
|
||||
// Should create account recovery key
|
||||
return client
|
||||
.createRecoveryKey(result.recoveryKeyId, result.recoveryData)
|
||||
.then((res) => assert.ok(res, 'empty response'))
|
||||
.then(() => server.mailbox.waitForEmail(email))
|
||||
.then((emailData) => {
|
||||
assert.equal(
|
||||
emailData.headers['x-template-name'],
|
||||
'postAddAccountRecovery'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should get account recovery key', () => {
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey(recoveryKeyId))
|
||||
.then((res) => {
|
||||
assert.equal(res.recoveryData, recoveryData, 'recoveryData returned');
|
||||
});
|
||||
});
|
||||
it('should get account recovery key', () => {
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey(recoveryKeyId))
|
||||
.then((res) => {
|
||||
assert.equal(res.recoveryData, recoveryData, 'recoveryData returned');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to get unknown account recovery key', () => {
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey('abce1234567890'))
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.errno, 159, 'account recovery key is not valid');
|
||||
});
|
||||
});
|
||||
it('should fail to get unknown account recovery key', () => {
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey('abce1234567890'))
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.errno, 159, 'account recovery key is not valid');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function checkPayloadV2 (mutate, restore) {
|
||||
await getAccountResetToken(client, server, email);
|
||||
await client.getRecoveryKey(recoveryKeyId);
|
||||
let err;
|
||||
try {
|
||||
mutate();
|
||||
await client.api
|
||||
.accountResetWithRecoveryKeyV2(
|
||||
async function checkPayloadV2(mutate, restore) {
|
||||
await getAccountResetToken(client, server, email);
|
||||
await client.getRecoveryKey(recoveryKeyId);
|
||||
let err;
|
||||
try {
|
||||
mutate();
|
||||
await client.api.accountResetWithRecoveryKeyV2(
|
||||
client.accountResetToken,
|
||||
client.authPW,
|
||||
client.authPWVersion2,
|
||||
|
@ -119,299 +115,305 @@ describe(`#integration${testOptions.version} - remote recovery keys`, function (
|
|||
recoveryKeyId,
|
||||
undefined,
|
||||
{}
|
||||
);
|
||||
} catch (error) {
|
||||
err = error;
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
|
||||
assert.exists(err);
|
||||
assert.equal(err.errno, 107, 'invalid param');
|
||||
}
|
||||
|
||||
it('should fail if wrapKb is missing and authPWVersion2 is provided', async function () {
|
||||
if (testOptions.version !== 'V2') {
|
||||
return this.skip();
|
||||
}
|
||||
const temp = client.wrapKb;
|
||||
await checkPayloadV2(
|
||||
() => {
|
||||
client.unwrapBKey = undefined;
|
||||
client.wrapKb = undefined;
|
||||
},
|
||||
() => {
|
||||
client.wrapKb = temp;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if wrapKbVersion2 is missing and authPWVersion2 is provided', async function () {
|
||||
if (testOptions.version !== 'V2') {
|
||||
return this.skip();
|
||||
}
|
||||
|
||||
const temp = client.wrapKbVersion2;
|
||||
await checkPayloadV2(
|
||||
() => {
|
||||
client.wrapKbVersion2 = undefined;
|
||||
},
|
||||
() => {
|
||||
client.wrapKbVersion2 = temp;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if clientSalt is missing and authPWVersion2 is provided', async function () {
|
||||
if (testOptions.version !== 'V2') {
|
||||
return this.skip();
|
||||
}
|
||||
const temp = client.clientSalt;
|
||||
await checkPayloadV2(
|
||||
() => {
|
||||
client.clientSalt = undefined;
|
||||
},
|
||||
() => {
|
||||
client.clientSalt = temp;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if recoveryKeyId is missing', () => {
|
||||
if (testOptions.version === 'V2') {
|
||||
return this.skip();
|
||||
}
|
||||
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey(recoveryKeyId))
|
||||
.then((res) =>
|
||||
assert.equal(res.recoveryData, recoveryData, 'recoveryData returned')
|
||||
)
|
||||
}
|
||||
catch (error) {
|
||||
err = error;
|
||||
}
|
||||
finally {
|
||||
restore();
|
||||
}
|
||||
|
||||
assert.exists(err);
|
||||
assert.equal(err.errno, 107, 'invalid param');
|
||||
}
|
||||
|
||||
it('should fail if wrapKb is missing and authPWVersion2 is provided', async function () {
|
||||
if (testOptions.version !== "V2") {
|
||||
return this.skip();
|
||||
}
|
||||
const temp = client.wrapKb;
|
||||
await checkPayloadV2(() => {
|
||||
client.unwrapBKey = undefined;
|
||||
client.wrapKb = undefined;
|
||||
}, () => {
|
||||
client.wrapKb = temp;
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if wrapKbVersion2 is missing and authPWVersion2 is provided', async function () {
|
||||
if (testOptions.version !== "V2") {
|
||||
return this.skip();
|
||||
}
|
||||
|
||||
const temp = client.wrapKbVersion2;
|
||||
await checkPayloadV2(() => {
|
||||
client.wrapKbVersion2 = undefined;
|
||||
}, () => {
|
||||
client.wrapKbVersion2 = temp;
|
||||
});
|
||||
})
|
||||
|
||||
it('should fail if clientSalt is missing and authPWVersion2 is provided', async function () {
|
||||
if (testOptions.version !== "V2") {
|
||||
return this.skip();
|
||||
}
|
||||
const temp = client.clientSalt;
|
||||
await checkPayloadV2(() => {
|
||||
client.clientSalt = undefined;
|
||||
}, () => {
|
||||
client.clientSalt = temp;
|
||||
});
|
||||
})
|
||||
|
||||
it('should fail if recoveryKeyId is missing', () => {
|
||||
if (testOptions.version === "V2") {
|
||||
return this.skip
|
||||
}
|
||||
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey(recoveryKeyId))
|
||||
.then((res) =>
|
||||
assert.equal(res.recoveryData, recoveryData, 'recoveryData returned')
|
||||
)
|
||||
.then(() =>
|
||||
client.resetAccountWithRecoveryKey(
|
||||
'newpass',
|
||||
keys.kB,
|
||||
undefined,
|
||||
{},
|
||||
{ keys: true }
|
||||
.then(() =>
|
||||
client.resetAccountWithRecoveryKey(
|
||||
'newpass',
|
||||
keys.kB,
|
||||
undefined,
|
||||
{},
|
||||
{ keys: true }
|
||||
)
|
||||
)
|
||||
)
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.errno, 107, 'invalid param');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if wrapKb is missing', () => {
|
||||
if (testOptions.version === "V2") {
|
||||
return this.skip
|
||||
}
|
||||
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey(recoveryKeyId))
|
||||
.then((res) =>
|
||||
assert.equal(res.recoveryData, recoveryData, 'recoveryData returned')
|
||||
)
|
||||
.then(() =>
|
||||
client.resetAccountWithRecoveryKey(
|
||||
'newpass',
|
||||
keys.kB,
|
||||
recoveryKeyId,
|
||||
{},
|
||||
{ keys: true, undefinedWrapKb: true }
|
||||
)
|
||||
)
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.errno, 107, 'invalid param');
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset password while keeping kB', async () => {
|
||||
await getAccountResetToken(client, server, email);
|
||||
let res = await client.getRecoveryKey(recoveryKeyId);
|
||||
assert.equal(res.recoveryData, recoveryData, 'recoveryData returned');
|
||||
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123',
|
||||
e: '65537',
|
||||
};
|
||||
const cert1 = jwtool.unverify(
|
||||
await client.sign(publicKey, duration)
|
||||
).payload;
|
||||
|
||||
res = await client.resetAccountWithRecoveryKey(
|
||||
'newpass',
|
||||
keys.kB,
|
||||
recoveryKeyId,
|
||||
{},
|
||||
{ keys: true }
|
||||
);
|
||||
assert.equal(res.uid, client.uid, 'uid returned');
|
||||
assert.ok(res.sessionToken, 'sessionToken return');
|
||||
|
||||
const emailData = await server.mailbox.waitForEmail(email);
|
||||
assert.equal(
|
||||
emailData.headers['x-template-name'],
|
||||
'passwordResetAccountRecovery',
|
||||
'correct template sent'
|
||||
);
|
||||
|
||||
res = await client.keys();
|
||||
assert.equal(res.kA, keys.kA, 'kA are equal returned');
|
||||
assert.equal(res.kB, keys.kB, 'kB are equal returned');
|
||||
|
||||
// Login with new password and check to see kB hasn't changed
|
||||
const c = await Client.login(config.publicUrl, email, 'newpass', {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
});
|
||||
assert.ok(c.sessionToken, 'sessionToken returned');
|
||||
res = await c.keys();
|
||||
assert.equal(res.kA, keys.kA, 'kA are equal returned');
|
||||
assert.equal(res.kB, keys.kB, 'kB are equal returned');
|
||||
|
||||
const cert2 = jwtool.unverify(await c.sign(publicKey, duration)).payload;
|
||||
|
||||
assert.equal(cert1['fxa-uid'], cert2['fxa-uid']);
|
||||
assert.ok(cert1['fxa-generation'] < cert2['fxa-generation']);
|
||||
assert.equal(cert1['fxa-keysChangedAt'], cert2['fxa-keysChangedAt']);
|
||||
});
|
||||
|
||||
it('should delete account recovery key', () => {
|
||||
return client.deleteRecoveryKey().then((res) => {
|
||||
assert.ok(res, 'empty response');
|
||||
return client
|
||||
.getRecoveryKeyExists()
|
||||
.then((result) => {
|
||||
assert.equal(result.exists, false, 'account recovery key deleted');
|
||||
})
|
||||
.then(() => server.mailbox.waitForEmail(email))
|
||||
.then((emailData) => {
|
||||
assert.equal(
|
||||
emailData.headers['x-template-name'],
|
||||
'postRemoveAccountRecovery'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create account recovery key when one already exists', () => {
|
||||
return createMockRecoveryKey(client.uid, keys.kB).then((result) => {
|
||||
recoveryKeyId = result.recoveryKeyId;
|
||||
recoveryData = result.recoveryData;
|
||||
return client
|
||||
.createRecoveryKey(result.recoveryKeyId, result.recoveryData)
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.errno, 161, 'correct errno');
|
||||
assert.equal(err.errno, 107, 'invalid param');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('check account recovery key status', () => {
|
||||
describe('with sessionToken', () => {
|
||||
it('should return true if account recovery key exists and enabled', () => {
|
||||
return client.getRecoveryKeyExists().then((res) => {
|
||||
assert.equal(res.exists, true, 'account recovery key exists');
|
||||
});
|
||||
});
|
||||
it('should fail if wrapKb is missing', () => {
|
||||
if (testOptions.version === 'V2') {
|
||||
return this.skip();
|
||||
}
|
||||
|
||||
it("should return false if account recovery key doesn't exist", () => {
|
||||
email = server.uniqueEmail();
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
return getAccountResetToken(client, server, email)
|
||||
.then(() => client.getRecoveryKey(recoveryKeyId))
|
||||
.then((res) =>
|
||||
assert.equal(res.recoveryData, recoveryData, 'recoveryData returned')
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.getRecoveryKeyExists();
|
||||
})
|
||||
.then((res) => {
|
||||
assert.equal(
|
||||
res.exists,
|
||||
false,
|
||||
'account recovery key doesnt exists'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if account recovery key exist but not enabled', async () => {
|
||||
const email2 = server.uniqueEmail();
|
||||
const client2 = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email2,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
);
|
||||
const recoveryKeyMock = await createMockRecoveryKey(
|
||||
client2.uid,
|
||||
keys.kB
|
||||
);
|
||||
let res = await client2.createRecoveryKey(
|
||||
recoveryKeyMock.recoveryKeyId,
|
||||
recoveryKeyMock.recoveryData,
|
||||
false
|
||||
);
|
||||
assert.deepEqual(res, {});
|
||||
|
||||
res = await client2.getRecoveryKeyExists();
|
||||
assert.equal(res.exists, false, 'account recovery key doesnt exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with email', () => {
|
||||
it('should return true if account recovery key exists', () => {
|
||||
return client.getRecoveryKeyExists(email).then((res) => {
|
||||
assert.equal(res.exists, true, 'account recovery key exists');
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if account recovery key doesn't exist", () => {
|
||||
email = server.uniqueEmail();
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
.then(() =>
|
||||
client.resetAccountWithRecoveryKey(
|
||||
'newpass',
|
||||
keys.kB,
|
||||
recoveryKeyId,
|
||||
{},
|
||||
{ keys: true, undefinedWrapKb: true }
|
||||
)
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.getRecoveryKeyExists(email);
|
||||
})
|
||||
.then((res) => {
|
||||
assert.equal(
|
||||
res.exists,
|
||||
false,
|
||||
"account recovery key doesn't exist"
|
||||
);
|
||||
});
|
||||
});
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.errno, 107, 'invalid param');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return TestServer.stop(server);
|
||||
});
|
||||
});
|
||||
it('should reset password while keeping kB', async () => {
|
||||
await getAccountResetToken(client, server, email);
|
||||
let res = await client.getRecoveryKey(recoveryKeyId);
|
||||
assert.equal(res.recoveryData, recoveryData, 'recoveryData returned');
|
||||
|
||||
function getAccountResetToken(client, server, email) {
|
||||
return client
|
||||
.forgotPassword()
|
||||
.then(() => server.mailbox.waitForCode(email))
|
||||
.then((code) =>
|
||||
client.verifyPasswordResetCode(
|
||||
code,
|
||||
const duration = 1000 * 60 * 60 * 24; // 24 hours
|
||||
const publicKey = {
|
||||
algorithm: 'RS',
|
||||
n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123',
|
||||
e: '65537',
|
||||
};
|
||||
const cert1 = JWTool.unverify(
|
||||
await client.sign(publicKey, duration)
|
||||
).payload;
|
||||
|
||||
res = await client.resetAccountWithRecoveryKey(
|
||||
'newpass',
|
||||
keys.kB,
|
||||
recoveryKeyId,
|
||||
{},
|
||||
{ accountResetWithRecoveryKey: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
{ keys: true }
|
||||
);
|
||||
assert.equal(res.uid, client.uid, 'uid returned');
|
||||
assert.ok(res.sessionToken, 'sessionToken return');
|
||||
|
||||
const emailData = await server.mailbox.waitForEmail(email);
|
||||
assert.equal(
|
||||
emailData.headers['x-template-name'],
|
||||
'passwordResetAccountRecovery',
|
||||
'correct template sent'
|
||||
);
|
||||
|
||||
res = await client.keys();
|
||||
assert.equal(res.kA, keys.kA, 'kA are equal returned');
|
||||
assert.equal(res.kB, keys.kB, 'kB are equal returned');
|
||||
|
||||
// Login with new password and check to see kB hasn't changed
|
||||
const c = await Client.login(config.publicUrl, email, 'newpass', {
|
||||
...testOptions,
|
||||
keys: true,
|
||||
});
|
||||
assert.ok(c.sessionToken, 'sessionToken returned');
|
||||
res = await c.keys();
|
||||
assert.equal(res.kA, keys.kA, 'kA are equal returned');
|
||||
assert.equal(res.kB, keys.kB, 'kB are equal returned');
|
||||
|
||||
const cert2 = JWTool.unverify(await c.sign(publicKey, duration)).payload;
|
||||
|
||||
assert.equal(cert1['fxa-uid'], cert2['fxa-uid']);
|
||||
assert.ok(cert1['fxa-generation'] < cert2['fxa-generation']);
|
||||
assert.equal(cert1['fxa-keysChangedAt'], cert2['fxa-keysChangedAt']);
|
||||
});
|
||||
|
||||
it('should delete account recovery key', () => {
|
||||
return client.deleteRecoveryKey().then((res) => {
|
||||
assert.ok(res, 'empty response');
|
||||
return client
|
||||
.getRecoveryKeyExists()
|
||||
.then((result) => {
|
||||
assert.equal(result.exists, false, 'account recovery key deleted');
|
||||
})
|
||||
.then(() => server.mailbox.waitForEmail(email))
|
||||
.then((emailData) => {
|
||||
assert.equal(
|
||||
emailData.headers['x-template-name'],
|
||||
'postRemoveAccountRecovery'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create account recovery key when one already exists', () => {
|
||||
return createMockRecoveryKey(client.uid, keys.kB).then((result) => {
|
||||
recoveryKeyId = result.recoveryKeyId;
|
||||
recoveryData = result.recoveryData;
|
||||
return client
|
||||
.createRecoveryKey(result.recoveryKeyId, result.recoveryData)
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.errno, 161, 'correct errno');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('check account recovery key status', () => {
|
||||
describe('with sessionToken', () => {
|
||||
it('should return true if account recovery key exists and enabled', () => {
|
||||
return client.getRecoveryKeyExists().then((res) => {
|
||||
assert.equal(res.exists, true, 'account recovery key exists');
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if account recovery key doesn't exist", () => {
|
||||
email = server.uniqueEmail();
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.getRecoveryKeyExists();
|
||||
})
|
||||
.then((res) => {
|
||||
assert.equal(
|
||||
res.exists,
|
||||
false,
|
||||
'account recovery key doesnt exists'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if account recovery key exist but not enabled', async () => {
|
||||
const email2 = server.uniqueEmail();
|
||||
const client2 = await Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email2,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
);
|
||||
const recoveryKeyMock = await createMockRecoveryKey(
|
||||
client2.uid,
|
||||
keys.kB
|
||||
);
|
||||
let res = await client2.createRecoveryKey(
|
||||
recoveryKeyMock.recoveryKeyId,
|
||||
recoveryKeyMock.recoveryData,
|
||||
false
|
||||
);
|
||||
assert.deepEqual(res, {});
|
||||
|
||||
res = await client2.getRecoveryKeyExists();
|
||||
assert.equal(res.exists, false, 'account recovery key doesnt exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with email', () => {
|
||||
it('should return true if account recovery key exists', () => {
|
||||
return client.getRecoveryKeyExists(email).then((res) => {
|
||||
assert.equal(res.exists, true, 'account recovery key exists');
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if account recovery key doesn't exist", () => {
|
||||
email = server.uniqueEmail();
|
||||
return Client.createAndVerify(
|
||||
config.publicUrl,
|
||||
email,
|
||||
password,
|
||||
server.mailbox,
|
||||
{
|
||||
...testOptions,
|
||||
keys: true,
|
||||
}
|
||||
)
|
||||
.then((c) => {
|
||||
client = c;
|
||||
return client.getRecoveryKeyExists(email);
|
||||
})
|
||||
.then((res) => {
|
||||
assert.equal(
|
||||
res.exists,
|
||||
false,
|
||||
"account recovery key doesn't exist"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
return TestServer.stop(server);
|
||||
});
|
||||
});
|
||||
|
||||
function getAccountResetToken(client, server, email) {
|
||||
return client
|
||||
.forgotPassword()
|
||||
.then(() => server.mailbox.waitForCode(email))
|
||||
.then((code) =>
|
||||
client.verifyPasswordResetCode(
|
||||
code,
|
||||
{},
|
||||
{ accountResetWithRecoveryKey: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,19 @@
|
|||
const { pathsToModuleNameMapper } = require('ts-jest');
|
||||
const { compilerOptions } = require('./tsconfig.build.json');
|
||||
module.exports = {
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
|
||||
modulePaths: ['<rootDir>/../dist/'],
|
||||
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||
rootDir: 'src',
|
||||
testRegex: '.spec.ts$',
|
||||
transform: {
|
||||
'^.+\\.(t|j)s$': [
|
||||
'ts-jest',
|
||||
{
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
coverageDirectory: './coverage',
|
||||
testEnvironment: 'node',
|
||||
};
|
|
@ -42,7 +42,6 @@
|
|||
"convict-format-with-moment": "^6.2.0",
|
||||
"convict-format-with-validator": "^6.2.0",
|
||||
"express": "^4.19.2",
|
||||
"fxa-jwtool": "^0.7.2",
|
||||
"fxa-shared": "workspace:*",
|
||||
"google-auth-library": "^9.8.0",
|
||||
"hot-shots": "^10.0.0",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import jwtool from 'fxa-jwtool';
|
||||
import { JWTool, PublicJWK } from '@fxa/vendored/jwtool';
|
||||
|
||||
import { JwtsetService } from './jwtset.service';
|
||||
import {
|
||||
|
@ -35,7 +35,7 @@ const TEST_PUBLIC_KEY = {
|
|||
n: TEST_KEY.n,
|
||||
};
|
||||
const TEST_CLIENT_ID = 'abc1234';
|
||||
const PUBLIC_JWT = jwtool.JWK.fromObject(TEST_PUBLIC_KEY);
|
||||
const PUBLIC_JWT = JWTool.JWK.fromObject(TEST_PUBLIC_KEY) as PublicJWK;
|
||||
const CHANGE_TIME = Date.now();
|
||||
|
||||
describe('JwtsetService', () => {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { JWTool, PrivateJWK } from '@fxa/vendored/jwtool';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import jwtool from 'fxa-jwtool';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AppConfig } from '../config';
|
||||
import * as set from './set.interface';
|
||||
|
@ -19,14 +21,14 @@ import * as set from './set.interface';
|
|||
@Injectable()
|
||||
export class JwtsetService {
|
||||
private issuer: string;
|
||||
private tokenKey: jwtool.PrivateJWK;
|
||||
private tokenKey: PrivateJWK;
|
||||
|
||||
constructor(configService: ConfigService<AppConfig>) {
|
||||
const config = configService.get('openid') as AppConfig['openid'];
|
||||
this.issuer = config.issuer;
|
||||
this.tokenKey = jwtool.JWK.fromObject(config.key as any, {
|
||||
this.tokenKey = JWTool.JWK.fromObject(config.key as any, {
|
||||
iss: this.issuer,
|
||||
});
|
||||
}) as PrivateJWK;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
"paths": {
|
||||
"@fxa/shared/pem-jwk": ["libs/shared/pem-jwk/src/index"],
|
||||
"@fxa/vendored/jwtool": ["libs/vendored/jwtool/src/index"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"outDir": "./dist",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true
|
||||
"importHelpers": true,
|
||||
"types": ["jest"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@
|
|||
],
|
||||
"@fxa/vendored/incremental-encoder": [
|
||||
"libs/vendored/incremental-encoder/src/index.ts"
|
||||
]
|
||||
],
|
||||
"@fxa/vendored/jwtool": ["libs/vendored/jwtool/src/index.ts"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"./types",
|
||||
|
|
|
@ -1,224 +0,0 @@
|
|||
declare module 'fxa-jwtool' {
|
||||
export = fxa_jwtool;
|
||||
}
|
||||
|
||||
declare class fxa_jwtool {
|
||||
constructor(trusted: any);
|
||||
|
||||
fetch(jku: any, kid: any): any;
|
||||
|
||||
verify(str: any, defaults: any): any;
|
||||
|
||||
static JWTVerificationError(msg: any): void;
|
||||
|
||||
static sign(jwt: any, pem: any): any;
|
||||
|
||||
static unverify(str: any): any;
|
||||
|
||||
static verify(str: any, pem: any): any;
|
||||
}
|
||||
|
||||
declare namespace fxa_jwtool {
|
||||
class BN {
|
||||
constructor(number: any, base: any, endian: any);
|
||||
|
||||
abs(): any;
|
||||
|
||||
add(num: any): any;
|
||||
|
||||
addn(num: any): any;
|
||||
|
||||
and(num: any): any;
|
||||
|
||||
andln(num: any): any;
|
||||
|
||||
bincn(bit: any): any;
|
||||
|
||||
bitLength(): any;
|
||||
|
||||
byteLength(): any;
|
||||
|
||||
clone(): any;
|
||||
|
||||
cmp(num: any): any;
|
||||
|
||||
cmpn(num: any): any;
|
||||
|
||||
copy(dest: any): void;
|
||||
|
||||
div(num: any): any;
|
||||
|
||||
divRound(num: any): any;
|
||||
|
||||
divmod(num: any, mode: any): any;
|
||||
|
||||
divn(num: any): any;
|
||||
|
||||
forceRed(ctx: any): any;
|
||||
|
||||
fromRed(): any;
|
||||
|
||||
gcd(num: any): any;
|
||||
|
||||
iabs(): any;
|
||||
|
||||
iadd(num: any): any;
|
||||
|
||||
iaddn(num: any): any;
|
||||
|
||||
iand(num: any): any;
|
||||
|
||||
idivn(num: any): any;
|
||||
|
||||
imaskn(bits: any): any;
|
||||
|
||||
imul(num: any): any;
|
||||
|
||||
imuln(num: any): any;
|
||||
|
||||
inspect(): any;
|
||||
|
||||
invm(num: any): any;
|
||||
|
||||
ior(num: any): any;
|
||||
|
||||
isEven(): any;
|
||||
|
||||
isOdd(): any;
|
||||
|
||||
ishln(bits: any): any;
|
||||
|
||||
ishrn(bits: any, hint: any, extended: any): any;
|
||||
|
||||
isqr(): any;
|
||||
|
||||
isub(num: any): any;
|
||||
|
||||
isubn(num: any): any;
|
||||
|
||||
ixor(num: any): any;
|
||||
|
||||
maskn(bits: any): any;
|
||||
|
||||
mod(num: any): any;
|
||||
|
||||
modn(num: any): any;
|
||||
|
||||
mul(num: any): any;
|
||||
|
||||
mulTo(num: any, out: any): any;
|
||||
|
||||
neg(): any;
|
||||
|
||||
or(num: any): any;
|
||||
|
||||
redAdd(num: any): any;
|
||||
|
||||
redIAdd(num: any): any;
|
||||
|
||||
redIMul(num: any): any;
|
||||
|
||||
redISqr(): any;
|
||||
|
||||
redISub(num: any): any;
|
||||
|
||||
redInvm(): any;
|
||||
|
||||
redMul(num: any): any;
|
||||
|
||||
redNeg(): any;
|
||||
|
||||
redPow(num: any): any;
|
||||
|
||||
redShl(num: any): any;
|
||||
|
||||
redSqr(): any;
|
||||
|
||||
redSqrt(): any;
|
||||
|
||||
redSub(num: any): any;
|
||||
|
||||
setn(bit: any, val: any): any;
|
||||
|
||||
shln(bits: any): any;
|
||||
|
||||
shrn(bits: any): any;
|
||||
|
||||
sqr(): any;
|
||||
|
||||
strip(): any;
|
||||
|
||||
sub(num: any): any;
|
||||
|
||||
subn(num: any): any;
|
||||
|
||||
testn(bit: any): any;
|
||||
|
||||
toArray(): any;
|
||||
|
||||
toJSON(): any;
|
||||
|
||||
toRed(ctx: any): any;
|
||||
|
||||
toString(base: any, padding: any): any;
|
||||
|
||||
ucmp(num: any): any;
|
||||
|
||||
xor(num: any): any;
|
||||
|
||||
static BN: any;
|
||||
|
||||
static mont(num: any): any;
|
||||
|
||||
static red(num: any): any;
|
||||
|
||||
static wordSize: number;
|
||||
}
|
||||
|
||||
type PublicKeyType = {
|
||||
e: string;
|
||||
'fxa-createdAt'?: number;
|
||||
kty: string;
|
||||
kid?: string;
|
||||
n: string;
|
||||
};
|
||||
|
||||
type PrivateKeyTye = PublicKeyType & {
|
||||
d: string;
|
||||
p: string;
|
||||
dp: string;
|
||||
dq: string;
|
||||
qi: string;
|
||||
};
|
||||
|
||||
class JWK {
|
||||
constructor(jwk: any, pem: any);
|
||||
|
||||
toJSON(): any;
|
||||
|
||||
static fromFile<PrivateJWK>(filename: any, extras: any): PrivateJWK;
|
||||
static fromFile<PublicJWK>(filename: any, extras: any): PublicJWK;
|
||||
|
||||
static fromObject(obj: PrivateKeyTye, extras?: any): PrivateJWK;
|
||||
static fromObject(obj: PublicKeyType, extras?: any): PublicJWK;
|
||||
|
||||
static fromPEM<PrivateJWK>(pem: any, extras: any): PrivateJWK;
|
||||
static fromPEM<PublicJWK>(pem: any, extras: any): PublicJWK;
|
||||
}
|
||||
|
||||
class PrivateJWK {
|
||||
constructor(jwk: any, pem: any);
|
||||
|
||||
sign(data: any): Promise<any>;
|
||||
|
||||
signSync(data: any): Promise<any>;
|
||||
}
|
||||
|
||||
class PublicJWK {
|
||||
constructor(jwk: any, pem: any);
|
||||
|
||||
verify(str: any): Promise<any>;
|
||||
|
||||
verifySync(str: any): any;
|
||||
}
|
||||
}
|
91
yarn.lock
91
yarn.lock
|
@ -15,7 +15,6 @@ __metadata:
|
|||
convict-format-with-validator: ^6.2.0
|
||||
eslint: ^7.32.0
|
||||
express: ^4.19.2
|
||||
fxa-jwtool: ^0.7.2
|
||||
ioredis: ^5.0.6
|
||||
morgan: ^1.10.0
|
||||
node-rsa: 1.1.1
|
||||
|
@ -20960,6 +20959,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/bn.js@npm:^5":
|
||||
version: 5.1.5
|
||||
resolution: "@types/bn.js@npm:5.1.5"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: c87b28c4af74545624f8a3dae5294b16aa190c222626e8d4b2e327b33b1a3f1eeb43e7a24d914a9774bca43d8cd6e1cb0325c1f4b3a244af6693a024e1d918e6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/body-parser@npm:*":
|
||||
version: 1.19.0
|
||||
resolution: "@types/body-parser@npm:1.19.0"
|
||||
|
@ -22138,6 +22146,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/module-alias@npm:^2":
|
||||
version: 2.0.4
|
||||
resolution: "@types/module-alias@npm:2.0.4"
|
||||
checksum: f324ec96e07955270b8729cbc03e56405780f7a87f1f8e75009b40ba47c07a08f22ac5fe27e8e9a8c08b7951565618357429f4cec1cfffb2b777e569a0ffb966
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ms@npm:*":
|
||||
version: 0.7.31
|
||||
resolution: "@types/ms@npm:0.7.31"
|
||||
|
@ -25387,20 +25402,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"asn1.js@npm:1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "asn1.js@npm:1.0.3"
|
||||
dependencies:
|
||||
bn.js: ^1.0.0
|
||||
inherits: ^2.0.1
|
||||
minimalistic-assert: ^1.0.0
|
||||
dependenciesMeta:
|
||||
bn.js:
|
||||
optional: true
|
||||
checksum: 7ed1aa61668f0a371c68cb8da685cd2e05c03638bd729bdc819f78faca769108baec225da418893d7ae63145b77808508be93d691d2f15d8cf84ef7c41d1b483
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"asn1.js@npm:^4.0.0, asn1.js@npm:^4.10.1":
|
||||
version: 4.10.1
|
||||
resolution: "asn1.js@npm:4.10.1"
|
||||
|
@ -27587,13 +27588,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bluebird@npm:3.0.5":
|
||||
version: 3.0.5
|
||||
resolution: "bluebird@npm:3.0.5"
|
||||
checksum: f20af97ac3b1faf1e85f5b3167ad4bb4011d67e71396eae345360f4620f454262d2b4c837ee6fb8dd6efb674400ec05c815b62b69ded15dc682e63bf57f27f54
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bluebird@npm:3.5.1":
|
||||
version: 3.5.1
|
||||
resolution: "bluebird@npm:3.5.1"
|
||||
|
@ -27629,13 +27623,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bn.js@npm:^1.0.0":
|
||||
version: 1.3.0
|
||||
resolution: "bn.js@npm:1.3.0"
|
||||
checksum: 3f075a33f938fa573943b00aa60d93e9a6649cd8cd592af40a3686e104ae8ce655794ea6bc1d542df2ce65c8fe523b9ec31b10302eee6014b3f1304216725451
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0":
|
||||
version: 4.11.8
|
||||
resolution: "bn.js@npm:4.11.8"
|
||||
|
@ -33308,7 +33295,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"encoding@npm:*, encoding@npm:^0.1.11, encoding@npm:^0.1.12, encoding@npm:~0.1.7":
|
||||
"encoding@npm:^0.1.11, encoding@npm:^0.1.12, encoding@npm:~0.1.7":
|
||||
version: 0.1.12
|
||||
resolution: "encoding@npm:0.1.12"
|
||||
dependencies:
|
||||
|
@ -36133,15 +36120,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fetch@npm:0.3.6":
|
||||
version: 0.3.6
|
||||
resolution: "fetch@npm:0.3.6"
|
||||
dependencies:
|
||||
encoding: "*"
|
||||
checksum: 8a03d81f87659cdde7e56087b0684fee8daec923bc207bf8da7c50b5d5958ecaa4ef0e37d2a1b5e41dfd8ba118d97f374a0776ee165614742d50534ca2548145
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fflate@npm:^0.8.0":
|
||||
version: 0.8.0
|
||||
resolution: "fflate@npm:0.8.0"
|
||||
|
@ -37598,7 +37576,6 @@ fsevents@~2.1.1:
|
|||
eslint: ^7.32.0
|
||||
fxa-customs-server: "workspace:*"
|
||||
fxa-geodb: "workspace:*"
|
||||
fxa-jwtool: ^0.7.2
|
||||
fxa-shared: "workspace:*"
|
||||
google-auth-library: ^9.8.0
|
||||
googleapis: ^135.1.0
|
||||
|
@ -37947,7 +37924,6 @@ fsevents@~2.1.1:
|
|||
eslint-plugin-import: ^2.29.1
|
||||
express: ^4.19.2
|
||||
factory-bot-ts: ^0.1.5
|
||||
fxa-jwtool: ^0.7.2
|
||||
fxa-shared: "workspace:*"
|
||||
google-auth-library: ^9.8.0
|
||||
hot-shots: ^10.0.0
|
||||
|
@ -38049,17 +38025,6 @@ fsevents@~2.1.1:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"fxa-jwtool@npm:^0.7.2":
|
||||
version: 0.7.2
|
||||
resolution: "fxa-jwtool@npm:0.7.2"
|
||||
dependencies:
|
||||
bluebird: 3.0.5
|
||||
fetch: 0.3.6
|
||||
pem-jwk: 1.5.1
|
||||
checksum: d6e2a5f2467f3fa119e5f703115d520afc38180af09155e3fef5237038961ec9af057777d5c7947706b755e4b3a10288cd1808ef14772b6136b1fbb75250ea69
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fxa-mustache-loader@npm:0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "fxa-mustache-loader@npm:0.0.2"
|
||||
|
@ -38584,7 +38549,9 @@ fsevents@~2.1.1:
|
|||
"@type-cacheable/core": ^14.0.1
|
||||
"@type-cacheable/ioredis-adapter": ^10.0.4
|
||||
"@types/babel__core": ^7
|
||||
"@types/bn.js": ^5
|
||||
"@types/jest": ^29.5.1
|
||||
"@types/module-alias": ^2
|
||||
"@types/mysql": ^2
|
||||
"@types/node": ^20.11.1
|
||||
"@types/passport": ^1.0.6
|
||||
|
@ -38599,6 +38566,7 @@ fsevents@~2.1.1:
|
|||
"@typescript-eslint/parser": ^7.1.1
|
||||
autoprefixer: ^10.4.14
|
||||
babel-jest: ^29.7.0
|
||||
bn.js: ^5.2.1
|
||||
class-transformer: ^0.5.1
|
||||
class-validator: ^0.14.1
|
||||
diffparser: ^2.0.1
|
||||
|
@ -38631,6 +38599,7 @@ fsevents@~2.1.1:
|
|||
lint-staged: ^15.2.0
|
||||
mocha-junit-reporter: ^2.2.0
|
||||
mocha-multi: ^1.1.7
|
||||
module-alias: ^2.2.3
|
||||
mysql: ^2.18.1
|
||||
mysql2: ^3.9.7
|
||||
nest-typed-config: ^2.9.2
|
||||
|
@ -49769,6 +49738,13 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"module-alias@npm:^2.2.3":
|
||||
version: 2.2.3
|
||||
resolution: "module-alias@npm:2.2.3"
|
||||
checksum: 6169187f69de8dcf8af8fab4d9e53ada6338a43f7670d38d0b27a089c28f9eb18d85a6fd46f11b54c63079a68449b85d071d7db0ac067f9f7faedbcd6231456d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"module-deps@npm:^4.0.2":
|
||||
version: 4.1.1
|
||||
resolution: "module-deps@npm:4.1.1"
|
||||
|
@ -52827,17 +52803,6 @@ fsevents@~2.1.1:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pem-jwk@npm:1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "pem-jwk@npm:1.5.1"
|
||||
dependencies:
|
||||
asn1.js: 1.0.3
|
||||
bin:
|
||||
pem-jwk: ./bin/pem-jwk.js
|
||||
checksum: 8a7a0192d5935c56f1a13b7c4c432bef5e9d609b9160a278e25678412ca6649ea8636274ec4d4269717f420b22076a79ce78a8a0d6ab1779b7d28913e2d15b05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pend@npm:~1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "pend@npm:1.2.0"
|
||||
|
|
Загрузка…
Ссылка в новой задаче