Skip to content

Commit

Permalink
updates and export fix
Browse files Browse the repository at this point in the history
  • Loading branch information
mharj committed Apr 12, 2024
1 parent ae52ed8 commit 1db8b51
Show file tree
Hide file tree
Showing 13 changed files with 1,310 additions and 793 deletions.
1,711 changes: 1,107 additions & 604 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"test": "nyc mocha",
"azure-test": "nyc mocha",
"coverage": "nyc report --reporter=lcovonly",
"lint": "eslint src"
"lint": "eslint src test"
},
"mocha": {
"exit": true,
Expand Down Expand Up @@ -62,41 +62,41 @@
"dist"
],
"devDependencies": {
"@types/chai": "^4.3.11",
"@types/chai": "^4.3.14",
"@types/chai-as-promised": "^7.1.8",
"@types/jsonwebtoken": "^9.0.5",
"@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^10.0.6",
"@types/node": "^16.18.74",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@types/node": "^16.18.96",
"@typescript-eslint/eslint-plugin": "^7.6.0",
"@typescript-eslint/parser": "^7.6.0",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.1",
"dotenv": "^16.3.2",
"eslint": "^8.56.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-sonarjs": "^0.23.0",
"google-auth-library": "^9.4.2",
"googleapis": "^131.0.0",
"mocha": "^10.2.0",
"eslint-plugin-sonarjs": "^0.25.1",
"google-auth-library": "^9.7.0",
"googleapis": "^134.0.0",
"mocha": "^10.4.0",
"mocha-junit-reporter": "^2.2.1",
"nyc": "^15.1.0",
"prettier": "^3.2.4",
"prettier": "^3.2.5",
"source-map-support": "^0.5.21",
"tachyon-drive-node-fs": "^0.3.2",
"tachyon-expire-cache": "^0.3.0",
"ts-node": "^10.9.2",
"typed-emitter": "^2.1.0",
"typescript": "^5.3.3"
"typescript": "^5.4.5"
},
"dependencies": {
"@avanio/auth-header": "^0.0.1",
"@avanio/expire-cache": "^0.3.2",
"@avanio/logger-like": "^0.1.1",
"jsonwebtoken": "^9.0.2",
"tachyon-drive": "^0.3.4",
"tachyon-drive": "^0.3.5",
"zod": "^3.22.4"
}
}
3 changes: 3 additions & 0 deletions src/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './CertCache';
export * from './FileCertCache';
export * from './TachyonCertCache';
164 changes: 3 additions & 161 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,161 +1,3 @@
import * as jwt from 'jsonwebtoken';
import {
assertIssuerToken,
assertIsTokenFullDecoded,
FullDecodedIssuerTokenStructure,
FullDecodedTokenStructure,
isRawJwtToken,
RawJwtToken,
TokenPayload,
} from './interfaces/token';
import {ExpireCache, ICacheOrAsync} from '@avanio/expire-cache';
import {AuthHeader} from '@avanio/auth-header';
import {buildCertFrame} from './rsaPublicKeyPem';
import {CertCache} from './cache/CertCache';
import {getTokenOrAuthHeader} from './lib/authUtil';
import {ILoggerLike} from '@avanio/logger-like';
import {IssuerCertLoader} from './issuerCertLoader';
import {JwtHeaderError} from './JwtHeaderError';
import {jwtVerifyPromise} from './lib/jwtUtil';
export * from './cache/FileCertCache';
export * from './cache/TachyonCertCache';
export * from './lib/jwtUtil';
export * from './interfaces/token';

/**
* Default instance of IssuerCertLoader
*/
let certLoaderInstance = new IssuerCertLoader();

/**
* Cache for resolved token payloads, default is in memory cache
*/
let tokenCache: ICacheOrAsync<TokenPayload, RawJwtToken> = new ExpireCache<TokenPayload, RawJwtToken>();
/***
* Setup token cache for verified payloads, on production this should be encrypted if persisted
*/
export function setTokenCache(cache: ICacheOrAsync<TokenPayload, RawJwtToken>) {
tokenCache = cache;
}

