Skip to content

Commit

Permalink
fix: Remove internal redirects in the OAuth2 flow (#896)
Browse files Browse the repository at this point in the history
* fix: Remove internal redirects in the OAuth2 flow

* fix: PR changes
  • Loading branch information
anku255 authored Jul 26, 2024
1 parent e84eb49 commit 9c7a22d
Show file tree
Hide file tree
Showing 5 changed files with 412 additions and 93 deletions.
74 changes: 25 additions & 49 deletions lib/build/recipe/oauth2/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,53 +20,23 @@ var __importDefault =
};
Object.defineProperty(exports, "__esModule", { value: true });
const supertokens_1 = __importDefault(require("../../../supertokens"));
const utils_1 = require("./utils");
function getAPIImplementation() {
return {
loginGET: async ({ loginChallenge, options, session, userContext }) => {
var _a, _b;
const request = await options.recipeImplementation.getLoginRequest({
challenge: loginChallenge,
const response = await utils_1.loginGET({
recipeImplementation: options.recipeImplementation,
loginChallenge,
session,
userContext,
});
return utils_1.handleInternalRedirects({
response,
cookie: options.req.getHeaderValue("cookie"),
recipeImplementation: options.recipeImplementation,
session,
userContext,
});
if (request.skip) {
const accept = await options.recipeImplementation.acceptLoginRequest({
challenge: loginChallenge,
subject: request.subject,
userContext,
});
return { redirectTo: accept.redirectTo };
} else if (session) {
if (session.getUserId() !== request.subject) {
// TODO?
}
const accept = await options.recipeImplementation.acceptLoginRequest({
challenge: loginChallenge,
subject: session.getUserId(),
identityProviderSessionId: session.getHandle(),
userContext,
});
return { redirectTo: accept.redirectTo };
}
const appInfo = supertokens_1.default.getInstanceOrThrowError().appInfo;
const websiteDomain = appInfo
.getOrigin({
request: options.req,
userContext: userContext,
})
.getAsStringDangerous();
const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous();
// TODO:
return {
redirectTo:
websiteDomain +
websiteBasePath +
`?hint=${
(_b = (_a = request.oidcContext) === null || _a === void 0 ? void 0 : _a.login_hint) !== null &&
_b !== void 0
? _b
: ""
}&loginChallenge=${loginChallenge}`,
};
},
loginPOST: async ({ loginChallenge, accept, options, session, userContext }) => {
const res = accept
Expand Down Expand Up @@ -162,14 +132,20 @@ function getAPIImplementation() {
});
return { redirectTo: res.redirectTo };
},
authGET: async (input) => {
const res = await input.options.recipeImplementation.authorization({
params: input.params,
cookies: input.cookie,
session: input.session,
userContext: input.userContext,
authGET: async ({ options, params, cookie, session, userContext }) => {
const response = await options.recipeImplementation.authorization({
params,
cookies: cookie,
session,
userContext,
});
return utils_1.handleInternalRedirects({
response,
recipeImplementation: options.recipeImplementation,
cookie,
session,
userContext,
});
return res;
},
tokenPOST: async (input) => {
return input.options.recipeImplementation.token({ body: input.body, userContext: input.userContext });
Expand Down
39 changes: 39 additions & 0 deletions lib/build/recipe/oauth2/api/utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// @ts-nocheck
import { UserContext } from "../../../types";
import { SessionContainerInterface } from "../../session/types";
import { RecipeInterface } from "../types";
export declare function loginGET({
recipeImplementation,
loginChallenge,
session,
setCookie,
userContext,
}: {
recipeImplementation: RecipeInterface;
loginChallenge: string;
session?: SessionContainerInterface;
setCookie?: string;
userContext: UserContext;
}): Promise<{
redirectTo: string;
setCookie: string | undefined;
}>;
export declare function handleInternalRedirects({
response,
recipeImplementation,
session,
cookie,
userContext,
}: {
response: {
redirectTo: string;
setCookie: string | undefined;
};
recipeImplementation: RecipeInterface;
session?: SessionContainerInterface;
cookie?: string;
userContext: UserContext;
}): Promise<{
redirectTo: string;
setCookie: string | undefined;
}>;
146 changes: 146 additions & 0 deletions lib/build/recipe/oauth2/api/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"use strict";
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleInternalRedirects = exports.loginGET = void 0;
const supertokens_1 = __importDefault(require("../../../supertokens"));
const constants_1 = require("../constants");
const set_cookie_parser_1 = __importDefault(require("set-cookie-parser"));
// API implementation for the loginGET function.
// Extracted for use in both apiImplementation and handleInternalRedirects.
async function loginGET({ recipeImplementation, loginChallenge, session, setCookie, userContext }) {
var _a, _b;
const request = await recipeImplementation.getLoginRequest({
challenge: loginChallenge,
userContext,
});
if (request.skip) {
const accept = await recipeImplementation.acceptLoginRequest({
challenge: loginChallenge,
subject: request.subject,
userContext,
});
return { redirectTo: accept.redirectTo, setCookie };
} else if (session) {
if (session.getUserId() !== request.subject) {
// TODO?
}
const accept = await recipeImplementation.acceptLoginRequest({
challenge: loginChallenge,
subject: session.getUserId(),
identityProviderSessionId: session.getHandle(),
userContext,
});
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();
return {
redirectTo:
websiteDomain +
websiteBasePath +
`?hint=${
(_b = (_a = request.oidcContext) === null || _a === void 0 ? void 0 : _a.login_hint) !== null &&
_b !== void 0
? _b
: ""
}&loginChallenge=${loginChallenge}`,
setCookie,
};
}
exports.loginGET = loginGET;
function getMergedCookies({ cookie = "", setCookie }) {
if (!setCookie) {
return cookie;
}
const cookieMap = cookie.split(";").reduce((acc, curr) => {
const [name, value] = curr.split("=");
return Object.assign(Object.assign({}, acc), { [name.trim()]: value });
}, {});
const setCookies = set_cookie_parser_1.default.parse(set_cookie_parser_1.default.splitCookiesString(setCookie));
for (const { name, value, expires } of setCookies) {
if (expires && new Date(expires) < new Date()) {
delete cookieMap[name];
} else {
cookieMap[name] = value;
}
}
return Object.entries(cookieMap)
.map(([key, value]) => `${key}=${value}`)
.join(";");
}
function mergeSetCookieHeaders(setCookie1, setCookie2) {
if (!setCookie1) {
return setCookie2 || "";
}
if (!setCookie2 || setCookie1 === setCookie2) {
return setCookie1;
}
return `${setCookie1};${setCookie2}`;
}
function isInternalRedirect(redirectTo) {
const { apiDomain, apiBasePath } = supertokens_1.default.getInstanceOrThrowError().appInfo;
const basePath = `${apiDomain.getAsStringDangerous()}${apiBasePath.getAsStringDangerous()}`;
return [constants_1.LOGIN_PATH, constants_1.AUTH_PATH].some((path) => redirectTo.startsWith(`${basePath}${path}`));
}
// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip.
// If an internal redirect is identified, it's handled directly by this function.
// Currently, we only need to handle redirects to /oauth2/login and /oauth2/auth endpoints.
async function handleInternalRedirects({ response, recipeImplementation, session, cookie = "", userContext }) {
var _a;
if (!isInternalRedirect(response.redirectTo)) {
return response;
}
// Typically, there are no more than 2 internal redirects per API call but we are allowing upto 10.
// This safety net prevents infinite redirect loops in case there are more redirects than expected.
const maxRedirects = 10;
let redirectCount = 0;
while (redirectCount < maxRedirects && isInternalRedirect(response.redirectTo)) {
cookie = getMergedCookies({ cookie, setCookie: response.setCookie });
const queryString = response.redirectTo.split("?")[1];
const params = new URLSearchParams(queryString);
if (response.redirectTo.includes(constants_1.LOGIN_PATH)) {
const loginChallenge =
(_a = params.get("login_challenge")) !== null && _a !== void 0 ? _a : params.get("loginChallenge");
if (!loginChallenge) {
throw new Error(`Expected loginChallenge in ${response.redirectTo}`);
}
const loginRes = await loginGET({
recipeImplementation,
loginChallenge,
session,
setCookie: response.setCookie,
userContext,
});
response = {
redirectTo: loginRes.redirectTo,
setCookie: mergeSetCookieHeaders(loginRes.setCookie, response.setCookie),
};
} else if (response.redirectTo.includes(constants_1.AUTH_PATH)) {
const authRes = await recipeImplementation.authorization({
params: Object.fromEntries(params.entries()),
cookies: cookie,
session,
userContext,
});
response = {
redirectTo: authRes.redirectTo,
setCookie: mergeSetCookieHeaders(authRes.setCookie, response.setCookie),
};
} else {
throw new Error(`Unexpected internal redirect ${response.redirectTo}`);
}
redirectCount++;
}
return response;
}
exports.handleInternalRedirects = handleInternalRedirects;
70 changes: 26 additions & 44 deletions lib/ts/recipe/oauth2/api/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,24 @@

import SuperTokens from "../../../supertokens";
import { APIInterface } from "../types";
import { handleInternalRedirects, loginGET } from "./utils";

export default function getAPIImplementation(): APIInterface {
return {
loginGET: async ({ loginChallenge, options, session, userContext }) => {
const request = await options.recipeImplementation.getLoginRequest({
challenge: loginChallenge,
const response = await loginGET({
recipeImplementation: options.recipeImplementation,
loginChallenge,
session,
userContext,
});
return handleInternalRedirects({
response,
cookie: options.req.getHeaderValue("cookie"),
recipeImplementation: options.recipeImplementation,
session,
userContext,
});

if (request.skip) {
const accept = await options.recipeImplementation.acceptLoginRequest({
challenge: loginChallenge,
subject: request.subject,
userContext,
});
return { redirectTo: accept.redirectTo };
} else if (session) {
if (session.getUserId() !== request.subject) {
// TODO?
}
const accept = await options.recipeImplementation.acceptLoginRequest({
challenge: loginChallenge,
subject: session.getUserId(),
identityProviderSessionId: session.getHandle(),
userContext,
});
return { redirectTo: accept.redirectTo };
}
const appInfo = SuperTokens.getInstanceOrThrowError().appInfo;
const websiteDomain = appInfo
.getOrigin({
request: options.req,
userContext: userContext,
})
.getAsStringDangerous();
const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous();
// TODO:
return {
redirectTo:
websiteDomain +
websiteBasePath +
`?hint=${request.oidcContext?.login_hint ?? ""}&loginChallenge=${loginChallenge}`,
};
},
loginPOST: async ({ loginChallenge, accept, options, session, userContext }) => {
const res = accept
Expand Down Expand Up @@ -159,14 +134,21 @@ export default function getAPIImplementation(): APIInterface {
});
return { redirectTo: res.redirectTo };
},
authGET: async (input) => {
const res = await input.options.recipeImplementation.authorization({
params: input.params,
cookies: input.cookie,
session: input.session,
userContext: input.userContext,
authGET: async ({ options, params, cookie, session, userContext }) => {
const response = await options.recipeImplementation.authorization({
params,
cookies: cookie,
session,
userContext,
});

return handleInternalRedirects({
response,
recipeImplementation: options.recipeImplementation,
cookie,
session,
userContext,
});
return res;
},
tokenPOST: async (input) => {
return input.options.recipeImplementation.token({ body: input.body, userContext: input.userContext });
Expand Down
Loading

0 comments on commit 9c7a22d

Please sign in to comment.