oidc-provider
Version:
OAuth 2.0 Authorization Server implementation for Node.js with OpenID Connect
208 lines (171 loc) • 5.84 kB
JavaScript
import {
CompactEncrypt,
CompactSign,
compactDecrypt,
compactVerify,
decodeJwt,
decodeProtectedHeader,
errors,
} from 'jose';
import { ExternalSigningKey } from './keystore.js';
import * as base64url from './base64url.js';
import epochTime from './epoch_time.js';
const { JWEDecryptionFailed, JWKSNoMatchingKey, JWSSignatureVerificationFailed } = errors;
function verifyAudience({ aud }, expected) {
if (Array.isArray(aud)) {
const match = aud.some((actual) => actual === expected);
if (!match) throw new Error(`jwt audience missing ${expected}`);
} else if (aud !== expected) {
throw new Error(`jwt audience missing ${expected}`);
}
}
export async function sign(payload, key, alg, options = {}) {
const protectedHeader = {
alg,
typ: options.typ,
...options.fields,
};
const timestamp = epochTime();
const iat = options.noIat ? undefined : timestamp;
Object.assign(payload, {
aud: options.audience !== undefined ? options.audience : payload.aud,
exp: options.expiresIn !== undefined ? timestamp + options.expiresIn : payload.exp,
iat: payload.iat !== undefined ? payload.iat : iat,
iss: options.issuer !== undefined ? options.issuer : payload.iss,
sub: options.subject !== undefined ? options.subject : payload.sub,
});
if (key instanceof ExternalSigningKey) {
const parts = [
base64url.encode(JSON.stringify(protectedHeader)),
base64url.encode(JSON.stringify(payload)),
];
const data = Buffer.from(parts.join('.'));
parts.push(base64url.encodeBuffer(await key.sign(data)));
return parts.join('.');
}
return new CompactSign(Buffer.from(JSON.stringify(payload)))
.setProtectedHeader(protectedHeader)
.sign(key);
}
export function decode(input) {
let jwt;
if (Buffer.isBuffer(input)) {
jwt = input.toString('utf8');
} else if (typeof input !== 'string') {
throw new TypeError('invalid JWT.decode input type');
} else {
jwt = input;
}
const { 0: protectedHeader, 1: payload, length } = jwt.split('.');
if (length !== 3) {
throw new TypeError('invalid JWT.decode input');
}
return {
header: JSON.parse(base64url.decode(protectedHeader)),
payload: JSON.parse(base64url.decode(payload)),
};
}
export function header(jwt) {
return JSON.parse(base64url.decode(jwt.toString().split('.')[0]));
}
export function assertPayload(payload, {
clockTolerance = 0, audience, ignoreExpiration,
ignoreAzp, ignoreIssued, ignoreNotBefore, issuer,
subject = false,
} = {}) {
const timestamp = epochTime();
if (typeof payload !== 'object') throw new Error('payload is not of JWT type (JSON serialized object)');
if (payload.nbf !== undefined && !ignoreNotBefore) {
if (typeof payload.nbf !== 'number') throw new Error('invalid nbf value');
if (payload.nbf > timestamp + clockTolerance) throw new Error('jwt not active yet');
}
if (payload.iat !== undefined && !ignoreIssued) {
if (typeof payload.iat !== 'number') throw new Error('invalid iat value');
if (payload.exp === undefined && payload.iat > timestamp + clockTolerance) {
throw new Error('jwt issued in the future');
}
}
if (payload.exp !== undefined && !ignoreExpiration) {
if (typeof payload.exp !== 'number') throw new Error('invalid exp value');
if (timestamp - clockTolerance >= payload.exp) throw new Error('jwt expired');
}
if (payload.jti !== undefined && typeof payload.jti !== 'string') {
throw new Error('invalid jti value');
}
if (payload.iss !== undefined && typeof payload.iss !== 'string') {
throw new Error('invalid iss value');
}
if (subject && typeof payload.sub !== 'string') {
throw new Error('invalid sub value');
}
if (audience) {
verifyAudience(
payload,
audience,
!ignoreAzp,
);
}
if (issuer && payload.iss !== issuer) throw new Error('jwt issuer invalid');
}
export async function verify(jwt, keystore, options = {}) {
let verified;
let protectedHeader;
try {
protectedHeader = decodeProtectedHeader(jwt);
const keys = keystore.selectForVerify({ alg: protectedHeader.alg, kid: protectedHeader.kid });
if (keys.length === 0) {
throw new JWKSNoMatchingKey();
} else {
for (const key of keys) {
try {
verified = await compactVerify(
jwt,
await keystore.getKeyObject(key, true),
{ algorithms: options.algorithm ? [options.algorithm] : undefined },
);
} catch {}
}
}
if (!verified) {
throw new JWSSignatureVerificationFailed();
}
} catch (err) {
if (typeof keystore.fresh !== 'function' || keystore.fresh()) {
throw err;
}
await keystore.refresh();
// eslint-disable-next-line prefer-rest-params
return verify(...arguments);
}
const payload = decodeJwt(jwt);
assertPayload(payload, options);
return { payload, header: protectedHeader };
}
export async function encrypt(cleartext, key, {
enc, alg, fields,
} = {}) {
const protectedHeader = {
alg, enc, ...fields,
};
return new CompactEncrypt(Buffer.from(cleartext))
.setProtectedHeader(protectedHeader)
.encrypt(key);
}
export async function decrypt(jwe, keystore) {
const protectedHeader = decodeProtectedHeader(jwe);
const keys = keystore.selectForDecrypt({ alg: protectedHeader.alg === 'dir' ? protectedHeader.enc : protectedHeader.alg, kid: protectedHeader.kid, epk: protectedHeader.epk });
let decrypted;
if (keys.length === 0) {
throw new JWKSNoMatchingKey();
} else {
for (const key of keys) {
try {
decrypted = await compactDecrypt(jwe, keystore.getKeyObject(key));
} catch {}
}
}
if (!decrypted) {
throw new JWEDecryptionFailed();
}
return Buffer.from(decrypted.plaintext);
}