UNPKG

@auth0/nextjs-auth0

Version:
144 lines (143 loc) 6.88 kB
/** * Converts a TokenSet to an AccessTokenSet, including optional audience and scope. * @param tokenSet the TokenSet to convert * @param options object containing optional audience * @returns AccessTokenSet */ export function accessTokenSetFromTokenSet(tokenSet, options) { return { accessToken: tokenSet.accessToken, expiresAt: tokenSet.expiresAt, audience: options.audience, scope: tokenSet.scope, requestedScope: tokenSet.requestedScope, ...(tokenSet.token_type && { token_type: tokenSet.token_type }) }; } /** * Converts an AccessTokenSet and a partial TokenSet into a partial TokenSet. * This is useful for merging an AccessTokenSet back into a TokenSet structure, * while preserving other properties of the TokenSet. * @param accessTokenSet the AccessTokenSet to convert * @param tokenSet the partial TokenSet to merge with * @returns The merged partial TokenSet */ export function tokenSetFromAccessTokenSet(accessTokenSet, tokenSet) { return { ...tokenSet, accessToken: accessTokenSet?.accessToken, expiresAt: accessTokenSet?.expiresAt, scope: accessTokenSet?.scope, requestedScope: accessTokenSet?.requestedScope, audience: accessTokenSet?.audience, ...(accessTokenSet?.token_type && { token_type: accessTokenSet.token_type }) }; } /** * Parses a scope string into an array of individual scopes, filtering out empty strings. * @param scopes Space-separated scope string * @returns Array of scope strings */ function parseScopesToArray(scopes) { if (!scopes) return []; return scopes.trim().split(" ").filter(Boolean); } /** * Compares two sets of scopes to determine if all required scopes are present in the provided scopes. * @param scopes Scopes to compare (space-separated string) * @param requiredScopes Scopes required to be present in the scopes (space-separated string) * @param options Optional settings for comparison * @param options.strict If true, requires an exact match of scopes (no extra scopes allowed) * @returns True if all required scopes are present in the scopes, false otherwise */ export const compareScopes = (scopes, requiredScopes, options = {}) => { // When the scopes and requiredScopes are exactly the same, return true if (scopes === requiredScopes) { return true; } if (!scopes || !requiredScopes) { return false; } const scopesSet = new Set(parseScopesToArray(scopes)); const requiredScopesSet = new Set(parseScopesToArray(requiredScopes)); const requiredScopesArray = Array.from(requiredScopesSet); const hasAllRequiredScopes = requiredScopesArray.every((scope) => scopesSet.has(scope)); if (options.strict) { return hasAllRequiredScopes && scopesSet.size === requiredScopesSet.size; } return hasAllRequiredScopes; }; /** * Merges two space-separated scope strings into one, removing duplicates. * Properly handles whitespace by trimming and normalizing scope values. * @param scopes1 The first scope string * @param scopes2 The second scope string * @returns Merged scope string with unique scopes, sorted alphabetically for consistency */ export function mergeScopes(scopes1, scopes2) { const scopes1Array = scopes1 ? parseScopesToArray(scopes1) : []; const scopes2Array = scopes2 ? parseScopesToArray(scopes2) : []; // Use a Set to remove duplicates const uniqueScopes = new Set([...scopes1Array, ...scopes2Array]); // Convert back to array and join as a space-separated string return Array.from(uniqueScopes).join(" "); } /** * Finds the best matching AccessTokenSet in the session by audience and scope. * * The function determines a "match" if an AccessTokenSet's scope property contains all the items * from the `options.scope`. From the potential matches, it selects the best one * based on the following criteria, in order of priority: * * 1. An "exact match" is preferred above all. This is where the AccessTokenSet's scope * has the exact same items as the `options.scope` (length and content are identical, * order does not matter). * 2. If no exact match is found, the "best partial match" is chosen. This is the * matching AccessTokenSet whose scope has the fewest additional items. * 3. If multiple matches with the exact same scopes are found, we take the first one. * However, this should not happen in practice as the session should not contain * duplicate AccessTokenSet's. * * @param sessionData The session data containing accessTokens array. * @param {Object} options * @param {number} options.scope - The scope to match against (space-separated string). * @param {string} options.audience - The audience to match against. * @param {"requestedScope" | "scope"} [options.matchMode="requestedScope"] - The mode to use for matching scopes. * @returns The best matching AccessTokenSet, or undefined if no match is found. */ export function findAccessTokenSet(sessionData, options) { const matchMode = options.matchMode ?? "requestedScope"; const accessTokenSets = sessionData?.accessTokens; // 1. When there are no access tokens, we can exit early. if (!accessTokenSets || accessTokenSets.length === 0) { return; } // 2. Filter the list to find all AccessTokenSet's that are valid matches. // A valid match's audience must match the provided `options.audience`, // and its scope must contain all items from `options.scope`. const allMatches = accessTokenSets.filter((accessTokenSet) => { return (accessTokenSet.audience === options.audience && compareScopes(matchMode === "scope" ? accessTokenSet.scope : (accessTokenSet.requestedScope ?? accessTokenSet.scope), options.scope, { strict: matchMode === "scope" })); }); // If no potential matches were found, we can exit early. if (allMatches.length === 0) { return; } // 3. Sort the valid matches to find the best one. // The best match is the one with the smallest scope array, as it has the fewest // extra permissions. An exact match will naturally be sorted first. // This also works for null/undefined scopes, as they would have been matched // against a null/undefined `options.scope` and will all be equally valid. // Note: This sorting algorithm also takes care of duplicate scopes, as they will // be converted to a Set and thus have the same size as a non-duplicate scope array. allMatches.sort((a, b) => { const aScopesUnique = new Set(parseScopesToArray(a.scope)); const bScopesUnique = new Set(parseScopesToArray(b.scope)); return aScopesUnique.size - bScopesUnique.size; }); // The first item in the sorted list is the best possible match. return allMatches[0]; }