oidc-client-ts
Version:
OpenID Connect (OIDC) & OAuth2 client library
1,513 lines (1,490 loc) • 119 kB
JavaScript
"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;
}