oauth-entra-id
Version:
🛡️ A Secure, Performant, and Feature-Rich OAuth 2.0 Integration for Microsoft Entra ID — Fully Abstracted and Production-Ready.
1,415 lines • 59.4 kB
JavaScript
import { z, ZodError } from 'zod';
import { ENCRYPTION_REGEX, stringifyObj, parseToObj } from 'cipher-kit';
import { COMPRESSION_REGEX, compressObj, decompressObj } from 'compress-kit';
import { ConfidentialClientApplication, CryptoProvider } from '@azure/msal-node';
import { JwksClient } from 'jwks-rsa';
import { generateUuid, createSecretKey, isNodeKey, encrypt, decrypt } from 'cipher-kit/node';
import { generateUuid as generateUuid$1, isWebApiKey, createSecretKey as createSecretKey$1, encrypt as encrypt$1, decrypt as decrypt$1 } from 'cipher-kit/web-api';
import jwt from 'jsonwebtoken';
// src/error.ts
function $isStr(value) {
return value !== null && value !== void 0 && typeof value === "string" && value.trim().length > 0;
}
function $isObj(value) {
return typeof value === "object" && value !== null && value !== void 0 && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
}
var base64urlWithDotRegex = /^[A-Za-z0-9._-]+$/;
var zStr = z.string().trim();
var zUuid = z.uuid();
var zUrl = z.url();
var zEmail = z.email({ pattern: z.regexes.html5Email });
z.base64url();
var zLooseBase64 = zStr.regex(base64urlWithDotRegex);
zStr.regex(COMPRESSION_REGEX.GENERAL);
var zLoginPrompt = z.enum(["email", "select-account", "sso"]);
var zTimeUnit = z.enum(["ms", "sec"]);
var zCryptoType = z.enum(["web-api", "node"]);
var zAccessTokenExpiry = z.number().positive();
var zRefreshTokenExpiry = z.number().min(3600);
var zOneOrMoreUrls = z.union([zUrl.max(2048).transform((url) => [url]), z.array(zUrl.max(2048)).min(1)]);
var zEncrypted = zStr.max(4096).regex(ENCRYPTION_REGEX.GENERAL);
var zJwt = z.jwt().max(4096);
var zTenantId = z.union([z.literal("common"), zUuid]);
var zScope = zStr.min(3).max(128);
var zEncryptionKey = zStr.min(32).max(64);
var zServiceName = zStr.min(1).max(64);
var zAzure = z.object({
clientId: zUuid,
tenantId: zTenantId,
scopes: z.array(zScope).min(1),
clientSecret: zStr.min(32).max(128),
downstreamServices: z.array(
z.object({
serviceName: zServiceName,
scope: zScope,
serviceUrl: zOneOrMoreUrls,
encryptionKey: zEncryptionKey,
cryptoType: zCryptoType.default("node"),
accessTokenExpiry: zAccessTokenExpiry.default(3600)
})
).min(1).optional(),
b2bApps: z.array(z.object({ appName: zServiceName, scope: zScope })).min(1).optional()
});
var zConfig = z.object({
azure: z.union([zAzure.transform((azure) => [azure]), z.array(zAzure).min(1)]),
frontendUrl: zOneOrMoreUrls,
serverCallbackUrl: zUrl.max(2048),
encryptionKey: zEncryptionKey,
advanced: z.object({
loginPrompt: zLoginPrompt.default("sso"),
acceptB2BRequests: z.boolean().default(false),
cryptoType: zCryptoType.default("node"),
disableCompression: z.boolean().default(false),
cookies: z.object({
timeUnit: zTimeUnit.default("sec"),
disableSecure: z.boolean().default(false),
disableSameSite: z.boolean().default(false),
accessTokenExpiry: zAccessTokenExpiry.default(3600),
refreshTokenExpiry: zRefreshTokenExpiry.default(2592e3)
}).prefault({})
}).prefault({})
});
var zJwtClientConfigBase = z.object({ clientId: zUuid, tenantId: zTenantId });
var zJwtClientConfig = z.object({
azure: z.union([
zJwtClientConfigBase,
zJwtClientConfigBase.extend({
clientSecret: zStr.min(32).max(128),
b2bApps: z.array(z.object({ appName: zServiceName, scope: zScope })).min(1).optional()
})
])
});
var zState = z.object({
azureId: zUuid,
frontendUrl: zUrl.max(2048),
codeVerifier: zStr.max(256),
nonce: zUuid,
email: zEmail.max(320).optional(),
prompt: z.enum(["login", "select_account"]).optional(),
ticketId: zUuid
});
var zInjectedData = z.record(zStr, z.any()).optional();
var zAtStruct = z.object({
at: zJwt,
inj: zStr.max(4096).optional(),
exp: z.number().int().positive(),
aid: zUuid
});
var zRtStruct = z.object({
rt: zLooseBase64,
exp: z.number().int().positive(),
aid: zUuid
});
var zMethods = {
getAuthUrl: z.object({
loginPrompt: zLoginPrompt.optional(),
email: zEmail.max(320).optional(),
frontendUrl: zUrl.max(2048).optional(),
azureId: zUuid.optional()
}).default({}),
getTokenByCode: z.object({
code: zStr.max(2048).regex(base64urlWithDotRegex),
state: zEncrypted
}),
getLogoutUrl: z.object({
frontendUrl: zUrl.max(2048).optional(),
azureId: zUuid.optional()
}).default({}),
tryGetB2BToken: z.union([
z.object({
azureId: zUuid.optional(),
app: zServiceName
}).transform((data) => ({
azureId: data.azureId,
apps: [data.app]
})),
z.object({
azureId: zUuid.optional(),
apps: z.array(zServiceName).min(1)
})
]),
getTokenOnBehalfOf: z.union([
z.object({
accessToken: z.union([zJwt, zEncrypted]),
service: zServiceName,
azureId: zUuid.optional()
}).transform((data) => ({
accessToken: data.accessToken,
services: [data.service],
azureId: data.azureId
})),
z.object({
accessToken: z.union([zJwt, zEncrypted]),
services: z.array(zServiceName).min(1),
azureId: zUuid.optional()
})
])
};
// src/error.ts
function $ok(result) {
if ($isObj(result)) {
return { success: true, ...result };
}
return { success: true, result };
}
function $err(err) {
if (err instanceof OAuthError) {
return {
success: false,
error: { message: err.message, description: err.description, statusCode: err.statusCode }
};
}
return {
success: false,
error: "msg" in err && "desc" in err ? { message: err.msg, description: err.desc, statusCode: err.status ?? 400 } : { message: err.message, description: err.description, statusCode: err.statusCode ?? 400 }
};
}
var OAuthError = class extends Error {
constructor(err) {
if ("error" in err && "success" in err) {
super(err.error.message);
this.statusCode = err.error.statusCode;
this.description = err.error.description;
} else if ("msg" in err && "desc" in err) {
super(err.msg);
this.statusCode = err.status ?? 400;
this.description = err.desc;
} else if ("message" in err && "description" in err && "statusCode" in err) {
super(err.message);
this.statusCode = err.statusCode;
this.description = err.description;
} else {
super("An unknown error occurred");
this.statusCode = 500;
this.description = "An unknown error occurred";
}
this.name = "OAuthError";
Object.setPrototypeOf(this, new.target.prototype);
if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
}
};
function $stringErr(err) {
switch (true) {
case err instanceof OAuthError:
return `OAuthError: ${err.message} (${err.statusCode}) - ${err.description}`;
case err instanceof ZodError:
return `ZodError (Schema Validation): ${err.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "root"}: ${issue.message}`).join(". ")}`;
case err instanceof Error:
return `Error ${err.name}: ${err.message} - ${err.stack ?? "No stack trace available"}`;
case typeof err === "string":
return `String Error: ${err}`;
case (typeof err === "object" && err !== null):
switch (true) {
case ("success" in err && err.success === false && "error" in err && typeof err.error === "object" && err.error && "message" in err.error && "description" in err.error):
return `ResultErr Error: ${err.error.message}${"statusCode" in err.error ? ` (${err.error.statusCode})` : ""} - ${err.error.description}`;
case ("message" in err && "description" in err):
return `ResultErr Error: ${err.message}${"statusCode" in err ? ` (${err.statusCode})` : ""} - ${err.description}`;
case ("msg" in err && "desc" in err):
return `ResultErr Error: ${err.msg}${"status" in err ? ` (${err.status})` : ""} - ${err.desc}`;
default:
try {
return `Object Error: ${JSON.stringify(err, (_, v) => typeof v === "bigint" ? v.toString() : v)}`;
} catch {
return `Object Error: [Unserializable] ${String(err)}`;
}
}
default:
return `Unknown Error: ${String(err)}`;
}
}
// src/utils/cookie-options.ts
var ACCESS_TOKEN_NAME = "at";
var REFRESH_TOKEN_NAME = "rt";
function $getCookieOptions(params) {
const timeFrame = params.timeUnit === "sec" ? 1 : 1e3;
const baseOptions = {
httpOnly: true,
secure: params.secure,
sameSite: params.sameSite ? "strict" : params.secure ? "none" : void 0,
path: "/"
};
return {
accessTokenOptions: { ...baseOptions, maxAge: params.atExp * timeFrame },
refreshTokenOptions: { ...baseOptions, maxAge: params.rtExp * timeFrame },
deleteTokenOptions: { ...baseOptions, maxAge: 0 }
};
}
function $getCookieNames(clientId, secure) {
return {
accessTokenName: `${`${secure ? "__Host-" : ""}${ACCESS_TOKEN_NAME}-${clientId}`}`,
refreshTokenName: `${`${secure ? "__Host-" : ""}${REFRESH_TOKEN_NAME}-${clientId}`}`
};
}
function $generateUuid(cryptoType) {
const uuid = cryptoType === "node" ? generateUuid() : generateUuid$1();
if (uuid.error) return $err({ msg: "Failed to generate UUID", desc: $stringErr(uuid.error) });
return $ok({ uuid: uuid.result });
}
function $newSecretKeys(cryptoType, keys) {
if (cryptoType === "web-api") {
return $ok({ secretKeys: keys });
}
const secretKeys = {};
for (const name of Object.keys(keys)) {
const { secretKey, error } = createSecretKey(keys[name]);
if (error) return $err({ msg: `Failed to create ${name} key`, desc: `Key Creation - ${$stringErr(error)}` });
secretKeys[name] = secretKey;
}
return $ok({ secretKeys });
}
async function $encrypt(cryptoType, data, key) {
if (!data) return $err({ msg: "Invalid data", desc: "Empty string to encrypt" });
if (cryptoType === "node") {
if (!$isStr(key) && !isNodeKey(key)) {
return $err({ msg: "Invalid key type", desc: "Expected NodeKey or string" });
}
const { secretKey: secretKey2, error: secretKeyError2 } = createSecretKey(key);
if (secretKeyError2) {
return $err({ msg: "Failed to create Node secret key", desc: `Key Creation - ${$stringErr(secretKeyError2)}` });
}
const encrypted2 = encrypt(data, secretKey2);
if (encrypted2.error) return $err({ msg: "Encryption failed", desc: `Encryption - ${$stringErr(encrypted2.error)}` });
return $ok({ encrypted: encrypted2.result, newSecretKey: void 0 });
}
if (!$isStr(key) && !isWebApiKey(key)) {
return $err({ msg: "Invalid key type", desc: "Expected string or WebApiKey" });
}
const { secretKey, error: secretKeyError } = await createSecretKey$1(key);
if (secretKeyError) return $err({ msg: "Failed to create Web API secret key", desc: $stringErr(secretKeyError) });
const encrypted = await encrypt$1(data, secretKey);
if (encrypted.error) return $err({ msg: "Encryption failed", desc: `Encryption - ${$stringErr(encrypted.error)}` });
return $ok({ encrypted: encrypted.result, newSecretKey: secretKey });
}
async function $decrypt(cryptoType, encrypted, key) {
if (!encrypted) return $err({ msg: "Invalid data", desc: "Empty string to decrypt" });
if (cryptoType === "node") {
if (!$isStr(key) && !isNodeKey(key)) {
return $err({ msg: "Invalid key type", desc: "Expected NodeKey or string" });
}
const { secretKey: secretKey2, error: secretKeyError2 } = createSecretKey(key);
if (secretKeyError2) return $err({ msg: "Failed to create Node secret key", desc: $stringErr(secretKeyError2) });
const decrypted2 = decrypt(encrypted, secretKey2);
if (decrypted2.error) return $err({ msg: "Decryption failed", desc: `Decryption - ${$stringErr(decrypted2.error)}` });
return $ok({ result: decrypted2.result, newSecretKey: void 0 });
}
if (!$isStr(key) && !isWebApiKey(key)) {
return $err({ msg: "Invalid key type", desc: "Expected string or WebApiKey" });
}
const { secretKey, error: secretKeyError } = await createSecretKey$1(key);
if (secretKeyError) return $err({ msg: "Failed to create Web API secret key", desc: $stringErr(secretKeyError) });
const decrypted = await decrypt$1(encrypted, secretKey);
if (decrypted.error) return $err({ msg: "Decryption failed", desc: `Decryption - ${$stringErr(decrypted.error)}` });
return $ok({ result: decrypted.result, newSecretKey: secretKey });
}
async function $encryptObj(cryptoType, obj, key) {
if (!obj) return $err({ msg: "Invalid data", desc: "Empty object to encrypt" });
const { result, error } = stringifyObj(obj);
if (error) return $err({ msg: "Failed to stringify object", desc: $stringErr(error) });
return await $encrypt(cryptoType, result, key);
}
async function $decryptObj(cryptoType, encrypted, key) {
if (!encrypted) return $err({ msg: "Invalid data", desc: "Empty string to decrypt" });
const decrypted = await $decrypt(cryptoType, encrypted, key);
if (decrypted.error) return decrypted;
const { result, error } = parseToObj(decrypted.result);
if (error) return $err({ msg: "Failed to parse object", desc: $stringErr(error) });
return $ok({ result, newSecretKey: decrypted.newSecretKey });
}
// src/utils/config.ts
function $oauthConfig(configuration) {
const { data: config, error: configError } = zConfig.safeParse(configuration);
if (configError) return $err({ msg: "Invalid config", desc: $stringErr(configError), status: 500 });
const frontendUrls = config.frontendUrl;
const frontUrlObjects = frontendUrls.map((url) => new URL(url));
const serverUrlObject = new URL(config.serverCallbackUrl);
const frontendHosts = new Set(frontUrlObjects.map((url) => url.host));
const serverHost = serverUrlObject.host;
const { cookies } = config.advanced;
const baseCookieOptions = $getCookieOptions({
timeUnit: cookies.timeUnit,
atExp: cookies.accessTokenExpiry,
rtExp: cookies.refreshTokenExpiry,
secure: !cookies.disableSecure && [serverUrlObject, ...frontUrlObjects].every((url) => url.protocol === "https:"),
sameSite: !cookies.disableSameSite && frontendHosts.size === 1 && frontendHosts.has(serverHost)
});
try {
const azures = config.azure.map((azure) => {
const cca = $createCca({
clientId: azure.clientId,
tenantId: azure.tenantId,
clientSecret: azure.clientSecret
});
const b2b = $getB2B(azure.b2bApps);
const obo = $getObo({
oboServices: azure.downstreamServices,
secure: baseCookieOptions.accessTokenOptions.secure,
sameSite: baseCookieOptions.accessTokenOptions.sameSite,
atExp: config.advanced.cookies.accessTokenExpiry,
serverUrlObject
});
const cookieNames = $getCookieNames(azure.clientId, baseCookieOptions.accessTokenOptions.secure);
return {
clientId: azure.clientId,
tenantId: azure.tenantId,
scopes: azure.scopes,
cookiesNames: cookieNames,
cca,
b2b: b2b.map,
b2bNames: b2b.names,
obo: obo.map,
oboNames: obo.names
};
}).filter((azure) => !!azure);
if (azures.length === 0) {
throw new OAuthError({
msg: "No valid Azure configurations found",
desc: "Ensure at least one Azure configuration is provided in the config file.",
status: 500
});
}
const { secretKeys: encryptionKeys, error: secretKeysError } = $newSecretKeys(config.advanced.cryptoType, {
accessToken: `access-token-${config.encryptionKey}`,
refreshToken: `refresh-token-${config.encryptionKey}`,
state: `state-${config.encryptionKey}`,
ticket: `ticket-${config.encryptionKey}`
});
if (secretKeysError) return $err(secretKeysError);
const msalCryptoProvider = new CryptoProvider();
const { jwksClient, error: jwksError } = $createJwks(azures.length === 1 ? azures[0].tenantId : "common");
if (jwksError) return $err(jwksError);
const settings = {
loginPrompt: config.advanced.loginPrompt,
acceptB2BRequests: config.advanced.acceptB2BRequests,
cryptoType: config.advanced.cryptoType,
disableCompression: config.advanced.disableCompression,
b2bApps: azures.some((azure) => azure.b2bNames) ? azures.map((azure) => ({ azureId: azure.clientId, names: azure.b2bNames })).filter((azure) => !!azure) : void 0,
downstreamServices: azures.some((azure) => azure.oboNames) ? azures.map((azure) => ({ azureId: azure.clientId, names: azure.oboNames })).filter((azure) => !!azure) : void 0,
azures: azures.map((azure) => ({ azureId: azure.clientId, tenantId: azure.tenantId })),
cookies: {
timeUnit: config.advanced.cookies.timeUnit,
isSecure: baseCookieOptions.accessTokenOptions.secure,
isSameSite: baseCookieOptions.accessTokenOptions.sameSite === "strict",
accessTokenName: azures[0].cookiesNames.accessTokenName,
accessTokenExpiry: config.advanced.cookies.accessTokenExpiry,
refreshTokenName: azures[0].cookiesNames.refreshTokenName,
refreshTokenExpiry: config.advanced.cookies.refreshTokenExpiry,
cookieNames: azures.map((azure) => ({
azureId: azure.clientId,
accessTokenName: azure.cookiesNames.accessTokenName,
refreshTokenName: azure.cookiesNames.refreshTokenName
})),
deleteOptions: baseCookieOptions.deleteTokenOptions
}
};
return $ok({
azures,
frontendUrls,
frontendWhitelist: frontendHosts,
serverCallbackUrl: config.serverCallbackUrl,
baseCookieOptions,
encryptionKeys,
msalCryptoProvider,
jwksClient,
settings
});
} catch (error) {
if (error instanceof OAuthError) return $err(error);
return $err({
msg: "Failed to create Azure configurations",
desc: `OAuth Provider Constructor - ${$stringErr(error)}`,
status: 500
});
}
}
function $jwtClientConfig(config) {
const { data: parsedConfig, error: configError } = zJwtClientConfig.safeParse(config);
if (configError) return $err({ msg: "Invalid config", desc: $stringErr(configError), status: 500 });
const { jwksClient, error: jwksError } = $createJwks(parsedConfig.azure.tenantId);
if (jwksError) return $err(jwksError);
if (!("clientSecret" in parsedConfig.azure)) {
return $ok({
azure: {
clientId: parsedConfig.azure.clientId,
tenantId: parsedConfig.azure.tenantId,
cca: void 0,
b2bApps: void 0
},
jwksClient
});
}
try {
const b2b = $getB2B(parsedConfig.azure.b2bApps);
const cca = $createCca({
clientId: parsedConfig.azure.clientId,
tenantId: parsedConfig.azure.tenantId,
clientSecret: parsedConfig.azure.clientSecret
});
return $ok({
azure: {
clientId: parsedConfig.azure.clientId,
tenantId: parsedConfig.azure.tenantId,
cca,
b2bApps: b2b.map
},
jwksClient
});
} catch (error) {
if (error instanceof OAuthError) return $err(error);
return $err({
msg: "Failed to create Azure configuration",
desc: `OAuth Lite Provider Constructor - ${$stringErr(error)}`,
status: 500
});
}
}
function $createJwks(tenantId) {
try {
return $ok({
jwksClient: new JwksClient({
cache: true,
cacheMaxEntries: 5,
cacheMaxAge: 10 * 60 * 1e3,
jwksUri: `https://login.microsoftonline.com/${tenantId}/discovery/v2.0/keys`
})
});
} catch (error) {
return $err({ msg: "Failed to create JWKS client", desc: $stringErr(error), status: 500 });
}
}
function $createCca(params) {
try {
return new ConfidentialClientApplication({
auth: {
clientId: params.clientId,
authority: `https://login.microsoftonline.com/${params.tenantId}`,
clientSecret: params.clientSecret
}
});
} catch (error) {
throw new OAuthError({
msg: "Failed to create Confidential Client Application",
desc: $stringErr(error),
status: 500
});
}
}
function $getB2B(b2bApps) {
if (!b2bApps) return { map: void 0, names: void 0 };
const map = new Map(
b2bApps.map((app) => [
app.appName,
{ appName: app.appName, scope: app.scope, token: null, exp: null, aud: null, msalResponse: null }
])
);
const names = Array.from(map.keys());
if (names.length !== b2bApps.length) {
throw new OAuthError({ msg: "Invalid config", desc: "B2B has duplicates", status: 500 });
}
return { map, names };
}
function $getObo({
oboServices,
secure,
sameSite,
atExp,
serverUrlObject
}) {
if (!oboServices) return { map: void 0, names: void 0 };
const map = new Map(
oboServices.map((service) => {
const serviceUrlObjects = service.serviceUrl.map((url) => new URL(url));
const serviceUrlHosts = new Set(serviceUrlObjects.map((url) => url.host));
return [
service.serviceName,
{
serviceName: service.serviceName,
scope: service.scope,
encryptionKey: service.encryptionKey,
cryptoType: service.cryptoType,
isSecure: secure && [serverUrlObject, ...serviceUrlObjects].every((url) => url.protocol === "https:"),
isSamesite: sameSite === "strict" && serviceUrlHosts.size === 1 && serviceUrlHosts.has(serverUrlObject.host),
atExp: service.accessTokenExpiry ?? atExp
}
];
})
);
const names = Array.from(map.keys());
if (names.length !== oboServices.length) {
throw new OAuthError({ msg: "Invalid config", desc: "OBO has duplicates", status: 500 });
}
return { map, names };
}
function $extractDataFromPayload(payload) {
if (!payload || typeof payload === "string") {
return $err({ msg: "Unauthorized", desc: "Payload is a string or null", status: 401 });
}
const isApp = payload.sub === payload.oid;
return $ok({
meta: {
audience: payload.aud,
issuer: payload.iss,
subject: payload.sub,
issuedAt: payload.iat,
expiration: payload.exp,
uniqueId: payload.oid,
azureId: payload.aud,
tenantId: payload.tid,
roles: payload.roles,
uniqueTokenId: payload.uti,
...isApp ? {
isApp: true,
appId: payload.azp
} : {
isApp: false,
name: payload.name,
email: payload.preferred_username
}
}
});
}
async function $verifyJwt({
jwtToken,
azure,
jwksClient
}) {
const { kid, tenantId, issuer, error } = $getKeyIdAndExtra(jwtToken);
if (error) return $err({ msg: "Unauthorized", desc: `Key ID Extraction - ${$stringErr(error)}`, status: 401 });
if (azure.tenantId !== "common" && tenantId !== azure.tenantId) {
return $err({
msg: "Unauthorized",
desc: `Invalid tenant ID (tid) claim, expected: ${azure.tenantId}, got: ${tenantId}`,
status: 401
});
}
if (issuer !== `https://login.microsoftonline.com/${tenantId}/v2.0`) {
return $err({
msg: "Unauthorized",
desc: `Invalid issuer (iss) claim, expected: https://login.microsoftonline.com/${tenantId}/v2.0, got: ${issuer}`,
status: 401
});
}
try {
const publicKey = await $getPublicKey(jwksClient, kid);
const decodedJwt = jwt.verify(jwtToken, publicKey, {
algorithms: ["RS256"],
audience: azure.clientId,
complete: true
});
const { meta, error: error2 } = $extractDataFromPayload(decodedJwt.payload);
if (error2) return $err(error2);
return $ok({ payload: decodedJwt.payload, meta });
} catch (err) {
return $err({
msg: "Unauthorized",
desc: `Failed to verify JWT token. Check your Azure Portal, make sure the 'accessTokenAcceptedVersion' is set to '2' in the 'Manifest' area. Error: ${err instanceof Error ? err.message : err}`,
status: 401
});
}
}
function $getPublicKey(jwksClient, kid) {
return new Promise((resolve, reject) => {
jwksClient.getSigningKey(kid, (err, key) => {
if (err || !key) {
reject(new Error("Error retrieving signing key"));
return;
}
const publicKey = key.getPublicKey();
if (!publicKey) {
reject(new Error("Public key not found"));
return;
}
resolve(publicKey);
});
});
}
function $decodeJwt(jwtToken) {
if (!$isStr(jwtToken)) return $err({ msg: "Invalid JWT token", desc: "Empty JWT" });
try {
const decodedJwt = jwt.decode(jwtToken, { complete: true });
if (!decodedJwt) return $err({ msg: "Invalid JWT token", desc: "Couldn't decode JWT token" });
return $ok({ decodedJwt });
} catch (error) {
return $err({
msg: "Invalid JWT token",
desc: `Decoding error: ${error instanceof Error ? error.message : typeof error === "string" ? error : String(error)}`
});
}
}
function $getExpiry(jwtToken) {
const { decodedJwt, error } = $decodeJwt(jwtToken);
if (error) return $err(error);
if (typeof decodedJwt.payload === "string") {
return $err({ msg: "Invalid JWT token", desc: "Couldn't get the JWT payload" });
}
const clientId = decodedJwt.payload.aud;
if (typeof clientId !== "string")
return $err({
msg: "Invalid JWT token",
desc: `Invalid audience (aud) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
});
const exp = decodedJwt.payload.exp;
if (typeof exp !== "number")
return $err({
msg: "Invalid JWT token",
desc: `Invalid expiration (exp) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
});
return $ok({ clientId, exp });
}
function $getKeyIdAndExtra(jwtToken) {
const { decodedJwt, error } = $decodeJwt(jwtToken);
if (error) return $err(error);
try {
const kid = decodedJwt.header.kid;
if (typeof kid !== "string")
return $err({
msg: "Invalid JWT token",
desc: `Invalid key ID (kid) claim, header: ${JSON.stringify(decodedJwt.header)}`
});
if (typeof decodedJwt.payload === "string") {
return $err({ msg: "Invalid JWT token", desc: "Couldn't get the JWT payload" });
}
const tenantId = decodedJwt.payload.tid;
if (typeof tenantId !== "string")
return $err({
msg: "Invalid JWT token",
desc: `Invalid tenant ID (tid) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
});
const issuer = decodedJwt.payload.iss;
if (typeof issuer !== "string")
return $err({
msg: "Invalid JWT token",
desc: `Invalid issuer (iss) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
});
return $ok({ kid, tenantId, issuer });
} catch (error2) {
return $err({
msg: "Invalid JWT token",
desc: `Error extracting key ID (kid), tenant ID (tid), and issuer (iss): ${error2 instanceof Error ? error2.message : String(error2)}`
});
}
}
function $getClientId(jwtToken) {
const { decodedJwt, error } = $decodeJwt(jwtToken);
if (error) return $err(error);
if (typeof decodedJwt.payload === "string") {
return $err({ msg: "Invalid JWT token", desc: "Couldn't get the JWT payload" });
}
const clientId = decodedJwt.payload.aud;
if (typeof clientId !== "string")
return $err({
msg: "Invalid JWT token",
desc: `Invalid audience (aud) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
});
return $ok({ clientId });
}
// src/utils/encrypt-tokens.ts
async function $encryptAccessToken(value, params) {
const { data: accessToken, error: jwtError } = zJwt.safeParse(value);
if (jwtError) return $err({ msg: "Invalid access token format", desc: $stringErr(jwtError) });
const { data: dataToInject, error: injectError } = zInjectedData.safeParse(params.dataToInject);
if (injectError) return $err({ msg: "Invalid injected data format", desc: $stringErr(injectError) });
const injectedData = dataToInject && Object.keys(dataToInject).length !== 0 && !params.disableCompression ? compressObj(dataToInject) : void 0;
if (injectedData?.error) {
return $err({ msg: "Failed to compress injected data", desc: `Compression - ${$stringErr(injectedData.error)}` });
}
const struct = {
at: accessToken,
inj: injectedData?.result,
exp: Date.now() + params.expiry * 1e3,
aid: params.azureId
};
const { encrypted, newSecretKey, error } = await $encryptObj(params.cryptoType, struct, params.key);
if (error) return $err({ msg: "Failed to encrypt access token", desc: `Encryption - ${$stringErr(error)}` });
if (params.isOtherKey === false) params.$updateSecretKey("accessToken", newSecretKey);
if (encrypted.length > 4096) {
return $err({
msg: "Token too long",
desc: `Encrypted access token exceeds 4096 characters. Encrypted length: ${encrypted.length}, original length: ${accessToken.length}, injected data length: ${injectedData?.result.length ?? 0}`
});
}
return $ok({ encrypted });
}
async function $decryptAccessToken(value, params) {
const { data: jwtToken, success: jwtSuccess } = zJwt.safeParse(value);
if (jwtSuccess) {
const { clientId, error: jwtError } = $getClientId(jwtToken);
if (jwtError) return $err(jwtError);
return $ok({ decrypted: jwtToken, azureId: clientId, injectedData: void 0, wasEncrypted: false });
}
const { data: encryptedAt, error: encryptedAtError } = zEncrypted.safeParse(value);
if (encryptedAtError) return $err({ msg: "Unauthorized", desc: $stringErr(encryptedAtError) });
const { result, newSecretKey, error } = await $decryptObj(params.cryptoType, encryptedAt, params.key);
if (error) return $err({ msg: "Failed to decrypt access token", desc: `Decryption - ${$stringErr(error)}` });
params.$updateSecretKey("accessToken", newSecretKey);
const { data: atStruct, error: atStructError } = zAtStruct.safeParse(result);
if (atStructError) return $err({ msg: "Invalid access token format", desc: $stringErr(atStructError) });
if (atStruct.exp < Date.now()) {
return $err({
msg: "Access token expired",
desc: `Access token expired at ${new Date(atStruct.exp).toISOString()}`
});
}
const decompressedInjectedData = atStruct.inj ? decompressObj(atStruct.inj) : void 0;
if (decompressedInjectedData?.error) {
return $err({
msg: "Failed to decompress injected data",
desc: `Decompression - ${$stringErr(decompressedInjectedData.error)}`
});
}
return $ok({
decrypted: atStruct.at,
azureId: atStruct.aid,
injectedData: decompressedInjectedData ? decompressedInjectedData.result : void 0,
wasEncrypted: true
});
}
async function $encryptRefreshToken(value, params) {
const { data, error: parseError } = zLooseBase64.safeParse(value);
if (parseError) return $err({ msg: "Invalid refresh token format", desc: $stringErr(parseError) });
const struct = {
rt: data,
exp: Date.now() + params.expiry * 1e3,
aid: params.azureId
};
const { encrypted, newSecretKey, error } = await $encryptObj(params.cryptoType, struct, params.key);
if (error) return $err({ msg: "Failed to encrypt refresh token", desc: `Encryption - ${$stringErr(error)}` });
params.$updateSecretKey("refreshToken", newSecretKey);
if (encrypted.length > 4096) {
return $err({
msg: "Invalid format",
desc: `Encrypted refresh token exceeds 4096 characters. Encrypted length: ${encrypted.length}, original length: ${data.length}`
});
}
return $ok({ encrypted });
}
async function $decryptRefreshToken(value, params) {
const { data: encryptedRefreshToken, error: encryptedRefreshTokenError } = zEncrypted.safeParse(value);
if (encryptedRefreshTokenError) return $err({ msg: "Invalid format", desc: $stringErr(encryptedRefreshTokenError) });
const { result, newSecretKey, error } = await $decryptObj(params.cryptoType, encryptedRefreshToken, params.key);
if (error) return $err({ msg: "Failed to decrypt refresh token", desc: `Decryption - ${$stringErr(error)}` });
const { data: rtStruct, error: rtStructError } = zRtStruct.safeParse(result);
if (rtStructError) return $err({ msg: "Invalid format", desc: $stringErr(rtStructError) });
if (rtStruct.exp < Date.now()) {
return $err({ msg: "Invalid Params", desc: `Refresh token expired at ${new Date(rtStruct.exp).toISOString()}` });
}
params.$updateSecretKey("refreshToken", newSecretKey);
return $ok({ decrypted: rtStruct.rt, azureId: rtStruct.aid });
}
async function $encryptState(value, params) {
const { data, error: parseError } = zState.safeParse(value);
if (parseError) return $err({ msg: "Invalid format", desc: $stringErr(parseError) });
const { encrypted, newSecretKey, error } = await $encryptObj(params.cryptoType, data, params.key);
if (error) return $err({ msg: "Failed to encrypt state", desc: `Encryption - ${$stringErr(error)}` });
params.$updateSecretKey("state", newSecretKey);
return $ok({ encrypted });
}
async function $decryptState(value, params) {
const { data: encryptedState, error: encryptedStateError } = zEncrypted.safeParse(value);
if (encryptedStateError) return $err({ msg: "Invalid format", desc: $stringErr(encryptedStateError) });
const { result, newSecretKey, error } = await $decryptObj(params.cryptoType, encryptedState, params.key);
if (error) return $err({ msg: "Failed to decrypt state", desc: $stringErr(error) });
params.$updateSecretKey("state", newSecretKey);
const { data: state, error: stateError } = zState.safeParse(result);
if (stateError) return $err({ msg: "Invalid format", desc: $stringErr(stateError) });
return $ok({ decrypted: state });
}
async function $encryptTicket(ticketId, params) {
const { data, error: parseError } = zUuid.safeParse(ticketId);
if (parseError) return $err({ msg: "Invalid format", desc: $stringErr(parseError) });
const { encrypted, newSecretKey, error } = await $encrypt(params.cryptoType, data, params.key);
if (error) return $err({ msg: "Failed to encrypt ticket", desc: `Encryption - ${$stringErr(error)}` });
params.$updateSecretKey("ticket", newSecretKey);
return $ok({ encrypted });
}
async function $decryptTicket(value, params) {
const { data, error: encryptedStateError } = zEncrypted.safeParse(value);
if (encryptedStateError) return $err({ msg: "Invalid format", desc: $stringErr(encryptedStateError) });
const { result, newSecretKey, error } = await $decrypt(params.cryptoType, data, params.key);
if (error) return $err({ msg: "Failed to decrypt ticket", desc: `Decryption - ${$stringErr(error)}` });
params.$updateSecretKey("ticket", newSecretKey);
const { data: ticketId, error: stateError } = zUuid.safeParse(result);
if (stateError) return $err({ msg: "Invalid format", desc: $stringErr(stateError) });
return $ok({ decrypted: ticketId });
}
// src/utils/helpers.ts
var TIME_SKEW = 5 * 60;
function $transformToMsalPrompt(prompt, email) {
if (email || prompt === "email") return "login";
if (prompt === "select-account") return "select_account";
return void 0;
}
async function $mapAndFilter(items, callback) {
return (await Promise.all(
items.map(async (item) => {
try {
return await callback(item);
} catch {
return null;
}
})
)).filter((result) => !!result);
}
// src/core.ts
var OAuthProvider = class {
/**
* @param configuration The OAuth configuration object:
* - `azure`: clientId, tenantId, scopes, clientSecret, B2B apps, and downstream services. Can be an array of Azure configurations.
* - `frontendUrl`: allowed redirect URIs
* - `serverCallbackUrl`: your server’s Azure callback endpoint
* - `encryptionKey`: 32 characters base encryption secret
* - `advanced`: optional behaviors
* @throws {OAuthError} if the config fails validation or has duplicate service names
*/
constructor(configuration) {
const result = $oauthConfig(configuration);
if (result.error) throw new OAuthError(result.error);
this.azures = result.azures;
this.frontendUrls = result.frontendUrls;
this.frontendWhitelist = result.frontendWhitelist;
this.serverCallbackUrl = result.serverCallbackUrl;
this.baseCookieOptions = result.baseCookieOptions;
this.encryptionKeys = result.encryptionKeys;
this.msalCryptoProvider = result.msalCryptoProvider;
this.jwksClient = result.jwksClient;
this.settings = result.settings;
}
/**
* Generate an OAuth2 authorization URL for user login (PKCE-backed).
*
* @param params (optional) - Parameters to customize the auth URL:
* - `loginPrompt` (optional) - Override the default prompt (`sso`|`email`|`select-account`)
* - `email` (optional) - Email address to pre-fill the login form
* - `frontendUrl` (optional) - Frontend URL override to redirect the user after authentication
* - `azureId` (optional) - Azure configuration ID to use, relevant if multiple Azure configurations (Defaults to the first one)
* @returns A result containing the authorization URL and a ticket (which is used for bearer flow only)
* @throws {OAuthError} if something goes wrong.
*/
async getAuthUrl(params) {
const { data: parsedParams, error: paramsError } = zMethods.getAuthUrl.safeParse(params);
if (paramsError) throw new OAuthError({ msg: "Invalid Params", desc: $stringErr(paramsError) });
const { azure, error: azureError } = this.$getAzure({
azureId: parsedParams.azureId,
fallbackToDefault: true,
status: 400
});
if (azureError) throw new OAuthError(azureError);
if (parsedParams.loginPrompt === "email" && !parsedParams.email) {
throw new OAuthError({ msg: "Invalid params", desc: 'Email is required when loginPrompt is set to "email"' });
}
if (parsedParams.frontendUrl && !this.frontendWhitelist.has(new URL(parsedParams.frontendUrl).host)) {
throw new OAuthError({ msg: "Forbidden", desc: "Unlisted host frontend URL", status: 403 });
}
const { uuid: ticketId, error: uuidError } = $generateUuid(this.settings.cryptoType);
if (uuidError) throw new OAuthError(uuidError);
try {
const [pkce, { encrypted, error: ticketError }] = await Promise.all([
this.msalCryptoProvider.generatePkceCodes(),
this.$encryptToken("ticket", ticketId)
]);
if (ticketError) throw new OAuthError(ticketError);
const prompt = $transformToMsalPrompt(parsedParams.loginPrompt ?? this.settings.loginPrompt, parsedParams.email);
const params2 = { nonce: this.msalCryptoProvider.createNewGuid(), loginHint: parsedParams.email, prompt };
const { encrypted: encryptedState, error: encryptError } = await this.$encryptToken("state", {
azureId: azure.clientId,
frontendUrl: parsedParams.frontendUrl ?? this.frontendUrls[0],
codeVerifier: pkce.verifier,
ticketId,
...params2
});
if (encryptError) throw new OAuthError(encryptError);
const authUrl = await azure.cca.getAuthCodeUrl({
...params2,
state: encryptedState,
scopes: azure.scopes,
redirectUri: this.serverCallbackUrl,
responseMode: "form_post",
codeChallengeMethod: "S256",
codeChallenge: pkce.challenge
});
if (new URL(authUrl).hostname !== "login.microsoftonline.com") {
throw new OAuthError({
msg: "Invalid auth URL",
desc: "The generated auth URL does not point to Microsoft Entra ID",
status: 500
});
}
return { authUrl, ticket: encrypted };
} catch (error) {
if (error instanceof OAuthError) throw error;
throw new OAuthError({ msg: "Failed to generate auth URL", desc: `Auth URL Generation - ${$stringErr(error)}` });
}
}
/**
* Exchange an authorization code for encrypted tokens and metadata.
*
* @param params - The parameters containing the authorization code and state.
* - `code` - The authorization code received from the OAuth flow.
* - `state` - The state parameter received from Microsoft.
* @returns A result containing the access token, refresh token (if available), frontend URL, and MSAL response.
* @throws {OAuthError} if something goes wrong.
*/
async getTokenByCode(params) {
const { data: parsedParams, error: paramsError } = zMethods.getTokenByCode.safeParse(params);
if (paramsError) throw new OAuthError({ msg: "Invalid Params", desc: $stringErr(paramsError) });
const { decrypted: state, error: decryptError } = await this.$decryptToken("state", parsedParams.state);
if (decryptError) throw new OAuthError(decryptError);
const { azure, error: azureError } = this.$getAzure({ azureId: state.azureId, status: 400 });
if (azureError) throw new OAuthError(azureError);
if (!this.frontendWhitelist.has(new URL(state.frontendUrl).host)) {
throw new OAuthError({ msg: "Forbidden", desc: "Unlisted host frontend URL", status: 403 });
}
try {
const msalResponse = await azure.cca.acquireTokenByCode({
code: parsedParams.code,
scopes: azure.scopes,
redirectUri: this.serverCallbackUrl,
...state
});
const { encryptedAccessToken, encryptedRefreshToken, error } = await this.$extractTokens(azure, msalResponse);
if (error) throw new OAuthError(error);
return {
accessToken: {
name: azure.cookiesNames.accessTokenName,
value: encryptedAccessToken,
options: this.baseCookieOptions.accessTokenOptions
},
refreshToken: encryptedRefreshToken ? {
name: azure.cookiesNames.refreshTokenName,
value: encryptedRefreshToken,
options: this.baseCookieOptions.refreshTokenOptions
} : null,
frontendUrl: state.frontendUrl,
ticketId: state.ticketId,
msalResponse
};
} catch (err) {
if (err instanceof OAuthError) throw err;
throw new OAuthError({
msg: "Failed to get token by code",
desc: `Token Exchange - ${$stringErr(err)} ,Make sure to check Azure credentials (client ID, tenant ID, client secret) and scopes.`
});
}
}
/**
* Build a logout URL and cookie-deletion instructions.
*
* @param params (optional) - Parameters to customize the logout URL:
* - `frontendUrl` (optional) - Frontend URL override to redirect the user after log out
* - `azureId` (optional) - Azure configuration ID to use, relevant if multiple Azure configurations (Defaults to the first one)
* @returns A result containing the logout URL and cookie deletion instructions.
* @throws {OAuthError} if something goes wrong.
*/
async getLogoutUrl(params) {
const { data: parsedParams, error: paramsError } = zMethods.getLogoutUrl.safeParse(params);
if (paramsError) throw new OAuthError({ msg: "Invalid Params", desc: $stringErr(paramsError) });
const { azure, error: azureError } = this.$getAzure({
azureId: parsedParams.azureId,
fallbackToDefault: true,
status: 400
});
if (azureError) throw new OAuthError(azureError);
if (parsedParams.frontendUrl && !this.frontendWhitelist.has(new URL(parsedParams.frontendUrl).host)) {
throw new OAuthError({ msg: "Forbidden", desc: "Unlisted host frontend URL", status: 403 });
}
const logoutUrl = new URL(`https://login.microsoftonline.com/${azure.tenantId}/oauth2/v2.0/logout`);
logoutUrl.searchParams.set("post_logout_redirect_uri", parsedParams.frontendUrl ?? this.frontendUrls[0]);
return {
logoutUrl: logoutUrl.toString(),
deleteAccessToken: {
name: azure.cookiesNames.accessTokenName,
value: "",
options: this.baseCookieOptions.deleteTokenOptions
},
deleteRefreshToken: {
name: azure.cookiesNames.refreshTokenName,
value: "",
options: this.baseCookieOptions.deleteTokenOptions
}
};
}
/**
* Verify the access token (either encrypted or in JWT format) and extract its payload.
* Make sure that user access tokens are encrypted and app tokens aren't
*
* @param accessToken - The access token string either encrypted or in JWT format
* @returns A result containing the raw access token, its payload, any injected data, and whether it is an app token.
* @template T - Type of any injected data in the encrypted token
*/
async verifyAccessToken(accessToken) {
const { decrypted, azureId, injectedData, wasEncrypted, error } = await this.$decryptToken(
"accessToken",
accessToken
);
if (error) return $err({ msg: "Unauthorized", desc: `Token Decryption - ${$stringErr(error)}`, status: 401 });
const { azure, error: azureError } = this.$getAzure({ azureId });
if (azureError) return $err(azureError);
const at = await $verifyJwt({ jwtToken: decrypted, azure, jwksClient: this.jwksClient });
if (at.error) return $err(at.error);
if (this.settings.acceptB2BRequests === false && at.meta.isApp === true) {
return $err({
msg: "B2B requests not allowed",
desc: "B2B requests are not allowed, please enable them in the configuration",
status: 403
});
}
if (at.meta.isApp === wasEncrypted) {
return $err({
msg: "Unauthorized",
desc: "User tokens must be encrypted, app tokens must not be encrypted",
status: 401
});
}
return $ok({
rawJwt: decrypted,
payload: at.payload,
meta: at.meta,
injectedData,
hasInjectedData: !!injectedData
});
}
/**
* Verifies and uses the refresh token to get new set of access and refresh tokens.
*
* @param refreshToken - Encrypted refresh-token value
* @returns A result containing the new access token, optional new refresh token, the raw access token, its payload, and the MSAL response.
*/
async tryRefreshTokens(refreshToken) {
if (!refreshToken) {
return $err({ msg: "Unauthorized", desc: "Refresh token is required", status: 401 });
}
const {
decrypted: rawRefreshToken,
azureId,
error: decryptError
} = await this.$decryptToken("refreshToken", refreshToken);
if (decryptError) {
return $err({ msg: "Unauthorized", desc: `Refresh Token Decryption - ${$stringErr(decryptError)}`, status: 401 });
}
const { azure, error: azureError } = this.$getAzure({ azureId });
if (azureError) return $err(azureError);
try {
const msalResponse = await azure.cca.acquireTokenByRefreshToken({
refreshToken: rawRefreshToken,
scopes: azure.scopes,
forceCache: true
});
if (!msalResponse) {
return $err({ msg: "Unauthorized", desc: "Failed to refresh token, no msal response", status: 401 });
}
const at = await $verifyJwt({
jwtToken: msalResponse.accessToken,
jwksClient: this.jwksClient,
azure
});
if (at.error) {
return $err({ msg: "Unauthorized", desc: `Access Token Verification - ${$stringErr(at.error)}`, status: 401 });
}
const { encryptedAccessToken, encryptedRefreshToken, error } = await this.$extractTokens(azure, msalResponse);
if (error) return $err({ msg: "Unauthorized", desc: `Extract Tokens - ${$stringErr(error)}`, status: 401 });
return $ok({
rawJwt: msalResponse.accessToken,
payload: at.payload,
meta: at.meta,
newAccessToken: {
name: azure.cookiesNames.accessTokenName,
value: encryptedAccessToken,
options: this.baseCookieOptions.accessTokenOptions
},
newRefreshToken: encryptedRefreshToken ? {
name: azure.cookiesNames.refreshTokenName,
value: encryptedRefreshToken,
options: this.baseCookieOptions.refreshTokenOptions
} : null,
msalResponse
});
} catch (err) {
if (err instanceof OAuthError) return $err(err);
return $err({ msg: "Unauthorized", desc: `Token Refresh - ${$stringErr(err)}`, status: 401 });
}
}
/**
* Inject non-sensitive metadata into the access token.
*
* @param params - The parameters containing the access token and data to inject.
* - `accessToken` - The encrypted access token to inject data into.
* - `data` - The data to inject into the access token.
* @returns A result containing the new encrypted access token with injected data and the injected data.
* @template T - Type of the data to inject into the access token.
*/
async tryInjectData(params) {
const { decrypted: rawAccessToken, azureId, error } = await this.$decryptToken("accessToken", params.accessToken);
if (error) return $err(error);
const { azure, error: azureError } = this.$getAzure({ azureId });
if (azureError) return $err(azureError);
const { data: dataToInject, error: dataToInjectError } = zInjectedData.safeParse(params.data);
if (dataToInjectError) return $err({ msg: "Invalid Params", desc: $stringErr(dataToInjectError) });
const { encrypted, error: encryptError } = await this.$encryptToken("accessToken", rawAccessToken, {
azureId,
expiry: this.settings.cookies.accessTokenExpiry,
dataToInject
});
if (encryptError) return $err(encryptError);
return $ok({
newAccessToken: {
name: azure.cookiesNames.accessTokenName,
value: encrypted,
options: this.baseCookieOptions.accessTokenOptions
},
injectedData: dataToInject
});
}
/**
* Decrypts a ticket and returns the ticket ID.
* Useful for bearer flow.
*
* @param ticket - The encrypted ticket string to decrypt (generated by getAuthUrl).
* @returns A result containing the ticket ID (returned by getTokenByCode).
*/
async tryDecryptTicket(ticket) {
const { decrypted, error } = await this.$decryptToken("ticket", ticket);
if (error) return $err(error);
return $ok({ ticketId: decrypted });
}
async tryGetB2BToken(params) {
const { data: parsedParams, error: paramsError } = zMethods.tryGetB2BToken.safeParse(params);
if (paramsError) return $err({ msg: "Invalid Params", desc: $stringErr(paramsError) });
const { azure, error: azureError } = this.$getAzure({
azureId: parsedParams.azureId,
fallbackToDefault: true,
status: 400
});
if (azureError) throw new OAuthError(azureError);
if (!azure.b2b) return $err({ msg: "Misconfiguration", desc: "B2B apps not configured", status: 500 });
const apps = parsedParams.apps.map((app) => azure.b2b?.get(app)).filter((app) => !!app);
if (!apps || apps.length === 0) {
return $err({ msg: "Invalid Params", desc: "B2B app not found", status: 400 });
}
try {
const results = await $mapAndFilter(apps, async (app) => {
if (app.token && app.exp > Date.now() / 1e3) {
return {
clientId: azure.clientId,
appName: app.appName,
appId: app.aud,
token: app.token,
msalResponse: app.msalResponse,
isCached: true,
expiresAt: app.exp
};
}
const msalResponse = await azure.cca.acquireTokenByClientCredential({ scopes: [app.scope], skipCache: true });
if (!msalResponse) return null;
const { clientId, exp, error: audError } = $getExpiry(msalResponse.accessToken);
if (audError) return null;
azure.b2b?.set(app.appName, {
appName: app.appName,
scope: app.scope,
token: msalResponse.accessToken,
exp: exp - TIME_SKEW,
aud: clientId,
msalResponse
});
return {
clientId: azure.clientId,
appName: app.appName,
appId: clientId,
token: msalResponse.accessToken,
msalResponse,
isCached: false,
expiresAt: 0
};
});
if (!results || results.length === 0) {
return $err({ msg: "Internal Server Error", desc: "Failed to get B2B token", status: 500 });
}
return $ok("app" in params ? { result: results[0] } : { results });
} catch (err) {
if (err instanceof OAuthError) return $err(err);
return $err({ msg: "Internal Server Error", desc: $stringErr(err), status: 500 });
}
}
async getTokenOnBehalfOf(params) {
const { data: parsedParams, error: paramsError } = zMethods.getTokenOnBehalfOf.safeParse(params);
if (paramsError) throw new OAuthError({ msg: "Invalid Params", desc: $stringErr(paramsError) });
const { azure, error: azureError } = this.$getAzure({
azureId: parsedParams.azureId,
fallbackToDefault: true,
status: 400
});
if (azureError) throw new OAuthError(azureError);
if (!azure.obo) throw new OAuthError({ msg: "Misconfiguration", desc: "OBO services not configured", status: 500 });
const services = parsedParams.services.map((service) => azure.obo?.get(service)).filter((service) => !!service);
if (!services || services.length === 0) {
throw new OAuthError({ msg: "Invalid Params", desc: "OBO service not found", status: 400 });
}
const { decrypted: rawAccessToken, error } = await this.$decryptToken("accessToken", parsedParams.accessToken);
if (error) throw new OAuthError(error);
try {
const results = await $mapAndFilter(services, async (service) => {
const msalResponse = await azure.cca.acquireTokenOnBehalfOf({
oboAssertion: rawAccessToken,
scopes: [service.scope],
skipCache: true
});
if (!msalResponse) return null;
const { clientId, error: audError } = $getExpiry(msalResponse.accessToken);
if (audError) return null;
const { encrypted, error: error2 } = await this.$encryptToken("accessToken", msalResponse.accessToken, {
azureId: clientId,
expiry: service.atExp,
cryptoType: service.cryptoType,
otherSecretKey: `access-token-${service.encryptionKey}`
});
if (error2) return null;
const cookieOptions = $getCookieOptions({
secure: service.isSecure,
sameSite: service.isSamesite,
timeUnit: this.settings.cookies.timeUnit,
atExp: service.atExp,
rtExp: 0
// No refresh token
});
const { accessTokenName } = $getCookieNames(clientId, service.isSecure);
return {
clientId: azure.clientId,
serviceName: service.serviceName,
serviceId: clientId,
accessToken: { name: accessTokenName, value: encrypted, options: cookieOptions.accessTokenOptions },
msalResponse
};
});
if (!results || results.length === 0) {
throw new OAuthError({ msg: "Internal Server Error", desc: "Failed to get OBO token", status: 500 });
}
return "service" in params ? { result: results[0] } : { results };
} catch (error2) {
if (error2 instanceof OAuthError) throw error2;
throw new OAuthError({ msg: "Internal Server Error", desc: $stringErr(error2), status: 500 });
}
}
$getAzure({
azureId,
fallbackToDefault = false,
status = 401
}) {
const azure = azureId ? this.azures.find((azure2) => azure2.clientId === azureId) : void 0;
if (azure) return $ok({ azure });
if (fallbackToDefault) return $ok({ azure: this.azures[0] });
return $err({
msg: status === 401 ? "Unauthorized" : "Invalid Params",
desc: "Azure configuration not found for the given client ID",
status
});
}
/** Extracts and encrypts both tokens */
async $extractTokens(azure, msalResponse) {
const [accessTokenRes, refreshTokenRes] = await Promise.all([
this.$encryptToken("accessToken", msalResponse.accessToken, {
azureId: azure.clientId,
expiry: this.settings.cookies.accessTokenExpiry
}),
this.$obtainRefreshToken(azure, msalResponse)
]);
if (accessTokenRes.error) return $err(accessTokenRes.error);
return $ok({
encryptedAccessToken: accessTokenRes.encrypted,
encryptedRefreshToken: refreshTokenRes.encrypted ?? null
});
}
/** Extracts the refresh token from the cache that msal created, and removes the account from the cache. */
async $obtainRefreshToken(azure, msalResponse) {
try {
const cache = azure.cca.getTokenCache();
const serializedCache = JSON.parse(cache.serialize());
const refreshTokens = serializedCache?.RefreshToken ?? {};
const keys = typeof refreshTokens === "object" ? Object.keys(refreshTokens) : [];
const refreshTokenKey = keys.find((key) => key.startsWith(msalResponse.uniqueId));
if (msalResponse.account) await cache.removeAccount(msalResponse.account);
const refreshToken = refreshTokenKey ? refreshTokens[refreshTokenKey].secret : void 0;
return await this.$encryptToken("refreshToken", refreshToken, {
azureId: azure.clientId,
expiry: this.settings.cookies.refreshTokenExpiry
});
} catch {
return $err({
msg: "Failed to obtain refresh token",
desc: "Failed to obtain refresh token from MSAL cache",
status: 500
});
}
}
/** Updates the secret key for a specific token type if it is a string. */
$updateSecretKey(keyType, secretKey) {
if (this.settings.cryptoType !== "web-api" || !secretKey) return;
const currentKey = this.encryptionKeys[keyType];
if (typeof currentKey === "string") {
this.encryptionKeys[keyType] = secretKey;
}
}
async $encryptToken(keyType, value, params) {
const baseParams = {
key: params?.otherSecretKey ?? this.encryptionKeys[keyType],
cryptoType: params?.cryptoType ?? this.settings.cryptoType,
$updateSecretKey: this.$updateSecretKey.bind(this)
};
switch (keyType) {
case "accessToken":
return $encryptAccessToken(value, {
...baseParams,
expiry: params?.expiry,
azureId: params?.azureId,
isOtherKey: !!params?.otherSecretKey,
dataToInject: params?.dataToInject,
disableCompression: this.settings.disableCompression
});
case "refreshToken":
return $encryptRefreshToken(value, {
...baseParams,
expiry: params?.expiry,
azureId: params?.azureId
});
case "state":
return $encryptState(value, baseParams);
case "ticket":
return $encryptTicket(value, baseParams);
default:
return $err({
msg: "Invalid encryption key type",
desc: `Key type '${keyType}' is not supported for encryption`
});
}
}
async $decryptToken(keyType, value) {
const baseParams = {
key: this.encryptionKeys[keyType],
cryptoType: this.settings.cryptoType,
$updateSecretKey: this.$updateSecretKey.bind(this)
};
switch (keyType) {
case "accessToken":
return $decryptAccessToken(value, baseParams);
case "refreshToken":
return $decryptRefreshToken(value, baseParams);
case "state":
return $decryptState(value, baseParams);
case "ticket":
return $decryptTicket(value, baseParams);
default:
return $err({
msg: "Invalid encryption key type",
desc: `Key type '${keyType}' is not supported for encryption`
});
}
}
};
export { $err, $getExpiry, $jwtClientConfig, $mapAndFilter, $ok, $stringErr, $verifyJwt, OAuthError, OAuthProvider, TIME_SKEW, zJwt, zMethods };
//# sourceMappingURL=chunk-VN6QIMDR.js.map
//# sourceMappingURL=chunk-VN6QIMDR.js.map