From 6b3eee671b184f5e13c785baa47b4613ffdcc03e Mon Sep 17 00:00:00 2001 From: Alex Layton Date: Fri, 1 Apr 2022 13:21:52 -0400 Subject: [PATCH] Cleanup --- package.json | 13 +++--- src/generate.ts | 95 ++++++++++++++++++++++++++++++++++++++++ src/index.ts | 112 ++---------------------------------------------- src/verify.ts | 77 +++++++++++++++++++++++++++++++++ yarn.lock | 44 +++++++++---------- 5 files changed, 205 insertions(+), 136 deletions(-) create mode 100644 src/generate.ts create mode 100644 src/verify.ts diff --git a/package.json b/package.json index c1d436b..38eaf4f 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "jwt-bearer-client-auth", - "version": "2.0.0", + "version": "2.0.1", "description": "Create and verify JWT bearer client assertions from the OAuth-JWT-bearer RFC", "main": "dist/index.js", "files": [ + "src/**/*", "dist/**/*" ], "engines": { @@ -46,6 +47,8 @@ "exclude": [ "*.d.ts", ".pnp.*", + ".pnp.*", + "test", ".test" ] }, @@ -74,7 +77,7 @@ }, "homepage": "https://github.com/OADA/jwt-bearer-client-auth", "dependencies": { - "@oada/certs": "^4.0.5", + "@oada/certs": "^4.1.0", "jsonwebtoken": "^8.5.1", "pem-jwk": "^2.0.0", "tslib": "^2.3.1" @@ -128,7 +131,7 @@ "eslint-plugin-regexp": "^1.6.0", "eslint-plugin-security": "^1.4.0", "eslint-plugin-sonarjs": "^0.13.0", - "eslint-plugin-unicorn": "^41.0.1", + "eslint-plugin-unicorn": "^42.0.0", "events": "^3.3.0", "jws": "^4.0.0", "karma": "^6.3.17", @@ -144,7 +147,7 @@ "path-browserify": "^1.0.1", "prettier": "^2.6.1", "process": "^0.11.10", - "puppeteer": "^13.5.1", + "puppeteer": "^13.5.2", "stream-browserify": "^3.0.0", "string_decoder": "^1.3.0", "superagent": "^7.1.2", @@ -154,7 +157,7 @@ "typescript": "^4.6.3", "url": "^0.11.0", "util": "^0.12.4", - "webpack": "^5.70.0" + "webpack": "^5.71.0" }, "packageManager": "yarn@3.2.0" } diff --git a/src/generate.ts b/src/generate.ts new file mode 100644 index 0000000..64786ca --- /dev/null +++ b/src/generate.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2015-2022 Open Ag Data Alliance + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RSA_JWK, jwk2pem } from 'pem-jwk'; +import { SignOptions, sign } from 'jsonwebtoken'; + +import type { JWKpem } from '@oada/certs/dist/jwks-utils'; +import { jwksUtils as jwks } from '@oada/certs'; + +/** + * Ensure all required claims are present + */ +function checkClaims({ + key, + issuer, + clientId, + tokenEndpoint, + expiresIn, +}: GenerateOptions) { + // Ensure the required claims are present + if (!jwks.isJWK(key)) { + throw new TypeError('key must be a JWK'); + } + + if (typeof issuer !== 'string') { + throw new TypeError('issuer must be a string'); + } + + if (typeof clientId !== 'string') { + throw new TypeError('clientId must be a string'); + } + + if (typeof tokenEndpoint !== 'string') { + throw new TypeError('tokenEndpoint must be a string'); + } + + if (typeof expiresIn !== 'number') { + throw new TypeError('expiresIn must be a number'); + } +} + +export interface GenerateOptions { + key: jwks.JWK; + issuer: string; + clientId: string; + tokenEndpoint: string; + expiresIn: number; + payload?: string | Record; + options?: { [key: string]: unknown; header?: Record }; +} +export async function generate({ + key, + issuer, + clientId, + tokenEndpoint, + expiresIn, + payload = {}, + options: { header = {}, ...options } = {}, +}: GenerateOptions) { + checkClaims({ key, issuer, clientId, tokenEndpoint, expiresIn }); + + // Build JWT options + const jwtOptions: SignOptions = { + ...options, + algorithm: 'RS256', + issuer, + subject: clientId, + audience: tokenEndpoint, + expiresIn, + // @ts-expect-error IDEK + header: { + // Add keyId if its available + kid: key.kid, + ...header, + }, + }; + + const pem = key.kty === 'PEM' ? (key as JWKpem).pem : jwk2pem(key as RSA_JWK); + + return sign(payload, pem, jwtOptions); +} diff --git a/src/index.ts b/src/index.ts index 4f8f6ca..006daa3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2015 Open Ag Data Alliance + * Copyright 2022 Open Ag Data Alliance * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,112 +15,6 @@ * limitations under the License. */ -import { RSA_JWK, jwk2pem } from 'pem-jwk'; -import { SignOptions, verify as jwtVerify, sign } from 'jsonwebtoken'; +export { generate } from './generate.js'; -import { jwksUtils as jwks } from '@oada/certs'; - -export interface GenerateOptions { - key: jwks.JWK; - issuer: string; - clientId: string; - tokenEndpoint: string; - expiresIn: number; - payload?: Record; - options?: { [key: string]: unknown; header?: Record }; -} -export async function generate({ - key, - issuer, - clientId, - tokenEndpoint, - expiresIn, - payload = {}, - options: { header = {}, ...options } = {}, -}: GenerateOptions) { - // Ensure the required claims are present - if ( - !jwks.isJWK(key) || - typeof issuer !== 'string' || - typeof clientId !== 'string' || - typeof tokenEndpoint !== 'string' || - typeof expiresIn !== 'number' - ) { - throw new TypeError('Invalid parameters'); - } - - // Build JWT options - const jwtOptions: SignOptions = { - ...options, - algorithm: 'RS256', - issuer, - subject: clientId, - audience: tokenEndpoint, - expiresIn, - // @ts-expect-error IDEK - header: { - // Add keyId if its available - kid: key.kid, - ...header, - }, - }; - - const pem = key.kty === 'PEM' ? key.pem! : jwk2pem(key as RSA_JWK); - - return sign(payload, pem, jwtOptions); -} - -export interface VerifyOptions { - token: string; - hint: string | false | jwks.JWKs | jwks.JWK; - issuer: string; - clientId: string; - tokenEndpoint: string; - payload?: Record; -} -export async function verify({ - token, - hint, - issuer, - clientId, - tokenEndpoint, - payload, -}: VerifyOptions) { - const jwk = await jwks.jwkForSignature(token, hint); - const key = jwk.kty === 'PEM' ? jwk.pem! : jwk2pem(jwk as RSA_JWK); - - const verifyOptions = { - issuer, - audience: tokenEndpoint, - }; - - const jwtPayload = jwtVerify(token, key, verifyOptions); - if (typeof jwtPayload === 'string') { - throw new TypeError(`Failed to parse payload: ${jwtPayload}`); - } - - if (!jwtPayload.exp) { - throw new Error('exp claim is required'); - } - - // Check required sub key - if (jwtPayload.sub !== clientId) { - throw new Error('sub claim is inconsistent with clientId'); - } - - // Check for optional not before property - if (jwtPayload.nbf && Math.floor(Date.now() / 1000) <= jwtPayload.nbf) { - throw new Error('nbf claim violated'); - } - - // Check for any other user required claims - if (typeof payload === 'object') { - for (const [k, v] of Object.entries(payload)) { - if (jwtPayload[k] !== v) { - throw new Error(`${k} claim is inconsistent`); - } - } - } - - return jwtPayload; -} +export { verify } from './verify.js'; diff --git a/src/verify.ts b/src/verify.ts new file mode 100644 index 0000000..e13916a --- /dev/null +++ b/src/verify.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2015-2022 Open Ag Data Alliance + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RSA_JWK, jwk2pem } from 'pem-jwk'; +import { verify as jwtVerify } from 'jsonwebtoken'; + +import type { JWKpem } from '@oada/certs/dist/jwks-utils'; +import { jwksUtils as jwks } from '@oada/certs'; + +export interface VerifyOptions { + token: string; + hint: string | false | jwks.JWKs | jwks.JWK; + issuer: string; + clientId: string; + tokenEndpoint: string; + payload?: Record; +} +export async function verify({ + token, + hint, + issuer, + clientId, + tokenEndpoint, + payload, +}: VerifyOptions) { + const jwk = await jwks.jwkForSignature(token, hint); + const key = jwk.kty === 'PEM' ? (jwk as JWKpem).pem : jwk2pem(jwk as RSA_JWK); + + const verifyOptions = { + issuer, + audience: tokenEndpoint, + }; + + const jwtPayload = jwtVerify(token, key, verifyOptions); + if (typeof jwtPayload === 'string') { + throw new TypeError(`Failed to parse payload: ${jwtPayload}`); + } + + if (!jwtPayload.exp) { + throw new Error('exp claim is required'); + } + + // Check required sub key + if (jwtPayload.sub !== clientId) { + throw new Error('sub claim is inconsistent with clientId'); + } + + // Check for optional not before property + if (jwtPayload.nbf && Math.floor(Date.now() / 1000) <= jwtPayload.nbf) { + throw new Error('nbf claim violated'); + } + + // Check for any other user required claims + if (typeof payload === 'object') { + for (const [k, v] of Object.entries(payload)) { + if (jwtPayload[k] !== v) { + throw new Error(`${k} claim is inconsistent`); + } + } + } + + return jwtPayload; +} diff --git a/yarn.lock b/yarn.lock index 8bbff46..cd905b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -181,9 +181,9 @@ __metadata: languageName: node linkType: hard -"@oada/certs@npm:^4.0.5": - version: 4.0.5 - resolution: "@oada/certs@npm:4.0.5" +"@oada/certs@npm:^4.1.0": + version: 4.1.0 + resolution: "@oada/certs@npm:4.1.0" dependencies: clone-deep: ^4.0.1 debug: ^4.3.4 @@ -205,7 +205,7 @@ __metadata: bin: certs: ./dist/cli.mjs oada-certs: ./dist/cli.mjs - checksum: 702588513058bda77282d2a268b1eda8c8974f09b4a6cc57d6c8511c9354d875ee5db50d7b07e257932154aba4a9c47327e36f946c2d4c29b2e10b23fbd40e69 + checksum: 48fda835589c6bc8aebf5a3eb30baec05f0ff8e11f268687e57be0d6574393cb01393a255684dd8e93b620521a33daf82178d614ff7ad9eff8b7376a1566bc5c languageName: node linkType: hard @@ -2336,7 +2336,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": +"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -3288,9 +3288,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-unicorn@npm:^41.0.1": - version: 41.0.1 - resolution: "eslint-plugin-unicorn@npm:41.0.1" +"eslint-plugin-unicorn@npm:^42.0.0": + version: 42.0.0 + resolution: "eslint-plugin-unicorn@npm:42.0.0" dependencies: "@babel/helper-validator-identifier": ^7.15.7 ci-info: ^3.3.0 @@ -3308,7 +3308,7 @@ __metadata: strip-indent: ^3.0.0 peerDependencies: eslint: ">=8.8.0" - checksum: 90c9dd93b57ec2cdd650168364b4e2220e2c72c1ffc8ea43cd1be2dabadc7821ec5077656f44fa4622e284caa39ca497058e1dbe609f530d392bdb2da3d73b70 + checksum: 03757cbf417d39691fe04048ac9352585162a4dd68c2f26f5bc0956409625c7c4841487f0fa623e0d6dd5ff9cc3e758b74e4d170e3b0a877bbd0114995310058 languageName: node linkType: hard @@ -4938,7 +4938,7 @@ __metadata: version: 0.0.0-use.local resolution: "jwt-bearer-client-auth@workspace:." dependencies: - "@oada/certs": ^4.0.5 + "@oada/certs": ^4.1.0 "@tsconfig/node12": ^1.0.9 "@types/chai": ^4.3.0 "@types/chai-as-promised": ^7.1.5 @@ -4987,7 +4987,7 @@ __metadata: eslint-plugin-regexp: ^1.6.0 eslint-plugin-security: ^1.4.0 eslint-plugin-sonarjs: ^0.13.0 - eslint-plugin-unicorn: ^41.0.1 + eslint-plugin-unicorn: ^42.0.0 events: ^3.3.0 jsonwebtoken: ^8.5.1 jws: ^4.0.0 @@ -5005,7 +5005,7 @@ __metadata: pem-jwk: ^2.0.0 prettier: ^2.6.1 process: ^0.11.10 - puppeteer: ^13.5.1 + puppeteer: ^13.5.2 stream-browserify: ^3.0.0 string_decoder: ^1.3.0 superagent: ^7.1.2 @@ -5016,7 +5016,7 @@ __metadata: typescript: ^4.6.3 url: ^0.11.0 util: ^0.12.4 - webpack: ^5.70.0 + webpack: ^5.71.0 languageName: unknown linkType: soft @@ -6552,12 +6552,12 @@ __metadata: languageName: node linkType: hard -"puppeteer@npm:^13.5.1": - version: 13.5.1 - resolution: "puppeteer@npm:13.5.1" +"puppeteer@npm:^13.5.2": + version: 13.5.2 + resolution: "puppeteer@npm:13.5.2" dependencies: cross-fetch: 3.1.5 - debug: 4.3.3 + debug: 4.3.4 devtools-protocol: 0.0.969999 extract-zip: 2.0.1 https-proxy-agent: 5.0.0 @@ -6568,7 +6568,7 @@ __metadata: tar-fs: 2.1.1 unbzip2-stream: 1.4.3 ws: 8.5.0 - checksum: adf7511977d9300386cc2b2699d83c7f4c2e890fb00a5d7a7b41a11561b599b1f19b26ac4ae9d469ce567c87a22be6362160166f31aca135d94148f68aeff2cc + checksum: 5f03c8a606067ae114ea646d9773b76b3e2c8f0654ef4453c5f813686a218481d934708fefc607822b75673c6e82b812755892f57d02e42a0dadeaabe3c56138 languageName: node linkType: hard @@ -8253,9 +8253,9 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5.70.0": - version: 5.70.0 - resolution: "webpack@npm:5.70.0" +"webpack@npm:^5.71.0": + version: 5.71.0 + resolution: "webpack@npm:5.71.0" dependencies: "@types/eslint-scope": ^3.7.3 "@types/estree": ^0.0.51 @@ -8286,7 +8286,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 00439884a9cdd5305aed3ce93735635785a15c5464a6d2cfce87e17727a07585de02420913e82aa85ddd2ae7322175d2cfda6ac0878a17f061cb605e6a7db57a + checksum: 84b273a15180d45dafe4fc4a3ccccba2f72210f327a1af39713b3ef78148768afb0e18fa0cddaea4af5dd54ace199fbbdfcef9aec8da7e9c248f8b1b7cc413e1 languageName: node linkType: hard