Skip to content

Commit

Permalink
Cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
awlayton committed Apr 1, 2022
1 parent 8479743 commit 6b3eee6
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 136 deletions.
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -46,6 +47,8 @@
"exclude": [
"*.d.ts",
".pnp.*",
".pnp.*",
"test",
".test"
]
},
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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": "[email protected]"
}
95 changes: 95 additions & 0 deletions src/generate.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
options?: { [key: string]: unknown; header?: Record<string, unknown> };
}
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);
}
112 changes: 3 additions & 109 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<string, unknown>;
options?: { [key: string]: unknown; header?: Record<string, unknown> };
}
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<string, unknown>;
}
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';
77 changes: 77 additions & 0 deletions src/verify.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
}
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;
}
Loading

0 comments on commit 6b3eee6

Please sign in to comment.