2025-12-03 20:54:35 +01:00

1310 lines
47 KiB
JavaScript

import * as oauth from 'oauth4webapi';
import { compactDecrypt } from 'jose/jwe/compact/decrypt';
import { JOSEError } from 'jose/errors';
let headers;
let USER_AGENT;
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
const NAME = 'openid-client';
const VERSION = 'v6.8.1';
USER_AGENT = `${NAME}/${VERSION}`;
headers = { 'user-agent': USER_AGENT };
}
const int = (config) => {
return props.get(config);
};
let props;
export { AuthorizationResponseError, ResponseBodyError, WWWAuthenticateChallengeError, } from 'oauth4webapi';
let tbi;
export function ClientSecretPost(clientSecret) {
if (clientSecret !== undefined) {
return oauth.ClientSecretPost(clientSecret);
}
tbi ||= new WeakMap();
return (as, client, body, headers) => {
let auth;
if (!(auth = tbi.get(client))) {
assertString(client.client_secret, '"metadata.client_secret"');
auth = oauth.ClientSecretPost(client.client_secret);
tbi.set(client, auth);
}
return auth(as, client, body, headers);
};
}
function assertString(input, it) {
if (typeof input !== 'string') {
throw CodedTypeError(`${it} must be a string`, ERR_INVALID_ARG_TYPE);
}
if (input.length === 0) {
throw CodedTypeError(`${it} must not be empty`, ERR_INVALID_ARG_VALUE);
}
}
export function ClientSecretBasic(clientSecret) {
if (clientSecret !== undefined) {
return oauth.ClientSecretBasic(clientSecret);
}
tbi ||= new WeakMap();
return (as, client, body, headers) => {
let auth;
if (!(auth = tbi.get(client))) {
assertString(client.client_secret, '"metadata.client_secret"');
auth = oauth.ClientSecretBasic(client.client_secret);
tbi.set(client, auth);
}
return auth(as, client, body, headers);
};
}
export function ClientSecretJwt(clientSecret, options) {
if (clientSecret !== undefined) {
return oauth.ClientSecretJwt(clientSecret, options);
}
tbi ||= new WeakMap();
return (as, client, body, headers) => {
let auth;
if (!(auth = tbi.get(client))) {
assertString(client.client_secret, '"metadata.client_secret"');
auth = oauth.ClientSecretJwt(client.client_secret, options);
tbi.set(client, auth);
}
return auth(as, client, body, headers);
};
}
export function None() {
return oauth.None();
}
export function PrivateKeyJwt(clientPrivateKey, options) {
return oauth.PrivateKeyJwt(clientPrivateKey, options);
}
export function TlsClientAuth() {
return oauth.TlsClientAuth();
}
export const skipStateCheck = oauth.skipStateCheck;
export const skipSubjectCheck = oauth.skipSubjectCheck;
export const customFetch = oauth.customFetch;
export const modifyAssertion = oauth.modifyAssertion;
export const clockSkew = oauth.clockSkew;
export const clockTolerance = oauth.clockTolerance;
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 function calculatePKCECodeChallenge(codeVerifier) {
return oauth.calculatePKCECodeChallenge(codeVerifier);
}
export function randomPKCECodeVerifier() {
return oauth.generateRandomCodeVerifier();
}
export function randomNonce() {
return oauth.generateRandomNonce();
}
export function randomState() {
return oauth.generateRandomState();
}
export class ClientError extends Error {
code;
constructor(message, options) {
super(message, options);
this.name = this.constructor.name;
this.code = options?.code;
Error.captureStackTrace?.(this, this.constructor);
}
}
const decoder = new TextDecoder();
function e(msg, cause, code) {
return new ClientError(msg, { cause, code });
}
function errorHandler(err) {
if (err instanceof TypeError ||
err instanceof ClientError ||
err instanceof oauth.ResponseBodyError ||
err instanceof oauth.AuthorizationResponseError ||
err instanceof oauth.WWWAuthenticateChallengeError) {
throw err;
}
if (err instanceof oauth.OperationProcessingError) {
switch (err.code) {
case oauth.HTTP_REQUEST_FORBIDDEN:
throw e('only requests to HTTPS are allowed', err, err.code);
case oauth.REQUEST_PROTOCOL_FORBIDDEN:
throw e('only requests to HTTP or HTTPS are allowed', err, err.code);
case oauth.RESPONSE_IS_NOT_CONFORM:
throw e('unexpected HTTP response status code', err.cause, err.code);
case oauth.RESPONSE_IS_NOT_JSON:
throw e('unexpected response content-type', err.cause, err.code);
case oauth.PARSE_ERROR:
throw e('parsing error occured', err, err.code);
case oauth.INVALID_RESPONSE:
throw e('invalid response encountered', err, err.code);
case oauth.JWT_CLAIM_COMPARISON:
throw e('unexpected JWT claim value encountered', err, err.code);
case oauth.JSON_ATTRIBUTE_COMPARISON:
throw e('unexpected JSON attribute value encountered', err, err.code);
case oauth.JWT_TIMESTAMP_CHECK:
throw e('JWT timestamp claim value failed validation', err, err.code);
default:
throw e(err.message, err, err.code);
}
}
if (err instanceof oauth.UnsupportedOperationError) {
throw e('unsupported operation', err, err.code);
}
if (err instanceof DOMException) {
switch (err.name) {
case 'OperationError':
throw e('runtime operation error', err, oauth.UNSUPPORTED_OPERATION);
case 'NotSupportedError':
throw e('runtime unsupported operation', err, oauth.UNSUPPORTED_OPERATION);
case 'TimeoutError':
throw e('operation timed out', err, 'OAUTH_TIMEOUT');
case 'AbortError':
throw e('operation aborted', err, 'OAUTH_ABORT');
}
}
throw new ClientError('something went wrong', { cause: err });
}
export function randomDPoPKeyPair(alg, options) {
return oauth
.generateKeyPair(alg ?? 'ES256', {
extractable: options?.extractable,
})
.catch(errorHandler);
}
function handleEntraId(server, as, options) {
if (server.origin === 'https://login.microsoftonline.com' &&
(!options?.algorithm || options.algorithm === 'oidc')) {
as[kEntraId] = true;
return true;
}
return false;
}
function handleB2Clogin(server, options) {
if (server.hostname.endsWith('.b2clogin.com') &&
(!options?.algorithm || options.algorithm === 'oidc')) {
return true;
}
return false;
}
export async function dynamicClientRegistration(server, metadata, clientAuthentication, options) {
let as;
if (options?.flag === retry) {
as = options.as;
}
else {
as = await performDiscovery(server, options);
}
const clockSkew = metadata[oauth.clockSkew] ?? 0;
const clockTolerance = metadata[oauth.clockTolerance] ?? 30;
metadata = structuredClone(metadata);
const timeout = options?.timeout ?? 30;
const signal = AbortSignal.timeout(timeout * 1000);
let registered;
try {
registered = await oauth
.dynamicClientRegistrationRequest(as, metadata, {
initialAccessToken: options?.initialAccessToken,
DPoP: options?.DPoP,
headers: new Headers(headers),
[oauth.customFetch]: options?.[customFetch],
[oauth.allowInsecureRequests]: options?.execute?.includes(allowInsecureRequests),
signal,
})
.then(oauth.processDynamicClientRegistrationResponse);
}
catch (err) {
if (retryable(err, options)) {
return dynamicClientRegistration(server, metadata, clientAuthentication, {
...options,
flag: retry,
as,
});
}
errorHandler(err);
}
registered[oauth.clockSkew] = clockSkew;
registered[oauth.clockTolerance] = clockTolerance;
const instance = new Configuration(as, registered.client_id, registered, clientAuthentication);
let internals = int(instance);
if (options?.[customFetch]) {
internals.fetch = options[customFetch];
}
if (options?.timeout) {
internals.timeout = options.timeout;
}
if (options?.execute) {
for (const extension of options.execute) {
extension(instance);
}
}
return instance;
}
export async function discovery(server, clientId, metadata, clientAuthentication, options) {
const as = await performDiscovery(server, options);
const instance = new Configuration(as, clientId, metadata, clientAuthentication);
let internals = int(instance);
if (options?.[customFetch]) {
internals.fetch = options[customFetch];
}
if (options?.timeout) {
internals.timeout = options.timeout;
}
if (options?.execute) {
for (const extension of options.execute) {
extension(instance);
}
}
return instance;
}
async function performDiscovery(server, options) {
if (!(server instanceof URL)) {
throw CodedTypeError('"server" must be an instance of URL', ERR_INVALID_ARG_TYPE);
}
const resolve = !server.href.includes('/.well-known/');
const timeout = options?.timeout ?? 30;
const signal = AbortSignal.timeout(timeout * 1000);
const as = await (resolve
? oauth.discoveryRequest(server, {
algorithm: options?.algorithm,
[oauth.customFetch]: options?.[customFetch],
[oauth.allowInsecureRequests]: options?.execute?.includes(allowInsecureRequests),
signal,
headers: new Headers(headers),
})
: (options?.[customFetch] || fetch)((() => {
oauth.checkProtocol(server, options?.execute?.includes(allowInsecureRequests) ? false : true);
return server.href;
})(), {
headers: Object.fromEntries(new Headers({ accept: 'application/json', ...headers }).entries()),
body: undefined,
method: 'GET',
redirect: 'manual',
signal,
}))
.then((response) => oauth.processDiscoveryResponse(oauth._nodiscoverycheck, response))
.catch(errorHandler);
if (resolve && new URL(as.issuer).href !== server.href) {
handleEntraId(server, as, options) ||
handleB2Clogin(server, options) ||
(() => {
throw new ClientError('discovered metadata issuer does not match the expected issuer', {
code: oauth.JSON_ATTRIBUTE_COMPARISON,
cause: {
expected: server.href,
body: as,
attribute: 'issuer',
},
});
})();
}
return as;
}
function isRsaOaep(input) {
return input.name === 'RSA-OAEP';
}
function isEcdh(input) {
return input.name === 'ECDH';
}
const ecdhEs = 'ECDH-ES';
const ecdhEsA128Kw = 'ECDH-ES+A128KW';
const ecdhEsA192Kw = 'ECDH-ES+A192KW';
const ecdhEsA256Kw = 'ECDH-ES+A256KW';
function checkEcdhAlg(algs, alg, pk) {
switch (alg) {
case undefined:
algs.add(ecdhEs);
algs.add(ecdhEsA128Kw);
algs.add(ecdhEsA192Kw);
algs.add(ecdhEsA256Kw);
break;
case ecdhEs:
case ecdhEsA128Kw:
case ecdhEsA192Kw:
case ecdhEsA256Kw:
algs.add(alg);
break;
default:
throw CodedTypeError('invalid key alg', ERR_INVALID_ARG_VALUE, { pk });
}
}
export function enableDecryptingResponses(config, contentEncryptionAlgorithms = [
'A128GCM',
'A192GCM',
'A256GCM',
'A128CBC-HS256',
'A192CBC-HS384',
'A256CBC-HS512',
], ...keys) {
if (int(config).decrypt !== undefined) {
throw new TypeError('enableDecryptingResponses can only be called on a given Configuration instance once');
}
if (keys.length === 0) {
throw CodedTypeError('no keys were provided', ERR_INVALID_ARG_VALUE);
}
const algs = new Set();
const normalized = [];
for (const pk of keys) {
let key;
if ('key' in pk) {
key = { key: pk.key };
if (typeof pk.alg === 'string')
key.alg = pk.alg;
if (typeof pk.kid === 'string')
key.kid = pk.kid;
}
else {
key = { key: pk };
}
if (key.key.type !== 'private') {
throw CodedTypeError('only private keys must be provided', ERR_INVALID_ARG_VALUE);
}
if (isRsaOaep(key.key.algorithm)) {
switch (key.key.algorithm.hash.name) {
case 'SHA-1':
case 'SHA-256':
case 'SHA-384':
case 'SHA-512': {
let alg = 'RSA-OAEP';
let sha;
if ((sha = parseInt(key.key.algorithm.hash.name.slice(-3), 10))) {
alg = `${alg}-${sha}`;
}
key.alg ||= alg;
if (alg !== key.alg)
throw CodedTypeError('invalid key alg', ERR_INVALID_ARG_VALUE, {
pk,
});
algs.add(key.alg);
break;
}
default:
throw CodedTypeError('only SHA-512, SHA-384, SHA-256, and SHA-1 RSA-OAEP keys are supported', ERR_INVALID_ARG_VALUE);
}
}
else if (isEcdh(key.key.algorithm)) {
if (key.key.algorithm.namedCurve !== 'P-256') {
throw CodedTypeError('Only P-256 ECDH keys are supported', ERR_INVALID_ARG_VALUE);
}
checkEcdhAlg(algs, key.alg, pk);
}
else if (key.key.algorithm.name === 'X25519') {
checkEcdhAlg(algs, key.alg, pk);
}
else {
throw CodedTypeError('only RSA-OAEP, ECDH, or X25519 keys are supported', ERR_INVALID_ARG_VALUE);
}
normalized.push(key);
}
int(config).decrypt = async (jwe) => decrypt(normalized, jwe, contentEncryptionAlgorithms, [...algs]).catch(errorHandler);
}
function checkCryptoKey(key, alg, epk) {
if (alg.startsWith('RSA-OAEP')) {
return true;
}
if (alg.startsWith('ECDH-ES')) {
if (key.algorithm.name !== 'ECDH' && key.algorithm.name !== 'X25519') {
return false;
}
if (key.algorithm.name === 'ECDH') {
return epk?.crv === key.algorithm.namedCurve;
}
if (key.algorithm.name === 'X25519') {
return epk?.crv === 'X25519';
}
}
return false;
}
function selectCryptoKeyForDecryption(keys, alg, kid, epk) {
const { 0: key, length } = keys.filter((key) => {
if (kid !== key.kid) {
return false;
}
if (key.alg && alg !== key.alg) {
return false;
}
return checkCryptoKey(key.key, alg, epk);
});
if (!key) {
throw e('no applicable decryption key selected', undefined, 'OAUTH_DECRYPTION_FAILED');
}
if (length !== 1) {
throw e('multiple applicable decryption keys selected', undefined, 'OAUTH_DECRYPTION_FAILED');
}
return key.key;
}
async function decrypt(keys, jwe, contentEncryptionAlgorithms, keyManagementAlgorithms) {
return decoder.decode((await compactDecrypt(jwe, (header) => {
const { kid, alg, epk } = header;
return selectCryptoKeyForDecryption(keys, alg, kid, epk);
}, { keyManagementAlgorithms, contentEncryptionAlgorithms }).catch((err) => {
if (err instanceof JOSEError) {
throw e('decryption failed', err, 'OAUTH_DECRYPTION_FAILED');
}
errorHandler(err);
})).plaintext);
}
function getServerHelpers(metadata) {
return {
supportsPKCE: {
__proto__: null,
value(method = 'S256') {
return (metadata.code_challenge_methods_supported?.includes(method) === true);
},
},
};
}
function addServerHelpers(metadata) {
Object.defineProperties(metadata, getServerHelpers(metadata));
}
const kEntraId = Symbol();
export class Configuration {
constructor(server, clientId, metadata, clientAuthentication) {
if (typeof clientId !== 'string' || !clientId.length) {
throw CodedTypeError('"clientId" must be a non-empty string', ERR_INVALID_ARG_TYPE);
}
if (typeof metadata === 'string') {
metadata = { client_secret: metadata };
}
if (metadata?.client_id !== undefined && clientId !== metadata.client_id) {
throw CodedTypeError('"clientId" and "metadata.client_id" must be the same', ERR_INVALID_ARG_VALUE);
}
const client = {
...structuredClone(metadata),
client_id: clientId,
};
client[oauth.clockSkew] = metadata?.[oauth.clockSkew] ?? 0;
client[oauth.clockTolerance] = metadata?.[oauth.clockTolerance] ?? 30;
let auth;
if (clientAuthentication) {
auth = clientAuthentication;
}
else {
if (typeof client.client_secret === 'string' &&
client.client_secret.length) {
auth = ClientSecretPost(client.client_secret);
}
else {
auth = None();
}
}
let c = Object.freeze(client);
const clone = structuredClone(server);
if (kEntraId in server) {
clone[oauth._expectedIssuer] = ({ claims: { tid } }) => server.issuer.replace('{tenantid}', tid);
}
let as = Object.freeze(clone);
props ||= new WeakMap();
props.set(this, {
__proto__: null,
as,
c,
auth,
tlsOnly: true,
jwksCache: {},
});
}
serverMetadata() {
const metadata = structuredClone(int(this).as);
addServerHelpers(metadata);
return metadata;
}
clientMetadata() {
const metadata = structuredClone(int(this).c);
return metadata;
}
get timeout() {
return int(this).timeout;
}
set timeout(value) {
int(this).timeout = value;
}
get [customFetch]() {
return int(this).fetch;
}
set [customFetch](value) {
int(this).fetch = value;
}
}
Object.freeze(Configuration.prototype);
function getHelpers(response) {
let exp = undefined;
if (response.expires_in !== undefined) {
const now = new Date();
now.setSeconds(now.getSeconds() + response.expires_in);
exp = now.getTime();
}
return {
expiresIn: {
__proto__: null,
value() {
if (exp) {
const now = Date.now();
if (exp > now) {
return Math.floor((exp - now) / 1000);
}
return 0;
}
return undefined;
},
},
claims: {
__proto__: null,
value() {
try {
return oauth.getValidatedIdTokenClaims(this);
}
catch {
return undefined;
}
},
},
};
}
function addHelpers(response) {
Object.defineProperties(response, getHelpers(response));
}
export function getDPoPHandle(config, keyPair, options) {
checkConfig(config);
return oauth.DPoP(int(config).c, keyPair, options);
}
async function handleRetryAfter(response, currentInterval, signal, throwIfInvalid = false) {
const retryAfter = response.headers.get('retry-after')?.trim();
if (retryAfter === undefined)
return;
let delaySeconds;
if (/^\d+$/.test(retryAfter)) {
delaySeconds = parseInt(retryAfter, 10);
}
else {
const retryDate = new Date(retryAfter);
if (Number.isFinite(retryDate.getTime())) {
const now = new Date();
const delayMs = retryDate.getTime() - now.getTime();
if (delayMs > 0) {
delaySeconds = Math.ceil(delayMs / 1000);
}
}
}
if (throwIfInvalid && !Number.isFinite(delaySeconds)) {
throw new oauth.OperationProcessingError('invalid Retry-After header value', { cause: response });
}
if (delaySeconds > currentInterval) {
await wait(delaySeconds - currentInterval, signal);
}
}
function wait(duration, signal) {
return new Promise((resolve, reject) => {
const waitStep = (remaining) => {
try {
signal.throwIfAborted();
}
catch (err) {
reject(err);
return;
}
if (remaining <= 0) {
resolve();
return;
}
const currentWait = Math.min(remaining, 5);
setTimeout(() => waitStep(remaining - currentWait), currentWait * 1000);
};
waitStep(duration);
});
}
export async function pollDeviceAuthorizationGrant(config, deviceAuthorizationResponse, parameters, options) {
checkConfig(config);
parameters = new URLSearchParams(parameters);
let interval = deviceAuthorizationResponse.interval ?? 5;
const pollingSignal = options?.signal ??
AbortSignal.timeout(deviceAuthorizationResponse.expires_in * 1000);
try {
await wait(interval, pollingSignal);
}
catch (err) {
errorHandler(err);
}
const { as, c, auth, fetch, tlsOnly, nonRepudiation, timeout, decrypt } = int(config);
const retryPoll = (updatedInterval, flag) => pollDeviceAuthorizationGrant(config, {
...deviceAuthorizationResponse,
interval: updatedInterval,
}, parameters, {
...options,
signal: pollingSignal,
flag,
});
const response = await oauth
.deviceCodeGrantRequest(as, c, auth, deviceAuthorizationResponse.device_code, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
additionalParameters: parameters,
DPoP: options?.DPoP,
headers: new Headers(headers),
signal: pollingSignal.aborted ? pollingSignal : signal(timeout),
})
.catch(errorHandler);
if (response.status === 503 && response.headers.has('retry-after')) {
await handleRetryAfter(response, interval, pollingSignal, true);
await response.body?.cancel();
return retryPoll(interval);
}
const p = oauth.processDeviceCodeResponse(as, c, response, {
[oauth.jweDecrypt]: decrypt,
});
let result;
try {
result = await p;
}
catch (err) {
if (retryable(err, options)) {
return retryPoll(interval, retry);
}
if (err instanceof oauth.ResponseBodyError) {
switch (err.error) {
case 'slow_down':
interval += 5;
case 'authorization_pending':
await handleRetryAfter(err.response, interval, pollingSignal);
return retryPoll(interval);
}
}
errorHandler(err);
}
result.id_token && (await nonRepudiation?.(response));
addHelpers(result);
return result;
}
export async function initiateDeviceAuthorization(config, parameters) {
checkConfig(config);
const { as, c, auth, fetch, tlsOnly, timeout } = int(config);
return oauth
.deviceAuthorizationRequest(as, c, auth, parameters, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
headers: new Headers(headers),
signal: signal(timeout),
})
.then((response) => oauth.processDeviceAuthorizationResponse(as, c, response))
.catch(errorHandler);
}
export async function initiateBackchannelAuthentication(config, parameters) {
checkConfig(config);
const { as, c, auth, fetch, tlsOnly, timeout } = int(config);
return oauth
.backchannelAuthenticationRequest(as, c, auth, parameters, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
headers: new Headers(headers),
signal: signal(timeout),
})
.then((response) => oauth.processBackchannelAuthenticationResponse(as, c, response))
.catch(errorHandler);
}
export async function pollBackchannelAuthenticationGrant(config, backchannelAuthenticationResponse, parameters, options) {
checkConfig(config);
parameters = new URLSearchParams(parameters);
let interval = backchannelAuthenticationResponse.interval ?? 5;
const pollingSignal = options?.signal ??
AbortSignal.timeout(backchannelAuthenticationResponse.expires_in * 1000);
try {
await wait(interval, pollingSignal);
}
catch (err) {
errorHandler(err);
}
const { as, c, auth, fetch, tlsOnly, nonRepudiation, timeout, decrypt } = int(config);
const retryPoll = (updatedInterval, flag) => pollBackchannelAuthenticationGrant(config, {
...backchannelAuthenticationResponse,
interval: updatedInterval,
}, parameters, {
...options,
signal: pollingSignal,
flag,
});
const response = await oauth
.backchannelAuthenticationGrantRequest(as, c, auth, backchannelAuthenticationResponse.auth_req_id, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
additionalParameters: parameters,
DPoP: options?.DPoP,
headers: new Headers(headers),
signal: pollingSignal.aborted ? pollingSignal : signal(timeout),
})
.catch(errorHandler);
if (response.status === 503 && response.headers.has('retry-after')) {
await handleRetryAfter(response, interval, pollingSignal, true);
await response.body?.cancel();
return retryPoll(interval);
}
const p = oauth.processBackchannelAuthenticationGrantResponse(as, c, response, {
[oauth.jweDecrypt]: decrypt,
});
let result;
try {
result = await p;
}
catch (err) {
if (retryable(err, options)) {
return retryPoll(interval, retry);
}
if (err instanceof oauth.ResponseBodyError) {
switch (err.error) {
case 'slow_down':
interval += 5;
case 'authorization_pending':
await handleRetryAfter(err.response, interval, pollingSignal);
return retryPoll(interval);
}
}
errorHandler(err);
}
result.id_token && (await nonRepudiation?.(response));
addHelpers(result);
return result;
}
export function allowInsecureRequests(config) {
int(config).tlsOnly = false;
}
export function setJwksCache(config, jwksCache) {
int(config).jwksCache = structuredClone(jwksCache);
}
export function getJwksCache(config) {
const cache = int(config).jwksCache;
if (cache.uat) {
return cache;
}
return undefined;
}
export function enableNonRepudiationChecks(config) {
checkConfig(config);
int(config).nonRepudiation = (response) => {
const { as, fetch, tlsOnly, timeout, jwksCache } = int(config);
return oauth
.validateApplicationLevelSignature(as, response, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
headers: new Headers(headers),
signal: signal(timeout),
[oauth.jwksCache]: jwksCache,
})
.catch(errorHandler);
};
}
export function useJwtResponseMode(config) {
checkConfig(config);
const { hybrid, implicit } = int(config);
if (hybrid || implicit) {
throw e('JARM cannot be combined with a hybrid or implicit response types', undefined, oauth.UNSUPPORTED_OPERATION);
}
int(config).jarm = (authorizationResponse, expectedState) => validateJARMResponse(config, authorizationResponse, expectedState);
}
export function enableDetachedSignatureResponseChecks(config) {
if (!int(config).hybrid) {
throw e('"code id_token" response type must be configured to be used first', undefined, oauth.UNSUPPORTED_OPERATION);
}
int(config).hybrid = (authorizationResponse, expectedNonce, expectedState, maxAge) => validateCodeIdTokenResponse(config, authorizationResponse, expectedNonce, expectedState, maxAge, true);
}
export async function implicitAuthentication(config, currentUrl, expectedNonce, checks) {
checkConfig(config);
if (!(currentUrl instanceof URL) &&
!webInstanceOf(currentUrl, 'Request')) {
throw CodedTypeError('"currentUrl" must be an instance of URL, or Request', ERR_INVALID_ARG_TYPE);
}
if (typeof expectedNonce !== 'string') {
throw CodedTypeError('"expectedNonce" must be a string', ERR_INVALID_ARG_TYPE);
}
const { as, c, fetch, tlsOnly, timeout, decrypt, implicit, jwksCache } = int(config);
if (!implicit) {
throw new TypeError('implicitAuthentication() cannot be used by clients using flows other than response_type=id_token');
}
let params;
if (!(currentUrl instanceof URL)) {
const request = currentUrl;
switch (request.method) {
case 'GET':
params = new URLSearchParams(new URL(request.url).hash.slice(1));
break;
case 'POST':
params = new URLSearchParams(await oauth.formPostResponse(request));
break;
default:
throw CodedTypeError('unexpected Request HTTP method', ERR_INVALID_ARG_VALUE);
}
}
else {
params = new URLSearchParams(currentUrl.hash.slice(1));
}
try {
{
const decoy = new URLSearchParams(params);
decoy.delete('id_token');
oauth.validateAuthResponse({
...as,
authorization_response_iss_parameter_supported: undefined,
}, c, decoy, checks?.expectedState);
}
{
const decoy = new Response(JSON.stringify({
access_token: 'decoy',
token_type: 'bearer',
id_token: params.get('id_token'),
}), {
headers: new Headers({ 'content-type': 'application/json' }),
});
const ref = await oauth.processAuthorizationCodeResponse(as, c, decoy, {
expectedNonce,
maxAge: checks?.maxAge,
[oauth.jweDecrypt]: decrypt,
});
await oauth.validateApplicationLevelSignature(as, decoy, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
headers: new Headers(headers),
signal: signal(timeout),
[oauth.jwksCache]: jwksCache,
});
return oauth.getValidatedIdTokenClaims(ref);
}
}
catch (err) {
errorHandler(err);
}
}
export function useCodeIdTokenResponseType(config) {
checkConfig(config);
const { jarm, implicit } = int(config);
if (jarm || implicit) {
throw e('"code id_token" response type cannot be combined with JARM or implicit response type', undefined, oauth.UNSUPPORTED_OPERATION);
}
int(config).hybrid = (authorizationResponse, expectedNonce, expectedState, maxAge) => validateCodeIdTokenResponse(config, authorizationResponse, expectedNonce, expectedState, maxAge, false);
}
export function useIdTokenResponseType(config) {
checkConfig(config);
const { jarm, hybrid } = int(config);
if (jarm || hybrid) {
throw e('"id_token" response type cannot be combined with JARM or hybrid response type', undefined, oauth.UNSUPPORTED_OPERATION);
}
int(config).implicit = true;
}
function stripParams(url) {
url = new URL(url);
url.search = '';
url.hash = '';
return url.href;
}
function webInstanceOf(input, toStringTag) {
try {
return Object.getPrototypeOf(input)[Symbol.toStringTag] === toStringTag;
}
catch {
return false;
}
}
export async function authorizationCodeGrant(config, currentUrl, checks, tokenEndpointParameters, options) {
checkConfig(config);
if (options?.flag !== retry &&
!(currentUrl instanceof URL) &&
!webInstanceOf(currentUrl, 'Request')) {
throw CodedTypeError('"currentUrl" must be an instance of URL, or Request', ERR_INVALID_ARG_TYPE);
}
let authResponse;
let redirectUri;
const { as, c, auth, fetch, tlsOnly, jarm, hybrid, nonRepudiation, timeout, decrypt, implicit } = int(config);
if (options?.flag === retry) {
authResponse = options.authResponse;
redirectUri = options.redirectUri;
}
else {
if (!(currentUrl instanceof URL)) {
const request = currentUrl;
currentUrl = new URL(currentUrl.url);
switch (request.method) {
case 'GET':
break;
case 'POST':
const params = new URLSearchParams(await oauth.formPostResponse(request));
if (hybrid) {
currentUrl.hash = params.toString();
}
else {
for (const [k, v] of params.entries()) {
currentUrl.searchParams.append(k, v);
}
}
break;
default:
throw CodedTypeError('unexpected Request HTTP method', ERR_INVALID_ARG_VALUE);
}
}
redirectUri = stripParams(currentUrl);
switch (true) {
case !!jarm:
authResponse = await jarm(currentUrl, checks?.expectedState);
break;
case !!hybrid:
authResponse = await hybrid(currentUrl, checks?.expectedNonce, checks?.expectedState, checks?.maxAge);
break;
case !!implicit:
throw new TypeError('authorizationCodeGrant() cannot be used by response_type=id_token clients');
default:
try {
authResponse = oauth.validateAuthResponse(as, c, currentUrl.searchParams, checks?.expectedState);
}
catch (err) {
errorHandler(err);
}
}
}
const response = await oauth
.authorizationCodeGrantRequest(as, c, auth, authResponse, redirectUri, checks?.pkceCodeVerifier || oauth.nopkce, {
additionalParameters: tokenEndpointParameters,
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
DPoP: options?.DPoP,
headers: new Headers(headers),
signal: signal(timeout),
})
.catch(errorHandler);
if (typeof checks?.expectedNonce === 'string' ||
typeof checks?.maxAge === 'number') {
checks.idTokenExpected = true;
}
const p = oauth.processAuthorizationCodeResponse(as, c, response, {
expectedNonce: checks?.expectedNonce,
maxAge: checks?.maxAge,
requireIdToken: checks?.idTokenExpected,
[oauth.jweDecrypt]: decrypt,
});
let result;
try {
result = await p;
}
catch (err) {
if (retryable(err, options)) {
return authorizationCodeGrant(config, undefined, checks, tokenEndpointParameters, {
...options,
flag: retry,
authResponse: authResponse,
redirectUri: redirectUri,
});
}
errorHandler(err);
}
result.id_token && (await nonRepudiation?.(response));
addHelpers(result);
return result;
}
async function validateJARMResponse(config, authorizationResponse, expectedState) {
const { as, c, fetch, tlsOnly, timeout, decrypt, jwksCache } = int(config);
return oauth
.validateJwtAuthResponse(as, c, authorizationResponse, expectedState, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
headers: new Headers(headers),
signal: signal(timeout),
[oauth.jweDecrypt]: decrypt,
[oauth.jwksCache]: jwksCache,
})
.catch(errorHandler);
}
async function validateCodeIdTokenResponse(config, authorizationResponse, expectedNonce, expectedState, maxAge, fapi) {
if (typeof expectedNonce !== 'string') {
throw CodedTypeError('"expectedNonce" must be a string', ERR_INVALID_ARG_TYPE);
}
if (expectedState !== undefined && typeof expectedState !== 'string') {
throw CodedTypeError('"expectedState" must be a string', ERR_INVALID_ARG_TYPE);
}
const { as, c, fetch, tlsOnly, timeout, decrypt, jwksCache } = int(config);
return (fapi
? oauth.validateDetachedSignatureResponse
: oauth.validateCodeIdTokenResponse)(as, c, authorizationResponse, expectedNonce, expectedState, maxAge, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
headers: new Headers(headers),
signal: signal(timeout),
[oauth.jweDecrypt]: decrypt,
[oauth.jwksCache]: jwksCache,
}).catch(errorHandler);
}
export async function refreshTokenGrant(config, refreshToken, parameters, options) {
checkConfig(config);
parameters = new URLSearchParams(parameters);
const { as, c, auth, fetch, tlsOnly, nonRepudiation, timeout, decrypt } = int(config);
const response = await oauth
.refreshTokenGrantRequest(as, c, auth, refreshToken, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
additionalParameters: parameters,
DPoP: options?.DPoP,
headers: new Headers(headers),
signal: signal(timeout),
})
.catch(errorHandler);
const p = oauth.processRefreshTokenResponse(as, c, response, {
[oauth.jweDecrypt]: decrypt,
});
let result;
try {
result = await p;
}
catch (err) {
if (retryable(err, options)) {
return refreshTokenGrant(config, refreshToken, parameters, {
...options,
flag: retry,
});
}
errorHandler(err);
}
result.id_token && (await nonRepudiation?.(response));
addHelpers(result);
return result;
}
export async function clientCredentialsGrant(config, parameters, options) {
checkConfig(config);
parameters = new URLSearchParams(parameters);
const { as, c, auth, fetch, tlsOnly, timeout } = int(config);
const response = await oauth
.clientCredentialsGrantRequest(as, c, auth, parameters, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
DPoP: options?.DPoP,
headers: new Headers(headers),
signal: signal(timeout),
})
.catch(errorHandler);
const p = oauth.processClientCredentialsResponse(as, c, response);
let result;
try {
result = await p;
}
catch (err) {
if (retryable(err, options)) {
return clientCredentialsGrant(config, parameters, {
...options,
flag: retry,
});
}
errorHandler(err);
}
addHelpers(result);
return result;
}
export function buildAuthorizationUrl(config, parameters) {
checkConfig(config);
const { as, c, tlsOnly, hybrid, jarm, implicit } = int(config);
const authorizationEndpoint = oauth.resolveEndpoint(as, 'authorization_endpoint', false, tlsOnly);
parameters = new URLSearchParams(parameters);
if (!parameters.has('client_id')) {
parameters.set('client_id', c.client_id);
}
if (!parameters.has('request_uri') && !parameters.has('request')) {
if (!parameters.has('response_type')) {
parameters.set('response_type', hybrid ? 'code id_token' : implicit ? 'id_token' : 'code');
}
if (implicit && !parameters.has('nonce')) {
throw CodedTypeError('response_type=id_token clients must provide a nonce parameter in their authorization request parameters', ERR_INVALID_ARG_VALUE);
}
if (jarm) {
parameters.set('response_mode', 'jwt');
}
}
for (const [k, v] of parameters.entries()) {
authorizationEndpoint.searchParams.append(k, v);
}
return authorizationEndpoint;
}
export async function buildAuthorizationUrlWithJAR(config, parameters, signingKey, options) {
checkConfig(config);
const authorizationEndpoint = buildAuthorizationUrl(config, parameters);
parameters = authorizationEndpoint.searchParams;
if (!signingKey) {
throw CodedTypeError('"signingKey" must be provided', ERR_INVALID_ARG_VALUE);
}
const { as, c } = int(config);
const request = await oauth
.issueRequestObject(as, c, parameters, signingKey, options)
.catch(errorHandler);
return buildAuthorizationUrl(config, { request });
}
export async function buildAuthorizationUrlWithPAR(config, parameters, options) {
checkConfig(config);
const authorizationEndpoint = buildAuthorizationUrl(config, parameters);
const { as, c, auth, fetch, tlsOnly, timeout } = int(config);
const response = await oauth
.pushedAuthorizationRequest(as, c, auth, authorizationEndpoint.searchParams, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
DPoP: options?.DPoP,
headers: new Headers(headers),
signal: signal(timeout),
})
.catch(errorHandler);
const p = oauth.processPushedAuthorizationResponse(as, c, response);
let result;
try {
result = await p;
}
catch (err) {
if (retryable(err, options)) {
return buildAuthorizationUrlWithPAR(config, parameters, {
...options,
flag: retry,
});
}
errorHandler(err);
}
return buildAuthorizationUrl(config, { request_uri: result.request_uri });
}
export function buildEndSessionUrl(config, parameters) {
checkConfig(config);
const { as, c, tlsOnly } = int(config);
const endSessionEndpoint = oauth.resolveEndpoint(as, 'end_session_endpoint', false, tlsOnly);
parameters = new URLSearchParams(parameters);
if (!parameters.has('client_id')) {
parameters.set('client_id', c.client_id);
}
for (const [k, v] of parameters.entries()) {
endSessionEndpoint.searchParams.append(k, v);
}
return endSessionEndpoint;
}
function checkConfig(input) {
if (!(input instanceof Configuration)) {
throw CodedTypeError('"config" must be an instance of Configuration', ERR_INVALID_ARG_TYPE);
}
if (Object.getPrototypeOf(input) !== Configuration.prototype) {
throw CodedTypeError('subclassing Configuration is not allowed', ERR_INVALID_ARG_VALUE);
}
}
function signal(timeout) {
return timeout ? AbortSignal.timeout(timeout * 1000) : undefined;
}
export async function fetchUserInfo(config, accessToken, expectedSubject, options) {
checkConfig(config);
const { as, c, fetch, tlsOnly, nonRepudiation, timeout, decrypt } = int(config);
const response = await oauth
.userInfoRequest(as, c, accessToken, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
DPoP: options?.DPoP,
headers: new Headers(headers),
signal: signal(timeout),
})
.catch(errorHandler);
let exec = oauth.processUserInfoResponse(as, c, expectedSubject, response, {
[oauth.jweDecrypt]: decrypt,
});
let result;
try {
result = await exec;
}
catch (err) {
if (retryable(err, options)) {
return fetchUserInfo(config, accessToken, expectedSubject, {
...options,
flag: retry,
});
}
errorHandler(err);
}
oauth.getContentType(response) === 'application/jwt' &&
(await nonRepudiation?.(response));
return result;
}
function retryable(err, options) {
if (options?.DPoP && options.flag !== retry) {
return oauth.isDPoPNonceError(err);
}
return false;
}
export async function tokenIntrospection(config, token, parameters) {
checkConfig(config);
const { as, c, auth, fetch, tlsOnly, nonRepudiation, timeout, decrypt } = int(config);
const response = await oauth
.introspectionRequest(as, c, auth, token, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
additionalParameters: new URLSearchParams(parameters),
headers: new Headers(headers),
signal: signal(timeout),
})
.catch(errorHandler);
const result = await oauth
.processIntrospectionResponse(as, c, response, {
[oauth.jweDecrypt]: decrypt,
})
.catch(errorHandler);
oauth.getContentType(response) === 'application/token-introspection+jwt' &&
(await nonRepudiation?.(response));
return result;
}
const retry = Symbol();
export async function genericGrantRequest(config, grantType, parameters, options) {
checkConfig(config);
const { as, c, auth, fetch, tlsOnly, timeout, decrypt } = int(config);
const result = await oauth
.genericTokenEndpointRequest(as, c, auth, grantType, new URLSearchParams(parameters), {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
DPoP: options?.DPoP,
headers: new Headers(headers),
signal: signal(timeout),
})
.then((response) => {
let recognizedTokenTypes;
if (grantType === 'urn:ietf:params:oauth:grant-type:token-exchange') {
recognizedTokenTypes = { n_a: () => { } };
}
return oauth.processGenericTokenEndpointResponse(as, c, response, {
[oauth.jweDecrypt]: decrypt,
recognizedTokenTypes,
});
})
.catch(errorHandler);
addHelpers(result);
return result;
}
export async function tokenRevocation(config, token, parameters) {
checkConfig(config);
const { as, c, auth, fetch, tlsOnly, timeout } = int(config);
return oauth
.revocationRequest(as, c, auth, token, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
additionalParameters: new URLSearchParams(parameters),
headers: new Headers(headers),
signal: signal(timeout),
})
.then(oauth.processRevocationResponse)
.catch(errorHandler);
}
export async function fetchProtectedResource(config, accessToken, url, method, body, headers, options) {
checkConfig(config);
headers ||= new Headers();
if (!headers.has('user-agent')) {
headers.set('user-agent', USER_AGENT);
}
const { fetch, tlsOnly, timeout } = int(config);
const exec = oauth.protectedResourceRequest(accessToken, method, url, headers, body, {
[oauth.customFetch]: fetch,
[oauth.allowInsecureRequests]: !tlsOnly,
DPoP: options?.DPoP,
signal: signal(timeout),
});
let result;
try {
result = await exec;
}
catch (err) {
if (retryable(err, options)) {
return fetchProtectedResource(config, accessToken, url, method, body, headers, {
...options,
flag: retry,
});
}
errorHandler(err);
}
return result;
}
//# sourceMappingURL=index.js.map