@clerk/shared
Version:
Internal package utils used by the Clerk SDKs
274 lines (272 loc) • 8.8 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
//#region src/authorization.ts
const TYPES_TO_OBJECTS = {
strict_mfa: {
afterMinutes: 10,
level: "multi_factor"
},
strict: {
afterMinutes: 10,
level: "second_factor"
},
moderate: {
afterMinutes: 60,
level: "second_factor"
},
lax: {
afterMinutes: 1440,
level: "second_factor"
}
};
const ALLOWED_LEVELS = new Set([
"first_factor",
"second_factor",
"multi_factor"
]);
const ALLOWED_TYPES = new Set([
"strict_mfa",
"strict",
"moderate",
"lax"
]);
const ORG_SCOPES = new Set([
"o",
"org",
"organization"
]);
const USER_SCOPES = new Set(["u", "user"]);
const isValidMaxAge = (maxAge) => typeof maxAge === "number" && maxAge > 0;
const isValidLevel = (level) => ALLOWED_LEVELS.has(level);
const isValidVerificationType = (type) => ALLOWED_TYPES.has(type);
const isValidFactorAge = (x) => typeof x === "number" && Number.isFinite(x) && (x === -1 || x >= 0);
const prefixWithOrg = (value) => value.replace(/^(org:)*/, "org:");
/**
* Checks if a user has the required organization-level authorization.
* If both role and permission are provided, both must match (AND).
*/
const checkOrgAuthorization = (params, options) => {
const { orgId, orgRole, orgPermissions } = options;
const roleAsked = params.role !== void 0;
const permissionAsked = params.permission !== void 0;
if (!roleAsked && !permissionAsked) return "skip";
if (roleAsked && typeof params.role !== "string") return "fail";
if (permissionAsked && typeof params.permission !== "string") return "fail";
if (!orgId) return "fail";
if (roleAsked) {
if (typeof orgRole !== "string" || !orgRole) return "fail";
if (prefixWithOrg(orgRole) !== prefixWithOrg(params.role)) return "fail";
}
if (permissionAsked) {
if (!Array.isArray(orgPermissions)) return "fail";
if (!orgPermissions.includes(prefixWithOrg(params.permission))) return "fail";
}
return "pass";
};
const checkForFeatureOrPlan = (claim, featureOrPlan) => {
const { org: orgFeatures, user: userFeatures } = splitByScope(claim);
const [rawScope, rawId] = featureOrPlan.split(":");
const hasExplicitScope = rawId !== void 0;
const scope = rawScope;
const id = rawId || rawScope;
if (hasExplicitScope && !ORG_SCOPES.has(scope) && !USER_SCOPES.has(scope)) throw new Error(`Invalid scope: ${scope}`);
if (hasExplicitScope) {
if (ORG_SCOPES.has(scope)) return orgFeatures.includes(id);
if (USER_SCOPES.has(scope)) return userFeatures.includes(id);
}
return [...orgFeatures, ...userFeatures].includes(id);
};
/**
* Checks if a user is entitled to the requested feature or plan.
* If both feature and plan are provided, both must match (AND).
*/
const checkBillingAuthorization = (params, options) => {
const { features, plans } = options;
const featureAsked = params.feature !== void 0;
const planAsked = params.plan !== void 0;
if (!featureAsked && !planAsked) return "skip";
if (featureAsked && typeof params.feature !== "string") return "fail";
if (planAsked && typeof params.plan !== "string") return "fail";
if (featureAsked) {
if (typeof features !== "string" || !features) return "fail";
try {
if (!checkForFeatureOrPlan(features, params.feature)) return "fail";
} catch {
return "fail";
}
}
if (planAsked) {
if (typeof plans !== "string" || !plans) return "fail";
try {
if (!checkForFeatureOrPlan(plans, params.plan)) return "fail";
} catch {
return "fail";
}
}
return "pass";
};
const splitByScope = (fea) => {
const org = [];
const user = [];
if (!fea) return {
org,
user
};
const parts = fea.split(",");
for (let i = 0; i < parts.length; i++) {
const part = parts[i].trim();
const colonIndex = part.indexOf(":");
if (colonIndex === -1) throw new Error(`Invalid claim element (missing colon): ${part}`);
const scope = part.slice(0, colonIndex);
const value = part.slice(colonIndex + 1);
if (scope === "o") org.push(value);
else if (scope === "u") user.push(value);
else if (scope === "ou" || scope === "uo") {
org.push(value);
user.push(value);
}
}
return {
org,
user
};
};
const validateReverificationConfig = (config) => {
if (!config) return false;
const convertConfigToObject = (config) => {
if (typeof config === "string") return TYPES_TO_OBJECTS[config];
return config;
};
const isValidStringValue = typeof config === "string" && isValidVerificationType(config);
const isValidObjectValue = typeof config === "object" && isValidLevel(config.level) && isValidMaxAge(config.afterMinutes);
if (isValidStringValue || isValidObjectValue) return convertConfigToObject.bind(null, config);
return false;
};
/**
* Evaluates if the user meets re-verification authentication requirements.
* Handles different verification levels (first factor, second factor, multi-factor).
*/
const checkReverificationAuthorization = (params, { factorVerificationAge }) => {
if (params.reverification === void 0) return "skip";
if (!factorVerificationAge) return "fail";
if (!Array.isArray(factorVerificationAge) || factorVerificationAge.length !== 2 || !isValidFactorAge(factorVerificationAge[0]) || !isValidFactorAge(factorVerificationAge[1])) return "fail";
const getConfig = validateReverificationConfig(params.reverification);
if (!getConfig) return "fail";
const { level, afterMinutes } = getConfig();
const [factor1Age, factor2Age] = factorVerificationAge;
if (factor1Age === -1 && factor2Age === -1) return "fail";
const factor1FreshEnough = factor1Age !== -1 && afterMinutes > factor1Age;
const factor2FreshEnough = factor2Age !== -1 && afterMinutes > factor2Age;
switch (level) {
case "first_factor": return factor1FreshEnough ? "pass" : "fail";
case "second_factor":
if (factor2Age === -1) return factor1FreshEnough ? "pass" : "fail";
if (factor1Age === -1) return factor2FreshEnough ? "pass" : "fail";
return factor2FreshEnough ? "pass" : "fail";
case "multi_factor":
if (factor2Age === -1) return factor1FreshEnough ? "pass" : "fail";
if (factor1Age === -1) return "fail";
return factor1FreshEnough && factor2FreshEnough ? "pass" : "fail";
}
};
const combine = (results) => results.some((r) => r === "pass") && results.every((r) => r === "pass" || r === "skip");
/**
* Creates a function for comprehensive user authorization checks.
* Combines organization, billing, and reverification checks. The returned function
* authorizes only when every requested dimension passes; any requested dimension
* that cannot be satisfied (including missing or malformed session data) denies
* the request. Fails if `userId` is missing.
*/
const createCheckAuthorization = (options) => {
return (params) => {
if (!options.userId) return false;
return combine([
checkOrgAuthorization(params, options),
checkBillingAuthorization(params, options),
checkReverificationAuthorization(params, options)
]);
};
};
/**
* Shared utility function that centralizes auth state resolution logic,
* preventing duplication across different packages.
*
* @internal
*/
const resolveAuthState = ({ authObject: { sessionId, sessionStatus, userId, actor, orgId, orgRole, orgSlug, signOut, getToken, has, sessionClaims }, options: { treatPendingAsSignedOut = true } }) => {
if (sessionId === void 0 && userId === void 0) return {
actor: void 0,
getToken,
has: () => false,
isLoaded: false,
isSignedIn: void 0,
orgId: void 0,
orgRole: void 0,
orgSlug: void 0,
sessionClaims: void 0,
sessionId,
signOut,
userId
};
if (sessionId === null && userId === null) return {
actor: null,
getToken,
has: () => false,
isLoaded: true,
isSignedIn: false,
orgId: null,
orgRole: null,
orgSlug: null,
sessionClaims: null,
sessionId,
signOut,
userId
};
if (treatPendingAsSignedOut && sessionStatus === "pending") return {
actor: null,
getToken,
has: () => false,
isLoaded: true,
isSignedIn: false,
orgId: null,
orgRole: null,
orgSlug: null,
sessionClaims: null,
sessionId: null,
signOut,
userId: null
};
if (!!sessionId && !!sessionClaims && !!userId && !!orgId && !!orgRole) return {
actor: actor || null,
getToken,
has,
isLoaded: true,
isSignedIn: true,
orgId,
orgRole,
orgSlug: orgSlug || null,
sessionClaims,
sessionId,
signOut,
userId
};
if (!!sessionId && !!sessionClaims && !!userId && !orgId) return {
actor: actor || null,
getToken,
has,
isLoaded: true,
isSignedIn: true,
orgId: null,
orgRole: null,
orgSlug: null,
sessionClaims,
sessionId,
signOut,
userId
};
};
//#endregion
exports.createCheckAuthorization = createCheckAuthorization;
exports.resolveAuthState = resolveAuthState;
exports.splitByScope = splitByScope;
exports.validateReverificationConfig = validateReverificationConfig;
//# sourceMappingURL=authorization.js.map