From 376128ac314d2512b3168cf25569c261c6c5e79c Mon Sep 17 00:00:00 2001 From: Mihaly Lengyel Date: Mon, 23 Sep 2024 20:19:35 +0200 Subject: [PATCH] feat: make the frontend redirection urls overrideable --- lib/build/recipe/oauth2provider/api/utils.js | 46 +++++------------ .../oauth2provider/recipeImplementation.js | 47 ++++++++++++++--- lib/build/recipe/oauth2provider/types.d.ts | 25 +++++++++ lib/ts/recipe/oauth2provider/api/utils.ts | 51 +++++-------------- .../oauth2provider/recipeImplementation.ts | 51 ++++++++++++++++--- lib/ts/recipe/oauth2provider/types.ts | 25 +++++++++ 6 files changed, 160 insertions(+), 85 deletions(-) diff --git a/lib/build/recipe/oauth2provider/api/utils.js b/lib/build/recipe/oauth2provider/api/utils.js index d9683dddd..58390408c 100644 --- a/lib/build/recipe/oauth2provider/api/utils.js +++ b/lib/build/recipe/oauth2provider/api/utils.js @@ -88,27 +88,13 @@ async function loginGET({ }); return { redirectTo: accept.redirectTo, setCookie }; } - const appInfo = supertokens_1.default.getInstanceOrThrowError().appInfo; - const websiteDomain = appInfo - .getOrigin({ - request: undefined, - userContext: userContext, - }) - .getAsStringDangerous(); - const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); if (shouldTryRefresh && promptParam !== "login") { - const websiteDomain = appInfo - .getOrigin({ - request: undefined, - userContext: userContext, - }) - .getAsStringDangerous(); - const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); - const queryParamsForTryRefreshPage = new URLSearchParams({ - loginChallenge, - }); return { - redirectTo: websiteDomain + websiteBasePath + `/try-refresh?${queryParamsForTryRefreshPage.toString()}`, + redirectTo: await recipeImplementation.getFrontendRedirectionURL({ + type: "try-refresh", + loginChallenge, + userContext, + }), setCookie, }; } @@ -124,20 +110,16 @@ async function loginGET({ }); return { redirectTo: reject.redirectTo, setCookie }; } - const queryParamsForAuthPage = new URLSearchParams({ - loginChallenge, - }); - if ((_b = loginRequest.oidcContext) === null || _b === void 0 ? void 0 : _b.login_hint) { - queryParamsForAuthPage.set("hint", loginRequest.oidcContext.login_hint); - } - if (session !== undefined || promptParam === "login") { - queryParamsForAuthPage.set("forceFreshAuth", "true"); - } - if (tenantIdParam !== null && tenantIdParam !== constants_1.DEFAULT_TENANT_ID) { - queryParamsForAuthPage.set("tenantId", tenantIdParam); - } return { - redirectTo: websiteDomain + websiteBasePath + `?${queryParamsForAuthPage.toString()}`, + redirectTo: await recipeImplementation.getFrontendRedirectionURL({ + type: "login", + loginChallenge, + forceFreshAuth: session !== undefined || promptParam === "login", + tenantId: + tenantIdParam !== null && tenantIdParam !== void 0 ? tenantIdParam : constants_1.DEFAULT_TENANT_ID, + hint: (_b = loginRequest.oidcContext) === null || _b === void 0 ? void 0 : _b.login_hint, + userContext, + }), setCookie, }; } diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js index 615391453..63decf3eb 100644 --- a/lib/build/recipe/oauth2provider/recipeImplementation.js +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -61,6 +61,7 @@ const OAuth2Client_1 = require("./OAuth2Client"); const __1 = require("../.."); const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); const recipe_1 = __importDefault(require("../session/recipe")); +const constants_1 = require("../multitenancy/constants"); function getUpdatedRedirectTo(appInfo, redirectTo) { return redirectTo.replace( "{apiDomain}", @@ -528,6 +529,34 @@ function getRecipeInterface( buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); }, + getFrontendRedirectionURL: async function (input) { + const websiteDomain = appInfo + .getOrigin({ request: undefined, userContext: input.userContext }) + .getAsStringDangerous(); + const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + if (input.type === "login") { + const queryParams = new URLSearchParams({ + loginChallenge: input.loginChallenge, + }); + if (input.tenantId !== undefined && input.tenantId !== constants_1.DEFAULT_TENANT_ID) { + queryParams.set("tenantId", input.tenantId); + } + if (input.hint !== undefined) { + queryParams.set("hint", input.hint); + } + if (input.forceFreshAuth) { + queryParams.set("forceFreshAuth", "true"); + } + return `${websiteDomain}${websiteBasePath}?${queryParams.toString()}`; + } else if (input.type === "try-refresh") { + return `${websiteDomain}${websiteBasePath}/try-refresh?loginChallenge=${input.loginChallenge}`; + } else if (input.type === "post-logout-fallback") { + return `${websiteDomain}${websiteBasePath}`; + } else if (input.type === "logout-confirmation") { + return `${websiteDomain}${websiteBasePath}/oauth/logout?logoutChallenge=${input.logoutChallenge}`; + } + throw new Error("This should never happen: invalid type passed to getFrontendRedirectionURL"); + }, validateOAuth2AccessToken: async function (input) { var _a, _b, _c, _d, _e; const payload = ( @@ -676,10 +705,6 @@ function getRecipeInterface( if (redirectTo === undefined) { throw new Error(resp.body); } - const websiteDomain = appInfo - .getOrigin({ request: undefined, userContext: input.userContext }) - .getAsStringDangerous(); - const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); const redirectToURL = new URL(redirectTo); const logoutChallenge = redirectToURL.searchParams.get("logout_challenge"); // CASE 1 (See above notes) @@ -687,8 +712,11 @@ function getRecipeInterface( // Redirect to the frontend to ask for logout confirmation if there is a valid or expired supertokens session if (input.session !== undefined || input.shouldTryRefresh) { return { - redirectTo: - websiteDomain + websiteBasePath + "/oauth/logout" + `?logoutChallenge=${logoutChallenge}`, + redirectTo: await this.getFrontendRedirectionURL({ + type: "logout-confirmation", + logoutChallenge, + userContext: input.userContext, + }), }; } else { // Accept the logout challenge immediately as there is no supertokens session @@ -703,7 +731,12 @@ function getRecipeInterface( // NOTE: If no post_logout_redirect_uri is provided, Hydra redirects to a fallback page. // In this case, we redirect the user to the /auth page. if (redirectTo.endsWith("/oauth/fallbacks/logout/callback")) { - return { redirectTo: `${websiteDomain}${websiteBasePath}` }; + return { + redirectTo: await this.getFrontendRedirectionURL({ + type: "post-logout-fallback", + userContext: input.userContext, + }), + }; } return { redirectTo }; }, diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts index 9b3fb7119..9a1435ade 100644 --- a/lib/build/recipe/oauth2provider/types.d.ts +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -265,6 +265,31 @@ export declare type RecipeInterface = { tenantId: string; userContext: UserContext; }): Promise; + getFrontendRedirectionURL( + input: + | { + type: "login"; + loginChallenge: string; + tenantId: string; + forceFreshAuth: boolean; + hint: string | undefined; + userContext: UserContext; + } + | { + type: "try-refresh"; + loginChallenge: string; + userContext: UserContext; + } + | { + type: "logout-confirmation"; + logoutChallenge: string; + userContext: UserContext; + } + | { + type: "post-logout-fallback"; + userContext: UserContext; + } + ): Promise; revokeToken( input: { token: string; diff --git a/lib/ts/recipe/oauth2provider/api/utils.ts b/lib/ts/recipe/oauth2provider/api/utils.ts index d0a12a009..8d3481f24 100644 --- a/lib/ts/recipe/oauth2provider/api/utils.ts +++ b/lib/ts/recipe/oauth2provider/api/utils.ts @@ -86,30 +86,14 @@ export async function loginGET({ }); return { redirectTo: accept.redirectTo, setCookie }; } - const appInfo = SuperTokens.getInstanceOrThrowError().appInfo; - const websiteDomain = appInfo - .getOrigin({ - request: undefined, - userContext: userContext, - }) - .getAsStringDangerous(); - const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); if (shouldTryRefresh && promptParam !== "login") { - const websiteDomain = appInfo - .getOrigin({ - request: undefined, - userContext: userContext, - }) - .getAsStringDangerous(); - const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); - - const queryParamsForTryRefreshPage = new URLSearchParams({ - loginChallenge, - }); - return { - redirectTo: websiteDomain + websiteBasePath + `/try-refresh?${queryParamsForTryRefreshPage.toString()}`, + redirectTo: await recipeImplementation.getFrontendRedirectionURL({ + type: "try-refresh", + loginChallenge, + userContext, + }), setCookie, }; } @@ -126,24 +110,15 @@ export async function loginGET({ return { redirectTo: reject.redirectTo, setCookie }; } - const queryParamsForAuthPage = new URLSearchParams({ - loginChallenge, - }); - - if (loginRequest.oidcContext?.login_hint) { - queryParamsForAuthPage.set("hint", loginRequest.oidcContext.login_hint); - } - - if (session !== undefined || promptParam === "login") { - queryParamsForAuthPage.set("forceFreshAuth", "true"); - } - - if (tenantIdParam !== null && tenantIdParam !== DEFAULT_TENANT_ID) { - queryParamsForAuthPage.set("tenantId", tenantIdParam); - } - return { - redirectTo: websiteDomain + websiteBasePath + `?${queryParamsForAuthPage.toString()}`, + redirectTo: await recipeImplementation.getFrontendRedirectionURL({ + type: "login", + loginChallenge, + forceFreshAuth: session !== undefined || promptParam === "login", + tenantId: tenantIdParam ?? DEFAULT_TENANT_ID, + hint: loginRequest.oidcContext?.login_hint, + userContext, + }), setCookie, }; } diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts index d1b4b77f3..f7825ccd5 100644 --- a/lib/ts/recipe/oauth2provider/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -29,6 +29,7 @@ import { OAuth2Client } from "./OAuth2Client"; import { getUser } from "../.."; import { getCombinedJWKS } from "../../combinedRemoteJWKSet"; import SessionRecipe from "../session/recipe"; +import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; function getUpdatedRedirectTo(appInfo: NormalisedAppinfo, redirectTo: string) { return redirectTo.replace( @@ -523,6 +524,37 @@ export default function getRecipeInterface( buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); }, + getFrontendRedirectionURL: async function (input) { + const websiteDomain = appInfo + .getOrigin({ request: undefined, userContext: input.userContext }) + .getAsStringDangerous(); + const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + + if (input.type === "login") { + const queryParams = new URLSearchParams({ + loginChallenge: input.loginChallenge, + }); + if (input.tenantId !== undefined && input.tenantId !== DEFAULT_TENANT_ID) { + queryParams.set("tenantId", input.tenantId); + } + if (input.hint !== undefined) { + queryParams.set("hint", input.hint); + } + if (input.forceFreshAuth) { + queryParams.set("forceFreshAuth", "true"); + } + + return `${websiteDomain}${websiteBasePath}?${queryParams.toString()}`; + } else if (input.type === "try-refresh") { + return `${websiteDomain}${websiteBasePath}/try-refresh?loginChallenge=${input.loginChallenge}`; + } else if (input.type === "post-logout-fallback") { + return `${websiteDomain}${websiteBasePath}`; + } else if (input.type === "logout-confirmation") { + return `${websiteDomain}${websiteBasePath}/oauth/logout?logoutChallenge=${input.logoutChallenge}`; + } + + throw new Error("This should never happen: invalid type passed to getFrontendRedirectionURL"); + }, validateOAuth2AccessToken: async function (input) { const payload = ( await jose.jwtVerify(input.token, getCombinedJWKS(SessionRecipe.getInstanceOrThrowError().config)) @@ -675,11 +707,6 @@ export default function getRecipeInterface( throw new Error(resp.body); } - const websiteDomain = appInfo - .getOrigin({ request: undefined, userContext: input.userContext }) - .getAsStringDangerous(); - const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); - const redirectToURL = new URL(redirectTo); const logoutChallenge = redirectToURL.searchParams.get("logout_challenge"); @@ -688,8 +715,11 @@ export default function getRecipeInterface( // Redirect to the frontend to ask for logout confirmation if there is a valid or expired supertokens session if (input.session !== undefined || input.shouldTryRefresh) { return { - redirectTo: - websiteDomain + websiteBasePath + "/oauth/logout" + `?logoutChallenge=${logoutChallenge}`, + redirectTo: await this.getFrontendRedirectionURL({ + type: "logout-confirmation", + logoutChallenge, + userContext: input.userContext, + }), }; } else { // Accept the logout challenge immediately as there is no supertokens session @@ -706,7 +736,12 @@ export default function getRecipeInterface( // NOTE: If no post_logout_redirect_uri is provided, Hydra redirects to a fallback page. // In this case, we redirect the user to the /auth page. if (redirectTo.endsWith("/oauth/fallbacks/logout/callback")) { - return { redirectTo: `${websiteDomain}${websiteBasePath}` }; + return { + redirectTo: await this.getFrontendRedirectionURL({ + type: "post-logout-fallback", + userContext: input.userContext, + }), + }; } return { redirectTo }; diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts index f985639b5..70947c2a4 100644 --- a/lib/ts/recipe/oauth2provider/types.ts +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -368,6 +368,31 @@ export type RecipeInterface = { tenantId: string; userContext: UserContext; }): Promise; + getFrontendRedirectionURL( + input: + | { + type: "login"; + loginChallenge: string; + tenantId: string; + forceFreshAuth: boolean; + hint: string | undefined; + userContext: UserContext; + } + | { + type: "try-refresh"; + loginChallenge: string; + userContext: UserContext; + } + | { + type: "logout-confirmation"; + logoutChallenge: string; + userContext: UserContext; + } + | { + type: "post-logout-fallback"; + userContext: UserContext; + } + ): Promise; revokeToken( input: { token: string;