UNPKG

oidc-client-ts

Version:

OpenID Connect (OIDC) & OAuth2 client library

1,522 lines (1,494 loc) 92.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { AccessTokenEvents: () => AccessTokenEvents, CheckSessionIFrame: () => CheckSessionIFrame, ErrorResponse: () => ErrorResponse, ErrorTimeout: () => ErrorTimeout, InMemoryWebStorage: () => InMemoryWebStorage, 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(src_exports); // src/utils/CryptoUtils.ts var import_core = __toESM(require("crypto-js/core.js")); var import_sha256 = __toESM(require("crypto-js/sha256.js")); var import_enc_base64 = __toESM(require("crypto-js/enc-base64.js")); var import_enc_utf8 = __toESM(require("crypto-js/enc-utf8.js")); // 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 { constructor(_name) { this._name = _name; } 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); } } 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; } 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); } } }; Log.reset(); // src/utils/CryptoUtils.ts var UUID_V4_TEMPLATE = "10000000-1000-4000-8000-100000000000"; var CryptoUtils = class { static _randomWord() { return import_core.default.lib.WordArray.random(1).words[0]; } static generateUUIDv4() { const uuid = UUID_V4_TEMPLATE.replace( /[018]/g, (c) => (+c ^ CryptoUtils._randomWord() & 15 >> +c / 4).toString(16) ); return uuid.replace(/-/g, ""); } static generateCodeVerifier() { return CryptoUtils.generateUUIDv4() + CryptoUtils.generateUUIDv4() + CryptoUtils.generateUUIDv4(); } static generateCodeChallenge(code_verifier) { try { const hashed = (0, import_sha256.default)(code_verifier); return import_enc_base64.default.stringify(hashed).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } catch (err) { Logger.error("CryptoUtils.generateCodeChallenge", err); throw err; } } static generateBasicAuth(client_id, client_secret) { const basicAuth = import_enc_utf8.default.parse([client_id, client_secret].join(":")); return import_enc_base64.default.stringify(basicAuth); } }; // src/utils/Event.ts var Event = class { constructor(_name) { this._name = _name; this._logger = new Logger(`Event('${this._name}')`); this._callbacks = []; } 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); } } raise(...ev) { this._logger.debug("raise:", ...ev); for (const cb of this._callbacks) { void cb(...ev); } } }; // src/utils/JwtUtils.ts var import_jwt_decode = __toESM(require("jwt-decode")); var JwtUtils = class { static decode(token) { try { return (0, import_jwt_decode.default)(token); } catch (err) { Logger.error("JwtUtils.decode", err); throw err; } } }; // src/utils/PopupUtils.ts var PopupUtils = class { 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 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(); super.raise(); } }; } 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, window.location.origin); const params = parsedUrl[responseMode === "fragment" ? "hash" : "search"]; return new URLSearchParams(params.slice(1)); } }; // 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; 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; } }; // src/errors/ErrorTimeout.ts var ErrorTimeout = class extends Error { constructor(message) { super(message); 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; } 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(); } } unload() { this._logger.debug("unload: canceling existing access token timers"); this._expiringTimer.cancel(); this._expiredTimer.cancel(); } addAccessTokenExpiring(cb) { return this._expiringTimer.addHandler(cb); } removeAccessTokenExpiring(cb) { this._expiringTimer.removeHandler(cb); } addAccessTokenExpired(cb) { return this._expiredTimer.addHandler(cb); } 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/JsonService.ts var JsonService = class { constructor(additionalContentTypes = [], _jwtHandler = null) { this._jwtHandler = _jwtHandler; 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 } = {}) { 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; } let response; try { logger2.debug("url:", url); response = await this.fetchWithTimeout(url, { method: "GET", headers, 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 }) { const logger2 = this._logger.create("postForm"); const headers = { "Accept": this._contentTypes.join(", "), "Content-Type": "application/x-www-form-urlencoded" }; if (basicAuth !== void 0) { headers["Authorization"] = "Basic " + basicAuth; } 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 (json.error) { throw new ErrorResponse(json, body); } throw new Error(`${response.statusText} (${response.status}): ${JSON.stringify(json)}`); } return json; } }; // src/MetadataService.ts var MetadataService = class { constructor(_settings) { this._settings = _settings; this._logger = new Logger("MetadataService"); this._jsonService = new JsonService(["application/jwk-set+json"]); this._signingKeys = null; this._metadata = null; this._metadataUrl = this._settings.metadataUrl; 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 }); logger2.debug("merging remote JSON with seed metadata"); this._metadata = Object.assign({}, this._settings.metadataSeed, metadata); 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); 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 DefaultResponseMode = "query"; var DefaultStaleStateAgeInSeconds = 60 * 15; var DefaultClockSkewInSeconds = 60 * 5; var OidcClientSettingsStore = class { constructor({ authority, metadataUrl, metadata, signingKeys, metadataSeed, client_id, client_secret, response_type = DefaultResponseType, scope = DefaultScope, redirect_uri, post_logout_redirect_uri, client_authentication = DefaultClientAuthentication, prompt, display, max_age, ui_locales, acr_values, resource, response_mode = DefaultResponseMode, filterProtocolClaims = true, loadUserInfo = false, staleStateAgeInSeconds = DefaultStaleStateAgeInSeconds, clockSkewInSeconds = DefaultClockSkewInSeconds, userInfoJwtIssuer = "OP", mergeClaims = false, stateStore, refreshTokenCredentials, revokeTokenAdditionalContentTypes, fetchRequestCredentials, extraQueryParams = {}, extraTokenParams = {} }) { 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; this.loadUserInfo = !!loadUserInfo; this.staleStateAgeInSeconds = staleStateAgeInSeconds; this.clockSkewInSeconds = clockSkewInSeconds; this.userInfoJwtIssuer = userInfoJwtIssuer; this.mergeClaims = !!mergeClaims; this.revokeTokenAdditionalContentTypes = revokeTokenAdditionalContentTypes; if (fetchRequestCredentials && refreshTokenCredentials) { console.warn("Both fetchRequestCredentials and refreshTokenCredentials is set. Only fetchRequestCredentials will be used."); } this.fetchRequestCredentials = fetchRequestCredentials ? fetchRequestCredentials : refreshTokenCredentials ? refreshTokenCredentials : "same-origin"; if (stateStore) { this.stateStore = stateStore; } else { const store = typeof window !== "undefined" ? window.localStorage : new InMemoryWebStorage(); this.stateStore = new WebStorageStateStore({ store }); } this.extraQueryParams = extraQueryParams; this.extraTokenParams = extraTokenParams; } }; // 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); } 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 }); 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); } async exchangeCode({ grant_type = "authorization_code", redirect_uri = this._settings.redirect_uri, client_id = this._settings.client_id, client_secret = this._settings.client_secret, ...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")); } if (!args.code_verifier) { logger2.throw(new Error("A code_verifier 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) { 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, initCredentials: this._settings.fetchRequestCredentials }); logger2.debug("got response"); return response; } async exchangeCredentials({ grant_type = "password", client_id = this._settings.client_id, client_secret = this._settings.client_secret, scope = this._settings.scope, username, password }) { const logger2 = this._logger.create("exchangeCredentials"); if (!client_id) { logger2.throw(new Error("A client_id is required")); } const params = new URLSearchParams({ grant_type, username, password, scope }); let basicAuth; switch (this._settings.client_authentication) { case "client_secret_basic": if (!client_secret) { 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, initCredentials: this._settings.fetchRequestCredentials }); logger2.debug("got response"); return response; } async exchangeRefreshToken({ grant_type = "refresh_token", client_id = this._settings.client_id, client_secret = this._settings.client_secret, timeoutInSeconds, ...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 (value != null) { params.set(key, value); } } let basicAuth; switch (this._settings.client_authentication) { case "client_secret_basic": if (!client_secret) { 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 }); logger2.debug("got response"); return response; } 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 }); logger2.debug("got response"); } }; // src/ResponseValidator.ts var ProtocolClaims = [ "iss", "aud", "exp", "nbf", "iat", "jti", "auth_time", "nonce", "acr", "amr", "azp", "at_hash" ]; var ResponseValidator = class { constructor(_settings, _metadataService) { this._settings = _settings; this._metadataService = _metadataService; 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) { const logger2 = this._logger.create("validateSigninResponse"); this._processSigninState(response, state); logger2.debug("state processed"); await this._processCode(response, state); 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) { 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; (_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")); } if (!state.code_verifier && response.code) { logger2.throw(new Error("Unexpected code in response")); } } async _processClaims(response, skipUserInfo = false, validateSub = true) { const logger2 = this._logger.create("_processClaims"); response.profile = this._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._mergeClaims(response.profile, this._filterProtocolClaims(claims)); logger2.debug("user info claims received, updated profile:", response.profile); } _mergeClaims(claims1, claims2) { const result = { ...claims1 }; for (const [claim, values] of Object.entries(claims2)) { for (const value of Array.isArray(values) ? values : [values]) { const previousValue = result[claim]; if (!previousValue) { result[claim] = value; } else if (Array.isArray(previousValue)) { if (!previousValue.includes(value)) { previousValue.push(value); } } else if (result[claim] !== value) { if (typeof value === "object" && this._settings.mergeClaims) { result[claim] = this._mergeClaims(previousValue, value); } else { result[claim] = [previousValue, value]; } } } } return result; } _filterProtocolClaims(claims) { const result = { ...claims }; if (this._settings.filterProtocolClaims) { for (const type of ProtocolClaims) { delete result[type]; } } return result; } async _processCode(response, state) { 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, ...state.extraTokenParams }); Object.assign(response, tokenResponse); } else { logger2.debug("No code to process"); } } _validateIdTokenAttributes(response, currentToken) { var _a; const logger2 = this._logger.create("_validateIdTokenAttributes"); logger2.debug("decoding ID Token JWT"); const profile = JwtUtils.decode((_a = response.id_token) != null ? _a : ""); if (!profile.sub) { logger2.throw(new Error("ID Token is missing a subject claim")); } if (currentToken) { const current = JwtUtils.decode(currentToken); if (current.sub !== profile.sub) { logger2.throw(new Error("sub in id_token does not match current sub")); } if (current.auth_time && current.auth_time !== profile.auth_time) { logger2.throw(new Error("auth_time in id_token does not match original auth_time")); } if (current.azp && current.azp !== profile.azp) { logger2.throw(new Error("azp in id_token does not match original azp")); } if (!current.azp && profile.azp) { logger2.throw(new Error("azp not in id_token, but present in original id_token")); } } response.profile = profile; } }; // src/State.ts var State = class { 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; } toStorageString() { new Logger("State").create("toStorageString"); return JSON.stringify({ id: this.id, data: this.data, created: this.created, request_type: this.request_type }); } static fromStorageString(storageString) { Logger.createStatic("State", "fromStorageString"); return 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 = 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; } } else { logger2.debug("no item in storage for key:", key); remove = true; } if (remove) { logger2.debug("removed item for key:", key); void storage.remove(key); } } } }; // src/SigninState.ts var SigninState = class extends State { constructor(args) { super(args); if (args.code_verifier === true) { this.code_verifier = CryptoUtils.generateCodeVerifier(); } else if (args.code_verifier) { this.code_verifier = args.code_verifier; } if (this.code_verifier) { this.code_challenge = CryptoUtils.generateCodeChallenge(this.code_verifier); } this.authority = args.authority; this.client_id = args.client_id; this.redirect_uri = args.redirect_uri; this.scope = args.scope; this.client_secret = args.client_secret; this.extraTokenParams = args.extraTokenParams; this.response_mode = args.response_mode; this.skipUserInfo = args.skipUserInfo; } toStorageString() { new Logger("SigninState").create("toStorageString"); return JSON.stringify({ id: this.id, data: this.data, created: this.created, request_type: this.request_type, code_verifier: this.code_verifier, authority: this.authority, client_id: this.client_id, redirect_uri: this.redirect_uri, scope: this.scope, client_secret: this.client_secret, extraTokenParams: this.extraTokenParams, response_mode: this.response_mode, skipUserInfo: this.skipUserInfo }); } static fromStorageString(storageString) { Logger.createStatic("SigninState", "fromStorageString"); const data = JSON.parse(storageString); return new SigninState(data); } }; // src/SigninRequest.ts var SigninRequest = class { constructor({ url, authority, client_id, redirect_uri, response_type, scope, state_data, response_mode, request_type, client_secret, nonce, skipUserInfo, extraQueryParams, extraTokenParams, ...optionalParams }) { this._logger = new Logger("SigninRequest"); if (!url) { this._logger.error("ctor: No url passed"); throw new Error("url"); } if (!client_id) { this._logger.error("ctor: No client_id passed"); throw new Error("client_id"); } if (!redirect_uri) { this._logger.error("ctor: No redirect_uri passed"); throw new Error("redirect_uri"); } if (!response_type) { this._logger.error("ctor: No response_type passed"); throw new Error("response_type"); } if (!scope) { this._logger.error("ctor: No scope passed"); throw new Error("scope"); } if (!authority) { this._logger.error("ctor: No authority passed"); throw new Error("authority"); } this.state = new SigninState({ data: state_data, request_type, code_verifier: true, client_id, authority, redirect_uri, response_mode, client_secret, scope, extraTokenParams, skipUserInfo }); const parsedUrl = new URL(url); parsedUrl.searchParams.append("client_id", client_id); parsedUrl.searchParams.append("redirect_uri", redirect_uri); parsedUrl.searchParams.append("response_type", response_type); parsedUrl.searchParams.append("scope", scope); if (nonce) { parsedUrl.searchParams.append("nonce", nonce); } parsedUrl.searchParams.append("state", this.state.id); if (this.state.code_challenge) { parsedUrl.searchParams.append("code_challenge", this.state.code_challenge); parsedUrl.searchParams.append("code_challenge_method", "S256"); } for (const [key, value] of Object.entries({ response_mode, ...optionalParams, ...extraQueryParams })) { if (value != null) { parsedUrl.searchParams.append(key, value.toString()); } } this.url = parsedUrl.href; } }; // src/SigninResponse.ts var OidcScope = "openid"; var SigninResponse = class { constructor(params) { this.access_token = ""; this.token_type = ""; this.profile = {}; this.state = params.get("state"); this.session_state = params.get("session_state"); this.error = params.get("error"); this.error_description = params.get("error_description"); this.error_uri = params.get("error_uri"); this.code = params.get("code"); } get expires_in() { if (this.expires_at === void 0) { return void 0; } return this.expires_at - Timer.getEpochTime(); } set expires_in(value) { if (typeof value === "string") value = Number(value); if (value !== void 0 && value >= 0) { this.expires_at = Math.floor(value) + Timer.getEpochTime(); } } get isOpenId() { var _a; return ((_a = this.scope) == null ? void 0 : _a.split(" ").includes(OidcScope)) || !!this.id_token; } }; // src/SignoutRequest.ts var SignoutRequest = class { constructor({ url, state_data, id_token_hint, post_logout_redirect_uri, extraQueryParams, request_type }) { this._logger = new Logger("SignoutRequest"); if (!url) { this._logger.error("ctor: No url passed"); throw new Error("url"); } const parsedUrl = new URL(url); if (id_token_hint) { parsedUrl.searchParams.append("id_token_hint", id_token_hint); } if (post_logout_redirect_uri) { parsedUrl.searchParams.append("post_logout_redirect_uri", post_logout_redirect_uri); if (state_data) { this.state = new State({ data: state_data, request_type }); parsedUrl.searchParams.append("state", this.state.id); } } for (const [key, value] of Object.entries({ ...extraQueryParams })) { if (value != null) { parsedUrl.searchParams.append(key, value.toString()); } } this.url = parsedUrl.href; } }; // src/SignoutResponse.ts var SignoutResponse = class { constructor(params) { this.state = params.get("state"); this.error = params.get("error"); this.error_description = params.get("error_description"); this.error_uri = params.get("error_uri"); } }; // src/OidcClient.ts var OidcClient = class { constructor(settings) { this._logger = new Logger("OidcClient"); this.settings = new OidcClientSettingsStore(settings); this.metadataService = new MetadataService(this.settings); this._validator = new ResponseValidator(this.settings, this.metadataService); this._tokenClient = new TokenClient(this.settings, this.metadataService); } async createSigninRequest({ state, request, request_uri, request_type, id_token_hint, login_hint, skipUserInfo, nonce, response_type = this.settings.response_type, scope = this.settings.scope, redirect_uri = this.settings.redirect_uri, prompt = this.settings.prompt, display = this.settings.display, max_age = this.settings.max_age, ui_locales = this.settings.ui_locales, acr_values = this.settings.acr_values, resource = this.settings.resource, response_mode = this.settings.response_mode, extraQueryParams = this.settings.extraQueryParams, extraTokenParams = this.settings.extraTokenParams }) { const logger2 = this._logger.create("createSigninRequest"); if (response_type !== "code") { throw new Error("Only the Authorization Code flow (with PKCE) is supported"