export function setJwtLogger(logger: ILoggerLike) {
certLoaderInstance.setLogger(logger);
}

/**
* Setup cache for public certificates
*/
export function useCache(cacheFunctions: CertCache) {
return certLoaderInstance.setCache(cacheFunctions);
}

export function testGetCache(): ICacheOrAsync<TokenPayload> {
/* istanbul ignore else */
if (process.env.NODE_ENV === 'testing') {
return tokenCache;
} else {
throw new Error('only for testing');
}
}

export function setCertLoader(newIcl: IssuerCertLoader) {
/* istanbul ignore else */
if (process.env.NODE_ENV === 'testing') {
certLoaderInstance = newIcl;
} else {
throw new Error('only for testing');
}
}

function getKeyIdAndSetOptions(decoded: FullDecodedTokenStructure, options: jwt.VerifyOptions = {}) {
const {kid, alg, typ} = decoded.header || {};
if (!kid) {
throw new JwtHeaderError('token header: missing kid parameter');
}
if (typ !== 'JWT') {
throw new JwtHeaderError(`token header: type "${typ}" is not valid`);
}
if (alg) {
options.algorithms = [alg];
}
return kid;
}

/**
* Validate full decoded token object that body have "iss" set
* @param decoded complete jwt decode with header
* @param options jwt verification options
* @returns IIssuerTokenStructure which have "iss" and valid issuer if limited on options
*/
function haveValidIssuer(decoded: unknown, options: jwt.VerifyOptions): FullDecodedIssuerTokenStructure {
assertIsTokenFullDecoded(decoded);
assertIssuerToken(decoded);
if (options.issuer) {
// prevent loading rogue issuers data if not valid issuer
const allowedIssuers = Array.isArray(options.issuer) ? options.issuer : [options.issuer];
if (!allowedIssuers.includes(decoded.payload.iss)) {
throw new JwtHeaderError('token header: issuer is not valid');
}
}
return decoded;
}

/**
* Takes token or auth header and return JWT token string
*/
function getTokenString(tokenOrBearer: string): string {
const currentToken = getTokenOrAuthHeader(tokenOrBearer);
// if Header only allow bearer as auth type
if (currentToken instanceof AuthHeader && currentToken.type !== 'BEARER') {
throw new JwtHeaderError('token header: wrong authentication header type');
}
return currentToken instanceof AuthHeader ? currentToken.credentials : currentToken;
}

/**
* Response have decoded body and information if was already verified and returned from cache
*/
export type JwtResponse<T extends object> = {body: T & TokenPayload; isCached: boolean};
/**
* Verify JWT token against issuer public certs
* @param tokenOrBearer jwt token or Bearer string with jwt token
* @param options jwt verify options
*/
export async function jwtVerify<T extends object>(tokenOrBearer: string, options: jwt.VerifyOptions = {}): Promise<JwtResponse<T>> {
const token = getTokenString(tokenOrBearer);
if (!isRawJwtToken(token)) {
throw new JwtHeaderError('Not JWT token string format');
}
const cached = await tokenCache.get(token);
if (cached) {
return {body: cached as TokenPayload & T, isCached: true};
}
const decoded = haveValidIssuer(jwt.decode(token, {complete: true}), options);
const certString = await certLoaderInstance.getCert(decoded.payload.iss, getKeyIdAndSetOptions(decoded, options));
const verifiedDecode = (await jwtVerifyPromise(token, buildCertFrame(certString), options)) as T & TokenPayload;
if (verifiedDecode.exp) {
await tokenCache.set(token, verifiedDecode, new Date(verifiedDecode.exp * 1000));
}
return {body: verifiedDecode, isCached: false};
}

