From 08183cee24ca948c4ed18e4489ad7d84167ce407 Mon Sep 17 00:00:00 2001 From: Ankit Tiwari Date: Wed, 24 Jul 2024 20:00:49 +0530 Subject: [PATCH] fix: PR changes --- lib/build/recipe/oauth2/api/implementation.js | 3 +- lib/build/recipe/oauth2/api/userInfo.d.ts | 1 + lib/build/recipe/oauth2/api/userInfo.js | 26 ++++++++--- lib/build/recipe/oauth2/recipe.d.ts | 3 +- lib/build/recipe/oauth2/recipe.js | 8 ++-- .../recipe/oauth2/recipeImplementation.js | 4 +- lib/build/recipe/oauth2/types.d.ts | 9 ++-- .../oauth2client/recipeImplementation.js | 7 ++- .../recipe/openid/recipeImplementation.js | 1 + lib/build/recipe/userroles/recipe.js | 34 ++++++++++++++ lib/ts/recipe/oauth2/api/implementation.ts | 3 +- lib/ts/recipe/oauth2/api/userInfo.ts | 27 +++++++++-- lib/ts/recipe/oauth2/recipe.ts | 7 +-- lib/ts/recipe/oauth2/recipeImplementation.ts | 4 +- lib/ts/recipe/oauth2/types.ts | 9 ++-- .../oauth2client/recipeImplementation.ts | 7 ++- lib/ts/recipe/openid/recipeImplementation.ts | 3 +- lib/ts/recipe/userroles/recipe.ts | 46 +++++++++++++++++++ 18 files changed, 162 insertions(+), 40 deletions(-) diff --git a/lib/build/recipe/oauth2/api/implementation.js b/lib/build/recipe/oauth2/api/implementation.js index 7219aa72d..472c83d38 100644 --- a/lib/build/recipe/oauth2/api/implementation.js +++ b/lib/build/recipe/oauth2/api/implementation.js @@ -190,11 +190,12 @@ function getAPIImplementation() { }, }; }, - userInfoGET: async ({ accessTokenPayload, user, scopes, options, userContext }) => { + userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => { const userInfo = await options.recipeImplementation.buildUserInfo({ user, accessTokenPayload, scopes, + tenantId, userContext, }); return { diff --git a/lib/build/recipe/oauth2/api/userInfo.d.ts b/lib/build/recipe/oauth2/api/userInfo.d.ts index 44085e2d4..d0b8cdf4e 100644 --- a/lib/build/recipe/oauth2/api/userInfo.d.ts +++ b/lib/build/recipe/oauth2/api/userInfo.d.ts @@ -3,6 +3,7 @@ import { APIInterface, APIOptions } from ".."; import { UserContext } from "../../../types"; export default function userInfoGET( apiImplementation: APIInterface, + tenantId: string, options: APIOptions, userContext: UserContext ): Promise; diff --git a/lib/build/recipe/oauth2/api/userInfo.js b/lib/build/recipe/oauth2/api/userInfo.js index 545df953f..dadeb840c 100644 --- a/lib/build/recipe/oauth2/api/userInfo.js +++ b/lib/build/recipe/oauth2/api/userInfo.js @@ -27,14 +27,15 @@ async function validateOAuth2AccessToken(accessToken) { }); return await resp.json(); } -async function userInfoGET(apiImplementation, options, userContext) { - var _a; +async function userInfoGET(apiImplementation, tenantId, options, userContext) { if (apiImplementation.userInfoGET === undefined) { return false; } const authHeader = options.req.getHeaderValue("authorization") || options.req.getHeaderValue("Authorization"); if (authHeader === undefined || !authHeader.startsWith("Bearer ")) { - utils_1.sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401); + // TODO: Returning a 400 instead of a 401 to prevent a potential refresh loop in the client SDK. + // When addressing this TODO, review other response codes in this function as well. + utils_1.sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 400); return true; } const accessToken = authHeader.replace(/^Bearer /, "").trim(); @@ -42,23 +43,36 @@ async function userInfoGET(apiImplementation, options, userContext) { try { accessTokenPayload = await validateOAuth2AccessToken(accessToken); } catch (error) { - utils_1.sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token!", 401); + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + utils_1.sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 400); + return true; + } + if ( + accessTokenPayload === null || + typeof accessTokenPayload !== "object" || + typeof accessTokenPayload.sub !== "string" || + typeof accessTokenPayload.scope !== "string" + ) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + utils_1.sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 400); return true; } const userId = accessTokenPayload.sub; const user = await __1.getUser(userId, userContext); if (user === undefined) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); utils_1.sendNon200ResponseWithMessage( options.res, "Couldn't find any user associated with the access token", - 401 + 400 ); return true; } const response = await apiImplementation.userInfoGET({ accessTokenPayload, user, - scopes: ((_a = accessTokenPayload.scope) !== null && _a !== void 0 ? _a : "").split(" "), + tenantId, + scopes: accessTokenPayload.scope.split(" "), options, userContext, }); diff --git a/lib/build/recipe/oauth2/recipe.d.ts b/lib/build/recipe/oauth2/recipe.d.ts index d4429d27d..19a890e21 100644 --- a/lib/build/recipe/oauth2/recipe.d.ts +++ b/lib/build/recipe/oauth2/recipe.d.ts @@ -31,7 +31,7 @@ export default class Recipe extends RecipeModule { getAPIsHandled(): APIHandled[]; handleAPIRequest: ( id: string, - _tenantId: string | undefined, + tenantId: string, req: BaseRequest, res: BaseResponse, _path: NormalisedURLPath, @@ -46,6 +46,7 @@ export default class Recipe extends RecipeModule { user: User, accessTokenPayload: JSONObject, scopes: string[], + tenantId: string, userContext: UserContext ): Promise; } diff --git a/lib/build/recipe/oauth2/recipe.js b/lib/build/recipe/oauth2/recipe.js index a17cd76fe..048abe7dd 100644 --- a/lib/build/recipe/oauth2/recipe.js +++ b/lib/build/recipe/oauth2/recipe.js @@ -43,7 +43,7 @@ class Recipe extends recipeModule_1.default { this.addUserInfoBuilderFromOtherRecipe = (userInfoBuilderFn) => { this.userInfoBuilders.push(userInfoBuilderFn); }; - this.handleAPIRequest = async (id, _tenantId, req, res, _path, _method, userContext) => { + this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { let options = { config: this.config, recipeId: this.getRecipeId(), @@ -71,7 +71,7 @@ class Recipe extends recipeModule_1.default { return loginInfo_1.default(this.apiImpl, options, userContext); } if (id === constants_1.USER_INFO_PATH) { - return userInfo_1.default(this.apiImpl, options, userContext); + return userInfo_1.default(this.apiImpl, tenantId, options, userContext); } throw new Error("Should never come here: handleAPIRequest called with unknown id"); }; @@ -215,7 +215,7 @@ class Recipe extends recipeModule_1.default { } return payload; } - async getDefaultUserInfoPayload(user, accessTokenPayload, scopes, userContext) { + async getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext) { let payload = { sub: accessTokenPayload.sub, }; @@ -236,7 +236,7 @@ class Recipe extends recipeModule_1.default { for (const fn of this.userInfoBuilders) { payload = Object.assign( Object.assign({}, payload), - await fn(user, accessTokenPayload, scopes, userContext) + await fn(user, accessTokenPayload, scopes, tenantId, userContext) ); } return payload; diff --git a/lib/build/recipe/oauth2/recipeImplementation.js b/lib/build/recipe/oauth2/recipeImplementation.js index 2c82dbb9a..a2158ccb9 100644 --- a/lib/build/recipe/oauth2/recipeImplementation.js +++ b/lib/build/recipe/oauth2/recipeImplementation.js @@ -394,8 +394,8 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload buildIdTokenPayload: async function (input) { return input.defaultPayload; }, - buildUserInfo: async function ({ user, accessTokenPayload, scopes, userContext }) { - return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, userContext); + buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { + return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); }, }; } diff --git a/lib/build/recipe/oauth2/types.d.ts b/lib/build/recipe/oauth2/types.d.ts index 63b690fe5..d8db28595 100644 --- a/lib/build/recipe/oauth2/types.d.ts +++ b/lib/build/recipe/oauth2/types.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse, JSONObject, NonNullableProperties, UserContext } from "../../types"; +import { GeneralErrorResponse, JSONObject, JSONValue, NonNullableProperties, UserContext } from "../../types"; import { SessionContainerInterface } from "../session/types"; import { OAuth2Client } from "./OAuth2Client"; import { User } from "../../user"; @@ -93,7 +93,7 @@ export declare type UserInfo = { email_verified?: boolean; phoneNumber?: string; phoneNumber_verified?: boolean; - [key: string]: any; + [key: string]: JSONValue; }; export declare type RecipeInterface = { authorization(input: { @@ -232,6 +232,7 @@ export declare type RecipeInterface = { user: User; accessTokenPayload: JSONObject; scopes: string[]; + tenantId: string; userContext: UserContext; }): Promise; }; @@ -357,6 +358,7 @@ export declare type APIInterface = { accessTokenPayload: JSONObject; user: User; scopes: string[]; + tenantId: string; options: APIOptions; userContext: UserContext; }) => Promise< @@ -471,5 +473,6 @@ export declare type UserInfoBuilderFunction = ( user: User, accessTokenPayload: JSONObject, scopes: string[], + tenantId: string, userContext: UserContext -) => Promise; +) => Promise; diff --git a/lib/build/recipe/oauth2client/recipeImplementation.js b/lib/build/recipe/oauth2client/recipeImplementation.js index 8343d5d61..012749192 100644 --- a/lib/build/recipe/oauth2client/recipeImplementation.js +++ b/lib/build/recipe/oauth2client/recipeImplementation.js @@ -63,10 +63,9 @@ function getRecipeImplementation(_querier, config) { if (oidcInfo.token_endpoint === undefined) { throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint."); } - // TODO: We currently don't have this - // if (oidcInfo.userinfo_endpoint === undefined) { - // throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); - // } + if (oidcInfo.userinfo_endpoint === undefined) { + throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); + } if (oidcInfo.jwks_uri === undefined) { throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); } diff --git a/lib/build/recipe/openid/recipeImplementation.js b/lib/build/recipe/openid/recipeImplementation.js index d3b582d2e..f01e8b272 100644 --- a/lib/build/recipe/openid/recipeImplementation.js +++ b/lib/build/recipe/openid/recipeImplementation.js @@ -24,6 +24,7 @@ function getRecipeInterface(config, jwtRecipeImplementation, appInfo) { jwks_uri, authorization_endpoint: apiBasePath + constants_2.AUTH_PATH, token_endpoint: apiBasePath + constants_2.TOKEN_PATH, + userinfo_endpoint: apiBasePath + constants_2.USER_INFO_PATH, subject_types_supported: ["public"], id_token_signing_alg_values_supported: ["RS256"], response_types_supported: ["code", "id_token", "id_token token"], diff --git a/lib/build/recipe/userroles/recipe.js b/lib/build/recipe/userroles/recipe.js index 53f5d56b6..4c1a8b531 100644 --- a/lib/build/recipe/userroles/recipe.js +++ b/lib/build/recipe/userroles/recipe.js @@ -27,6 +27,7 @@ const utils_1 = require("./utils"); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); const recipe_1 = __importDefault(require("../session/recipe")); +const recipe_2 = __importDefault(require("../oauth2/recipe")); const userRoleClaim_1 = require("./userRoleClaim"); const permissionClaim_1 = require("./permissionClaim"); class Recipe extends recipeModule_1.default { @@ -51,6 +52,39 @@ class Recipe extends recipeModule_1.default { if (!this.config.skipAddingPermissionsToAccessToken) { recipe_1.default.getInstanceOrThrowError().addClaimFromOtherRecipe(permissionClaim_1.PermissionClaim); } + recipe_2.default + .getInstanceOrThrowError() + .addUserInfoBuilderFromOtherRecipe(async (user, _accessTokenPayload, scopes, tenantId, userContext) => { + let userInfo = {}; + if (scopes.includes("roles")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId, + userContext, + }); + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userInfo.roles = res.roles; + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userInfo.roles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + userInfo.permissons = Array.from(userPermissions); + } + } + return userInfo; + }); }); } /* Init functions */ diff --git a/lib/ts/recipe/oauth2/api/implementation.ts b/lib/ts/recipe/oauth2/api/implementation.ts index ee6d9dcd3..f3f880137 100644 --- a/lib/ts/recipe/oauth2/api/implementation.ts +++ b/lib/ts/recipe/oauth2/api/implementation.ts @@ -188,11 +188,12 @@ export default function getAPIImplementation(): APIInterface { }, }; }, - userInfoGET: async ({ accessTokenPayload, user, scopes, options, userContext }) => { + userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => { const userInfo = await options.recipeImplementation.buildUserInfo({ user, accessTokenPayload, scopes, + tenantId, userContext, }); diff --git a/lib/ts/recipe/oauth2/api/userInfo.ts b/lib/ts/recipe/oauth2/api/userInfo.ts index 82d65d128..b3d601697 100644 --- a/lib/ts/recipe/oauth2/api/userInfo.ts +++ b/lib/ts/recipe/oauth2/api/userInfo.ts @@ -32,6 +32,7 @@ async function validateOAuth2AccessToken(accessToken: string) { export default async function userInfoGET( apiImplementation: APIInterface, + tenantId: string, options: APIOptions, userContext: UserContext ): Promise { @@ -42,7 +43,9 @@ export default async function userInfoGET( const authHeader = options.req.getHeaderValue("authorization") || options.req.getHeaderValue("Authorization"); if (authHeader === undefined || !authHeader.startsWith("Bearer ")) { - sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401); + // TODO: Returning a 400 instead of a 401 to prevent a potential refresh loop in the client SDK. + // When addressing this TODO, review other response codes in this function as well. + sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 400); return true; } @@ -53,23 +56,37 @@ export default async function userInfoGET( try { accessTokenPayload = await validateOAuth2AccessToken(accessToken); } catch (error) { - sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token!", 401); + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 400); return true; } - const userId = accessTokenPayload.sub as string; + if ( + accessTokenPayload === null || + typeof accessTokenPayload !== "object" || + typeof accessTokenPayload.sub !== "string" || + typeof accessTokenPayload.scope !== "string" + ) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 400); + return true; + } + + const userId = accessTokenPayload.sub; const user = await getUser(userId, userContext); if (user === undefined) { - sendNon200ResponseWithMessage(options.res, "Couldn't find any user associated with the access token", 401); + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + sendNon200ResponseWithMessage(options.res, "Couldn't find any user associated with the access token", 400); return true; } const response = await apiImplementation.userInfoGET({ accessTokenPayload, user, - scopes: ((accessTokenPayload.scope as string) ?? "").split(" "), + tenantId, + scopes: accessTokenPayload.scope.split(" "), options, userContext, }); diff --git a/lib/ts/recipe/oauth2/recipe.ts b/lib/ts/recipe/oauth2/recipe.ts index 076006e57..bb0948421 100644 --- a/lib/ts/recipe/oauth2/recipe.ts +++ b/lib/ts/recipe/oauth2/recipe.ts @@ -188,7 +188,7 @@ export default class Recipe extends RecipeModule { handleAPIRequest = async ( id: string, - _tenantId: string | undefined, + tenantId: string, req: BaseRequest, res: BaseResponse, _path: NormalisedURLPath, @@ -223,7 +223,7 @@ export default class Recipe extends RecipeModule { return loginInfoGET(this.apiImpl, options, userContext); } if (id === USER_INFO_PATH) { - return userInfoGET(this.apiImpl, options, userContext); + return userInfoGET(this.apiImpl, tenantId, options, userContext); } throw new Error("Should never come here: handleAPIRequest called with unknown id"); }; @@ -267,6 +267,7 @@ export default class Recipe extends RecipeModule { user: User, accessTokenPayload: JSONObject, scopes: string[], + tenantId: string, userContext: UserContext ) { let payload: JSONObject = { @@ -286,7 +287,7 @@ export default class Recipe extends RecipeModule { for (const fn of this.userInfoBuilders) { payload = { ...payload, - ...(await fn(user, accessTokenPayload, scopes, userContext)), + ...(await fn(user, accessTokenPayload, scopes, tenantId, userContext)), }; } diff --git a/lib/ts/recipe/oauth2/recipeImplementation.ts b/lib/ts/recipe/oauth2/recipeImplementation.ts index 9a5a40251..db697e68a 100644 --- a/lib/ts/recipe/oauth2/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2/recipeImplementation.ts @@ -425,8 +425,8 @@ export default function getRecipeInterface( buildIdTokenPayload: async function (input) { return input.defaultPayload; }, - buildUserInfo: async function ({ user, accessTokenPayload, scopes, userContext }) { - return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, userContext); + buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { + return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); }, }; } diff --git a/lib/ts/recipe/oauth2/types.ts b/lib/ts/recipe/oauth2/types.ts index ae088e9a2..cf113b5d2 100644 --- a/lib/ts/recipe/oauth2/types.ts +++ b/lib/ts/recipe/oauth2/types.ts @@ -15,7 +15,7 @@ import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse, JSONObject, NonNullableProperties, UserContext } from "../../types"; +import { GeneralErrorResponse, JSONObject, JSONValue, NonNullableProperties, UserContext } from "../../types"; import { SessionContainerInterface } from "../session/types"; import { OAuth2Client } from "./OAuth2Client"; import { User } from "../../user"; @@ -194,7 +194,7 @@ export type UserInfo = { email_verified?: boolean; phoneNumber?: string; phoneNumber_verified?: boolean; - [key: string]: any; + [key: string]: JSONValue; }; export type RecipeInterface = { @@ -362,6 +362,7 @@ export type RecipeInterface = { user: User; accessTokenPayload: JSONObject; scopes: string[]; + tenantId: string; userContext: UserContext; }): Promise; }; @@ -451,6 +452,7 @@ export type APIInterface = { accessTokenPayload: JSONObject; user: User; scopes: string[]; + tenantId: string; options: APIOptions; userContext: UserContext; }) => Promise<{ status: "OK"; info: JSONObject } | GeneralErrorResponse>); @@ -568,5 +570,6 @@ export type UserInfoBuilderFunction = ( user: User, accessTokenPayload: JSONObject, scopes: string[], + tenantId: string, userContext: UserContext -) => Promise; +) => Promise; diff --git a/lib/ts/recipe/oauth2client/recipeImplementation.ts b/lib/ts/recipe/oauth2client/recipeImplementation.ts index ef374b8f6..68748247b 100644 --- a/lib/ts/recipe/oauth2client/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2client/recipeImplementation.ts @@ -100,10 +100,9 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN if (oidcInfo.token_endpoint === undefined) { throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint."); } - // TODO: We currently don't have this - // if (oidcInfo.userinfo_endpoint === undefined) { - // throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); - // } + if (oidcInfo.userinfo_endpoint === undefined) { + throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); + } if (oidcInfo.jwks_uri === undefined) { throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); } diff --git a/lib/ts/recipe/openid/recipeImplementation.ts b/lib/ts/recipe/openid/recipeImplementation.ts index 2ed40f6f5..bbb633768 100644 --- a/lib/ts/recipe/openid/recipeImplementation.ts +++ b/lib/ts/recipe/openid/recipeImplementation.ts @@ -17,7 +17,7 @@ import { RecipeInterface as JWTRecipeInterface, JsonWebKey } from "../jwt/types" import NormalisedURLPath from "../../normalisedURLPath"; import { GET_JWKS_API } from "../jwt/constants"; import { NormalisedAppinfo, UserContext } from "../../types"; -import { AUTH_PATH, TOKEN_PATH } from "../oauth2/constants"; +import { AUTH_PATH, TOKEN_PATH, USER_INFO_PATH } from "../oauth2/constants"; export default function getRecipeInterface( config: TypeNormalisedInput, @@ -38,6 +38,7 @@ export default function getRecipeInterface( jwks_uri, authorization_endpoint: apiBasePath + AUTH_PATH, token_endpoint: apiBasePath + TOKEN_PATH, + userinfo_endpoint: apiBasePath + USER_INFO_PATH, subject_types_supported: ["public"], id_token_signing_alg_values_supported: ["RS256"], response_types_supported: ["code", "id_token", "id_token token"], diff --git a/lib/ts/recipe/userroles/recipe.ts b/lib/ts/recipe/userroles/recipe.ts index 0f0176c7b..18aa7b8ba 100644 --- a/lib/ts/recipe/userroles/recipe.ts +++ b/lib/ts/recipe/userroles/recipe.ts @@ -27,6 +27,7 @@ import { validateAndNormaliseUserInput } from "./utils"; import OverrideableBuilder from "supertokens-js-override"; import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; import SessionRecipe from "../session/recipe"; +import OAuth2Recipe from "../oauth2/recipe"; import { UserRoleClaim } from "./userRoleClaim"; import { PermissionClaim } from "./permissionClaim"; @@ -55,6 +56,51 @@ export default class Recipe extends RecipeModule { if (!this.config.skipAddingPermissionsToAccessToken) { SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(PermissionClaim); } + + OAuth2Recipe.getInstanceOrThrowError().addUserInfoBuilderFromOtherRecipe( + async (user, _accessTokenPayload, scopes, tenantId, userContext) => { + let userInfo: { + roles?: string[]; + permissons?: string[]; + } = {}; + + if (scopes.includes("roles")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId, + userContext, + }); + + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + + userInfo.roles = res.roles; + + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userInfo.roles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + + userInfo.permissons = Array.from(userPermissions); + } + } + + return userInfo; + } + ); }); }