From c6114c7c49bfd72b39ec48ac680f3ccc77a51df7 Mon Sep 17 00:00:00 2001 From: Jacob Veal Date: Tue, 21 Mar 2023 12:49:22 -0500 Subject: [PATCH] feat: add decode method --- README.md | 71 ++++++++++++++++++++++++++++++++++++- package.json | 6 +++- pnpm-lock.yaml | 38 ++++++++++++++++++++ src/token.ts | 42 +++++++++++++++++++++- src/types.ts | 6 ++++ src/utils.ts | 25 +++++++++---- test/decode-token.test.ts | 35 ++++++++++++++++++ test/identity-token.test.ts | 30 +++++++++++----- test/verify-token.test.ts | 29 +-------------- 9 files changed, 236 insertions(+), 46 deletions(-) create mode 100644 test/decode-token.test.ts diff --git a/README.md b/README.md index 078ce53..0b47475 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,72 @@ # Identity Token -> Validates and decodes access tokens issued by Sign in with BitGo +Validates and decodes access tokens issued by Sign in with BitGo + +## Installation + +```bash +npm install @bitgo/identity-token +``` + +## Usage + +### Decoding JWT + +Decode a JWT payload synchronously and validate its schema. If schema does not +much, an error is thrown. + +> Signature is not verified when decoding, this is useful in client applications since network calls are not made. + +```typescript +import { decodeIdentityToken } from "@bitgo/identity-token"; + +const identityToken = decodeIdentityToken(bearerToken); + +if (identityToken.isExpired()) { + throw new Error("Token is expired"); +} + +// shortcut properties +identityToken.userId; +identityToken.enterprises; + +// entire jwt payload is also available +identityToken.payload; +``` + +### Verifying JWT + +Verify a JWT signature was signed by BitGo and decode the JWT payload if verified. + +> Backend services needing authorization should use this method. + +```typescript +import { + getIdentityJWKSetFunction, + verifyIdentityToken, +} from "@bitgo/identity-token"; + +// fetches public certs from BitGo to verify signature when invoked +const identityJWKSetFunction = getIdentityJWKSetFunction(); +let identityToken; +try { + identityToken = await verifyIdentityToken( + bearerToken, + identityJWKSetFunction + ); +} catch (error) { + // token is either expired, failed to decode, or signature does not match + throw error; +} + +// Example Usage +if (!identityToken.isOriginAllowed(req.header.origin)) { + throw new Error("Request origin is not allowed"); +} + +if (!identityToken.hasScope("required_scope")) { + throw new Error("Token does not contain required scope"); +} +``` + + diff --git a/package.json b/package.json index 967e8d1..ed56b44 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,17 @@ "build": "tsc -p 'tsconfig.build.json'", "clean": "rm -rf -- dist", "lint": "eslint src", - "test": "c8 mocha -r ts-node/register test/**/*.ts --exit" + "test": "c8 mocha -r ts-node/register test/**/*.ts --exit", + "prepublishOnly": "pnpm run build" }, "dependencies": { "fp-ts": "^2.10.5", "io-ts": "2.1.3", + "io-ts-types": "^0.5.19", "jose": "^4.11.2", "jsonwebtoken": "^8.5.1", + "monocle-ts": "^2.3.13", + "newtype-ts": "^0.3.5", "superagent": "^8.0.9" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69d9023..be59ae6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,12 @@ specifiers: eslint-plugin-react-hooks: ^4.6.0 fp-ts: ^2.10.5 io-ts: 2.1.3 + io-ts-types: ^0.5.19 jose: ^4.11.2 jsonwebtoken: ^8.5.1 mocha: 10.0.0 + monocle-ts: ^2.3.13 + newtype-ts: ^0.3.5 nock: 13.3.0 prettier: ^2.8.4 superagent: ^8.0.9 @@ -32,8 +35,11 @@ specifiers: dependencies: fp-ts: 2.13.1 io-ts: 2.1.3_fp-ts@2.13.1 + io-ts-types: 0.5.19_2erii5un74vlpzg7jpsqiymuui jose: 4.13.1 jsonwebtoken: 8.5.1 + monocle-ts: 2.3.13_fp-ts@2.13.1 + newtype-ts: 0.3.5_y4ecnzjsatnzoei5elwf4wjeqa superagent: 8.0.9 devDependencies: @@ -1830,6 +1836,20 @@ packages: side-channel: 1.0.4 dev: true + /io-ts-types/0.5.19_2erii5un74vlpzg7jpsqiymuui: + resolution: {integrity: sha512-kQOYYDZG5vKre+INIDZbLeDJe+oM+4zLpUkjXyTMyUfoCpjJNyi29ZLkuEAwcPufaYo3yu/BsemZtbdD+NtRfQ==} + peerDependencies: + fp-ts: ^2.0.0 + io-ts: ^2.0.0 + monocle-ts: ^2.0.0 + newtype-ts: ^0.3.2 + dependencies: + fp-ts: 2.13.1 + io-ts: 2.1.3_fp-ts@2.13.1 + monocle-ts: 2.3.13_fp-ts@2.13.1 + newtype-ts: 0.3.5_y4ecnzjsatnzoei5elwf4wjeqa + dev: false + /io-ts/2.1.3_fp-ts@2.13.1: resolution: {integrity: sha512-QFMR2QEBSP6w1TPmkpfca6xkzBbXO+K7ubdbV26GlCGI7CP9LV59bfty422JYtWgbBITuL/zBb1+mziv9f5Wfg==} peerDependencies: @@ -2390,6 +2410,14 @@ packages: yargs-unparser: 2.0.0 dev: true + /monocle-ts/2.3.13_fp-ts@2.13.1: + resolution: {integrity: sha512-D5Ygd3oulEoAm3KuGO0eeJIrhFf1jlQIoEVV2DYsZUMz42j4tGxgct97Aq68+F8w4w4geEnwFa8HayTS/7lpKQ==} + peerDependencies: + fp-ts: ^2.5.0 + dependencies: + fp-ts: 2.13.1 + dev: false + /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -2410,6 +2438,16 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /newtype-ts/0.3.5_y4ecnzjsatnzoei5elwf4wjeqa: + resolution: {integrity: sha512-v83UEQMlVR75yf1OUdoSFssjitxzjZlqBAjiGQ4WJaML8Jdc68LJ+BaSAXUmKY4bNzp7hygkKLYTsDi14PxI2g==} + peerDependencies: + fp-ts: ^2.0.0 + monocle-ts: ^2.0.0 + dependencies: + fp-ts: 2.13.1 + monocle-ts: 2.3.13_fp-ts@2.13.1 + dev: false + /nock/13.3.0: resolution: {integrity: sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg==} engines: {node: '>= 10.13'} diff --git a/src/token.ts b/src/token.ts index aa130ed..d25cb82 100644 --- a/src/token.ts +++ b/src/token.ts @@ -1,12 +1,52 @@ import type { IdentityJWTPayload, LegacyAccessToken } from './types'; export class IdentityToken { + public readonly userId: string; + public readonly enterprises: string[]; + public readonly scopes: string[]; public readonly payload: IdentityJWTPayload; private readonly token: string; constructor(token: string, payload: IdentityJWTPayload) { this.payload = payload; this.token = token; + this.userId = payload.bitgo_id; + this.enterprises = payload.enterprises; + this.scopes = this.payload.scope.split(' '); + } + + /** + * Determines if the token has expired. + * @returns boolean + */ + public isExpired() { + return new Date() >= this.parseEpoch(this.payload.exp); + } + + /** + * Determines if the given request origin is registered with + * the requesting client. + * + * @param requestOrigin + * @returns boolean + */ + public isOriginAllowed(requestOrigin: string) { + const _origin = this.cleanOrigin(requestOrigin); + return this.payload['allowed-origins'].find((origin) => + origin.includes(_origin) + ) + ? true + : false; + } + + /** + * Determines if the identity token contains a given scope + * + * @param scope + * @returns boolean + */ + public hasScope(scope: string) { + return this.scopes.includes(scope); } /** @@ -21,7 +61,7 @@ export class IdentityToken { id: this.payload.jti || '', client: this.payload.azp, user: this.payload.bitgo_id, - scope: this.payload.scope.split(' '), + scope: this.scopes, created: this.parseEpoch(this.payload.iat), expires: this.parseEpoch(this.payload.exp), origin: this._extractWebOrigin( diff --git a/src/types.ts b/src/types.ts index ae19376..aa44f4d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ import * as t from 'io-ts'; import * as jose from 'jose'; +import { nonEmptyArray } from 'io-ts-types'; export type GetKeyFunction = ReturnType; @@ -12,6 +13,11 @@ export const IdentityJWTPayload = t.type({ */ bitgo_id: t.string, + /** Added to token payload via the bitgo-info token scope + * which maps the users enterprises as an user attribute. + */ + enterprises: nonEmptyArray(t.string), + /** Space seperated list of default and optional token scopes. * e.g. 'openid bitgo-info wallet-view-all' */ diff --git a/src/utils.ts b/src/utils.ts index 3c5d24b..1b6c54c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,6 +24,23 @@ export const getIdentityJWKSetFunction = ( ); }; +/** + * Decodes a JWT and returns a Identity Token class object. If the JWT + * payload does not match the expected schema, a decoding error will + * be thrown. + * + * @param jwt + * @returns IdentityToken + */ +export const decodeIdentityToken = (jwt: string) => { + const payload = jose.decodeJwt(jwt); + const decodedPayload = IdentityJWTPayload.decode(payload); + if (isRight(decodedPayload)) { + return new IdentityToken(jwt, decodedPayload.right); + } + throw decodedPayload.left; +}; + /** * Verifies the signature of the JWT against the identity service JWS. * @@ -34,10 +51,6 @@ export const getIdentityJWKSetFunction = ( * @param jwks the jws get function to verify jwt signature */ export const verifyIdentityToken = async (jwt: string, jws: GetKeyFunction) => { - const { payload } = await jose.jwtVerify(jwt, jws); - const decodedPayload = IdentityJWTPayload.decode(payload); - if (isRight(decodedPayload)) { - return new IdentityToken(jwt, decodedPayload.right); - } - throw decodedPayload.left; + await jose.jwtVerify(jwt, jws); + return decodeIdentityToken(jwt); }; diff --git a/test/decode-token.test.ts b/test/decode-token.test.ts new file mode 100644 index 0000000..104661b --- /dev/null +++ b/test/decode-token.test.ts @@ -0,0 +1,35 @@ +import { assert } from 'chai'; + +import { decodeIdentityToken } from '../src'; + +describe('Decode Identity Token', () => { + it('should return an identity token given a valid jwt', async () => { + const bearerToken = + 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1dTJqaHZQMGkyeU80a0lFUG96ejU5TW40RG4yR0VTc1hqTHg1Z2QxQmp3In0.eyJleHAiOjE4NTIyMTgyODcsImlhdCI6MTY3OTQxODI4NywiYXV0aF90aW1lIjoxNjc5NDE4Mjg3LCJqdGkiOiIwYzZkNzNlZC03MjhhLTQwMWYtOGJiOS05YWU0OWU5YWJmN2YiLCJpc3MiOiJodHRwczovL2lkZW50aXR5LmJpdGdvLWRldi5jb20vcmVhbG1zL2JpdGdvIiwic3ViIjoiZjoxMjY2ZWNmNy1kMzZiLTQ2NGEtOThiZi1kODBjZjE0YWZiNDg6ZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGVAYml0Z28uY29tIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY29tLmJpdGdvLnRlc3R3ZWJhcHAiLCJzZXNzaW9uX3N0YXRlIjoiMDZhZDZkMTMtYWM5NS00MDY4LTkyNmItYzA3Y2UxOTQ3MDBjIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJzY29wZSI6Im9wZW5pZCB1c2VyX21hbmFnZSB3YWxsZXRfc3BlbmRfYWxsIGJpdGdvLWluZm8gcHJvZmlsZSB3YWxsZXRfdmlld19hbGwgd2FsbGV0X2NyZWF0ZSIsInNpZCI6IjA2YWQ2ZDEzLWFjOTUtNDA2OC05MjZiLWMwN2NlMTk0NzAwYyIsImJpdGdvX2lkIjoiNWNhZTMxMzBmOGY0NTYxZDUxZGZjYmZkYWFmYmE5YjkiLCJuYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUgYWRtaW4iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20iLCJnaXZlbl9uYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUiLCJmYW1pbHlfbmFtZSI6ImFkbWluIiwiZW50ZXJwcmlzZXMiOlsiNWNhZTMxMzFmOGY0NTYxZDUxZGZjYzAwMTY2ODdiOTYiLCI1Y2FiZTNlOGExYjU2OTIzNTFjMzZmYzQ5ZmZkZDY4MCIsIjVjY2EwYzgxZTdlYTRhMzcwNWJlYjE3NDViNmEwNDJiIiwiNjFkZjNkZWUwMDA0NTgwMDA3M2QxMmU4NDRkYTk3ZGEiXX0.A1mmxX0_rXoPb5SEMRNE-zA5y44JYKRQlLN-Y8TSUkP8Yyo3RoA3QNr0351Da9TTNc73HpT2ahVwKoBPdT2z8unIvQ_Gsz-tHWmQyZ95HKt5Lja82lJvS0K2aRhCcTSF1Zw3AGLeaMesl7umMQLkIf5s4aN380Tyx1FeJReVF8dM1_bAvRzrffZQSOUFACU2Qd4LJ2JYaPrIrPLZkDOJ0vQzfBCOsRox-Y6m29oQ6Lw8-hbuN1gtk-DUkMX8AdWto4f74T0d0mKIN929-GYmmriieuqnrk5HqZ7blYrF3GB6jF8-eD5GJe3nhLJAEZ9OJVzKJS6fGyL6zgK-HyZsPg'; + + const identityToken = decodeIdentityToken(bearerToken); + + assert.isDefined(identityToken); + assert.isNotEmpty(identityToken?.payload); + }); + + it('should throw an error given a valid jwt but with an invalid payload schema', async () => { + const bearerToken = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ'; + + try { + decodeIdentityToken(bearerToken); + assert.fail(); + } catch (err) {} + }); + + it('should throw an error given an invalid jwt', async () => { + const bearerToken = + 'v2x0b75a97dd8caf93b94c0739c8f66478f841217f4ddad7a3cf2e68d2e6a8c5805'; + + try { + decodeIdentityToken(bearerToken); + assert.fail(); + } catch (err) {} + }); +}); diff --git a/test/identity-token.test.ts b/test/identity-token.test.ts index 4895e7f..3d36da4 100644 --- a/test/identity-token.test.ts +++ b/test/identity-token.test.ts @@ -12,10 +12,8 @@ describe('Identity Token', () => { let identityToken: IdentityToken; before(async () => { - // Expires Wed Aug 9th 2028 const bearerToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1dTJqaHZQMGkyeU80a0lFUG96ejU5TW40RG4yR0VTc1hqTHg1Z2QxQmp3In0.eyJleHAiOjE4NDk0NDM5MTQsImlhdCI6MTY3NjY0MzkxNCwianRpIjoiOGNiNWNjMTktNGIyNi00MzY5LTk0MmYtNTg4NzhhYjA1YTYzIiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5iaXRnby1kZXYuY29tL3JlYWxtcy9iaXRnbyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJmOjEyNjZlY2Y3LWQzNmItNDY0YS05OGJmLWQ4MGNmMTRhZmI0ODpleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20iLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjb20uYml0Z28uY2xpIiwic2Vzc2lvbl9zdGF0ZSI6ImE5ZDc4OTY4LTY0ODMtNGM2Ni05NWNiLWQ3YzM3MDcyYjc1MCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiY2hyb21lLWV4dGVuc2lvbjovL2twY29qaGdkaG5qbW1lZ2hpYmxwamVpY2Jrb2VsYm1mIiwiaHR0cDovL2xvY2FsaG9zdCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiYml0Z28taW5mbyB3YWxsZXRfc3BlbmQgZW1haWwgcHJvZmlsZSB3YWxsZXRfdmlldyB3YWxsZXRfY3JlYXRlIiwic2lkIjoiYTlkNzg5NjgtNjQ4My00YzY2LTk1Y2ItZDdjMzcwNzJiNzUwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImJpdGdvX2lkIjoiNWNhZTMxMzBmOGY0NTYxZDUxZGZjYmZkYWFmYmE5YjkiLCJuYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUgYWRtaW4iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20iLCJnaXZlbl9uYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUiLCJmYW1pbHlfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20ifQ.Oq_txq17ApirE6-o_RRBhjhvJmGW6NAa6G9Km7WpxQoJV0-8yN1ddSnQ5W3UljM4ArsQwwitG9NvTKxm1YuwZ-e7vzcOmtnmMbsC_DKGO3PyatG4ndQmAHw4XAw9eYKf8lVl_Mk_mJf45mbOOJ_zXM8SKBruHPJa1LqJxeMrWmuZKssPvuvB76UnqPNmdx0F-iiQE9Rs7_7y3OKFtaBrSG8K6euzx3AY8P7wkK-z6Wlfelz5hh9AAZ81tjlNwii5ZEUAMygxjPQWp9VD9wo4D7LEM51Ad4y4-vUQmZHAXyGPphQHcODNgQxJa_Uly_tOQGtdqRdodldwFZcGOxQX3Q'; - + 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1dTJqaHZQMGkyeU80a0lFUG96ejU5TW40RG4yR0VTc1hqTHg1Z2QxQmp3In0.eyJleHAiOjE4NTIyMjA2OTUsImlhdCI6MTY3OTQyMDY5NSwianRpIjoiYTZlMTJhNzktM2QyZC00Mzc2LWE3MTAtYWVlZDQxMjkyN2RhIiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5iaXRnby1kZXYuY29tL3JlYWxtcy9iaXRnbyIsInN1YiI6ImY6MTI2NmVjZjctZDM2Yi00NjRhLTk4YmYtZDgwY2YxNGFmYjQ4OmV4cGVyaWVuY2UrdGVzdC1hZG1pbitkby1ub3QtZGVsZXRlQGJpdGdvLmNvbSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImNvbS5iaXRnby5jbGkiLCJzZXNzaW9uX3N0YXRlIjoiNjRmOThiMTQtMTM5Zi00ZmRjLWEyYzItZGJkN2QzZjljZjAyIiwiYWxsb3dlZC1vcmlnaW5zIjpbImNocm9tZS1leHRlbnNpb246Ly9rcGNvamhnZGhuam1tZWdoaWJscGplaWNia29lbGJtZiIsImh0dHA6Ly9sb2NhbGhvc3QiXSwic2NvcGUiOiJ1c2VyX21hbmFnZSB3YWxsZXRfc3BlbmRfYWxsIGJpdGdvLWluZm8gcHJvZmlsZSB3YWxsZXRfdmlld19hbGwgd2FsbGV0X2NyZWF0ZSIsInNpZCI6IjY0Zjk4YjE0LTEzOWYtNGZkYy1hMmMyLWRiZDdkM2Y5Y2YwMiIsImJpdGdvX2lkIjoiNWNhZTMxMzBmOGY0NTYxZDUxZGZjYmZkYWFmYmE5YjkiLCJuYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUgYWRtaW4iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20iLCJnaXZlbl9uYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUiLCJmYW1pbHlfbmFtZSI6ImFkbWluIiwiZW50ZXJwcmlzZXMiOlsiNWNhZTMxMzFmOGY0NTYxZDUxZGZjYzAwMTY2ODdiOTYiLCI1Y2FiZTNlOGExYjU2OTIzNTFjMzZmYzQ5ZmZkZDY4MCIsIjVjY2EwYzgxZTdlYTRhMzcwNWJlYjE3NDViNmEwNDJiIiwiNjFkZjNkZWUwMDA0NTgwMDA3M2QxMmU4NDRkYTk3ZGEiXX0.BjgEc9Ou1j0A4X-78l_SkBnGUbIzPmh5j0_777olZqjcTnmNSaOqp3bhfqBVnNhcf4DedaGEDwE5D0O3FzfgO-r0MUekwmr_hEvLkctOGM9CfQEEW1e_JTtX8csG7-JdQYy_iIcv81zd6gHgscsvUCI0fFQNlrknFRNCwiU5lqEkIBBAxobuM6H37mREVFudn6vtWwhPVSb4ZHPgRPDXUM-16rfywBGIWBlZnBZKTp1pI0_yuWiHDdL1lrDz7j7IBsDncKPkT4wwjI-jgoZM5uxY9gfBiY6zbIdPk9r2zj75vqm9maz0cA4_PCln4L90XQc4TnOlteffGcd6eToeEw'; nockGetJWKSetCall(); const identityJWKSetFunction = getIdentityJWKSetFunction(); @@ -38,12 +36,6 @@ describe('Identity Token', () => { it('should map to legacy access token using selected web origin', async () => { const accessToken = identityToken.mapToLegacy('http://localhost:3000'); - - assert.equal(accessToken.client, 'com.bitgo.cli'); - assert.equal(accessToken.user, '5cae3130f8f4561d51dfcbfdaafba9b9'); - assert.equal(accessToken.label, 'identity-session'); - - // harbor dev extension assert.equal(accessToken.origin, 'localhost'); }); @@ -61,4 +53,24 @@ describe('Identity Token', () => { const origin = identityToken._extractWebOrigin([]); assert.equal(origin, ''); }); + + it('should check token expiration', () => { + const isExpired = identityToken.isExpired(); + assert.equal(isExpired, false); + }); + + it('should return true for a registred web origin', () => { + const isAllowed = identityToken.isOriginAllowed('http://localhost:3000'); + assert.equal(isAllowed, true); + }); + + it('should return false for a non registred web origin', () => { + const isAllowed = identityToken.isOriginAllowed('https://evil.com'); + assert.equal(isAllowed, false); + }); + + it('should return true given a registered token scope', () => { + const hasScope = identityToken.hasScope('wallet_view_all'); + assert.equal(hasScope, true); + }); }); diff --git a/test/verify-token.test.ts b/test/verify-token.test.ts index 52fd274..5b64d24 100644 --- a/test/verify-token.test.ts +++ b/test/verify-token.test.ts @@ -6,9 +6,8 @@ import { getIdentityJWKSetFunction, verifyIdentityToken } from '../src'; describe('Verify Identity Token', () => { it('should return an identity token given a valid jwt from the identity service', async () => { - // Expires Mon Jul 17 2028 const bearerToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1dTJqaHZQMGkyeU80a0lFUG96ejU5TW40RG4yR0VTc1hqTHg1Z2QxQmp3In0.eyJleHAiOjE4NDk0NDM5MTQsImlhdCI6MTY3NjY0MzkxNCwianRpIjoiOGNiNWNjMTktNGIyNi00MzY5LTk0MmYtNTg4NzhhYjA1YTYzIiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5iaXRnby1kZXYuY29tL3JlYWxtcy9iaXRnbyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJmOjEyNjZlY2Y3LWQzNmItNDY0YS05OGJmLWQ4MGNmMTRhZmI0ODpleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20iLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjb20uYml0Z28uY2xpIiwic2Vzc2lvbl9zdGF0ZSI6ImE5ZDc4OTY4LTY0ODMtNGM2Ni05NWNiLWQ3YzM3MDcyYjc1MCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiY2hyb21lLWV4dGVuc2lvbjovL2twY29qaGdkaG5qbW1lZ2hpYmxwamVpY2Jrb2VsYm1mIiwiaHR0cDovL2xvY2FsaG9zdCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiYml0Z28taW5mbyB3YWxsZXRfc3BlbmQgZW1haWwgcHJvZmlsZSB3YWxsZXRfdmlldyB3YWxsZXRfY3JlYXRlIiwic2lkIjoiYTlkNzg5NjgtNjQ4My00YzY2LTk1Y2ItZDdjMzcwNzJiNzUwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImJpdGdvX2lkIjoiNWNhZTMxMzBmOGY0NTYxZDUxZGZjYmZkYWFmYmE5YjkiLCJuYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUgYWRtaW4iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20iLCJnaXZlbl9uYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUiLCJmYW1pbHlfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20ifQ.Oq_txq17ApirE6-o_RRBhjhvJmGW6NAa6G9Km7WpxQoJV0-8yN1ddSnQ5W3UljM4ArsQwwitG9NvTKxm1YuwZ-e7vzcOmtnmMbsC_DKGO3PyatG4ndQmAHw4XAw9eYKf8lVl_Mk_mJf45mbOOJ_zXM8SKBruHPJa1LqJxeMrWmuZKssPvuvB76UnqPNmdx0F-iiQE9Rs7_7y3OKFtaBrSG8K6euzx3AY8P7wkK-z6Wlfelz5hh9AAZ81tjlNwii5ZEUAMygxjPQWp9VD9wo4D7LEM51Ad4y4-vUQmZHAXyGPphQHcODNgQxJa_Uly_tOQGtdqRdodldwFZcGOxQX3Q'; + 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1dTJqaHZQMGkyeU80a0lFUG96ejU5TW40RG4yR0VTc1hqTHg1Z2QxQmp3In0.eyJleHAiOjE4NTIyMjA2OTUsImlhdCI6MTY3OTQyMDY5NSwianRpIjoiYTZlMTJhNzktM2QyZC00Mzc2LWE3MTAtYWVlZDQxMjkyN2RhIiwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5iaXRnby1kZXYuY29tL3JlYWxtcy9iaXRnbyIsInN1YiI6ImY6MTI2NmVjZjctZDM2Yi00NjRhLTk4YmYtZDgwY2YxNGFmYjQ4OmV4cGVyaWVuY2UrdGVzdC1hZG1pbitkby1ub3QtZGVsZXRlQGJpdGdvLmNvbSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImNvbS5iaXRnby5jbGkiLCJzZXNzaW9uX3N0YXRlIjoiNjRmOThiMTQtMTM5Zi00ZmRjLWEyYzItZGJkN2QzZjljZjAyIiwiYWxsb3dlZC1vcmlnaW5zIjpbImNocm9tZS1leHRlbnNpb246Ly9rcGNvamhnZGhuam1tZWdoaWJscGplaWNia29lbGJtZiIsImh0dHA6Ly9sb2NhbGhvc3QiXSwic2NvcGUiOiJ1c2VyX21hbmFnZSB3YWxsZXRfc3BlbmRfYWxsIGJpdGdvLWluZm8gcHJvZmlsZSB3YWxsZXRfdmlld19hbGwgd2FsbGV0X2NyZWF0ZSIsInNpZCI6IjY0Zjk4YjE0LTEzOWYtNGZkYy1hMmMyLWRiZDdkM2Y5Y2YwMiIsImJpdGdvX2lkIjoiNWNhZTMxMzBmOGY0NTYxZDUxZGZjYmZkYWFmYmE5YjkiLCJuYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUgYWRtaW4iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJleHBlcmllbmNlK3Rlc3QtYWRtaW4rZG8tbm90LWRlbGV0ZUBiaXRnby5jb20iLCJnaXZlbl9uYW1lIjoiZXhwZXJpZW5jZSt0ZXN0LWFkbWluK2RvLW5vdC1kZWxldGUiLCJmYW1pbHlfbmFtZSI6ImFkbWluIiwiZW50ZXJwcmlzZXMiOlsiNWNhZTMxMzFmOGY0NTYxZDUxZGZjYzAwMTY2ODdiOTYiLCI1Y2FiZTNlOGExYjU2OTIzNTFjMzZmYzQ5ZmZkZDY4MCIsIjVjY2EwYzgxZTdlYTRhMzcwNWJlYjE3NDViNmEwNDJiIiwiNjFkZjNkZWUwMDA0NTgwMDA3M2QxMmU4NDRkYTk3ZGEiXX0.BjgEc9Ou1j0A4X-78l_SkBnGUbIzPmh5j0_777olZqjcTnmNSaOqp3bhfqBVnNhcf4DedaGEDwE5D0O3FzfgO-r0MUekwmr_hEvLkctOGM9CfQEEW1e_JTtX8csG7-JdQYy_iIcv81zd6gHgscsvUCI0fFQNlrknFRNCwiU5lqEkIBBAxobuM6H37mREVFudn6vtWwhPVSb4ZHPgRPDXUM-16rfywBGIWBlZnBZKTp1pI0_yuWiHDdL1lrDz7j7IBsDncKPkT4wwjI-jgoZM5uxY9gfBiY6zbIdPk9r2zj75vqm9maz0cA4_PCln4L90XQc4TnOlteffGcd6eToeEw'; nockGetJWKSetCall(); @@ -22,32 +21,6 @@ describe('Verify Identity Token', () => { assert.isNotEmpty(identityToken?.payload); }); - it('should fail given a valid jwt signed by a different source', async () => { - const bearerToken = - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ'; - nockGetJWKSetCall(); - - const identityJWKSetFunction = getIdentityJWKSetFunction(); - - try { - await verifyIdentityToken(bearerToken, identityJWKSetFunction); - assert.fail(); - } catch (err) {} - }); - - it('should not return an identity token given an invalid jwt', async () => { - const bearerToken = - 'v2x0b75a97dd8caf93b94c0739c8f66478f841217f4ddad7a3cf2e68d2e6a8c5805'; - nockGetJWKSetCall(); - - const identityJWKSetFunction = getIdentityJWKSetFunction(); - - try { - await verifyIdentityToken(bearerToken, identityJWKSetFunction); - assert.fail(); - } catch (err) {} - }); - it('should not return an identity token when unable to fetch JWKS', async () => { const bearerToken = 'v2x0b75a97dd8caf93b94c0739c8f66478f841217f4ddad7a3cf2e68d2e6a8c5805';