Skip to content

Commit

Permalink
feat: make the frontend redirection urls overrideable
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Sep 23, 2024
1 parent 3b42ee6 commit 376128a
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 85 deletions.
46 changes: 14 additions & 32 deletions lib/build/recipe/oauth2provider/api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Expand All @@ -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,
};
}
Expand Down
47 changes: 40 additions & 7 deletions lib/build/recipe/oauth2provider/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -676,19 +705,18 @@ 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)
if (logoutChallenge !== null) {
// 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
Expand All @@ -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 };
},
Expand Down
25 changes: 25 additions & 0 deletions lib/build/recipe/oauth2provider/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,31 @@ export declare type RecipeInterface = {
tenantId: string;
userContext: UserContext;
}): Promise<JSONObject>;
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<string>;
revokeToken(
input: {
token: string;
Expand Down
51 changes: 13 additions & 38 deletions lib/ts/recipe/oauth2provider/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Expand All @@ -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,
};
}
Expand Down
51 changes: 43 additions & 8 deletions lib/ts/recipe/oauth2provider/recipeImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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");

Expand All @@ -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
Expand All @@ -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 };
Expand Down
25 changes: 25 additions & 0 deletions lib/ts/recipe/oauth2provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,31 @@ export type RecipeInterface = {
tenantId: string;
userContext: UserContext;
}): Promise<JSONObject>;
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<string>;
revokeToken(
input: {
token: string;
Expand Down

0 comments on commit 376128a

Please sign in to comment.