From 825a95b03298af6d0d33abf2c547a87753d974d5 Mon Sep 17 00:00:00 2001 From: kg0r0 Date: Mon, 13 Apr 2020 08:52:29 +0900 Subject: [PATCH 1/4] feat: add verification for multiple audiences --- src/JWT.ts | 29 ++++++++++++++++++-- src/__tests__/JWT-test.ts | 13 +++++++++ src/__tests__/__snapshots__/JWT-test.ts.snap | 11 ++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/JWT.ts b/src/JWT.ts index 9a817400..8ee56621 100644 --- a/src/JWT.ts +++ b/src/JWT.ts @@ -48,7 +48,7 @@ interface JWTHeader { interface JWTPayload { iss?: string sub?: string - aud?: string + aud?: string | string[] iat?: number nbf?: number type?: string @@ -109,6 +109,16 @@ function isDIDOrMNID(mnidOrDid: string): RegExpMatchArray { return mnidOrDid && (mnidOrDid.match(/^did:/) || isMNID(mnidOrDid)) } +function isDIDOrMNIDArray(mnidOrDidArray: string[]): boolean { + let result:boolean = false + mnidOrDidArray.forEach(mnidOrDid => { + if (mnidOrDid && (mnidOrDid.match(/^did:/) || isMNID(mnidOrDid))) { + result = true + } + }) + return result; +} + export function normalizeDID(mnidOrDid: string): string { if (mnidOrDid.match(/^did:/)) return mnidOrDid // Backwards compatibility @@ -249,7 +259,22 @@ export async function verifyJWT( throw new Error(`JWT has expired: exp: ${payload.exp} < now: ${now}`) } if (payload.aud) { - if (isDIDOrMNID(payload.aud)) { + if (Array.isArray(payload.aud) && isDIDOrMNIDArray(payload.aud)) { + if (!aud) { + throw new Error( + 'JWT audience is required but your app address has not been configured' + ) + } + let result: boolean = false + payload.aud.forEach(audience => { + if (aud === normalizeDID(audience)) { + result = true + } + }); + if (!result) throw new Error( + `JWT audience does not match your DID: yours: ${aud}` + ) + } else if (typeof payload.aud === 'string' && isDIDOrMNID(payload.aud)) { if (!aud) { throw new Error( 'JWT audience is required but your app address has not been configured' diff --git a/src/__tests__/JWT-test.ts b/src/__tests__/JWT-test.ts index 3d01a5d8..4e2c1ca3 100644 --- a/src/__tests__/JWT-test.ts +++ b/src/__tests__/JWT-test.ts @@ -336,6 +336,19 @@ describe('verifyJWT()', () => { return expect(payload).toMatchSnapshot() }) + it('accepts multiple audiences', async () => { + const jwt = await createJWT({ aud: [did, aud] }, { issuer: did, signer }) + const { payload } = await verifyJWT(jwt, { resolver, audience: aud }) + return expect(payload).toMatchSnapshot() + }) + + it('rejects invalid multiple audiences', async () => { + const jwt = await createJWT({ aud: [did, did] }, { issuer: did, signer }) + expect(verifyJWT(jwt, { resolver, audience: aud })) + .rejects + .toThrow(/JWT audience does not match your DID/) + }) + it('accepts a valid audience using callback_url', async () => { const jwt = await createJWT( { aud: 'http://pututu.uport.me/unique' }, diff --git a/src/__tests__/__snapshots__/JWT-test.ts.snap b/src/__tests__/__snapshots__/JWT-test.ts.snap index a272b37a..21694266 100644 --- a/src/__tests__/__snapshots__/JWT-test.ts.snap +++ b/src/__tests__/__snapshots__/JWT-test.ts.snap @@ -73,6 +73,17 @@ Object { } `; +exports[`verifyJWT() accepts multiple audiences 1`] = ` +Object { + "aud": Array [ + "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74", + "did:ethr:0x20c769ec9c0996ba7737a4826c2aaff00b1b2040", + ], + "iat": 1485321133, + "iss": "did:ethr:0xf3beac30c498d9e26865f34fcaa57dbb935b0d74", +} +`; + exports[`verifyJWT() handles ES256K algorithm with ethereum address - github #14 1`] = ` Object { "hello": "world", From a624ce79bd5dee1163aaf4f8d9d0bf244a318bf4 Mon Sep 17 00:00:00 2001 From: Joel Torstensson Date: Fri, 24 Apr 2020 20:00:04 +0200 Subject: [PATCH 2/4] feat: add methods for creating and verifying JWS --- package.json | 2 +- src/JWT.ts | 63 ++++++++++++++++++++++++++++++--------- src/__tests__/JWT-test.ts | 2 +- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 6ac11473..1b99ab50 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "esm" ], "scripts": { - "test": "./node_modules/.bin/standard && jest --updateSnapshot", + "test": "./node_modules/.bin/standard && jest", "build:js": "./node_modules/.bin/microbundle", "build:browser": "./node_modules/.bin/webpack --config webpack.config.js", "build": "npm run build:js && npm test && npm run build:browser", diff --git a/src/JWT.ts b/src/JWT.ts index 9a817400..a5beab28 100644 --- a/src/JWT.ts +++ b/src/JWT.ts @@ -141,6 +141,30 @@ export function decodeJWT(jwt: string): JWTDecoded { throw new Error('Incorrect format JWT') } +/** + * Creates a signed JWS given a payload, a signer, and an optional header. + * + * @example + * const signer = SimpleSigner(process.env.PRIVATE_KEY) + * const jws = await createJWS({ my: 'payload' }, signer) + * + * @param {Object} payload payload object + * @param {SimpleSigner} signer a signer, reference our SimpleSigner.js + * @param {Object} header optional object to specify or customize the JWS header + * @return {Promise} a promise which resolves with a JWS string or rejects with an error + */ +export async function createJWS (payload: any, signer: Signer, header: Partial = {}): Promise { + if (!header.alg) header.alg = defaultAlg + const signingInput: string = [ + encodeSection(header), + encodeSection(payload) + ].join('.') + + const jwtSigner: SignerAlgorithm = SignerAlgorithm(header.alg) + const signature: string = await jwtSigner(signingInput, signer) + return [signingInput, signature].join('.') +} + /** * Creates a signed JWT given an address which becomes the issuer, a signer, and a payload for which the signature is over. * @@ -159,7 +183,6 @@ export function decodeJWT(jwt: string): JWTDecoded { * @param {Object} header optional object to specify or customize the JWT header * @return {Promise} a promise which resolves with a signed JSON Web Token or rejects with an error */ -// export async function createJWT(payload, { issuer, signer, alg, expiresIn }, header) { export async function createJWT( payload: any, { issuer, signer, alg, expiresIn }: JWTOptions, @@ -168,7 +191,7 @@ export async function createJWT( if (!signer) throw new Error('No Signer functionality has been configured') if (!issuer) throw new Error('No issuing DID has been configured') if (!header.typ) header.typ = 'JWT' - if (!header.alg) header.alg = alg || defaultAlg + if (!header.alg) header.alg = alg const timestamps: Partial = { iat: Math.floor(Date.now() / 1000), exp: undefined @@ -180,14 +203,30 @@ export async function createJWT( throw new Error('JWT expiresIn is not a number') } } - const signingInput: string = [ - encodeSection(header), - encodeSection({ ...timestamps, ...payload, iss: issuer }) - ].join('.') + const fullPayload = { ...timestamps, ...payload, iss: issuer } + return createJWS(fullPayload, signer, header) +} - const jwtSigner: SignerAlgorithm = SignerAlgorithm(header.alg) - const signature: string = await jwtSigner(signingInput, signer) - return [signingInput, signature].join('.') +/** + * Verifies given JWS. If the JWS is valid, returns the public key that was + * used to sign the JWS, or throws an `Error` if none of the `pubkeys` match. + * + * @example + * const pubkey = verifyJWT('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....', { publicKeyHex: '0x12341...' }) + * + * @param {String} jws A JWS string to verify + * @param {Array | PublicKey} pubkeys The public keys used to verify the JWS + * @return {PublicKey} The public key used to sign the JWS + */ +export function verifyJWS (jws: string, pubkeys: PublicKey | PublicKey[]): PublicKey { + if (!Array.isArray(pubkeys)) pubkeys = [pubkeys] + const { header, data, signature }: JWTDecoded = decodeJWT(jws) + const signer: PublicKey = VerifierAlgorithm(header.alg)( + data, + signature, + pubkeys + ) + return signer } /** @@ -230,11 +269,7 @@ export async function verifyJWT( payload.iss, options.auth ) - const signer: PublicKey = VerifierAlgorithm(header.alg)( - data, - signature, - authenticators - ) + const signer: PublicKey = await verifyJWS(jwt, authenticators) const now: number = Math.floor(Date.now() / 1000) if (signer) { const nowSkewed = now + NBF_SKEW diff --git a/src/__tests__/JWT-test.ts b/src/__tests__/JWT-test.ts index 3d01a5d8..af31197c 100644 --- a/src/__tests__/JWT-test.ts +++ b/src/__tests__/JWT-test.ts @@ -235,7 +235,7 @@ describe('verifyJWT()', () => { // tslint:disable-next-line: max-line-length const badJwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsImlzcyI6ImRpZDpldGhyOjB4MjBjNzY5ZWM5YzA5OTZiYTc3MzdhNDgyNmMyYWFmZjAwYjFiMjA0MCIsInJlcXVlc3RlZCI6WyJuYW1lIiwicGhvbmUiXX0.TTpuw77fUbd_AY3GJcCumd6F6hxnkskMDJYNpJlI2DQi5MKKudXya9NlyM9e8-KFgTLe-WnXgq9EjWLvjpdiXA' it('rejects a JWT with bad signature', async () => { - expect(verifyJWT(badJwt, { resolver })).rejects.toThrowError( + await expect(verifyJWT(badJwt, { resolver })).rejects.toThrowError( /Signature invalid for JWT/ ) }) From 1ebedeb4ddc8cb9f98f308ef6e088fadf6423c79 Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Sun, 26 Apr 2020 20:42:55 +0200 Subject: [PATCH 3/4] refactor: simplify audience processing --- src/JWT.ts | 81 +++++---------------------------------- src/__tests__/JWT-test.ts | 21 ++-------- 2 files changed, 12 insertions(+), 90 deletions(-) diff --git a/src/JWT.ts b/src/JWT.ts index ebfc3dd3..0111f996 100644 --- a/src/JWT.ts +++ b/src/JWT.ts @@ -99,33 +99,6 @@ export const NBF_SKEW: number = 300 /** @module did-jwt/JWT */ -function isMNID(id: string): RegExpMatchArray { - return id.match( - /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/ - ) -} - -function isDIDOrMNID(mnidOrDid: string): RegExpMatchArray { - return mnidOrDid && (mnidOrDid.match(/^did:/) || isMNID(mnidOrDid)) -} - -function isDIDOrMNIDArray(mnidOrDidArray: string[]): boolean { - let result:boolean = false - mnidOrDidArray.forEach(mnidOrDid => { - if (mnidOrDid && (mnidOrDid.match(/^did:/) || isMNID(mnidOrDid))) { - result = true - } - }) - return result; -} - -export function normalizeDID(mnidOrDid: string): string { - if (mnidOrDid.match(/^did:/)) return mnidOrDid - // Backwards compatibility - if (isMNID(mnidOrDid)) return `did:uport:${mnidOrDid}` - throw new Error(`Not a valid DID '${mnidOrDid}'`) -} - /** * Decodes a JWT and returns an object representing the payload * @@ -265,9 +238,6 @@ export async function verifyJWT( options: JWTVerifyOptions = { resolver: null, auth: null, audience: null, callbackUrl: null } ): Promise { if (!options.resolver) throw new Error('No DID resolver has been configured') - const aud: string = options.audience - ? normalizeDID(options.audience) - : undefined const { payload, header, signature, data }: JWTDecoded = decodeJWT(jwt) const { doc, @@ -294,48 +264,16 @@ export async function verifyJWT( throw new Error(`JWT has expired: exp: ${payload.exp} < now: ${now}`) } if (payload.aud) { - if (Array.isArray(payload.aud) && isDIDOrMNIDArray(payload.aud)) { - if (!aud) { - throw new Error( - 'JWT audience is required but your app address has not been configured' - ) - } - let result: boolean = false - payload.aud.forEach(audience => { - if (aud === normalizeDID(audience)) { - result = true - } - }); - if (!result) throw new Error( - `JWT audience does not match your DID: yours: ${aud}` + if (!options.audience && !options.callbackUrl) { + throw new Error( + 'JWT audience is required but your app address has not been configured' ) - } else if (typeof payload.aud === 'string' && isDIDOrMNID(payload.aud)) { - if (!aud) { - throw new Error( - 'JWT audience is required but your app address has not been configured' - ) - } + } + const audArray = Array.isArray(payload.aud) ? payload.aud : [payload.aud] + let matchedAudience = audArray.find((item) => options.audience === item || options.callbackUrl === item) - if (aud !== normalizeDID(payload.aud)) { - throw new Error( - `JWT audience does not match your DID: aud: ${ - payload.aud - } !== yours: ${aud}` - ) - } - } else { - if (!options.callbackUrl) { - throw new Error( - "JWT audience matching your callback url is required but one wasn't passed in" - ) - } - if (payload.aud !== options.callbackUrl) { - throw new Error( - `JWT audience does not match the callback url: aud: ${ - payload.aud - } !== url: ${options.callbackUrl}` - ) - } + if (typeof matchedAudience === 'undefined') { + throw new Error(`JWT audience does not match your DID or callback url`) } } return { payload, doc, issuer, signer, jwt } @@ -361,14 +299,13 @@ export async function verifyJWT( export async function resolveAuthenticator( resolver: Resolvable, alg: string, - mnidOrDid: string, + issuer: string, auth?: boolean ): Promise { const types: string[] = SUPPORTED_PUBLIC_KEY_TYPES[alg] if (!types || types.length === 0) { throw new Error(`No supported signature types for algorithm ${alg}`) } - const issuer: string = normalizeDID(mnidOrDid) const doc: DIDDocument = await resolver.resolve(issuer) if (!doc) throw new Error(`Unable to resolve DID document for ${issuer}`) // is there some way to have authenticationKeys be a single type? diff --git a/src/__tests__/JWT-test.ts b/src/__tests__/JWT-test.ts index 9ea15e99..20a51a11 100644 --- a/src/__tests__/JWT-test.ts +++ b/src/__tests__/JWT-test.ts @@ -3,8 +3,7 @@ import { verifyJWT, decodeJWT, resolveAuthenticator, - NBF_SKEW, - normalizeDID + NBF_SKEW } from '../JWT' import { TokenVerifier } from 'jsontokens' import SimpleSigner from '../SimpleSigner' @@ -375,7 +374,7 @@ describe('verifyJWT()', () => { ) expect(verifyJWT(jwt, { resolver, callbackUrl: 'http://pututu.uport.me/unique/1' })) .rejects - .toThrow(/JWT audience does not match the callback url/) + .toThrow(/JWT audience does not match your DID or callback url/) }) it('rejects an invalid audience using callback_url where callback is missing', async () => { @@ -392,7 +391,7 @@ describe('verifyJWT()', () => { const jwt = await createJWT({ aud }, { issuer: did, signer }) expect(verifyJWT(jwt, { resolver })) .rejects - .toThrow('JWT audience is required but your app address has not been configured') + .toThrow(/JWT audience does not match your DID or callback url/) }) }) @@ -576,20 +575,6 @@ describe('resolveAuthenticator()', () => { }) }) - describe('normalizeDID', () => { - it('returns the value if it is already a did', () => { - expect(normalizeDID(did)).toEqual(did) - }) - it('converts an mnid into a did', () => { - expect(normalizeDID('2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX')).toEqual( - 'did:uport:2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX' - ) - }) - it('throws if the value is neither a did nor an mnid', () => { - expect(() => normalizeDID('notadid!')).toThrow() - }) - }) - describe('incorrect format', () => { it('throws if token is not valid JWT format', () => { expect(() => decodeJWT('not a jwt')).toThrow() From d0d88a9282f02d52752cc776eb11a77879b9871d Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Sun, 26 Apr 2020 20:47:29 +0200 Subject: [PATCH 4/4] style: reformat code --- .prettierrc | 2 +- package.json | 3 + src/Digest.ts | 10 +- src/JWT.ts | 79 +++----- src/SignerAlgorithm.ts | 8 +- src/VerifierAlgorithm.ts | 78 +++----- src/__tests__/JWT-test.ts | 241 +++++++++--------------- src/__tests__/NaclSigner-test.ts | 4 +- src/__tests__/SignerAlgorithm-test.ts | 23 ++- src/__tests__/VerifierAlgorithm-test.ts | 18 +- src/index.ts | 10 +- 11 files changed, 189 insertions(+), 287 deletions(-) diff --git a/.prettierrc b/.prettierrc index d30c44dd..b742549a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,7 +2,7 @@ "jsxBracketSameLine": false, "trailingComma": "none", "tabWidth": 2, - "printWidth": 80, + "printWidth": 120, "singleQuote": true, "semi": false } \ No newline at end of file diff --git a/package.json b/package.json index 1b99ab50..615d421a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,10 @@ "build:browser": "./node_modules/.bin/webpack --config webpack.config.js", "build": "npm run build:js && npm test && npm run build:browser", "build:docs": "echo 'PLEASE UPDATE REFERENCE DOCS MANUALLY'", + "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", + "lint": "tslint -p tsconfig.json", "prepublish": "npm run build", + "prepublishOnly": "npm test && npm run lint", "prepare": "npm run build" }, "author": "Pelle Braendgaard ", diff --git a/src/Digest.ts b/src/Digest.ts index 19ca011c..d9db9671 100644 --- a/src/Digest.ts +++ b/src/Digest.ts @@ -2,14 +2,16 @@ import { sha256 as sha256js, Message } from 'js-sha256' import { keccak_256 } from 'js-sha3' // eslint-disable-line import { Buffer } from 'buffer' -export function sha256 (payload: Message): Buffer { +export function sha256(payload: Message): Buffer { return Buffer.from(sha256js.arrayBuffer(payload)) } -export function keccak (data: Message): Buffer { +export function keccak(data: Message): Buffer { return Buffer.from(keccak_256.arrayBuffer(data)) } -export function toEthereumAddress (hexPublicKey: string): string { - return `0x${keccak(Buffer.from(hexPublicKey.slice(2), 'hex')).slice(-20).toString('hex')}` +export function toEthereumAddress(hexPublicKey: string): string { + return `0x${keccak(Buffer.from(hexPublicKey.slice(2), 'hex')) + .slice(-20) + .toString('hex')}` } diff --git a/src/JWT.ts b/src/JWT.ts index 0111f996..7c35f090 100644 --- a/src/JWT.ts +++ b/src/JWT.ts @@ -10,10 +10,7 @@ export interface EcdsaSignature { } export type Signer = (data: string) => Promise -export type SignerAlgorithm = ( - payload: string, - signer: Signer -) => Promise +export type SignerAlgorithm = (payload: string, signer: Signer) => Promise interface JWTOptions { issuer: string @@ -76,16 +73,8 @@ interface PublicKeyTypes { [name: string]: string[] } const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = { - ES256K: [ - 'Secp256k1VerificationKey2018', - 'Secp256k1SignatureVerificationKey2018', - 'EcdsaPublicKeySecp256k1' - ], - 'ES256K-R': [ - 'Secp256k1VerificationKey2018', - 'Secp256k1SignatureVerificationKey2018', - 'EcdsaPublicKeySecp256k1' - ], + ES256K: ['Secp256k1VerificationKey2018', 'Secp256k1SignatureVerificationKey2018', 'EcdsaPublicKeySecp256k1'], + 'ES256K-R': ['Secp256k1VerificationKey2018', 'Secp256k1SignatureVerificationKey2018', 'EcdsaPublicKeySecp256k1'], Ed25519: ['ED25519SignatureVerification'] } @@ -110,9 +99,7 @@ export const NBF_SKEW: number = 300 */ export function decodeJWT(jwt: string): JWTDecoded { if (!jwt) throw new Error('no JWT passed into decodeJWT') - const parts: RegExpMatchArray = jwt.match( - /^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/ - ) + const parts: RegExpMatchArray = jwt.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) if (parts) { return { header: JSON.parse(base64url.decode(parts[1])), @@ -136,12 +123,9 @@ export function decodeJWT(jwt: string): JWTDecoded { * @param {Object} header optional object to specify or customize the JWS header * @return {Promise} a promise which resolves with a JWS string or rejects with an error */ -export async function createJWS (payload: any, signer: Signer, header: Partial = {}): Promise { +export async function createJWS(payload: any, signer: Signer, header: Partial = {}): Promise { if (!header.alg) header.alg = defaultAlg - const signingInput: string = [ - encodeSection(header), - encodeSection(payload) - ].join('.') + const signingInput: string = [encodeSection(header), encodeSection(payload)].join('.') const jwtSigner: SignerAlgorithm = SignerAlgorithm(header.alg) const signature: string = await jwtSigner(signingInput, signer) @@ -169,7 +153,7 @@ export async function createJWS (payload: any, signer: Signer, header: Partial = {}, + header: Partial = {} ): Promise { if (!signer) throw new Error('No Signer functionality has been configured') if (!issuer) throw new Error('No issuing DID has been configured') @@ -201,14 +185,10 @@ export async function createJWT( * @param {Array | PublicKey} pubkeys The public keys used to verify the JWS * @return {PublicKey} The public key used to sign the JWS */ -export function verifyJWS (jws: string, pubkeys: PublicKey | PublicKey[]): PublicKey { +export function verifyJWS(jws: string, pubkeys: PublicKey | PublicKey[]): PublicKey { if (!Array.isArray(pubkeys)) pubkeys = [pubkeys] const { header, data, signature }: JWTDecoded = decodeJWT(jws) - const signer: PublicKey = VerifierAlgorithm(header.alg)( - data, - signature, - pubkeys - ) + const signer: PublicKey = VerifierAlgorithm(header.alg)(data, signature, pubkeys) return signer } @@ -235,15 +215,16 @@ export function verifyJWS (jws: string, pubkeys: PublicKey | PublicKey[]): Publi */ export async function verifyJWT( jwt: string, - options: JWTVerifyOptions = { resolver: null, auth: null, audience: null, callbackUrl: null } + options: JWTVerifyOptions = { + resolver: null, + auth: null, + audience: null, + callbackUrl: null + } ): Promise { if (!options.resolver) throw new Error('No DID resolver has been configured') const { payload, header, signature, data }: JWTDecoded = decodeJWT(jwt) - const { - doc, - authenticators, - issuer - }: DIDAuthenticator = await resolveAuthenticator( + const { doc, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator( options.resolver, header.alg, payload.iss, @@ -265,12 +246,10 @@ export async function verifyJWT( } if (payload.aud) { if (!options.audience && !options.callbackUrl) { - throw new Error( - 'JWT audience is required but your app address has not been configured' - ) + throw new Error('JWT audience is required but your app address has not been configured') } const audArray = Array.isArray(payload.aud) ? payload.aud : [payload.aud] - let matchedAudience = audArray.find((item) => options.audience === item || options.callbackUrl === item) + const matchedAudience = audArray.find(item => options.audience === item || options.callbackUrl === item) if (typeof matchedAudience === 'undefined') { throw new Error(`JWT audience does not match your DID or callback url`) @@ -312,26 +291,18 @@ export async function resolveAuthenticator( const authenticationKeys: boolean | string[] = auth ? (doc.authentication || []).map(({ publicKey }) => publicKey) : true - const authenticators: PublicKey[] = (doc.publicKey || []).filter( - ({ type, id }) => - types.find( - supported => - supported === type && - (!auth || - (Array.isArray(authenticationKeys) && - authenticationKeys.indexOf(id) >= 0)) - ) + const authenticators: PublicKey[] = (doc.publicKey || []).filter(({ type, id }) => + types.find( + supported => + supported === type && (!auth || (Array.isArray(authenticationKeys) && authenticationKeys.indexOf(id) >= 0)) + ) ) if (auth && (!authenticators || authenticators.length === 0)) { - throw new Error( - `DID document for ${issuer} does not have public keys suitable for authenticationg user` - ) + throw new Error(`DID document for ${issuer} does not have public keys suitable for authenticationg user`) } if (!authenticators || authenticators.length === 0) { - throw new Error( - `DID document for ${issuer} does not have public keys for ${alg}` - ) + throw new Error(`DID document for ${issuer} does not have public keys for ${alg}`) } return { authenticators, issuer, doc } } diff --git a/src/SignerAlgorithm.ts b/src/SignerAlgorithm.ts index d2389739..24baaaa5 100644 --- a/src/SignerAlgorithm.ts +++ b/src/SignerAlgorithm.ts @@ -25,9 +25,7 @@ export function ES256KSigner(recoverable?: boolean): SignerAlgorithm { if (instanceOfEcdsaSignature(signature)) { return toJose(signature) } else { - throw new Error( - 'expected a signer function that returns a signature object instead of string' - ) + throw new Error('expected a signer function that returns a signature object instead of string') } } } @@ -38,9 +36,7 @@ export function Ed25519Signer(): SignerAlgorithm { if (!instanceOfEcdsaSignature(signature)) { return signature } else { - throw new Error( - 'expected a signer function that returns a string instead of signature object' - ) + throw new Error('expected a signer function that returns a string instead of signature object') } } } diff --git a/src/VerifierAlgorithm.ts b/src/VerifierAlgorithm.ts index a07f0ec2..8d01f37b 100644 --- a/src/VerifierAlgorithm.ts +++ b/src/VerifierAlgorithm.ts @@ -10,10 +10,7 @@ import { base64ToBytes } from './util' const secp256k1 = new EC('secp256k1') // converts a JOSE signature to it's components -export function toSignatureObject( - signature: string, - recoverable = false -): EcdsaSignature { +export function toSignatureObject(signature: string, recoverable = false): EcdsaSignature { const rawsig: Buffer = base64url.toBuffer(signature) if (rawsig.length !== (recoverable ? 65 : 64)) { throw new Error('wrong signature length') @@ -27,24 +24,23 @@ export function toSignatureObject( return sigObj } -export function verifyES256K( - data: string, - signature: string, - authenticators: PublicKey[] -): PublicKey { +export function verifyES256K(data: string, signature: string, authenticators: PublicKey[]): PublicKey { const hash: Buffer = sha256(data) const sigObj: EcdsaSignature = toSignatureObject(signature) - const fullPublicKeys = authenticators.filter(({ publicKeyHex }) => { return typeof publicKeyHex !== 'undefined' }) - const ethAddressKeys = authenticators.filter(({ ethereumAddress }) => { return typeof ethereumAddress !== 'undefined' }) + const fullPublicKeys = authenticators.filter(({ publicKeyHex }) => { + return typeof publicKeyHex !== 'undefined' + }) + const ethAddressKeys = authenticators.filter(({ ethereumAddress }) => { + return typeof ethereumAddress !== 'undefined' + }) let signer: PublicKey = fullPublicKeys.find(({ publicKeyHex }) => { - try { - return secp256k1.keyFromPublic(publicKeyHex, 'hex').verify(hash, sigObj) - } catch (err) { - return false - } + try { + return secp256k1.keyFromPublic(publicKeyHex, 'hex').verify(hash, sigObj) + } catch (err) { + return false } - ) + }) if (!signer && ethAddressKeys.length > 0) { signer = verifyRecoverableES256K(data, signature, ethAddressKeys) @@ -54,32 +50,20 @@ export function verifyES256K( return signer } -export function verifyRecoverableES256K( - data: string, - signature: string, - authenticators: PublicKey[] -): PublicKey { - +export function verifyRecoverableES256K(data: string, signature: string, authenticators: PublicKey[]): PublicKey { let signatures: EcdsaSignature[] if (signature.length > 86) { - signatures = [ toSignatureObject(signature, true) ] + signatures = [toSignatureObject(signature, true)] } else { const so = toSignatureObject(signature, false) - signatures = [ - {...so, recoveryParam: 0}, - {...so, recoveryParam: 1} - ] + signatures = [{ ...so, recoveryParam: 0 }, { ...so, recoveryParam: 1 }] } - const checkSignatureAgainstSigner = (sigObj: EcdsaSignature) : PublicKey => { + const checkSignatureAgainstSigner = (sigObj: EcdsaSignature): PublicKey => { const hash: Buffer = sha256(data) - const recoveredKey: any = secp256k1.recoverPubKey( - hash, - sigObj, - sigObj.recoveryParam - ) + const recoveredKey: any = secp256k1.recoverPubKey(hash, sigObj, sigObj.recoveryParam) const recoveredPublicKeyHex: string = recoveredKey.encode('hex') - const recoveredCompressedPublicKeyHex: string = recoveredKey.encode( 'hex', true ) + const recoveredCompressedPublicKeyHex: string = recoveredKey.encode('hex', true) const recoveredAddress: string = toEthereumAddress(recoveredPublicKeyHex) const signer: PublicKey = authenticators.find( @@ -92,37 +76,23 @@ export function verifyRecoverableES256K( return signer } - const signer: PublicKey[] = signatures - .map(checkSignatureAgainstSigner) - .filter( key => key != null ) + const signer: PublicKey[] = signatures.map(checkSignatureAgainstSigner).filter(key => key != null) - if (signer.length == 0) throw new Error('Signature invalid for JWT') + if (signer.length === 0) throw new Error('Signature invalid for JWT') return signer[0] } -export function verifyEd25519( - data: string, - signature: string, - authenticators: PublicKey[] -): PublicKey { +export function verifyEd25519(data: string, signature: string, authenticators: PublicKey[]): PublicKey { const clear: Uint8Array = encode(data) const sig: Uint8Array = base64ToBytes(base64url.toBase64(signature)) const signer: PublicKey = authenticators.find(({ publicKeyBase64 }) => - nacl.sign.detached.verify( - clear, - sig, - base64ToBytes(publicKeyBase64) - ) + nacl.sign.detached.verify(clear, sig, base64ToBytes(publicKeyBase64)) ) if (!signer) throw new Error('Signature invalid for JWT') return signer } -type Verifier = ( - data: string, - signature: string, - authenticators: PublicKey[] -) => PublicKey +type Verifier = (data: string, signature: string, authenticators: PublicKey[]) => PublicKey interface Algorithms { [name: string]: Verifier } diff --git a/src/__tests__/JWT-test.ts b/src/__tests__/JWT-test.ts index 20a51a11..d9d4b73d 100644 --- a/src/__tests__/JWT-test.ts +++ b/src/__tests__/JWT-test.ts @@ -1,10 +1,4 @@ -import { - createJWT, - verifyJWT, - decodeJWT, - resolveAuthenticator, - NBF_SKEW -} from '../JWT' +import { createJWT, verifyJWT, decodeJWT, resolveAuthenticator, NBF_SKEW } from '../JWT' import { TokenVerifier } from 'jsontokens' import SimpleSigner from '../SimpleSigner' import NaclSigner from '../NaclSigner' @@ -20,10 +14,8 @@ const address = '0xf3beac30c498d9e26865f34fcaa57dbb935b0d74' const did = `did:ethr:${address}` const alg = 'ES256K' -const privateKey = - '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f' -const publicKey = - '03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479' +const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f' +const publicKey = '03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479' const verifier = new TokenVerifier(alg, publicKey) const signer = SimpleSigner(privateKey) @@ -68,34 +60,22 @@ const didDocDefault = { describe('createJWT()', () => { describe('ES256K', () => { it('creates a valid JWT', async () => { - const jwt = await createJWT( - { requested: ['name', 'phone'] }, - { issuer: did, signer } - ) + const jwt = await createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer }) return expect(verifier.verify(jwt)).toBeTruthy() }) it('creates a valid JWT using a MNID', async () => { - const jwt = await createJWT( - { requested: ['name', 'phone'] }, - { issuer: address, signer } - ) + const jwt = await createJWT({ requested: ['name', 'phone'] }, { issuer: address, signer }) return expect(verifier.verify(jwt)).toBeTruthy() }) it('creates a JWT with correct format', async () => { - const jwt = await createJWT( - { requested: ['name', 'phone'] }, - { issuer: did, signer } - ) + const jwt = await createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer }) return expect(decodeJWT(jwt)).toMatchSnapshot() }) it('creates a JWT with correct legacy format', async () => { - const jwt = await createJWT( - { requested: ['name', 'phone'] }, - { issuer: address, signer } - ) + const jwt = await createJWT({ requested: ['name', 'phone'] }, { issuer: address, signer }) return expect(decodeJWT(jwt)).toMatchSnapshot() }) @@ -113,76 +93,52 @@ describe('createJWT()', () => { it('Uses iat if nbf is not defined but expiresIn is included', async () => { const { payload } = decodeJWT( - await createJWT( - { requested: ['name', 'phone'] }, - { issuer: did, signer, expiresIn: 10000 } - ) + await createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer, expiresIn: 10000 }) ) return expect(payload.exp).toEqual(payload.iat + 10000) }) it('sets iat to the current time by default', async () => { const timestamp = Math.floor(Date.now() / 1000) - const { payload } = decodeJWT( - await createJWT( - { requested: ['name', 'phone'] }, - { issuer: did, signer } - ) - ) + const { payload } = decodeJWT(await createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer })) return expect(payload.iat).toEqual(timestamp) }) it('sets iat to the value passed in payload', async () => { const timestamp = 2000000 const { payload } = decodeJWT( - await createJWT( - { requested: ['name', 'phone'], iat: timestamp }, - { issuer: did, signer } - ) + await createJWT({ requested: ['name', 'phone'], iat: timestamp }, { issuer: did, signer }) ) return expect(payload.iat).toEqual(timestamp) }) it('does not set iat if value in payload is undefined', async () => { const { payload } = decodeJWT( - await createJWT( - { requested: ['name', 'phone'], iat: undefined }, - { issuer: did, signer } - ) + await createJWT({ requested: ['name', 'phone'], iat: undefined }, { issuer: did, signer }) ) return expect(payload.iat).toBeUndefined() }) it('throws an error if unsupported algorithm is passed in', async () => { - expect( - createJWT( - { requested: ['name', 'phone'] }, - { issuer: did, signer, alg: 'BADALGO' } - ) - ).rejects.toThrow('Unsupported algorithm BADALGO') + expect(createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer, alg: 'BADALGO' })).rejects.toThrow( + 'Unsupported algorithm BADALGO' + ) }) }) describe('Ed25519', () => { - const ed25519PrivateKey = - 'nlXR4aofRVuLqtn9+XVQNlX4s1nVQvp+TOhBBtYls1IG+sHyIkDP/WN+rWZHGIQp+v2pyct+rkM4asF/YRFQdQ==' + const ed25519PrivateKey = 'nlXR4aofRVuLqtn9+XVQNlX4s1nVQvp+TOhBBtYls1IG+sHyIkDP/WN+rWZHGIQp+v2pyct+rkM4asF/YRFQdQ==' const did = 'did:nacl:BvrB8iJAz_1jfq1mRxiEKfr9qcnLfq5DOGrBf2ERUHU' const signer = NaclSigner(ed25519PrivateKey) const alg = 'Ed25519' it('creates a valid JWT', async () => { - const jwt = await createJWT( - { requested: ['name', 'phone'] }, - { alg, issuer: did, signer } - ) + const jwt = await createJWT({ requested: ['name', 'phone'] }, { alg, issuer: did, signer }) return expect(naclVerifyJWT(jwt)).toBeTruthy() }) it('creates a JWT with correct format', async () => { - const jwt = await createJWT( - { requested: ['name', 'phone'] }, - { alg, issuer: did, signer } - ) + const jwt = await createJWT({ requested: ['name', 'phone'] }, { alg, issuer: did, signer }) return expect(decodeJWT(jwt)).toMatchSnapshot() }) @@ -205,7 +161,8 @@ describe('verifyJWT()', () => { describe('pregenerated JWT', () => { // tslint:disable-next-line: max-line-length - const incomingJwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsImlzcyI6ImRpZDpldGhyOjB4OTBlNDVkNzViZDEyNDZlMDkyNDg3MjAxODY0N2RiYTk5NmE4ZTdiOSIsInJlcXVlc3RlZCI6WyJuYW1lIiwicGhvbmUiXX0.KIG2zUO8Quf3ucb9jIncZ1CmH0v-fAZlsKvesfsd9x4RzU0qrvinVd9d30DOeZOwdwEdXkET_wuPoOECwU0IKA' + const incomingJwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsImlzcyI6ImRpZDpldGhyOjB4OTBlNDVkNzViZDEyNDZlMDkyNDg3MjAxODY0N2RiYTk5NmE4ZTdiOSIsInJlcXVlc3RlZCI6WyJuYW1lIiwicGhvbmUiXX0.KIG2zUO8Quf3ucb9jIncZ1CmH0v-fAZlsKvesfsd9x4RzU0qrvinVd9d30DOeZOwdwEdXkET_wuPoOECwU0IKA' it('verifies the JWT and return correct payload', async () => { const { payload } = await verifyJWT(incomingJwt, { resolver }) return expect(payload).toMatchSnapshot() @@ -216,9 +173,7 @@ describe('verifyJWT()', () => { }) it('verifies the JWT and return correct did for the iss', async () => { const { issuer } = await verifyJWT(incomingJwt, { resolver }) - return expect(issuer).toEqual( - 'did:ethr:0x90e45d75bd1246e0924872018647dba996a8e7b9' - ) + return expect(issuer).toEqual('did:ethr:0x90e45d75bd1246e0924872018647dba996a8e7b9') }) it('verifies the JWT and return correct signer', async () => { const { signer } = await verifyJWT(incomingJwt, { resolver }) @@ -232,83 +187,80 @@ describe('verifyJWT()', () => { describe('badJwt', () => { // tslint:disable-next-line: max-line-length - const badJwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsImlzcyI6ImRpZDpldGhyOjB4MjBjNzY5ZWM5YzA5OTZiYTc3MzdhNDgyNmMyYWFmZjAwYjFiMjA0MCIsInJlcXVlc3RlZCI6WyJuYW1lIiwicGhvbmUiXX0.TTpuw77fUbd_AY3GJcCumd6F6hxnkskMDJYNpJlI2DQi5MKKudXya9NlyM9e8-KFgTLe-WnXgq9EjWLvjpdiXA' + const badJwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsImlzcyI6ImRpZDpldGhyOjB4MjBjNzY5ZWM5YzA5OTZiYTc3MzdhNDgyNmMyYWFmZjAwYjFiMjA0MCIsInJlcXVlc3RlZCI6WyJuYW1lIiwicGhvbmUiXX0.TTpuw77fUbd_AY3GJcCumd6F6hxnkskMDJYNpJlI2DQi5MKKudXya9NlyM9e8-KFgTLe-WnXgq9EjWLvjpdiXA' it('rejects a JWT with bad signature', async () => { - await expect(verifyJWT(badJwt, { resolver })).rejects.toThrowError( - /Signature invalid for JWT/ - ) + await expect(verifyJWT(badJwt, { resolver })).rejects.toThrowError(/Signature invalid for JWT/) }) }) describe('validFrom timestamp', () => { it('passes when nbf is in the past', async () => { // tslint:disable-next-line: max-line-length - const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsIm5iZiI6MTQ4NTI2MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.FUasGkOYqGVxQ7S-QQvh4abGO6Dwr961UjjOxtRTyUDnl6q6ElqHqAK-WMDTmOir21pFPKLYZMtLZ4LTLpm3cQ' + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsIm5iZiI6MTQ4NTI2MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.FUasGkOYqGVxQ7S-QQvh4abGO6Dwr961UjjOxtRTyUDnl6q6ElqHqAK-WMDTmOir21pFPKLYZMtLZ4LTLpm3cQ' // const jwt = await createJWT({nbf: PAST}, {issuer:did, signer}) expect(verifyJWT(jwt, { resolver })).resolves.not.toThrow() }) it('passes when nbf is in the past and iat is in the future', async () => { // tslint:disable-next-line: max-line-length - const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzODExMzMsIm5iZiI6MTQ4NTI2MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.8BPiSG2e6UBn1osnJ6PJYbPjtPMPaCeutTA9OCp-ZzI-QvvwPCVrrWqTu2YELbzUPwDIJCQ8v8N77xCEjIYSmQ' + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzODExMzMsIm5iZiI6MTQ4NTI2MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.8BPiSG2e6UBn1osnJ6PJYbPjtPMPaCeutTA9OCp-ZzI-QvvwPCVrrWqTu2YELbzUPwDIJCQ8v8N77xCEjIYSmQ' // const jwt = await createJWT({nbf:PAST,iat:FUTURE},{issuer:did,signer}) expect(verifyJWT(jwt, { resolver })).resolves.not.toThrow() }) it('fails when nbf is in the future', async () => { // tslint:disable-next-line: max-line-length - const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsIm5iZiI6MTQ4NTM4MTEzMywiaXNzIjoiZGlkOnVwb3J0OjJuUXRpUUc2Q2dtMUdZVEJhYUtBZ3I3NnVZN2lTZXhVa3FYIn0.rcFuhVHtie3Y09pWxBSf1dnjaVh6FFQLHh-83N-uLty3M5ADJ-jVFFkyt_Eupl8Kr735-oPGn_D1Nj9rl4s_Kw' + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzMjExMzMsIm5iZiI6MTQ4NTM4MTEzMywiaXNzIjoiZGlkOnVwb3J0OjJuUXRpUUc2Q2dtMUdZVEJhYUtBZ3I3NnVZN2lTZXhVa3FYIn0.rcFuhVHtie3Y09pWxBSf1dnjaVh6FFQLHh-83N-uLty3M5ADJ-jVFFkyt_Eupl8Kr735-oPGn_D1Nj9rl4s_Kw' // const jwt = await createJWT({nbf:FUTURE},{issuer:did,signer}) expect(verifyJWT(jwt, { resolver })).rejects.toThrow() }) it('fails when nbf is in the future and iat is in the past', async () => { // tslint:disable-next-line: max-line-length - const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUyNjExMzMsIm5iZiI6MTQ4NTM4MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.JjEn_huxI9SsBY_3PlD0ShpXvrRgUGFDKAgxJBc1Q5GToVpUTw007-o9BTt7JNi_G2XWmcu2aXXnDn0QFsRIrg' + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUyNjExMzMsIm5iZiI6MTQ4NTM4MTEzMywiaXNzIjoiZGlkOmV0aHI6MHhmM2JlYWMzMGM0OThkOWUyNjg2NWYzNGZjYWE1N2RiYjkzNWIwZDc0In0.JjEn_huxI9SsBY_3PlD0ShpXvrRgUGFDKAgxJBc1Q5GToVpUTw007-o9BTt7JNi_G2XWmcu2aXXnDn0QFsRIrg' // const jwt = await createJWT({nbf:FUTURE,iat:PAST},{issuer:did,signer}) expect(verifyJWT(jwt, { resolver })).rejects.toThrow() }) it('passes when nbf is missing and iat is in the past', async () => { // tslint:disable-next-line: max-line-length - const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUyNjExMzMsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9.jkzN5kIVtuRU-Fjte8w5r-ttf9OfhdN38oFJd61CWdI5WnvU1dPCvnx1_kdk2D6Xg-uPqp1VXAb7KA2ZECivmg' + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUyNjExMzMsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9.jkzN5kIVtuRU-Fjte8w5r-ttf9OfhdN38oFJd61CWdI5WnvU1dPCvnx1_kdk2D6Xg-uPqp1VXAb7KA2ZECivmg' // const jwt = await createJWT({iat:PAST},{issuer:did,signer}) expect(verifyJWT(jwt, { resolver })).resolves.not.toThrow() }) it('fails when nbf is missing and iat is in the future', async () => { // tslint:disable-next-line: max-line-length - const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzODExMzMsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9.FJuHvf9Tby7b4I54Cm1nh8CvLg4QH2wt2K0WfyQaLqlr3NKKI5hAdLalgZksI25gLhNrZwQFnC-nzEOs9PI1SQ' + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE0ODUzODExMzMsImlzcyI6ImRpZDpldGhyOjB4ZjNiZWFjMzBjNDk4ZDllMjY4NjVmMzRmY2FhNTdkYmI5MzViMGQ3NCJ9.FJuHvf9Tby7b4I54Cm1nh8CvLg4QH2wt2K0WfyQaLqlr3NKKI5hAdLalgZksI25gLhNrZwQFnC-nzEOs9PI1SQ' // const jwt = await createJWT({iat:FUTURE},{issuer:did,signer}) expect(verifyJWT(jwt, { resolver })).rejects.toThrow() }) it('passes when nbf and iat are both missing', async () => { // tslint:disable-next-line: max-line-length - const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6ZXRocjoweGYzYmVhYzMwYzQ5OGQ5ZTI2ODY1ZjM0ZmNhYTU3ZGJiOTM1YjBkNzQifQ.KgnwgMMz-QSOtpba2QMGHMWJoLvhp-H4odjjX1QKnqj4-8dkcK12y7rj7Zq24-1d-1ne86aJCdWtx5VJv3rM7w' + const jwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6ZXRocjoweGYzYmVhYzMwYzQ5OGQ5ZTI2ODY1ZjM0ZmNhYTU3ZGJiOTM1YjBkNzQifQ.KgnwgMMz-QSOtpba2QMGHMWJoLvhp-H4odjjX1QKnqj4-8dkcK12y7rj7Zq24-1d-1ne86aJCdWtx5VJv3rM7w' // const jwt = await createJWT({iat:undefined},{issuer:did,signer}) expect(verifyJWT(jwt, { resolver })).resolves.not.toThrow() }) }) it('handles ES256K-R algorithm', async () => { - const jwt = await createJWT( - { hello: 'world' }, - { issuer: did, signer, alg: 'ES256K-R' } - ) + const jwt = await createJWT({ hello: 'world' }, { issuer: did, signer, alg: 'ES256K-R' }) const { payload } = await verifyJWT(jwt, { resolver }) return expect(payload).toMatchSnapshot() }) it('handles ES256K-R algorithm with ethereum address', async () => { - const jwt = await createJWT( - { hello: 'world' }, - { issuer: aud, signer, alg: 'ES256K-R' } - ) + const jwt = await createJWT({ hello: 'world' }, { issuer: aud, signer, alg: 'ES256K-R' }) const { payload } = await verifyJWT(jwt, { resolver }) return expect(payload).toMatchSnapshot() }) it('handles ES256K algorithm with ethereum address - github #14', async () => { const ethResolver = { resolve: jest.fn().mockReturnValue(didDocDefault) } - const jwt = await createJWT( - { hello: 'world' }, - { issuer: aud, signer, alg: 'ES256K' } - ) + const jwt = await createJWT({ hello: 'world' }, { issuer: aud, signer, alg: 'ES256K' }) const { payload } = await verifyJWT(jwt, { resolver: ethResolver }) return expect(payload).toMatchSnapshot() }) @@ -320,13 +272,8 @@ describe('verifyJWT()', () => { }) it('rejects an expired JWT', async () => { - const jwt = await createJWT( - { exp: NOW - NBF_SKEW - 1 }, - { issuer: did, signer } - ) - expect(verifyJWT(jwt, { resolver })) - .rejects - .toThrow(/JWT has expired/) + const jwt = await createJWT({ exp: NOW - NBF_SKEW - 1 }, { issuer: did, signer }) + expect(verifyJWT(jwt, { resolver })).rejects.toThrow(/JWT has expired/) }) it('accepts a valid audience', async () => { @@ -343,16 +290,11 @@ describe('verifyJWT()', () => { it('rejects invalid multiple audiences', async () => { const jwt = await createJWT({ aud: [did, did] }, { issuer: did, signer }) - expect(verifyJWT(jwt, { resolver, audience: aud })) - .rejects - .toThrow(/JWT audience does not match your DID/) + expect(verifyJWT(jwt, { resolver, audience: aud })).rejects.toThrow(/JWT audience does not match your DID/) }) it('accepts a valid audience using callback_url', async () => { - const jwt = await createJWT( - { aud: 'http://pututu.uport.me/unique' }, - { issuer: did, signer } - ) + const jwt = await createJWT({ aud: 'http://pututu.uport.me/unique' }, { issuer: did, signer }) const { payload } = await verifyJWT(jwt, { resolver, callbackUrl: 'http://pututu.uport.me/unique' @@ -362,36 +304,29 @@ describe('verifyJWT()', () => { it('rejects invalid audience', async () => { const jwt = await createJWT({ aud }, { issuer: did, signer }) - expect(verifyJWT(jwt, { resolver, audience: did })) - .rejects - .toThrow(/JWT audience does not match your DID/) + expect(verifyJWT(jwt, { resolver, audience: did })).rejects.toThrow(/JWT audience does not match your DID/) }) it('rejects an invalid audience using callback_url where callback is wrong', async () => { - const jwt = await createJWT( - { aud: 'http://pututu.uport.me/unique' }, - { issuer: did, signer } - ) - expect(verifyJWT(jwt, { resolver, callbackUrl: 'http://pututu.uport.me/unique/1' })) - .rejects - .toThrow(/JWT audience does not match your DID or callback url/) + const jwt = await createJWT({ aud: 'http://pututu.uport.me/unique' }, { issuer: did, signer }) + expect( + verifyJWT(jwt, { + resolver, + callbackUrl: 'http://pututu.uport.me/unique/1' + }) + ).rejects.toThrow(/JWT audience does not match your DID or callback url/) }) it('rejects an invalid audience using callback_url where callback is missing', async () => { - const jwt = await createJWT( - { aud: 'http://pututu.uport.me/unique' }, - { issuer: did, signer } + const jwt = await createJWT({ aud: 'http://pututu.uport.me/unique' }, { issuer: did, signer }) + expect(verifyJWT(jwt, { resolver })).rejects.toThrow( + "JWT audience matching your callback url is required but one wasn't passed in" ) - expect(verifyJWT(jwt, { resolver })) - .rejects - .toThrow('JWT audience matching your callback url is required but one wasn\'t passed in') }) it('rejects invalid audience as no address is present', async () => { const jwt = await createJWT({ aud }, { issuer: did, signer }) - expect(verifyJWT(jwt, { resolver })) - .rejects - .toThrow(/JWT audience does not match your DID or callback url/) + expect(verifyJWT(jwt, { resolver })).rejects.toThrow(/JWT audience does not match your DID or callback url/) }) }) @@ -491,7 +426,11 @@ describe('resolveAuthenticator()', () => { }) it('filters out irrelevant public keys', async () => { - const authenticators = await resolveAuthenticator({ resolve: jest.fn().mockReturnValue(multipleKeys) }, alg, did) + const authenticators = await resolveAuthenticator( + { resolve: jest.fn().mockReturnValue(multipleKeys) }, + alg, + did + ) return expect(authenticators).toEqual({ authenticators: [ecKey1, ecKey2, ecKey3], issuer: did, @@ -500,7 +439,12 @@ describe('resolveAuthenticator()', () => { }) it('only list authenticators able to authenticate a user', async () => { - const authenticators = await resolveAuthenticator({ resolve: jest.fn().mockReturnValue(multipleKeys) }, alg, did, true) + const authenticators = await resolveAuthenticator( + { resolve: jest.fn().mockReturnValue(multipleKeys) }, + alg, + did, + true + ) return expect(authenticators).toEqual({ authenticators: [ecKey1, ecKey2], issuer: did, @@ -509,18 +453,20 @@ describe('resolveAuthenticator()', () => { }) it('errors if no suitable public keys exist', async () => { - return expect(resolveAuthenticator({ resolve: jest.fn().mockReturnValue(unsupportedFormat) }, alg, did)).rejects.toEqual( - new Error( - `DID document for ${did} does not have public keys for ${alg}` - ) - ) + return expect( + resolveAuthenticator({ resolve: jest.fn().mockReturnValue(unsupportedFormat) }, alg, did) + ).rejects.toEqual(new Error(`DID document for ${did} does not have public keys for ${alg}`)) }) }) describe('Ed25519', () => { const alg = 'Ed25519' it('filters out irrelevant public keys', async () => { - const authenticators = await resolveAuthenticator({ resolve: jest.fn().mockReturnValue(multipleKeys) }, alg, did) + const authenticators = await resolveAuthenticator( + { resolve: jest.fn().mockReturnValue(multipleKeys) }, + alg, + did + ) return expect(authenticators).toEqual({ authenticators: [edKey, edKey2], issuer: did, @@ -529,7 +475,12 @@ describe('resolveAuthenticator()', () => { }) it('only list authenticators able to authenticate a user', async () => { - const authenticators = await resolveAuthenticator({ resolve: jest.fn().mockReturnValue(multipleKeys) }, alg, did, true) + const authenticators = await resolveAuthenticator( + { resolve: jest.fn().mockReturnValue(multipleKeys) }, + alg, + did, + true + ) return expect(authenticators).toEqual({ authenticators: [edKey], issuer: did, @@ -538,28 +489,24 @@ describe('resolveAuthenticator()', () => { }) it('errors if no suitable public keys exist', async () => { - return expect(resolveAuthenticator({ resolve: jest.fn().mockReturnValue(unsupportedFormat) }, alg, did)).rejects.toEqual( - new Error( - `DID document for ${did} does not have public keys for ${alg}` - ) - ) + return expect( + resolveAuthenticator({ resolve: jest.fn().mockReturnValue(unsupportedFormat) }, alg, did) + ).rejects.toEqual(new Error(`DID document for ${did} does not have public keys for ${alg}`)) }) }) it('errors if no suitable public keys exist for authentication', async () => { - return expect(resolveAuthenticator({ resolve: jest.fn().mockReturnValue(singleKey) }, alg, did, true)).rejects.toEqual( - new Error( - `DID document for ${did} does not have public keys suitable for authenticationg user` - ) + return expect( + resolveAuthenticator({ resolve: jest.fn().mockReturnValue(singleKey) }, alg, did, true) + ).rejects.toEqual( + new Error(`DID document for ${did} does not have public keys suitable for authenticationg user`) ) }) it('errors if no public keys exist', async () => { - return expect(resolveAuthenticator({ resolve: jest.fn().mockReturnValue(noPublicKey) }, alg, did)).rejects.toEqual( - new Error( - `DID document for ${did} does not have public keys for ${alg}` - ) - ) + return expect( + resolveAuthenticator({ resolve: jest.fn().mockReturnValue(noPublicKey) }, alg, did) + ).rejects.toEqual(new Error(`DID document for ${did} does not have public keys for ${alg}`)) }) it('errors if no DID document exists', async () => { @@ -569,9 +516,9 @@ describe('resolveAuthenticator()', () => { }) it('errors if no supported signature types exist', async () => { - return expect(resolveAuthenticator({ resolve: jest.fn().mockReturnValue(singleKey) }, 'ESBAD', did)).rejects.toEqual( - new Error(`No supported signature types for algorithm ESBAD`) - ) + return expect( + resolveAuthenticator({ resolve: jest.fn().mockReturnValue(singleKey) }, 'ESBAD', did) + ).rejects.toEqual(new Error('No supported signature types for algorithm ESBAD')) }) }) diff --git a/src/__tests__/NaclSigner-test.ts b/src/__tests__/NaclSigner-test.ts index f2aeb3d0..6e158af0 100644 --- a/src/__tests__/NaclSigner-test.ts +++ b/src/__tests__/NaclSigner-test.ts @@ -4,5 +4,7 @@ const privateKey = 'nlXR4aofRVuLqtn9+XVQNlX4s1nVQvp+TOhBBtYls1IG+sHyIkDP/WN+rWZH const signer = NaclSigner(privateKey) it('signs data', async () => { const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer' - return expect(signer(plaintext)).resolves.toEqual('1y_N9v6xI4DyG9vIuloivxm91EV96nDM3HXBUI4P2Owk0IxazqX63rQ5jlBih6tP_4H5QhkHHqbree7ExmTBCw') + return expect(signer(plaintext)).resolves.toEqual( + '1y_N9v6xI4DyG9vIuloivxm91EV96nDM3HXBUI4P2Owk0IxazqX63rQ5jlBih6tP_4H5QhkHHqbree7ExmTBCw' + ) }) diff --git a/src/__tests__/SignerAlgorithm-test.ts b/src/__tests__/SignerAlgorithm-test.ts index 8a803030..763e04f1 100644 --- a/src/__tests__/SignerAlgorithm-test.ts +++ b/src/__tests__/SignerAlgorithm-test.ts @@ -38,7 +38,9 @@ describe('SignerAlgorithm', () => { describe('ES256K', () => { const jwtSigner = SignerAlgorithm('ES256K') it('returns correct signature', async () => { - return expect(jwtSigner('hello', signer)).resolves.toEqual('MaCPcIypS76TnvKSbhbPMG01BJvjQ6ouITV-mVt7_bfTZfGkEdwooSqbzPBHAlZXGzYYvrTnH4M9lF3OZMdpRQ') + return expect(jwtSigner('hello', signer)).resolves.toEqual( + 'MaCPcIypS76TnvKSbhbPMG01BJvjQ6ouITV-mVt7_bfTZfGkEdwooSqbzPBHAlZXGzYYvrTnH4M9lF3OZMdpRQ' + ) }) it('returns signature of 64 bytes', async () => { @@ -48,7 +50,10 @@ describe('ES256K', () => { it('contains only r and s of signature', async () => { const signature = await jwtSigner('hello', signer) - expect(toSignatureObject(signature)).toEqual({ r: '31a08f708ca94bbe939ef2926e16cf306d35049be343aa2e21357e995b7bfdb7', s: 'd365f1a411dc28a12a9bccf0470256571b3618beb4e71f833d945dce64c76945' }) + expect(toSignatureObject(signature)).toEqual({ + r: '31a08f708ca94bbe939ef2926e16cf306d35049be343aa2e21357e995b7bfdb7', + s: 'd365f1a411dc28a12a9bccf0470256571b3618beb4e71f833d945dce64c76945' + }) }) it('can verify the signature', async () => { @@ -60,7 +65,9 @@ describe('ES256K', () => { describe('ES256K-R', () => { const jwtSigner = SignerAlgorithm('ES256K-R') it('returns correct signature', async () => { - return expect(jwtSigner('hello', signer)).resolves.toEqual('MaCPcIypS76TnvKSbhbPMG01BJvjQ6ouITV-mVt7_bfTZfGkEdwooSqbzPBHAlZXGzYYvrTnH4M9lF3OZMdpRQE') + return expect(jwtSigner('hello', signer)).resolves.toEqual( + 'MaCPcIypS76TnvKSbhbPMG01BJvjQ6ouITV-mVt7_bfTZfGkEdwooSqbzPBHAlZXGzYYvrTnH4M9lF3OZMdpRQE' + ) }) it('returns signature of 64 bytes', async () => { @@ -70,7 +77,11 @@ describe('ES256K-R', () => { it('contains r, s and recoveryParam of signature', async () => { const signature = await jwtSigner('hello', signer) - expect(toSignatureObject(signature, true)).toEqual({ r: '31a08f708ca94bbe939ef2926e16cf306d35049be343aa2e21357e995b7bfdb7', s: 'd365f1a411dc28a12a9bccf0470256571b3618beb4e71f833d945dce64c76945', recoveryParam: 1 }) + expect(toSignatureObject(signature, true)).toEqual({ + r: '31a08f708ca94bbe939ef2926e16cf306d35049be343aa2e21357e995b7bfdb7', + s: 'd365f1a411dc28a12a9bccf0470256571b3618beb4e71f833d945dce64c76945', + recoveryParam: 1 + }) }) it('can verify the signature', async () => { @@ -82,7 +93,9 @@ describe('ES256K-R', () => { describe('Ed25519', () => { const jwtSigner = SignerAlgorithm('Ed25519') it('returns correct signature', async () => { - return expect(jwtSigner('hello', edSigner)).resolves.toEqual('lLY_SeplJc_4tgMP1BHmjfxS0UEi-Xvonzbss4GT7yuFz--H28uCwsRjlIwXL4I0ugCrM-zQoA2gW2JdnFRkDQ') + return expect(jwtSigner('hello', edSigner)).resolves.toEqual( + 'lLY_SeplJc_4tgMP1BHmjfxS0UEi-Xvonzbss4GT7yuFz--H28uCwsRjlIwXL4I0ugCrM-zQoA2gW2JdnFRkDQ' + ) }) it('can verify the signature', async () => { diff --git a/src/__tests__/VerifierAlgorithm-test.ts b/src/__tests__/VerifierAlgorithm-test.ts index 96b1d9ed..d5120585 100644 --- a/src/__tests__/VerifierAlgorithm-test.ts +++ b/src/__tests__/VerifierAlgorithm-test.ts @@ -42,7 +42,8 @@ const ecKey1 = { id: `${did}#keys-1`, type: 'Secp256k1VerificationKey2018', owner: did, - publicKeyHex: '04613bb3a4874d27032618f020614c21cbe4c4e4781687525f6674089f9bd3d6c7f6eb13569053d31715a3ba32e0b791b97922af6387f087d6b5548c06944ab062' + publicKeyHex: + '04613bb3a4874d27032618f020614c21cbe4c4e4781687525f6674089f9bd3d6c7f6eb13569053d31715a3ba32e0b791b97922af6387f087d6b5548c06944ab062' } const ecKey2 = { @@ -84,21 +85,24 @@ const malformedKey1 = { id: `${did}#keys-7`, type: 'Secp256k1VerificationKey2018', owner: did, - publicKeyHex: '05613bb3a4874d27032618f020614c21cbe4c4e4781687525f6674089f9bd3d6c7f6eb13569053d31715a3ba32e0b791b97922af6387f087d6b5548c06944ab062' + publicKeyHex: + '05613bb3a4874d27032618f020614c21cbe4c4e4781687525f6674089f9bd3d6c7f6eb13569053d31715a3ba32e0b791b97922af6387f087d6b5548c06944ab062' } const malformedKey2 = { id: `${did}#keys-8`, type: 'Secp256k1VerificationKey2018', owner: did, - publicKeyHex: '04613bb3a4874d27032618f020614c21cbe4c4e4781687525f6674089f9bd3d6c7f6eb13569053d31715a3ba32e0b791b97922af6387f087d6b5548c06944ab062aabbccdd' + publicKeyHex: + '04613bb3a4874d27032618f020614c21cbe4c4e4781687525f6674089f9bd3d6c7f6eb13569053d31715a3ba32e0b791b97922af6387f087d6b5548c06944ab062aabbccdd' } const malformedKey3 = { id: `${did}#keys-8`, type: 'Secp256k1VerificationKey2018', owner: did, - publicKeyHex: '04613bb3a4874d27032618f020614c21cbe4c4e4781687525f6674089f9bd3d6c7f6eb13569053d31715a3ba32e0b791b97922af6387f087d6b5548c06' + publicKeyHex: + '04613bb3a4874d27032618f020614c21cbe4c4e4781687525f6674089f9bd3d6c7f6eb13569053d31715a3ba32e0b791b97922af6387f087d6b5548c06' } describe('ES256K', () => { @@ -122,7 +126,7 @@ describe('ES256K', () => { }) it('throws error if invalid signature length', async () => { - const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }) + 'aa' + const jwt = (await createJWT({ bla: 'bla' }, { issuer: did, signer })) + 'aa' const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) return expect(() => verifier(parts[1], parts[2], [ecKey1])).toThrowError(new Error('wrong signature length')) }) @@ -130,7 +134,9 @@ describe('ES256K', () => { it('validates signature with compressed public key and picks correct public key when malformed keys are encountered first', async () => { const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }) const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) - return expect(verifier(parts[1], parts[2], [malformedKey1, malformedKey2, malformedKey3, compressedKey])).toEqual(compressedKey) + return expect(verifier(parts[1], parts[2], [malformedKey1, malformedKey2, malformedKey3, compressedKey])).toEqual( + compressedKey + ) }) it('validates signature produced by ethAddress - github #14', async () => { diff --git a/src/index.ts b/src/index.ts index e73ff617..de717cd4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,12 +3,4 @@ import NaclSigner from './NaclSigner' import { verifyJWT, createJWT, decodeJWT, Signer } from './JWT' import { toEthereumAddress } from './Digest' -export { - SimpleSigner, - NaclSigner, - verifyJWT, - createJWT, - decodeJWT, - toEthereumAddress, - Signer -} +export { SimpleSigner, NaclSigner, verifyJWT, createJWT, decodeJWT, toEthereumAddress, Signer }