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:
Ben Bangert 2024-05-03 18:11:04 -07:00
Родитель 477c091d36
Коммит 9a649c6441
Не найден ключ, соответствующий данной подписи
38 изменённых файлов: 3363 добавлений и 3113 удалений

Просмотреть файл

@ -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"
]
}

Просмотреть файл

@ -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",

224
types/fxa-jwtool/index.d.ts поставляемый
Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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"