oauth4webapi
Version:
Low-Level OAuth 2 / OpenID Connect Client API for JavaScript Runtimes
1,342 lines • 98.8 kB
JavaScript
let USER_AGENT;
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
const NAME = 'oauth4webapi';
const VERSION = 'v3.5.3';
USER_AGENT = `${NAME}/${VERSION}`;
}
function looseInstanceOf(input, expected) {
if (input == null) {
return false;
}
try {
return (input instanceof expected ||
Object.getPrototypeOf(input)[Symbol.toStringTag] === expected.prototype[Symbol.toStringTag]);
}
catch {
return false;
}
}
const ERR_INVALID_ARG_VALUE = 'ERR_INVALID_ARG_VALUE';
const ERR_INVALID_ARG_TYPE = 'ERR_INVALID_ARG_TYPE';
function CodedTypeError(message, code, cause) {
const err = new TypeError(message, { cause });
Object.assign(err, { code });
return err;
}
export const allowInsecureRequests = Symbol();
export const clockSkew = Symbol();
export const clockTolerance = Symbol();
export const customFetch = Symbol();
export const modifyAssertion = Symbol();
export const jweDecrypt = Symbol();
export const jwksCache = Symbol();
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function buf(input) {
if (typeof input === 'string') {
return encoder.encode(input);
}
return decoder.decode(input);
}
let encodeBase64Url;
if (Uint8Array.prototype.toBase64) {
encodeBase64Url = (input) => {
if (input instanceof ArrayBuffer) {
input = new Uint8Array(input);
}
return input.toBase64({ alphabet: 'base64url', omitPadding: true });
};
}
else {
const CHUNK_SIZE = 0x8000;
encodeBase64Url = (input) => {
if (input instanceof ArrayBuffer) {
input = new Uint8Array(input);
}
const arr = [];
for (let i = 0; i < input.byteLength; i += CHUNK_SIZE) {
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
}
return btoa(arr.join('')).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
};
}
let decodeBase64Url;
if (Uint8Array.fromBase64) {
decodeBase64Url = (input) => {
try {
return Uint8Array.fromBase64(input, { alphabet: 'base64url' });
}
catch (cause) {
throw CodedTypeError('The input to be decoded is not correctly encoded.', ERR_INVALID_ARG_VALUE, cause);
}
};
}
else {
decodeBase64Url = (input) => {
try {
const binary = atob(input.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''));
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
catch (cause) {
throw CodedTypeError('The input to be decoded is not correctly encoded.', ERR_INVALID_ARG_VALUE, cause);
}
};
}
function b64u(input) {
if (typeof input === 'string') {
return decodeBase64Url(input);
}
return encodeBase64Url(input);
}
export class UnsupportedOperationError extends Error {
code;
constructor(message, options) {
super(message, options);
this.name = this.constructor.name;
this.code = UNSUPPORTED_OPERATION;
Error.captureStackTrace?.(this, this.constructor);
}
}
export class OperationProcessingError extends Error {
code;
constructor(message, options) {
super(message, options);
this.name = this.constructor.name;
if (options?.code) {
this.code = options?.code;
}
Error.captureStackTrace?.(this, this.constructor);
}
}
function OPE(message, code, cause) {
return new OperationProcessingError(message, { code, cause });
}
function assertCryptoKey(key, it) {
if (!(key instanceof CryptoKey)) {
throw CodedTypeError(`${it} must be a CryptoKey`, ERR_INVALID_ARG_TYPE);
}
}
function assertPrivateKey(key, it) {
assertCryptoKey(key, it);
if (key.type !== 'private') {
throw CodedTypeError(`${it} must be a private CryptoKey`, ERR_INVALID_ARG_VALUE);
}
}
function assertPublicKey(key, it) {
assertCryptoKey(key, it);
if (key.type !== 'public') {
throw CodedTypeError(`${it} must be a public CryptoKey`, ERR_INVALID_ARG_VALUE);
}
}
function normalizeTyp(value) {
return value.toLowerCase().replace(/^application\//, '');
}
function isJsonObject(input) {
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
return false;
}
return true;
}
function prepareHeaders(input) {
if (looseInstanceOf(input, Headers)) {
input = Object.fromEntries(input.entries());
}
const headers = new Headers(input ?? {});
if (USER_AGENT && !headers.has('user-agent')) {
headers.set('user-agent', USER_AGENT);
}
if (headers.has('authorization')) {
throw CodedTypeError('"options.headers" must not include the "authorization" header name', ERR_INVALID_ARG_VALUE);
}
return headers;
}
function signal(value) {
if (typeof value === 'function') {
value = value();
}
if (!(value instanceof AbortSignal)) {
throw CodedTypeError('"options.signal" must return or be an instance of AbortSignal', ERR_INVALID_ARG_TYPE);
}
return value;
}
function replaceDoubleSlash(pathname) {
if (pathname.includes('//')) {
return pathname.replace('//', '/');
}
return pathname;
}
function prependWellKnown(url, wellKnown) {
if (url.pathname === '/') {
url.pathname = wellKnown;
}
else {
url.pathname = replaceDoubleSlash(`${wellKnown}/${url.pathname}`);
}
return url;
}
function appendWellKnown(url, wellKnown) {
url.pathname = replaceDoubleSlash(`${url.pathname}/${wellKnown}`);
return url;
}
async function performDiscovery(input, urlName, transform, options) {
if (!(input instanceof URL)) {
throw CodedTypeError(`"${urlName}" must be an instance of URL`, ERR_INVALID_ARG_TYPE);
}
checkProtocol(input, options?.[allowInsecureRequests] !== true);
const url = transform(new URL(input.href));
const headers = prepareHeaders(options?.headers);
headers.set('accept', 'application/json');
return (options?.[customFetch] || fetch)(url.href, {
body: undefined,
headers: Object.fromEntries(headers.entries()),
method: 'GET',
redirect: 'manual',
signal: options?.signal ? signal(options.signal) : undefined,
});
}
export async function discoveryRequest(issuerIdentifier, options) {
return performDiscovery(issuerIdentifier, 'issuerIdentifier', (url) => {
switch (options?.algorithm) {
case undefined:
case 'oidc':
appendWellKnown(url, '.well-known/openid-configuration');
break;
case 'oauth2':
prependWellKnown(url, '.well-known/oauth-authorization-server');
break;
default:
throw CodedTypeError('"options.algorithm" must be "oidc" (default), or "oauth2"', ERR_INVALID_ARG_VALUE);
}
return url;
}, options);
}
function assertNumber(input, allow0, it, code, cause) {
try {
if (typeof input !== 'number' || !Number.isFinite(input)) {
throw CodedTypeError(`${it} must be a number`, ERR_INVALID_ARG_TYPE, cause);
}
if (input > 0)
return;
if (allow0) {
if (input !== 0) {
throw CodedTypeError(`${it} must be a non-negative number`, ERR_INVALID_ARG_VALUE, cause);
}
return;
}
throw CodedTypeError(`${it} must be a positive number`, ERR_INVALID_ARG_VALUE, cause);
}
catch (err) {
if (code) {
throw OPE(err.message, code, cause);
}
throw err;
}
}
function assertString(input, it, code, cause) {
try {
if (typeof input !== 'string') {
throw CodedTypeError(`${it} must be a string`, ERR_INVALID_ARG_TYPE, cause);
}
if (input.length === 0) {
throw CodedTypeError(`${it} must not be empty`, ERR_INVALID_ARG_VALUE, cause);
}
}
catch (err) {
if (code) {
throw OPE(err.message, code, cause);
}
throw err;
}
}
export async function processDiscoveryResponse(expectedIssuerIdentifier, response) {
const expected = expectedIssuerIdentifier;
if (!(expected instanceof URL) && expected !== _nodiscoverycheck) {
throw CodedTypeError('"expectedIssuerIdentifier" must be an instance of URL', ERR_INVALID_ARG_TYPE);
}
if (!looseInstanceOf(response, Response)) {
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
}
if (response.status !== 200) {
throw OPE('"response" is not a conform Authorization Server Metadata response (unexpected HTTP status code)', RESPONSE_IS_NOT_CONFORM, response);
}
assertReadableResponse(response);
const json = await getResponseJsonBody(response);
assertString(json.issuer, '"response" body "issuer" property', INVALID_RESPONSE, { body: json });
if (expected !== _nodiscoverycheck && new URL(json.issuer).href !== expected.href) {
throw OPE('"response" body "issuer" property does not match the expected value', JSON_ATTRIBUTE_COMPARISON, { expected: expected.href, body: json, attribute: 'issuer' });
}
return json;
}
function assertApplicationJson(response) {
assertContentType(response, 'application/json');
}
function notJson(response, ...types) {
let msg = '"response" content-type must be ';
if (types.length > 2) {
const last = types.pop();
msg += `${types.join(', ')}, or ${last}`;
}
else if (types.length === 2) {
msg += `${types[0]} or ${types[1]}`;
}
else {
msg += types[0];
}
return OPE(msg, RESPONSE_IS_NOT_JSON, response);
}
function assertContentTypes(response, ...types) {
if (!types.includes(getContentType(response))) {
throw notJson(response, ...types);
}
}
function assertContentType(response, contentType) {
if (getContentType(response) !== contentType) {
throw notJson(response, contentType);
}
}
function randomBytes() {
return b64u(crypto.getRandomValues(new Uint8Array(32)));
}
export function generateRandomCodeVerifier() {
return randomBytes();
}
export function generateRandomState() {
return randomBytes();
}
export function generateRandomNonce() {
return randomBytes();
}
export async function calculatePKCECodeChallenge(codeVerifier) {
assertString(codeVerifier, 'codeVerifier');
return b64u(await crypto.subtle.digest('SHA-256', buf(codeVerifier)));
}
function getKeyAndKid(input) {
if (input instanceof CryptoKey) {
return { key: input };
}
if (!(input?.key instanceof CryptoKey)) {
return {};
}
if (input.kid !== undefined) {
assertString(input.kid, '"kid"');
}
return {
key: input.key,
kid: input.kid,
};
}
function psAlg(key) {
switch (key.algorithm.hash.name) {
case 'SHA-256':
return 'PS256';
case 'SHA-384':
return 'PS384';
case 'SHA-512':
return 'PS512';
default:
throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name', {
cause: key,
});
}
}
function rsAlg(key) {
switch (key.algorithm.hash.name) {
case 'SHA-256':
return 'RS256';
case 'SHA-384':
return 'RS384';
case 'SHA-512':
return 'RS512';
default:
throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name', {
cause: key,
});
}
}
function esAlg(key) {
switch (key.algorithm.namedCurve) {
case 'P-256':
return 'ES256';
case 'P-384':
return 'ES384';
case 'P-521':
return 'ES512';
default:
throw new UnsupportedOperationError('unsupported EcKeyAlgorithm namedCurve', { cause: key });
}
}
function keyToJws(key) {
switch (key.algorithm.name) {
case 'RSA-PSS':
return psAlg(key);
case 'RSASSA-PKCS1-v1_5':
return rsAlg(key);
case 'ECDSA':
return esAlg(key);
case 'Ed25519':
case 'EdDSA':
return 'Ed25519';
default:
throw new UnsupportedOperationError('unsupported CryptoKey algorithm name', { cause: key });
}
}
function getClockSkew(client) {
const skew = client?.[clockSkew];
return typeof skew === 'number' && Number.isFinite(skew) ? skew : 0;
}
function getClockTolerance(client) {
const tolerance = client?.[clockTolerance];
return typeof tolerance === 'number' && Number.isFinite(tolerance) && Math.sign(tolerance) !== -1
? tolerance
: 30;
}
function epochTime() {
return Math.floor(Date.now() / 1000);
}
function assertAs(as) {
if (typeof as !== 'object' || as === null) {
throw CodedTypeError('"as" must be an object', ERR_INVALID_ARG_TYPE);
}
assertString(as.issuer, '"as.issuer"');
}
function assertClient(client) {
if (typeof client !== 'object' || client === null) {
throw CodedTypeError('"client" must be an object', ERR_INVALID_ARG_TYPE);
}
assertString(client.client_id, '"client.client_id"');
}
function formUrlEncode(token) {
return encodeURIComponent(token).replace(/(?:[-_.!~*'()]|%20)/g, (substring) => {
switch (substring) {
case '-':
case '_':
case '.':
case '!':
case '~':
case '*':
case "'":
case '(':
case ')':
return `%${substring.charCodeAt(0).toString(16).toUpperCase()}`;
case '%20':
return '+';
default:
throw new Error();
}
});
}
export function ClientSecretPost(clientSecret) {
assertString(clientSecret, '"clientSecret"');
return (_as, client, body, _headers) => {
body.set('client_id', client.client_id);
body.set('client_secret', clientSecret);
};
}
export function ClientSecretBasic(clientSecret) {
assertString(clientSecret, '"clientSecret"');
return (_as, client, _body, headers) => {
const username = formUrlEncode(client.client_id);
const password = formUrlEncode(clientSecret);
const credentials = btoa(`${username}:${password}`);
headers.set('authorization', `Basic ${credentials}`);
};
}
function clientAssertionPayload(as, client) {
const now = epochTime() + getClockSkew(client);
return {
jti: randomBytes(),
aud: as.issuer,
exp: now + 60,
iat: now,
nbf: now,
iss: client.client_id,
sub: client.client_id,
};
}
export function PrivateKeyJwt(clientPrivateKey, options) {
const { key, kid } = getKeyAndKid(clientPrivateKey);
assertPrivateKey(key, '"clientPrivateKey.key"');
return async (as, client, body, _headers) => {
const header = { alg: keyToJws(key), kid };
const payload = clientAssertionPayload(as, client);
options?.[modifyAssertion]?.(header, payload);
body.set('client_id', client.client_id);
body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
body.set('client_assertion', await signJwt(header, payload, key));
};
}
export function ClientSecretJwt(clientSecret, options) {
assertString(clientSecret, '"clientSecret"');
const modify = options?.[modifyAssertion];
let key;
return async (as, client, body, _headers) => {
key ||= await crypto.subtle.importKey('raw', buf(clientSecret), { hash: 'SHA-256', name: 'HMAC' }, false, ['sign']);
const header = { alg: 'HS256' };
const payload = clientAssertionPayload(as, client);
modify?.(header, payload);
const data = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(payload)))}`;
const hmac = await crypto.subtle.sign(key.algorithm, key, buf(data));
body.set('client_id', client.client_id);
body.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
body.set('client_assertion', `${data}.${b64u(new Uint8Array(hmac))}`);
};
}
export function None() {
return (_as, client, body, _headers) => {
body.set('client_id', client.client_id);
};
}
export function TlsClientAuth() {
return None();
}
async function signJwt(header, payload, key) {
if (!key.usages.includes('sign')) {
throw CodedTypeError('CryptoKey instances used for signing assertions must include "sign" in their "usages"', ERR_INVALID_ARG_VALUE);
}
const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(payload)))}`;
const signature = b64u(await crypto.subtle.sign(keyToSubtle(key), key, buf(input)));
return `${input}.${signature}`;
}
export async function issueRequestObject(as, client, parameters, privateKey, options) {
assertAs(as);
assertClient(client);
parameters = new URLSearchParams(parameters);
const { key, kid } = getKeyAndKid(privateKey);
assertPrivateKey(key, '"privateKey.key"');
parameters.set('client_id', client.client_id);
const now = epochTime() + getClockSkew(client);
const claims = {
...Object.fromEntries(parameters.entries()),
jti: randomBytes(),
aud: as.issuer,
exp: now + 60,
iat: now,
nbf: now,
iss: client.client_id,
};
let resource;
if (parameters.has('resource') &&
(resource = parameters.getAll('resource')) &&
resource.length > 1) {
claims.resource = resource;
}
{
let value = parameters.get('max_age');
if (value !== null) {
claims.max_age = parseInt(value, 10);
assertNumber(claims.max_age, true, '"max_age" parameter');
}
}
{
let value = parameters.get('claims');
if (value !== null) {
try {
claims.claims = JSON.parse(value);
}
catch (cause) {
throw OPE('failed to parse the "claims" parameter as JSON', PARSE_ERROR, cause);
}
if (!isJsonObject(claims.claims)) {
throw CodedTypeError('"claims" parameter must be a JSON with a top level object', ERR_INVALID_ARG_VALUE);
}
}
}
{
let value = parameters.get('authorization_details');
if (value !== null) {
try {
claims.authorization_details = JSON.parse(value);
}
catch (cause) {
throw OPE('failed to parse the "authorization_details" parameter as JSON', PARSE_ERROR, cause);
}
if (!Array.isArray(claims.authorization_details)) {
throw CodedTypeError('"authorization_details" parameter must be a JSON with a top level array', ERR_INVALID_ARG_VALUE);
}
}
}
const header = {
alg: keyToJws(key),
typ: 'oauth-authz-req+jwt',
kid,
};
options?.[modifyAssertion]?.(header, claims);
return signJwt(header, claims, key);
}
let jwkCache;
async function getSetPublicJwkCache(key) {
const { kty, e, n, x, y, crv } = await crypto.subtle.exportKey('jwk', key);
const jwk = { kty, e, n, x, y, crv };
jwkCache.set(key, jwk);
return jwk;
}
async function publicJwk(key) {
jwkCache ||= new WeakMap();
return jwkCache.get(key) || getSetPublicJwkCache(key);
}
const URLParse = URL.parse
?
(url, base) => URL.parse(url, base)
: (url, base) => {
try {
return new URL(url, base);
}
catch {
return null;
}
};
export function checkProtocol(url, enforceHttps) {
if (enforceHttps && url.protocol !== 'https:') {
throw OPE('only requests to HTTPS are allowed', HTTP_REQUEST_FORBIDDEN, url);
}
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
throw OPE('only HTTP and HTTPS requests are allowed', REQUEST_PROTOCOL_FORBIDDEN, url);
}
}
function validateEndpoint(value, endpoint, useMtlsAlias, enforceHttps) {
let url;
if (typeof value !== 'string' || !(url = URLParse(value))) {
throw OPE(`authorization server metadata does not contain a valid ${useMtlsAlias ? `"as.mtls_endpoint_aliases.${endpoint}"` : `"as.${endpoint}"`}`, value === undefined ? MISSING_SERVER_METADATA : INVALID_SERVER_METADATA, { attribute: useMtlsAlias ? `mtls_endpoint_aliases.${endpoint}` : endpoint });
}
checkProtocol(url, enforceHttps);
return url;
}
export function resolveEndpoint(as, endpoint, useMtlsAlias, enforceHttps) {
if (useMtlsAlias && as.mtls_endpoint_aliases && endpoint in as.mtls_endpoint_aliases) {
return validateEndpoint(as.mtls_endpoint_aliases[endpoint], endpoint, useMtlsAlias, enforceHttps);
}
return validateEndpoint(as[endpoint], endpoint, useMtlsAlias, enforceHttps);
}
export async function pushedAuthorizationRequest(as, client, clientAuthentication, parameters, options) {
assertAs(as);
assertClient(client);
const url = resolveEndpoint(as, 'pushed_authorization_request_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
const body = new URLSearchParams(parameters);
body.set('client_id', client.client_id);
const headers = prepareHeaders(options?.headers);
headers.set('accept', 'application/json');
if (options?.DPoP !== undefined) {
assertDPoP(options.DPoP);
await options.DPoP.addProof(url, headers, 'POST');
}
const response = await authenticatedRequest(as, client, clientAuthentication, url, body, headers, options);
options?.DPoP?.cacheNonce(response);
return response;
}
class DPoPHandler {
#header;
#privateKey;
#publicKey;
#clockSkew;
#modifyAssertion;
#map;
#jkt;
constructor(client, keyPair, options) {
assertPrivateKey(keyPair?.privateKey, '"DPoP.privateKey"');
assertPublicKey(keyPair?.publicKey, '"DPoP.publicKey"');
if (!keyPair.publicKey.extractable) {
throw CodedTypeError('"DPoP.publicKey.extractable" must be true', ERR_INVALID_ARG_VALUE);
}
this.#modifyAssertion = options?.[modifyAssertion];
this.#clockSkew = getClockSkew(client);
this.#privateKey = keyPair.privateKey;
this.#publicKey = keyPair.publicKey;
branded.add(this);
}
#get(key) {
this.#map ||= new Map();
let item = this.#map.get(key);
if (item) {
this.#map.delete(key);
this.#map.set(key, item);
}
return item;
}
#set(key, val) {
this.#map ||= new Map();
this.#map.delete(key);
if (this.#map.size === 100) {
this.#map.delete(this.#map.keys().next().value);
}
this.#map.set(key, val);
}
async calculateThumbprint() {
if (!this.#jkt) {
const jwk = await crypto.subtle.exportKey('jwk', this.#publicKey);
let components;
switch (jwk.kty) {
case 'EC':
components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
break;
case 'OKP':
components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x };
break;
case 'RSA':
components = { e: jwk.e, kty: jwk.kty, n: jwk.n };
break;
default:
throw new UnsupportedOperationError('unsupported JWK', { cause: { jwk } });
}
this.#jkt ||= b64u(await crypto.subtle.digest({ name: 'SHA-256' }, buf(JSON.stringify(components))));
}
return this.#jkt;
}
async addProof(url, headers, htm, accessToken) {
this.#header ||= {
alg: keyToJws(this.#privateKey),
typ: 'dpop+jwt',
jwk: await publicJwk(this.#publicKey),
};
const nonce = this.#get(url.origin);
const now = epochTime() + this.#clockSkew;
const payload = {
iat: now,
jti: randomBytes(),
htm,
nonce,
htu: `${url.origin}${url.pathname}`,
ath: accessToken ? b64u(await crypto.subtle.digest('SHA-256', buf(accessToken))) : undefined,
};
this.#modifyAssertion?.(this.#header, payload);
headers.set('dpop', await signJwt(this.#header, payload, this.#privateKey));
}
cacheNonce(response) {
try {
const nonce = response.headers.get('dpop-nonce');
if (nonce) {
this.#set(new URL(response.url).origin, nonce);
}
}
catch { }
}
}
export function isDPoPNonceError(err) {
if (err instanceof WWWAuthenticateChallengeError) {
const { 0: challenge, length } = err.cause;
return (length === 1 && challenge.scheme === 'dpop' && challenge.parameters.error === 'use_dpop_nonce');
}
if (err instanceof ResponseBodyError) {
return err.error === 'use_dpop_nonce';
}
return false;
}
export function DPoP(client, keyPair, options) {
return new DPoPHandler(client, keyPair, options);
}
export class ResponseBodyError extends Error {
cause;
code;
error;
status;
error_description;
response;
constructor(message, options) {
super(message, options);
this.name = this.constructor.name;
this.code = RESPONSE_BODY_ERROR;
this.cause = options.cause;
this.error = options.cause.error;
this.status = options.response.status;
this.error_description = options.cause.error_description;
Object.defineProperty(this, 'response', { enumerable: false, value: options.response });
Error.captureStackTrace?.(this, this.constructor);
}
}
export class AuthorizationResponseError extends Error {
cause;
code;
error;
error_description;
constructor(message, options) {
super(message, options);
this.name = this.constructor.name;
this.code = AUTHORIZATION_RESPONSE_ERROR;
this.cause = options.cause;
this.error = options.cause.get('error');
this.error_description = options.cause.get('error_description') ?? undefined;
Error.captureStackTrace?.(this, this.constructor);
}
}
export class WWWAuthenticateChallengeError extends Error {
cause;
code;
response;
status;
constructor(message, options) {
super(message, options);
this.name = this.constructor.name;
this.code = WWW_AUTHENTICATE_CHALLENGE;
this.cause = options.cause;
this.status = options.response.status;
this.response = options.response;
Object.defineProperty(this, 'response', { enumerable: false });
Error.captureStackTrace?.(this, this.constructor);
}
}
const tokenMatch = "[a-zA-Z0-9!#$%&\\'\\*\\+\\-\\.\\^_`\\|~]+";
const token68Match = '[a-zA-Z0-9\\-\\._\\~\\+\\/]+[=]{0,2}';
const quotedMatch = '"((?:[^"\\\\]|\\\\.)*)"';
const quotedParamMatcher = '(' + tokenMatch + ')\\s*=\\s*' + quotedMatch;
const paramMatcher = '(' + tokenMatch + ')\\s*=\\s*(' + tokenMatch + ')';
const schemeRE = new RegExp('^[,\\s]*(' + tokenMatch + ')\\s(.*)');
const quotedParamRE = new RegExp('^[,\\s]*' + quotedParamMatcher + '[,\\s]*(.*)');
const unquotedParamRE = new RegExp('^[,\\s]*' + paramMatcher + '[,\\s]*(.*)');
const token68ParamRE = new RegExp('^(' + token68Match + ')(?:$|[,\\s])(.*)');
function parseWwwAuthenticateChallenges(response) {
if (!looseInstanceOf(response, Response)) {
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
}
const header = response.headers.get('www-authenticate');
if (header === null) {
return undefined;
}
const challenges = [];
let rest = header;
while (rest) {
let match = rest.match(schemeRE);
const scheme = match?.['1'].toLowerCase();
rest = match?.['2'];
if (!scheme) {
return undefined;
}
const parameters = {};
let token68;
while (rest) {
let key;
let value;
if ((match = rest.match(quotedParamRE))) {
;
[, key, value, rest] = match;
if (value.includes('\\')) {
try {
value = JSON.parse(`"${value}"`);
}
catch { }
}
parameters[key.toLowerCase()] = value;
continue;
}
if ((match = rest.match(unquotedParamRE))) {
;
[, key, value, rest] = match;
parameters[key.toLowerCase()] = value;
continue;
}
if ((match = rest.match(token68ParamRE))) {
if (Object.keys(parameters).length) {
break;
}
;
[, token68, rest] = match;
break;
}
return undefined;
}
const challenge = { scheme, parameters };
if (token68) {
challenge.token68 = token68;
}
challenges.push(challenge);
}
if (!challenges.length) {
return undefined;
}
return challenges;
}
export async function processPushedAuthorizationResponse(as, client, response) {
assertAs(as);
assertClient(client);
if (!looseInstanceOf(response, Response)) {
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
}
checkAuthenticationChallenges(response);
await checkOAuthBodyError(response, 201, 'Pushed Authorization Request Endpoint');
assertReadableResponse(response);
const json = await getResponseJsonBody(response);
assertString(json.request_uri, '"response" body "request_uri" property', INVALID_RESPONSE, {
body: json,
});
let expiresIn = typeof json.expires_in !== 'number' ? parseFloat(json.expires_in) : json.expires_in;
assertNumber(expiresIn, false, '"response" body "expires_in" property', INVALID_RESPONSE, {
body: json,
});
json.expires_in = expiresIn;
return json;
}
async function parseOAuthResponseErrorBody(response) {
if (response.status > 399 && response.status < 500) {
assertReadableResponse(response);
assertApplicationJson(response);
try {
const json = await response.clone().json();
if (isJsonObject(json) && typeof json.error === 'string' && json.error.length) {
return json;
}
}
catch { }
}
return undefined;
}
async function checkOAuthBodyError(response, expected, label) {
if (response.status !== expected) {
let err;
if ((err = await parseOAuthResponseErrorBody(response))) {
await response.body?.cancel();
throw new ResponseBodyError('server responded with an error in the response body', {
cause: err,
response,
});
}
throw OPE(`"response" is not a conform ${label} response (unexpected HTTP status code)`, RESPONSE_IS_NOT_CONFORM, response);
}
}
function assertDPoP(option) {
if (!branded.has(option)) {
throw CodedTypeError('"options.DPoP" is not a valid DPoPHandle', ERR_INVALID_ARG_VALUE);
}
}
async function resourceRequest(accessToken, method, url, headers, body, options) {
assertString(accessToken, '"accessToken"');
if (!(url instanceof URL)) {
throw CodedTypeError('"url" must be an instance of URL', ERR_INVALID_ARG_TYPE);
}
checkProtocol(url, options?.[allowInsecureRequests] !== true);
headers = prepareHeaders(headers);
if (options?.DPoP) {
assertDPoP(options.DPoP);
await options.DPoP.addProof(url, headers, method.toUpperCase(), accessToken);
}
headers.set('authorization', `${headers.has('dpop') ? 'DPoP' : 'Bearer'} ${accessToken}`);
const response = await (options?.[customFetch] || fetch)(url.href, {
body,
headers: Object.fromEntries(headers.entries()),
method,
redirect: 'manual',
signal: options?.signal ? signal(options.signal) : undefined,
});
options?.DPoP?.cacheNonce(response);
return response;
}
export async function protectedResourceRequest(accessToken, method, url, headers, body, options) {
const response = await resourceRequest(accessToken, method, url, headers, body, options);
checkAuthenticationChallenges(response);
return response;
}
export async function userInfoRequest(as, client, accessToken, options) {
assertAs(as);
assertClient(client);
const url = resolveEndpoint(as, 'userinfo_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
const headers = prepareHeaders(options?.headers);
if (client.userinfo_signed_response_alg) {
headers.set('accept', 'application/jwt');
}
else {
headers.set('accept', 'application/json');
headers.append('accept', 'application/jwt');
}
return resourceRequest(accessToken, 'GET', url, headers, null, {
...options,
[clockSkew]: getClockSkew(client),
});
}
let jwksMap;
function setJwksCache(as, jwks, uat, cache) {
jwksMap ||= new WeakMap();
jwksMap.set(as, {
jwks,
uat,
get age() {
return epochTime() - this.uat;
},
});
if (cache) {
Object.assign(cache, { jwks: structuredClone(jwks), uat });
}
}
function isFreshJwksCache(input) {
if (typeof input !== 'object' || input === null) {
return false;
}
if (!('uat' in input) || typeof input.uat !== 'number' || epochTime() - input.uat >= 300) {
return false;
}
if (!('jwks' in input) ||
!isJsonObject(input.jwks) ||
!Array.isArray(input.jwks.keys) ||
!Array.prototype.every.call(input.jwks.keys, isJsonObject)) {
return false;
}
return true;
}
function clearJwksCache(as, cache) {
jwksMap?.delete(as);
delete cache?.jwks;
delete cache?.uat;
}
async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
const { alg, kid } = header;
checkSupportedJwsAlg(header);
if (!jwksMap?.has(as) && isFreshJwksCache(options?.[jwksCache])) {
setJwksCache(as, options?.[jwksCache].jwks, options?.[jwksCache].uat);
}
let jwks;
let age;
if (jwksMap?.has(as)) {
;
({ jwks, age } = jwksMap.get(as));
if (age >= 300) {
clearJwksCache(as, options?.[jwksCache]);
return getPublicSigKeyFromIssuerJwksUri(as, options, header);
}
}
else {
jwks = await jwksRequest(as, options).then(processJwksResponse);
age = 0;
setJwksCache(as, jwks, epochTime(), options?.[jwksCache]);
}
let kty;
switch (alg.slice(0, 2)) {
case 'RS':
case 'PS':
kty = 'RSA';
break;
case 'ES':
kty = 'EC';
break;
case 'Ed':
kty = 'OKP';
break;
default:
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg } });
}
const candidates = jwks.keys.filter((jwk) => {
if (jwk.kty !== kty) {
return false;
}
if (kid !== undefined && kid !== jwk.kid) {
return false;
}
if (jwk.alg !== undefined && alg !== jwk.alg) {
return false;
}
if (jwk.use !== undefined && jwk.use !== 'sig') {
return false;
}
if (jwk.key_ops?.includes('verify') === false) {
return false;
}
switch (true) {
case alg === 'ES256' && jwk.crv !== 'P-256':
case alg === 'ES384' && jwk.crv !== 'P-384':
case alg === 'ES512' && jwk.crv !== 'P-521':
case alg === 'Ed25519' && jwk.crv !== 'Ed25519':
case alg === 'EdDSA' && jwk.crv !== 'Ed25519':
return false;
}
return true;
});
const { 0: jwk, length } = candidates;
if (!length) {
if (age >= 60) {
clearJwksCache(as, options?.[jwksCache]);
return getPublicSigKeyFromIssuerJwksUri(as, options, header);
}
throw OPE('error when selecting a JWT verification key, no applicable keys found', KEY_SELECTION, { header, candidates, jwks_uri: new URL(as.jwks_uri) });
}
if (length !== 1) {
throw OPE('error when selecting a JWT verification key, multiple applicable keys found, a "kid" JWT Header Parameter is required', KEY_SELECTION, { header, candidates, jwks_uri: new URL(as.jwks_uri) });
}
return importJwk(alg, jwk);
}
export const skipSubjectCheck = Symbol();
export function getContentType(input) {
return input.headers.get('content-type')?.split(';')[0];
}
export async function processUserInfoResponse(as, client, expectedSubject, response, options) {
assertAs(as);
assertClient(client);
if (!looseInstanceOf(response, Response)) {
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
}
checkAuthenticationChallenges(response);
if (response.status !== 200) {
throw OPE('"response" is not a conform UserInfo Endpoint response (unexpected HTTP status code)', RESPONSE_IS_NOT_CONFORM, response);
}
assertReadableResponse(response);
let json;
if (getContentType(response) === 'application/jwt') {
const { claims, jwt } = await validateJwt(await response.text(), checkSigningAlgorithm.bind(undefined, client.userinfo_signed_response_alg, as.userinfo_signing_alg_values_supported, undefined), getClockSkew(client), getClockTolerance(client), options?.[jweDecrypt])
.then(validateOptionalAudience.bind(undefined, client.client_id))
.then(validateOptionalIssuer.bind(undefined, as));
jwtRefs.set(response, jwt);
json = claims;
}
else {
if (client.userinfo_signed_response_alg) {
throw OPE('JWT UserInfo Response expected', JWT_USERINFO_EXPECTED, response);
}
json = await getResponseJsonBody(response);
}
assertString(json.sub, '"response" body "sub" property', INVALID_RESPONSE, { body: json });
switch (expectedSubject) {
case skipSubjectCheck:
break;
default:
assertString(expectedSubject, '"expectedSubject"');
if (json.sub !== expectedSubject) {
throw OPE('unexpected "response" body "sub" property value', JSON_ATTRIBUTE_COMPARISON, {
expected: expectedSubject,
body: json,
attribute: 'sub',
});
}
}
return json;
}
async function authenticatedRequest(as, client, clientAuthentication, url, body, headers, options) {
await clientAuthentication(as, client, body, headers);
headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
return (options?.[customFetch] || fetch)(url.href, {
body,
headers: Object.fromEntries(headers.entries()),
method: 'POST',
redirect: 'manual',
signal: options?.signal ? signal(options.signal) : undefined,
});
}
async function tokenEndpointRequest(as, client, clientAuthentication, grantType, parameters, options) {
const url = resolveEndpoint(as, 'token_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
parameters.set('grant_type', grantType);
const headers = prepareHeaders(options?.headers);
headers.set('accept', 'application/json');
if (options?.DPoP !== undefined) {
assertDPoP(options.DPoP);
await options.DPoP.addProof(url, headers, 'POST');
}
const response = await authenticatedRequest(as, client, clientAuthentication, url, parameters, headers, options);
options?.DPoP?.cacheNonce(response);
return response;
}
export async function refreshTokenGrantRequest(as, client, clientAuthentication, refreshToken, options) {
assertAs(as);
assertClient(client);
assertString(refreshToken, '"refreshToken"');
const parameters = new URLSearchParams(options?.additionalParameters);
parameters.set('refresh_token', refreshToken);
return tokenEndpointRequest(as, client, clientAuthentication, 'refresh_token', parameters, options);
}
const idTokenClaims = new WeakMap();
const jwtRefs = new WeakMap();
export function getValidatedIdTokenClaims(ref) {
if (!ref.id_token) {
return undefined;
}
const claims = idTokenClaims.get(ref);
if (!claims) {
throw CodedTypeError('"ref" was already garbage collected or did not resolve from the proper sources', ERR_INVALID_ARG_VALUE);
}
return claims;
}
export async function validateApplicationLevelSignature(as, ref, options) {
assertAs(as);
if (!jwtRefs.has(ref)) {
throw CodedTypeError('"ref" does not contain a processed JWT Response to verify the signature of', ERR_INVALID_ARG_VALUE);
}
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = jwtRefs.get(ref).split('.');
const header = JSON.parse(buf(b64u(protectedHeader)));
if (header.alg.startsWith('HS')) {
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg: header.alg } });
}
let key;
key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
await validateJwsSignature(protectedHeader, payload, key, b64u(encodedSignature));
}
async function processGenericAccessTokenResponse(as, client, response, additionalRequiredIdTokenClaims, options) {
assertAs(as);
assertClient(client);
if (!looseInstanceOf(response, Response)) {
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
}
checkAuthenticationChallenges(response);
await checkOAuthBodyError(response, 200, 'Token Endpoint');
assertReadableResponse(response);
const json = await getResponseJsonBody(response);
assertString(json.access_token, '"response" body "access_token" property', INVALID_RESPONSE, {
body: json,
});
assertString(json.token_type, '"response" body "token_type" property', INVALID_RESPONSE, {
body: json,
});
json.token_type = json.token_type.toLowerCase();
if (json.token_type !== 'dpop' && json.token_type !== 'bearer') {
throw new UnsupportedOperationError('unsupported `token_type` value', { cause: { body: json } });
}
if (json.expires_in !== undefined) {
let expiresIn = typeof json.expires_in !== 'number' ? parseFloat(json.expires_in) : json.expires_in;
assertNumber(expiresIn, false, '"response" body "expires_in" property', INVALID_RESPONSE, {
body: json,
});
json.expires_in = expiresIn;
}
if (json.refresh_token !== undefined) {
assertString(json.refresh_token, '"response" body "refresh_token" property', INVALID_RESPONSE, {
body: json,
});
}
if (json.scope !== undefined && typeof json.scope !== 'string') {
throw OPE('"response" body "scope" property must be a string', INVALID_RESPONSE, { body: json });
}
if (json.id_token !== undefined) {
assertString(json.id_token, '"response" body "id_token" property', INVALID_RESPONSE, {
body: json,
});
const requiredClaims = ['aud', 'exp', 'iat', 'iss', 'sub'];
if (client.require_auth_time === true) {
requiredClaims.push('auth_time');
}
if (client.default_max_age !== undefined) {
assertNumber(client.default_max_age, false, '"client.default_max_age"');
requiredClaims.push('auth_time');
}
if (additionalRequiredIdTokenClaims?.length) {
requiredClaims.push(...additionalRequiredIdTokenClaims);
}
const { claims, jwt } = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported, 'RS256'), getClockSkew(client), getClockTolerance(client), options?.[jweDecrypt])
.then(validatePresence.bind(undefined, requiredClaims))
.then(validateIssuer.bind(undefined, as))
.then(validateAudience.bind(undefined, client.client_id));
if (Array.isArray(claims.aud) && claims.aud.length !== 1) {
if (claims.azp === undefined) {
throw OPE('ID Token "aud" (audience) claim includes additional untrusted audiences', JWT_CLAIM_COMPARISON, { claims, claim: 'aud' });
}
if (claims.azp !== client.client_id) {
throw OPE('unexpected ID Token "azp" (authorized party) claim value', JWT_CLAIM_COMPARISON, { expected: client.client_id, claims, claim: 'azp' });
}
}
if (claims.auth_time !== undefined) {
assertNumber(claims.auth_time, false, 'ID Token "auth_time" (authentication time)', INVALID_RESPONSE, { claims });
}
jwtRefs.set(response, jwt);
idTokenClaims.set(json, claims);
}
return json;
}
function checkAuthenticationChallenges(response) {
let challenges;
if ((challenges = parseWwwAuthenticateChallenges(response))) {
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', { cause: challenges, response });
}
}
export async function processRefreshTokenResponse(as, client, response, options) {
return processGenericAccessTokenResponse(as, client, response, undefined, options);
}
function validateOptionalAudience(expected, result) {
if (result.claims.aud !== undefined) {
return validateAudience(expected, result);
}
return result;
}
function validateAudience(expected, result) {
if (Array.isArray(result.claims.aud)) {
if (!result.claims.aud.includes(expected)) {
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
expected,
claims: result.claims,
claim: 'aud',
});
}
}
else if (result.claims.aud !== expected) {
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
expected,
claims: result.claims,
claim: 'aud',
});
}
return result;
}
function validateOptionalIssuer(as, result) {
if (result.claims.iss !== undefined) {
return validateIssuer(as, result);
}
return result;
}
function validateIssuer(as, result) {
const expected = as[_expectedIssuer]?.(result) ?? as.issuer;
if (result.claims.iss !== expected) {
throw OPE('unexpected JWT "iss" (issuer) claim value', JWT_CLAIM_COMPARISON, {
expected,
claims: result.claims,
claim: 'iss',
});
}
return result;
}
const branded = new WeakSet();
function brand(searchParams) {
branded.add(searchParams);
return searchParams;
}
export const nopkce = Symbol();
export async function authorizationCodeGrantRequest(as, client, clientAuthentication, callbackParameters, redirectUri, codeVerifier, options) {
assertAs(as);
assertClient(client);
if (!branded.has(callbackParameters)) {
throw CodedTypeError('"callbackParameters" must be an instance of URLSearchParams obtained from "validateAuthResponse()", or "validateJwtAuthResponse()', ERR_INVALID_ARG_VALUE);
}
assertString(redirectUri, '"redirectUri"');
const code = getURLSearchParameter(callbackParameters, 'code');
if (!code) {
throw OPE('no authorization code in "callbackParameters"', INVALID_RESPONSE);
}
const parameters = new URLSearchParams(options?.additionalParameters);
parameters.set('redirect_uri', redirectUri);
parameters.set('code', code);
if (codeVerifier !== nopkce) {
assertString(codeVerifier, '"codeVerifier"');
parameters.set('code_verifier', codeVerifier);
}
return tokenEndpointRequest(as, client, clientAuthentication, 'authorization_code', parameters, options);
}
const jwtClaimNames = {
aud: 'audience',
c_hash: 'code hash',
client_id: 'client id',
exp: 'expiration time',
iat: 'issued at',
iss: 'issuer',
jti: 'jwt id',
nonce: 'nonce',
s_hash: 'state hash',
sub: 'subject',
ath: 'access token hash',
htm: 'http method',
htu: 'http uri',
cnf: 'confirmation',
auth_time: 'authentication time',
};
function validatePresence(required, result) {
for (const claim of required) {
if (result.claims[claim] === undefined) {
throw OPE(`JWT "${claim}" (${jwtClaimNames[claim]}) claim missing`, INVALID_RESPONSE, {
claims: result.claims,
});
}
}
return result;
}
export const expectNoNonce = Symbol();
export const skipAuthTimeCheck = Symbol();
export async function processAuthorizationCodeResponse(as, client, response, options) {
if (typeof options?.expectedNonce === 'string' ||
typeof options?.maxAge === 'number' ||
options?.requireIdToken) {
return processAuthorizationCodeOpenIDResponse(as, client, response, options.expectedNonce, options.maxAge, {
[jweDecrypt]: options[jweDecrypt],
});
}
return processAuthorizationCodeOAuth2Response(as, client, respon