oidc-client-ts
Version:
OpenID Connect (OIDC) & OAuth2 client library
1,522 lines (1,494 loc) • 92.8 kB
JavaScript
"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"