/**
* Verify auth "Bearer" header against issuer public certs
* @param authHeader raw authentication header with ^Bearer prefix
* @param options jwt verify options
*/
export function jwtBearerVerify<T extends object>(authHeader: string, options: jwt.VerifyOptions = {}): Promise<JwtResponse<T>> {
const match = authHeader.match(/^Bearer (.*?)$/);
if (!match) {
throw new Error('No authentication header');
}
return jwtVerify(match[1], options);
}

export function jwtDeleteKid(issuer: string, kid: string) {
certLoaderInstance.deleteKid(issuer, kid);
}

export function jwtHaveIssuer(issuer: string) {
return certLoaderInstance.haveIssuer(issuer);
}
export * from './cache';
export * from './lib';
export * from './interfaces';
5 changes: 5 additions & 0 deletions src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './CertRecords';
export * from './JsonWebKey';
export * from './OpenIdConfig';
export * from './OpenIdConfigCerts';
export * from './token';
2 changes: 1 addition & 1 deletion src/interfaces/token.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as jwt from 'jsonwebtoken';
import {JwtHeaderError} from '../JwtHeaderError';
import {JwtHeaderError} from '../lib/JwtHeaderError';

export type RawJwtToken = `${string}.${string}.${string}`;

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/lib/authUtil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {AuthHeader, isAuthHeaderLikeString} from '@avanio/auth-header';
import {JwtHeaderError} from '../JwtHeaderError';
import {JwtHeaderError} from './JwtHeaderError';

/**
* return AuthHeader instance or string
Expand Down
7 changes: 7 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './authUtil';
export * from './jwtUtil';
export * from './zodUtils';
export * from './rsaPublicKeyPem';
export * from './issuerCertLoader';
export * from './JwtHeaderError';
export * from './jwtValidate';
16 changes: 8 additions & 8 deletions src/issuerCertLoader.ts → src/lib/issuerCertLoader.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {CertIssuerRecord, CertRecords} from './interfaces/CertRecords';
import {CertIssuerRecord, CertRecords} from '../interfaces/CertRecords';
import {ExpireCache, ExpireCacheLogMapType} from '@avanio/expire-cache';
import {ILoggerLike, ISetOptionalLogger} from '@avanio/logger-like';
import {OpenIdConfig, openIdConfigSchema} from './interfaces/OpenIdConfig';
import {OpenIdConfigCerts, openIdConfigCertsSchema} from './interfaces/OpenIdConfigCerts';
import {CertCache} from './cache/CertCache';
import {formatZodError} from './lib/zodUtils';
import {JsonWebKey} from './interfaces/JsonWebKey';
import {OpenIdConfig, openIdConfigSchema} from '../interfaces/OpenIdConfig';
import {OpenIdConfigCerts, openIdConfigCertsSchema} from '../interfaces/OpenIdConfigCerts';
import {CertCache} from '../cache/CertCache';
import {formatZodError} from './zodUtils';
import {JsonWebKey} from '../interfaces/JsonWebKey';
import {posix as path} from 'path';
import {rsaPublicKeyPem} from './rsaPublicKeyPem';
import {URL} from 'url';
Expand Down Expand Up @@ -188,8 +188,8 @@ export class IssuerCertLoader implements ISetOptionalLogger {
private async fetchOpenIdConfig(issuerUrl: string): Promise<OpenIdConfig> {
const issuerObj = new URL(issuerUrl);
issuerObj.pathname = path.join(issuerObj.pathname, '/.well-known/openid-configuration');
this.logger?.debug(`jwt-util get JWT Configuration ${issuerObj.toString()}`);
const req = new Request(issuerObj.toString());
this.logger?.debug(`jwt-util get JWT Configuration ${issuerObj.href}`);
const req = new Request(issuerObj);
const res = await fetch(req);
const data = (await this.isValidResp(res).json()) as unknown;
this.isValidOpenIdConfig(data);
Expand Down
Loading

0 comments on commit 1db8b51

Please sign in to comment.