UNPKG

oidc-client-ts

Version:

OpenID Connect (OIDC) & OAuth2 client library

1,513 lines (1,490 loc) 119 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AccessTokenEvents: () => AccessTokenEvents, CheckSessionIFrame: () => CheckSessionIFrame, DPoPState: () => DPoPState, ErrorResponse: () => ErrorResponse, ErrorTimeout: () => ErrorTimeout, InMemoryWebStorage: () => InMemoryWebStorage, IndexedDbDPoPStore: () => IndexedDbDPoPStore, Log: () => Log, Logger: () => Logger, MetadataService: () => MetadataService, OidcClient: () => OidcClient, OidcClientSettingsStore: () => OidcClientSettingsStore, SessionMonitor: () => SessionMonitor, SigninResponse: () => SigninResponse, SigninState: () => SigninState, SignoutResponse: () => SignoutResponse, State: () => State, User: () => User, UserManager: () => UserManager, UserManagerSettingsStore: () => UserManagerSettingsStore, Version: () => Version, WebStorageStateStore: () => WebStorageStateStore }); module.exports = __toCommonJS(index_exports); // src/utils/Logger.ts var nopLogger = { debug: () => void 0, info: () => void 0, warn: () => void 0, error: () => void 0 }; var level; var logger; var Log = /* @__PURE__ */ ((Log2) => { Log2[Log2["NONE"] = 0] = "NONE"; Log2[Log2["ERROR"] = 1] = "ERROR"; Log2[Log2["WARN"] = 2] = "WARN"; Log2[Log2["INFO"] = 3] = "INFO"; Log2[Log2["DEBUG"] = 4] = "DEBUG"; return Log2; })(Log || {}); ((Log2) => { function reset() { level = 3 /* INFO */; logger = nopLogger; } Log2.reset = reset; function setLevel(value) { if (!(0 /* NONE */ <= value && value <= 4 /* DEBUG */)) { throw new Error("Invalid log level"); } level = value; } Log2.setLevel = setLevel; function setLogger(value) { logger = value; } Log2.setLogger = setLogger; })(Log || (Log = {})); var Logger = class _Logger { constructor(_name) { this._name = _name; } /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ debug(...args) { if (level >= 4 /* DEBUG */) { logger.debug(_Logger._format(this._name, this._method), ...args); } } info(...args) { if (level >= 3 /* INFO */) { logger.info(_Logger._format(this._name, this._method), ...args); } } warn(...args) { if (level >= 2 /* WARN */) { logger.warn(_Logger._format(this._name, this._method), ...args); } } error(...args) { if (level >= 1 /* ERROR */) { logger.error(_Logger._format(this._name, this._method), ...args); } } /* eslint-enable @typescript-eslint/no-unsafe-enum-comparison */ throw(err) { this.error(err); throw err; } create(method) { const methodLogger = Object.create(this); methodLogger._method = method; methodLogger.debug("begin"); return methodLogger; } static createStatic(name, staticMethod) { const staticLogger = new _Logger(`${name}.${staticMethod}`); staticLogger.debug("begin"); return staticLogger; } static _format(name, method) { const prefix = `[${name}]`; return method ? `${prefix} ${method}:` : prefix; } /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ // helpers for static class methods static debug(name, ...args) { if (level >= 4 /* DEBUG */) { logger.debug(_Logger._format(name), ...args); } } static info(name, ...args) { if (level >= 3 /* INFO */) { logger.info(_Logger._format(name), ...args); } } static warn(name, ...args) { if (level >= 2 /* WARN */) { logger.warn(_Logger._format(name), ...args); } } static error(name, ...args) { if (level >= 1 /* ERROR */) { logger.error(_Logger._format(name), ...args); } } /* eslint-enable @typescript-eslint/no-unsafe-enum-comparison */ }; Log.reset(); // src/utils/JwtUtils.ts var import_jwt_decode = require("jwt-decode"); var JwtUtils = class { // IMPORTANT: doesn't validate the token static decode(token) { try { return (0, import_jwt_decode.jwtDecode)(token); } catch (err) { Logger.error("JwtUtils.decode", err); throw err; } } static async generateSignedJwt(header, payload, privateKey) { const encodedHeader = CryptoUtils.encodeBase64Url(new TextEncoder().encode(JSON.stringify(header))); const encodedPayload = CryptoUtils.encodeBase64Url(new TextEncoder().encode(JSON.stringify(payload))); const encodedToken = `${encodedHeader}.${encodedPayload}`; const signature = await window.crypto.subtle.sign( { name: "ECDSA", hash: { name: "SHA-256" } }, privateKey, new TextEncoder().encode(encodedToken) ); const encodedSignature = CryptoUtils.encodeBase64Url(new Uint8Array(signature)); return `${encodedToken}.${encodedSignature}`; } }; // src/utils/CryptoUtils.ts var UUID_V4_TEMPLATE = "10000000-1000-4000-8000-100000000000"; var toBase64 = (val) => btoa([...new Uint8Array(val)].map((chr) => String.fromCharCode(chr)).join("")); var _CryptoUtils = class _CryptoUtils { static _randomWord() { const arr = new Uint32Array(1); crypto.getRandomValues(arr); return arr[0]; } /** * Generates RFC4122 version 4 guid */ static generateUUIDv4() { const uuid = UUID_V4_TEMPLATE.replace( /[018]/g, (c) => (+c ^ _CryptoUtils._randomWord() & 15 >> +c / 4).toString(16) ); return uuid.replace(/-/g, ""); } /** * PKCE: Generate a code verifier */ static generateCodeVerifier() { return _CryptoUtils.generateUUIDv4() + _CryptoUtils.generateUUIDv4() + _CryptoUtils.generateUUIDv4(); } /** * PKCE: Generate a code challenge */ static async generateCodeChallenge(code_verifier) { if (!crypto.subtle) { throw new Error("Crypto.subtle is available only in secure contexts (HTTPS)."); } try { const encoder = new TextEncoder(); const data = encoder.encode(code_verifier); const hashed = await crypto.subtle.digest("SHA-256", data); return toBase64(hashed).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } catch (err) { Logger.error("CryptoUtils.generateCodeChallenge", err); throw err; } } /** * Generates a base64-encoded string for a basic auth header */ static generateBasicAuth(client_id, client_secret) { const encoder = new TextEncoder(); const data = encoder.encode([client_id, client_secret].join(":")); return toBase64(data); } /** * Generates a hash of a string using a given algorithm * @param alg * @param message */ static async hash(alg, message) { const msgUint8 = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest(alg, msgUint8); return new Uint8Array(hashBuffer); } /** * Generates a rfc7638 compliant jwk thumbprint * @param jwk */ static async customCalculateJwkThumbprint(jwk) { let jsonObject; switch (jwk.kty) { case "RSA": jsonObject = { "e": jwk.e, "kty": jwk.kty, "n": jwk.n }; break; case "EC": jsonObject = { "crv": jwk.crv, "kty": jwk.kty, "x": jwk.x, "y": jwk.y }; break; case "OKP": jsonObject = { "crv": jwk.crv, "kty": jwk.kty, "x": jwk.x }; break; case "oct": jsonObject = { "crv": jwk.k, "kty": jwk.kty }; break; default: throw new Error("Unknown jwk type"); } const utf8encodedAndHashed = await _CryptoUtils.hash("SHA-256", JSON.stringify(jsonObject)); return _CryptoUtils.encodeBase64Url(utf8encodedAndHashed); } static async generateDPoPProof({ url, accessToken, httpMethod, keyPair, nonce }) { let hashedToken; let encodedHash; const payload = { "jti": window.crypto.randomUUID(), "htm": httpMethod != null ? httpMethod : "GET", "htu": url, "iat": Math.floor(Date.now() / 1e3) }; if (accessToken) { hashedToken = await _CryptoUtils.hash("SHA-256", accessToken); encodedHash = _CryptoUtils.encodeBase64Url(hashedToken); payload.ath = encodedHash; } if (nonce) { payload.nonce = nonce; } try { const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); const header = { "alg": "ES256", "typ": "dpop+jwt", "jwk": { "crv": publicJwk.crv, "kty": publicJwk.kty, "x": publicJwk.x, "y": publicJwk.y } }; return await JwtUtils.generateSignedJwt(header, payload, keyPair.privateKey); } catch (err) { if (err instanceof TypeError) { throw new Error(`Error exporting dpop public key: ${err.message}`); } else { throw err; } } } static async generateDPoPJkt(keyPair) { try { const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey); return await _CryptoUtils.customCalculateJwkThumbprint(publicJwk); } catch (err) { if (err instanceof TypeError) { throw new Error(`Could not retrieve dpop keys from storage: ${err.message}`); } else { throw err; } } } static async generateDPoPKeys() { return await window.crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"] ); } }; /** * Generates a base64url encoded string */ _CryptoUtils.encodeBase64Url = (input) => { return toBase64(input).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); }; var CryptoUtils = _CryptoUtils; // src/utils/Event.ts var Event = class { constructor(_name) { this._name = _name; this._callbacks = []; this._logger = new Logger(`Event('${this._name}')`); } addHandler(cb) { this._callbacks.push(cb); return () => this.removeHandler(cb); } removeHandler(cb) { const idx = this._callbacks.lastIndexOf(cb); if (idx >= 0) { this._callbacks.splice(idx, 1); } } async raise(...ev) { this._logger.debug("raise:", ...ev); for (const cb of this._callbacks) { await cb(...ev); } } }; // src/utils/PopupUtils.ts var PopupUtils = class { /** * Populates a map of window features with a placement centered in front of * the current window. If no explicit width is given, a default value is * binned into [800, 720, 600, 480, 360] based on the current window's width. */ static center({ ...features }) { var _a, _b, _c; if (features.width == null) features.width = (_a = [800, 720, 600, 480].find((width) => width <= window.outerWidth / 1.618)) != null ? _a : 360; (_b = features.left) != null ? _b : features.left = Math.max(0, Math.round(window.screenX + (window.outerWidth - features.width) / 2)); if (features.height != null) (_c = features.top) != null ? _c : features.top = Math.max(0, Math.round(window.screenY + (window.outerHeight - features.height) / 2)); return features; } static serialize(features) { return Object.entries(features).filter(([, value]) => value != null).map(([key, value]) => `${key}=${typeof value !== "boolean" ? value : value ? "yes" : "no"}`).join(","); } }; // src/utils/Timer.ts var Timer = class _Timer extends Event { constructor() { super(...arguments); this._logger = new Logger(`Timer('${this._name}')`); this._timerHandle = null; this._expiration = 0; this._callback = () => { const diff = this._expiration - _Timer.getEpochTime(); this._logger.debug("timer completes in", diff); if (this._expiration <= _Timer.getEpochTime()) { this.cancel(); void super.raise(); } }; } // get the time static getEpochTime() { return Math.floor(Date.now() / 1e3); } init(durationInSeconds) { const logger2 = this._logger.create("init"); durationInSeconds = Math.max(Math.floor(durationInSeconds), 1); const expiration = _Timer.getEpochTime() + durationInSeconds; if (this.expiration === expiration && this._timerHandle) { logger2.debug("skipping since already initialized for expiration at", this.expiration); return; } this.cancel(); logger2.debug("using duration", durationInSeconds); this._expiration = expiration; const timerDurationInSeconds = Math.min(durationInSeconds, 5); this._timerHandle = setInterval(this._callback, timerDurationInSeconds * 1e3); } get expiration() { return this._expiration; } cancel() { this._logger.create("cancel"); if (this._timerHandle) { clearInterval(this._timerHandle); this._timerHandle = null; } } }; // src/utils/UrlUtils.ts var UrlUtils = class { static readParams(url, responseMode = "query") { if (!url) throw new TypeError("Invalid URL"); const parsedUrl = new URL(url, "http://127.0.0.1"); const params = parsedUrl[responseMode === "fragment" ? "hash" : "search"]; return new URLSearchParams(params.slice(1)); } }; var URL_STATE_DELIMITER = ";"; // src/errors/ErrorResponse.ts var ErrorResponse = class extends Error { constructor(args, form) { var _a, _b, _c; super(args.error_description || args.error || ""); this.form = form; /** Marker to detect class: "ErrorResponse" */ this.name = "ErrorResponse"; if (!args.error) { Logger.error("ErrorResponse", "No error passed"); throw new Error("No error passed"); } this.error = args.error; this.error_description = (_a = args.error_description) != null ? _a : null; this.error_uri = (_b = args.error_uri) != null ? _b : null; this.state = args.userState; this.session_state = (_c = args.session_state) != null ? _c : null; this.url_state = args.url_state; } }; // src/errors/ErrorTimeout.ts var ErrorTimeout = class extends Error { constructor(message) { super(message); /** Marker to detect class: "ErrorTimeout" */ this.name = "ErrorTimeout"; } }; // src/AccessTokenEvents.ts var AccessTokenEvents = class { constructor(args) { this._logger = new Logger("AccessTokenEvents"); this._expiringTimer = new Timer("Access token expiring"); this._expiredTimer = new Timer("Access token expired"); this._expiringNotificationTimeInSeconds = args.expiringNotificationTimeInSeconds; } async load(container) { const logger2 = this._logger.create("load"); if (container.access_token && container.expires_in !== void 0) { const duration = container.expires_in; logger2.debug("access token present, remaining duration:", duration); if (duration > 0) { let expiring = duration - this._expiringNotificationTimeInSeconds; if (expiring <= 0) { expiring = 1; } logger2.debug("registering expiring timer, raising in", expiring, "seconds"); this._expiringTimer.init(expiring); } else { logger2.debug("canceling existing expiring timer because we're past expiration."); this._expiringTimer.cancel(); } const expired = duration + 1; logger2.debug("registering expired timer, raising in", expired, "seconds"); this._expiredTimer.init(expired); } else { this._expiringTimer.cancel(); this._expiredTimer.cancel(); } } async unload() { this._logger.debug("unload: canceling existing access token timers"); this._expiringTimer.cancel(); this._expiredTimer.cancel(); } /** * Add callback: Raised prior to the access token expiring. */ addAccessTokenExpiring(cb) { return this._expiringTimer.addHandler(cb); } /** * Remove callback: Raised prior to the access token expiring. */ removeAccessTokenExpiring(cb) { this._expiringTimer.removeHandler(cb); } /** * Add callback: Raised after the access token has expired. */ addAccessTokenExpired(cb) { return this._expiredTimer.addHandler(cb); } /** * Remove callback: Raised after the access token has expired. */ removeAccessTokenExpired(cb) { this._expiredTimer.removeHandler(cb); } }; // src/CheckSessionIFrame.ts var CheckSessionIFrame = class { constructor(_callback, _client_id, url, _intervalInSeconds, _stopOnError) { this._callback = _callback; this._client_id = _client_id; this._intervalInSeconds = _intervalInSeconds; this._stopOnError = _stopOnError; this._logger = new Logger("CheckSessionIFrame"); this._timer = null; this._session_state = null; this._message = (e) => { if (e.origin === this._frame_origin && e.source === this._frame.contentWindow) { if (e.data === "error") { this._logger.error("error message from check session op iframe"); if (this._stopOnError) { this.stop(); } } else if (e.data === "changed") { this._logger.debug("changed message from check session op iframe"); this.stop(); void this._callback(); } else { this._logger.debug(e.data + " message from check session op iframe"); } } }; const parsedUrl = new URL(url); this._frame_origin = parsedUrl.origin; this._frame = window.document.createElement("iframe"); this._frame.style.visibility = "hidden"; this._frame.style.position = "fixed"; this._frame.style.left = "-1000px"; this._frame.style.top = "0"; this._frame.width = "0"; this._frame.height = "0"; this._frame.src = parsedUrl.href; } load() { return new Promise((resolve) => { this._frame.onload = () => { resolve(); }; window.document.body.appendChild(this._frame); window.addEventListener("message", this._message, false); }); } start(session_state) { if (this._session_state === session_state) { return; } this._logger.create("start"); this.stop(); this._session_state = session_state; const send = () => { if (!this._frame.contentWindow || !this._session_state) { return; } this._frame.contentWindow.postMessage(this._client_id + " " + this._session_state, this._frame_origin); }; send(); this._timer = setInterval(send, this._intervalInSeconds * 1e3); } stop() { this._logger.create("stop"); this._session_state = null; if (this._timer) { clearInterval(this._timer); this._timer = null; } } }; // src/InMemoryWebStorage.ts var InMemoryWebStorage = class { constructor() { this._logger = new Logger("InMemoryWebStorage"); this._data = {}; } clear() { this._logger.create("clear"); this._data = {}; } getItem(key) { this._logger.create(`getItem('${key}')`); return this._data[key]; } setItem(key, value) { this._logger.create(`setItem('${key}')`); this._data[key] = value; } removeItem(key) { this._logger.create(`removeItem('${key}')`); delete this._data[key]; } get length() { return Object.getOwnPropertyNames(this._data).length; } key(index) { return Object.getOwnPropertyNames(this._data)[index]; } }; // src/errors/ErrorDPoPNonce.ts var ErrorDPoPNonce = class extends Error { constructor(nonce, message) { super(message); /** Marker to detect class: "ErrorDPoPNonce" */ this.name = "ErrorDPoPNonce"; this.nonce = nonce; } }; // src/JsonService.ts var JsonService = class { constructor(additionalContentTypes = [], _jwtHandler = null, _extraHeaders = {}) { this._jwtHandler = _jwtHandler; this._extraHeaders = _extraHeaders; this._logger = new Logger("JsonService"); this._contentTypes = []; this._contentTypes.push(...additionalContentTypes, "application/json"); if (_jwtHandler) { this._contentTypes.push("application/jwt"); } } async fetchWithTimeout(input, init = {}) { const { timeoutInSeconds, ...initFetch } = init; if (!timeoutInSeconds) { return await fetch(input, initFetch); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutInSeconds * 1e3); try { const response = await fetch(input, { ...init, signal: controller.signal }); return response; } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { throw new ErrorTimeout("Network timed out"); } throw err; } finally { clearTimeout(timeoutId); } } async getJson(url, { token, credentials, timeoutInSeconds } = {}) { const logger2 = this._logger.create("getJson"); const headers = { "Accept": this._contentTypes.join(", ") }; if (token) { logger2.debug("token passed, setting Authorization header"); headers["Authorization"] = "Bearer " + token; } this._appendExtraHeaders(headers); let response; try { logger2.debug("url:", url); response = await this.fetchWithTimeout(url, { method: "GET", headers, timeoutInSeconds, credentials }); } catch (err) { logger2.error("Network Error"); throw err; } logger2.debug("HTTP response received, status", response.status); const contentType = response.headers.get("Content-Type"); if (contentType && !this._contentTypes.find((item) => contentType.startsWith(item))) { logger2.throw(new Error(`Invalid response Content-Type: ${contentType != null ? contentType : "undefined"}, from URL: ${url}`)); } if (response.ok && this._jwtHandler && (contentType == null ? void 0 : contentType.startsWith("application/jwt"))) { return await this._jwtHandler(await response.text()); } let json; try { json = await response.json(); } catch (err) { logger2.error("Error parsing JSON response", err); if (response.ok) throw err; throw new Error(`${response.statusText} (${response.status})`); } if (!response.ok) { logger2.error("Error from server:", json); if (json.error) { throw new ErrorResponse(json); } throw new Error(`${response.statusText} (${response.status}): ${JSON.stringify(json)}`); } return json; } async postForm(url, { body, basicAuth, timeoutInSeconds, initCredentials, extraHeaders }) { const logger2 = this._logger.create("postForm"); const headers = { "Accept": this._contentTypes.join(", "), "Content-Type": "application/x-www-form-urlencoded", ...extraHeaders }; if (basicAuth !== void 0) { headers["Authorization"] = "Basic " + basicAuth; } this._appendExtraHeaders(headers); let response; try { logger2.debug("url:", url); response = await this.fetchWithTimeout(url, { method: "POST", headers, body, timeoutInSeconds, credentials: initCredentials }); } catch (err) { logger2.error("Network error"); throw err; } logger2.debug("HTTP response received, status", response.status); const contentType = response.headers.get("Content-Type"); if (contentType && !this._contentTypes.find((item) => contentType.startsWith(item))) { throw new Error(`Invalid response Content-Type: ${contentType != null ? contentType : "undefined"}, from URL: ${url}`); } const responseText = await response.text(); let json = {}; if (responseText) { try { json = JSON.parse(responseText); } catch (err) { logger2.error("Error parsing JSON response", err); if (response.ok) throw err; throw new Error(`${response.statusText} (${response.status})`); } } if (!response.ok) { logger2.error("Error from server:", json); if (response.headers.has("dpop-nonce")) { const nonce = response.headers.get("dpop-nonce"); throw new ErrorDPoPNonce(nonce, `${JSON.stringify(json)}`); } if (json.error) { throw new ErrorResponse(json, body); } throw new Error(`${response.statusText} (${response.status}): ${JSON.stringify(json)}`); } return json; } _appendExtraHeaders(headers) { const logger2 = this._logger.create("appendExtraHeaders"); const customKeys = Object.keys(this._extraHeaders); const protectedHeaders = [ "accept", "content-type" ]; const preventOverride = [ "authorization" ]; if (customKeys.length === 0) { return; } customKeys.forEach((headerName) => { if (protectedHeaders.includes(headerName.toLocaleLowerCase())) { logger2.warn("Protected header could not be set", headerName, protectedHeaders); return; } if (preventOverride.includes(headerName.toLocaleLowerCase()) && Object.keys(headers).includes(headerName)) { logger2.warn("Header could not be overridden", headerName, preventOverride); return; } const content = typeof this._extraHeaders[headerName] === "function" ? this._extraHeaders[headerName]() : this._extraHeaders[headerName]; if (content && content !== "") { headers[headerName] = content; } }); } }; // src/MetadataService.ts var MetadataService = class { constructor(_settings) { this._settings = _settings; this._logger = new Logger("MetadataService"); this._signingKeys = null; this._metadata = null; this._metadataUrl = this._settings.metadataUrl; this._jsonService = new JsonService( ["application/jwk-set+json"], null, this._settings.extraHeaders ); if (this._settings.signingKeys) { this._logger.debug("using signingKeys from settings"); this._signingKeys = this._settings.signingKeys; } if (this._settings.metadata) { this._logger.debug("using metadata from settings"); this._metadata = this._settings.metadata; } if (this._settings.fetchRequestCredentials) { this._logger.debug("using fetchRequestCredentials from settings"); this._fetchRequestCredentials = this._settings.fetchRequestCredentials; } } resetSigningKeys() { this._signingKeys = null; } async getMetadata() { const logger2 = this._logger.create("getMetadata"); if (this._metadata) { logger2.debug("using cached values"); return this._metadata; } if (!this._metadataUrl) { logger2.throw(new Error("No authority or metadataUrl configured on settings")); throw null; } logger2.debug("getting metadata from", this._metadataUrl); const metadata = await this._jsonService.getJson(this._metadataUrl, { credentials: this._fetchRequestCredentials, timeoutInSeconds: this._settings.requestTimeoutInSeconds }); logger2.debug("merging remote JSON with seed metadata"); this._metadata = Object.assign({}, metadata, this._settings.metadataSeed); return this._metadata; } getIssuer() { return this._getMetadataProperty("issuer"); } getAuthorizationEndpoint() { return this._getMetadataProperty("authorization_endpoint"); } getUserInfoEndpoint() { return this._getMetadataProperty("userinfo_endpoint"); } getTokenEndpoint(optional = true) { return this._getMetadataProperty("token_endpoint", optional); } getCheckSessionIframe() { return this._getMetadataProperty("check_session_iframe", true); } getEndSessionEndpoint() { return this._getMetadataProperty("end_session_endpoint", true); } getRevocationEndpoint(optional = true) { return this._getMetadataProperty("revocation_endpoint", optional); } getKeysEndpoint(optional = true) { return this._getMetadataProperty("jwks_uri", optional); } async _getMetadataProperty(name, optional = false) { const logger2 = this._logger.create(`_getMetadataProperty('${name}')`); const metadata = await this.getMetadata(); logger2.debug("resolved"); if (metadata[name] === void 0) { if (optional === true) { logger2.warn("Metadata does not contain optional property"); return void 0; } logger2.throw(new Error("Metadata does not contain property " + name)); } return metadata[name]; } async getSigningKeys() { const logger2 = this._logger.create("getSigningKeys"); if (this._signingKeys) { logger2.debug("returning signingKeys from cache"); return this._signingKeys; } const jwks_uri = await this.getKeysEndpoint(false); logger2.debug("got jwks_uri", jwks_uri); const keySet = await this._jsonService.getJson(jwks_uri, { timeoutInSeconds: this._settings.requestTimeoutInSeconds }); logger2.debug("got key set", keySet); if (!Array.isArray(keySet.keys)) { logger2.throw(new Error("Missing keys on keyset")); throw null; } this._signingKeys = keySet.keys; return this._signingKeys; } }; // src/WebStorageStateStore.ts var WebStorageStateStore = class { constructor({ prefix = "oidc.", store = localStorage } = {}) { this._logger = new Logger("WebStorageStateStore"); this._store = store; this._prefix = prefix; } async set(key, value) { this._logger.create(`set('${key}')`); key = this._prefix + key; await this._store.setItem(key, value); } async get(key) { this._logger.create(`get('${key}')`); key = this._prefix + key; const item = await this._store.getItem(key); return item; } async remove(key) { this._logger.create(`remove('${key}')`); key = this._prefix + key; const item = await this._store.getItem(key); await this._store.removeItem(key); return item; } async getAllKeys() { this._logger.create("getAllKeys"); const len = await this._store.length; const keys = []; for (let index = 0; index < len; index++) { const key = await this._store.key(index); if (key && key.indexOf(this._prefix) === 0) { keys.push(key.substr(this._prefix.length)); } } return keys; } }; // src/OidcClientSettings.ts var DefaultResponseType = "code"; var DefaultScope = "openid"; var DefaultClientAuthentication = "client_secret_post"; var DefaultStaleStateAgeInSeconds = 60 * 15; var OidcClientSettingsStore = class { constructor({ // metadata related authority, metadataUrl, metadata, signingKeys, metadataSeed, // client related client_id, client_secret, response_type = DefaultResponseType, scope = DefaultScope, redirect_uri, post_logout_redirect_uri, client_authentication = DefaultClientAuthentication, // optional protocol prompt, display, max_age, ui_locales, acr_values, resource, response_mode, // behavior flags filterProtocolClaims = true, loadUserInfo = false, requestTimeoutInSeconds, staleStateAgeInSeconds = DefaultStaleStateAgeInSeconds, mergeClaimsStrategy = { array: "replace" }, disablePKCE = false, // other behavior stateStore, revokeTokenAdditionalContentTypes, fetchRequestCredentials, refreshTokenAllowedScope, // extra extraQueryParams = {}, extraTokenParams = {}, extraHeaders = {}, dpop, omitScopeWhenRequesting = false }) { var _a; this.authority = authority; if (metadataUrl) { this.metadataUrl = metadataUrl; } else { this.metadataUrl = authority; if (authority) { if (!this.metadataUrl.endsWith("/")) { this.metadataUrl += "/"; } this.metadataUrl += ".well-known/openid-configuration"; } } this.metadata = metadata; this.metadataSeed = metadataSeed; this.signingKeys = signingKeys; this.client_id = client_id; this.client_secret = client_secret; this.response_type = response_type; this.scope = scope; this.redirect_uri = redirect_uri; this.post_logout_redirect_uri = post_logout_redirect_uri; this.client_authentication = client_authentication; this.prompt = prompt; this.display = display; this.max_age = max_age; this.ui_locales = ui_locales; this.acr_values = acr_values; this.resource = resource; this.response_mode = response_mode; this.filterProtocolClaims = filterProtocolClaims != null ? filterProtocolClaims : true; this.loadUserInfo = !!loadUserInfo; this.staleStateAgeInSeconds = staleStateAgeInSeconds; this.mergeClaimsStrategy = mergeClaimsStrategy; this.omitScopeWhenRequesting = omitScopeWhenRequesting; this.disablePKCE = !!disablePKCE; this.revokeTokenAdditionalContentTypes = revokeTokenAdditionalContentTypes; this.fetchRequestCredentials = fetchRequestCredentials ? fetchRequestCredentials : "same-origin"; this.requestTimeoutInSeconds = requestTimeoutInSeconds; if (stateStore) { this.stateStore = stateStore; } else { const store = typeof window !== "undefined" ? window.localStorage : new InMemoryWebStorage(); this.stateStore = new WebStorageStateStore({ store }); } this.refreshTokenAllowedScope = refreshTokenAllowedScope; this.extraQueryParams = extraQueryParams; this.extraTokenParams = extraTokenParams; this.extraHeaders = extraHeaders; this.dpop = dpop; if (this.dpop && !((_a = this.dpop) == null ? void 0 : _a.store)) { throw new Error("A DPoPStore is required when dpop is enabled"); } } }; // src/UserInfoService.ts var UserInfoService = class { constructor(_settings, _metadataService) { this._settings = _settings; this._metadataService = _metadataService; this._logger = new Logger("UserInfoService"); this._getClaimsFromJwt = async (responseText) => { const logger2 = this._logger.create("_getClaimsFromJwt"); try { const payload = JwtUtils.decode(responseText); logger2.debug("JWT decoding successful"); return payload; } catch (err) { logger2.error("Error parsing JWT response"); throw err; } }; this._jsonService = new JsonService( void 0, this._getClaimsFromJwt, this._settings.extraHeaders ); } async getClaims(token) { const logger2 = this._logger.create("getClaims"); if (!token) { this._logger.throw(new Error("No token passed")); } const url = await this._metadataService.getUserInfoEndpoint(); logger2.debug("got userinfo url", url); const claims = await this._jsonService.getJson(url, { token, credentials: this._settings.fetchRequestCredentials, timeoutInSeconds: this._settings.requestTimeoutInSeconds }); logger2.debug("got claims", claims); return claims; } }; // src/TokenClient.ts var TokenClient = class { constructor(_settings, _metadataService) { this._settings = _settings; this._metadataService = _metadataService; this._logger = new Logger("TokenClient"); this._jsonService = new JsonService( this._settings.revokeTokenAdditionalContentTypes, null, this._settings.extraHeaders ); } /** * Exchange code. * * @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3 */ async exchangeCode({ grant_type = "authorization_code", redirect_uri = this._settings.redirect_uri, client_id = this._settings.client_id, client_secret = this._settings.client_secret, extraHeaders, ...args }) { const logger2 = this._logger.create("exchangeCode"); if (!client_id) { logger2.throw(new Error("A client_id is required")); } if (!redirect_uri) { logger2.throw(new Error("A redirect_uri is required")); } if (!args.code) { logger2.throw(new Error("A code is required")); } const params = new URLSearchParams({ grant_type, redirect_uri }); for (const [key, value] of Object.entries(args)) { if (value != null) { params.set(key, value); } } let basicAuth; switch (this._settings.client_authentication) { case "client_secret_basic": if (client_secret === void 0 || client_secret === null) { logger2.throw(new Error("A client_secret is required")); throw null; } basicAuth = CryptoUtils.generateBasicAuth(client_id, client_secret); break; case "client_secret_post": params.append("client_id", client_id); if (client_secret) { params.append("client_secret", client_secret); } break; } const url = await this._metadataService.getTokenEndpoint(false); logger2.debug("got token endpoint"); const response = await this._jsonService.postForm(url, { body: params, basicAuth, timeoutInSeconds: this._settings.requestTimeoutInSeconds, initCredentials: this._settings.fetchRequestCredentials, extraHeaders }); logger2.debug("got response"); return response; } /** * Exchange credentials. * * @see https://www.rfc-editor.org/rfc/rfc6749#section-4.3.2 */ async exchangeCredentials({ grant_type = "password", client_id = this._settings.client_id, client_secret = this._settings.client_secret, scope = this._settings.scope, ...args }) { const logger2 = this._logger.create("exchangeCredentials"); if (!client_id) { logger2.throw(new Error("A client_id is required")); } const params = new URLSearchParams({ grant_type }); if (!this._settings.omitScopeWhenRequesting) { params.set("scope", scope); } for (const [key, value] of Object.entries(args)) { if (value != null) { params.set(key, value); } } let basicAuth; switch (this._settings.client_authentication) { case "client_secret_basic": if (client_secret === void 0 || client_secret === null) { logger2.throw(new Error("A client_secret is required")); throw null; } basicAuth = CryptoUtils.generateBasicAuth(client_id, client_secret); break; case "client_secret_post": params.append("client_id", client_id); if (client_secret) { params.append("client_secret", client_secret); } break; } const url = await this._metadataService.getTokenEndpoint(false); logger2.debug("got token endpoint"); const response = await this._jsonService.postForm(url, { body: params, basicAuth, timeoutInSeconds: this._settings.requestTimeoutInSeconds, initCredentials: this._settings.fetchRequestCredentials }); logger2.debug("got response"); return response; } /** * Exchange a refresh token. * * @see https://www.rfc-editor.org/rfc/rfc6749#section-6 */ async exchangeRefreshToken({ grant_type = "refresh_token", client_id = this._settings.client_id, client_secret = this._settings.client_secret, timeoutInSeconds, extraHeaders, ...args }) { const logger2 = this._logger.create("exchangeRefreshToken"); if (!client_id) { logger2.throw(new Error("A client_id is required")); } if (!args.refresh_token) { logger2.throw(new Error("A refresh_token is required")); } const params = new URLSearchParams({ grant_type }); for (const [key, value] of Object.entries(args)) { if (Array.isArray(value)) { value.forEach((param) => params.append(key, param)); } else if (value != null) { params.set(key, value); } } let basicAuth; switch (this._settings.client_authentication) { case "client_secret_basic": if (client_secret === void 0 || client_secret === null) { logger2.throw(new Error("A client_secret is required")); throw null; } basicAuth = CryptoUtils.generateBasicAuth(client_id, client_secret); break; case "client_secret_post": params.append("client_id", client_id); if (client_secret) { params.append("client_secret", client_secret); } break; } const url = await this._metadataService.getTokenEndpoint(false); logger2.debug("got token endpoint"); const response = await this._jsonService.postForm(url, { body: params, basicAuth, timeoutInSeconds, initCredentials: this._settings.fetchRequestCredentials, extraHeaders }); logger2.debug("got response"); return response; } /** * Revoke an access or refresh token. * * @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 */ async revoke(args) { var _a; const logger2 = this._logger.create("revoke"); if (!args.token) { logger2.throw(new Error("A token is required")); } const url = await this._metadataService.getRevocationEndpoint(false); logger2.debug(`got revocation endpoint, revoking ${(_a = args.token_type_hint) != null ? _a : "default token type"}`); const params = new URLSearchParams(); for (const [key, value] of Object.entries(args)) { if (value != null) { params.set(key, value); } } params.set("client_id", this._settings.client_id); if (this._settings.client_secret) { params.set("client_secret", this._settings.client_secret); } await this._jsonService.postForm(url, { body: params, timeoutInSeconds: this._settings.requestTimeoutInSeconds }); logger2.debug("got response"); } }; // src/ResponseValidator.ts var ResponseValidator = class { constructor(_settings, _metadataService, _claimsService) { this._settings = _settings; this._metadataService = _metadataService; this._claimsService = _claimsService; this._logger = new Logger("ResponseValidator"); this._userInfoService = new UserInfoService(this._settings, this._metadataService); this._tokenClient = new TokenClient(this._settings, this._metadataService); } async validateSigninResponse(response, state, extraHeaders) { const logger2 = this._logger.create("validateSigninResponse"); this._processSigninState(response, state); logger2.debug("state processed"); await this._processCode(response, state, extraHeaders); logger2.debug("code processed"); if (response.isOpenId) { this._validateIdTokenAttributes(response); } logger2.debug("tokens validated"); await this._processClaims(response, state == null ? void 0 : state.skipUserInfo, response.isOpenId); logger2.debug("claims processed"); } async validateCredentialsResponse(response, skipUserInfo) { const logger2 = this._logger.create("validateCredentialsResponse"); if (response.isOpenId && !!response.id_token) { this._validateIdTokenAttributes(response); } logger2.debug("tokens validated"); await this._processClaims(response, skipUserInfo, response.isOpenId); logger2.debug("claims processed"); } async validateRefreshResponse(response, state) { var _a, _b; const logger2 = this._logger.create("validateRefreshResponse"); response.userState = state.data; (_a = response.session_state) != null ? _a : response.session_state = state.session_state; (_b = response.scope) != null ? _b : response.scope = state.scope; if (response.isOpenId && !!response.id_token) { this._validateIdTokenAttributes(response, state.id_token); logger2.debug("ID Token validated"); } if (!response.id_token) { response.id_token = state.id_token; response.profile = state.profile; } const hasIdToken = response.isOpenId && !!response.id_token; await this._processClaims(response, false, hasIdToken); logger2.debug("claims processed"); } validateSignoutResponse(response, state) { const logger2 = this._logger.create("validateSignoutResponse"); if (state.id !== response.state) { logger2.throw(new Error("State does not match")); } logger2.debug("state validated"); response.userState = state.data; if (response.error) { logger2.warn("Response was error", response.error); throw new ErrorResponse(response); } } _processSigninState(response, state) { var _a; const logger2 = this._logger.create("_processSigninState"); if (state.id !== response.state) { logger2.throw(new Error("State does not match")); } if (!state.client_id) { logger2.throw(new Error("No client_id on state")); } if (!state.authority) { logger2.throw(new Error("No authority on state")); } if (this._settings.authority !== state.authority) { logger2.throw(new Error("authority mismatch on settings vs. signin state")); } if (this._settings.client_id && this._settings.client_id !== state.client_id) { logger2.throw(new Error("client_id mismatch on settings vs. signin state")); } logger2.debug("state validated"); response.userState = state.data; response.url_state = state.url_state; (_a = response.scope) != null ? _a : response.scope = state.scope; if (response.error) { logger2.warn("Response was error", response.error); throw new ErrorResponse(response); } if (state.code_verifier && !response.code) { logger2.throw(new Error("Expected code in response")); } } async _processClaims(response, skipUserInfo = false, validateSub = true) { const logger2 = this._logger.create("_processClaims"); response.profile = this._claimsService.filterProtocolClaims(response.profile); if (skipUserInfo || !this._settings.loadUserInfo || !response.access_token) { logger2.debug("not loading user info"); return; } logger2.debug("loading user info"); const claims = await this._userInfoService.getClaims(response.access_token); logger2.debug("user info claims received from user info endpoint"); if (validateSub && claims.sub !== response.profile.sub) { logger2.throw(new Error("subject from UserInfo response does not match subject in ID Token")); } response.profile = this._claimsService.mergeClaims(response.profile, this._claimsService.filterProtocolClaims(claims)); logger2.debug("user info claims received, updated profile:", response.profile); } async _processCode(response, state, extraHeaders) { const logger2 = this._logger.create("_processCode"); if (response.code) { logger2.debug("Validating code"); const tokenResponse = await this._tokenClient.exchangeCode({ client_id: state.client_id, client_secret: state.client_secret, code: response.code, redirect_uri: state.redirect_uri, code_verifier: state.code_verifier, extraHeaders, ...state.extraTokenParams }); Object.assign(response, tokenResponse); } else { logger2.debug("No code to process"); } } _validateIdTokenAttributes(response, existingToken) { var _a; const logger2 = this._logger.create("_validateIdTokenAttributes"); logger2.debug("decoding ID Token JWT"); const incoming = JwtUtils.decode((_a = response.id_token) != null ? _a : ""); if (!incoming.sub) { logger2.throw(new Error("ID Token is missing a subject claim")); } if (existingToken) { const existing = JwtUtils.decode(existingToken); if (incoming.sub !== existing.sub) { logger2.throw(new Error("sub in id_token does not match current sub")); } if (incoming.auth_time && incoming.auth_time !== existing.auth_time) { logger2.throw(new Error("auth_time in id_token does not match original auth_time")); } if (incoming.azp && incoming.azp !== existing.azp) { logger2.throw(new Error("azp in id_token does not match original azp")); } if (!incoming.azp && existing.azp) { logger2.throw(new Error("azp not in id_token, but present in original id_token")); } } response.profile = incoming; } }; // src/State.ts var State = class _State { constructor(args) { this.id = args.id || CryptoUtils.generateUUIDv4(); this.data = args.data; if (args.created && args.created > 0) { this.created = args.created; } else { this.created = Timer.getEpochTime(); } this.request_type = args.request_type; this.url_state = args.url_state; } toStorageString() { new Logger("State").create("toStorageString"); return JSON.stringify({ id: this.id, data: this.data, created: this.created, request_type: this.request_type, url_state: this.url_state }); } static fromStorageString(storageString) { Logger.createStatic("State", "fromStorageString"); return Promise.resolve(new _State(JSON.parse(storageString))); } static async clearStaleState(storage, age) { const logger2 = Logger.createStatic("State", "clearStaleState"); const cutoff = Timer.getEpochTime() - age; const keys = await storage.getAllKeys(); logger2.debug("got keys", keys); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const item = await storage.get(key); let remove = false; if (item) { try { const state = await _State.fromStorageString(item); logger2.debug("got item from key:", key, state.created); if (state.created <= cutoff) { remove = true; } } catch (err) { logger2.error("Error parsing state for key:", key, err); remove = true; }