diff --git a/lib/constants.ts b/lib/constants.ts index ea7e93bbcc..52c46ec028 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -8,3 +8,5 @@ export const authorizedTenantsClaimName = 'tenants'; export const permissionsClaimName = 'permissions'; /** The key of the roles claims in the claims map either under tenant or top level */ export const rolesClaimName = 'roles'; +/** The key of the scopes claims in the claims map */ +export const scopesClaimName = 'scopes'; diff --git a/lib/helpers.ts b/lib/helpers.ts index e4b8d998ae..0d5584ec84 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,5 +1,5 @@ import type { SdkFnWrapper } from '@descope/core-js-sdk'; -import { authorizedTenantsClaimName, refreshTokenCookieName } from './constants'; +import { authorizedTenantsClaimName, refreshTokenCookieName, scopesClaimName } from './constants'; import { AuthenticationInfo } from './types'; /** @@ -86,3 +86,35 @@ export function getAuthorizationClaimItems( export function isUserAssociatedWithTenant(authInfo: AuthenticationInfo, tenant: string): boolean { return !!authInfo.token[authorizedTenantsClaimName]?.[tenant]; } + +/** + * Get the scopes from the JWT token + * @param authInfo The parsed authentication info from the JWT + * @returns the scopes from the top-level claim + */ +export function getScopes(authInfo: AuthenticationInfo): string[] { + const value = authInfo.token[scopesClaimName]; + return Array.isArray(value) ? value : []; +} + +/** + * Check if the user has all the required scopes + * @param authInfo The parsed authentication info from the JWT + * @param scopes list of scopes to check for + * @returns true if user has all required scopes, false otherwise + */ +export function hasScopes(authInfo: AuthenticationInfo, scopes: string[]): boolean { + const userScopes = getScopes(authInfo); + return scopes.every((scope) => userScopes.includes(scope)); +} + +/** + * Get the scopes that match the required scopes + * @param authInfo The parsed authentication info from the JWT + * @param scopes list of scopes to match against + * @returns array of scopes that match the required scopes + */ +export function getMatchedScopes(authInfo: AuthenticationInfo, scopes: string[]): string[] { + const userScopes = getScopes(authInfo); + return scopes.filter((scope) => userScopes.includes(scope)); +} diff --git a/lib/index.ts b/lib/index.ts index b0fc930076..7c4d1ed6ec 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -12,6 +12,7 @@ import { permissionsClaimName, refreshTokenCookieName, rolesClaimName, + scopesClaimName, sessionTokenCookieName, } from './constants'; import fetch from './fetch-polyfill'; @@ -20,6 +21,8 @@ import { getCookieValue, isUserAssociatedWithTenant, withCookie, + hasScopes, + getMatchedScopes, } from './helpers'; import withManagement from './management'; import { AuthenticationInfo, RefreshAuthenticationInfo, VerifyOptions } from './types'; @@ -422,6 +425,26 @@ const nodeSdk = ({ authManagementKey, managementKey, publicKey, ...config }: Nod const membership = getAuthorizationClaimItems(authInfo, rolesClaimName, tenant); return roles.filter((role) => membership.includes(role)); }, + + /** + * Make sure that all given scopes exist on the parsed JWT top level claims + * @param authInfo JWT parsed info + * @param scopes list of scopes to make sure they exist on the JWT claims + * @returns true if all scopes exist, false otherwise + */ + validateScopes(authInfo: AuthenticationInfo, scopes: string[]): boolean { + return hasScopes(authInfo, scopes); + }, + + /** + * Retrieves the scopes from JWT top level claims that match the specified scopes list + * @param authInfo JWT parsed info containing the scopes + * @param scopes List of scopes to match against the JWT claims + * @returns An array of scopes that are both in the JWT claims and the specified list. Returns an empty array if no matches are found + */ + getMatchedScopes(authInfo: AuthenticationInfo, scopes: string[]): string[] { + return getMatchedScopes(authInfo, scopes); + }, }; return wrapWith( @@ -464,6 +487,7 @@ const nodeSdk = ({ authManagementKey, managementKey, publicKey, ...config }: Nod nodeSdk.RefreshTokenCookieName = refreshTokenCookieName; nodeSdk.SessionTokenCookieName = sessionTokenCookieName; +nodeSdk.ScopesClaimName = scopesClaimName; nodeSdk.DescopeErrors = descopeErrors; export default nodeSdk; diff --git a/lib/types.ts b/lib/types.ts index 42e9251e3f..a134f1f78c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -7,6 +7,7 @@ interface Token { sub?: string; exp?: number; iss?: string; + scopes?: string[]; [claim: string]: unknown; }