diff --git a/lib/build/recipe/oauth2provider/api/implementation.js b/lib/build/recipe/oauth2provider/api/implementation.js index 9739a22ed..cee232fec 100644 --- a/lib/build/recipe/oauth2provider/api/implementation.js +++ b/lib/build/recipe/oauth2provider/api/implementation.js @@ -22,6 +22,7 @@ function getAPIImplementation() { recipeImplementation: options.recipeImplementation, loginChallenge, session, + isDirectCall: true, userContext, }); return utils_1.handleInternalRedirects({ diff --git a/lib/build/recipe/oauth2provider/api/login.js b/lib/build/recipe/oauth2provider/api/login.js index dc42e7ceb..57dbed8c4 100644 --- a/lib/build/recipe/oauth2provider/api/login.js +++ b/lib/build/recipe/oauth2provider/api/login.js @@ -19,6 +19,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +const set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); const utils_1 = require("../../../utils"); const session_1 = __importDefault(require("../../session")); const error_1 = __importDefault(require("../../../error")); @@ -54,6 +55,22 @@ async function login(apiImplementation, options, userContext) { if ("status" in response) { utils_1.send200Response(options.res, response); } else { + if (response.setCookie) { + const cookieStr = set_cookie_parser_1.default.splitCookiesString(response.setCookie); + const cookies = set_cookie_parser_1.default.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires).getTime(), + cookie.path || "/", + cookie.sameSite + ); + } + } options.res.original.redirect(response.redirectTo); } return true; diff --git a/lib/build/recipe/oauth2provider/api/utils.d.ts b/lib/build/recipe/oauth2provider/api/utils.d.ts index 0a430568c..2271ba148 100644 --- a/lib/build/recipe/oauth2provider/api/utils.d.ts +++ b/lib/build/recipe/oauth2provider/api/utils.d.ts @@ -7,6 +7,7 @@ export declare function loginGET({ loginChallenge, session, setCookie, + isDirectCall, userContext, }: { recipeImplementation: RecipeInterface; @@ -14,6 +15,7 @@ export declare function loginGET({ session?: SessionContainerInterface; setCookie?: string; userContext: UserContext; + isDirectCall: boolean; }): Promise<{ redirectTo: string; setCookie: string | undefined; diff --git a/lib/build/recipe/oauth2provider/api/utils.js b/lib/build/recipe/oauth2provider/api/utils.js index c65d1dbb2..7d4277336 100644 --- a/lib/build/recipe/oauth2provider/api/utils.js +++ b/lib/build/recipe/oauth2provider/api/utils.js @@ -7,35 +7,51 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); exports.handleInternalRedirects = exports.loginGET = void 0; const supertokens_1 = __importDefault(require("../../../supertokens")); +const session_1 = require("../../session"); 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 }) { +async function loginGET({ recipeImplementation, loginChallenge, session, setCookie, isDirectCall, userContext }) { var _a; - const request = await recipeImplementation.getLoginRequest({ + const loginRequest = await recipeImplementation.getLoginRequest({ challenge: loginChallenge, userContext, }); - const queryParams = new URLSearchParams({ - loginChallenge, - }); - if ((_a = request.oidcContext) === null || _a === void 0 ? void 0 : _a.login_hint) { - queryParams.set("hint", request.oidcContext.login_hint); + const sessionInfo = + session !== undefined + ? await session_1.getSessionInformation( + session === null || session === void 0 ? void 0 : session.getHandle() + ) + : undefined; + if (!sessionInfo) { + session = undefined; } - if (request.skip) { + const incomingAuthUrlQueryParams = new URLSearchParams(loginRequest.requestUrl.split("?")[1]); + const promptParam = incomingAuthUrlQueryParams.get("prompt"); + const maxAgeParam = incomingAuthUrlQueryParams.get("max_age"); + if (loginRequest.skip) { const accept = await recipeImplementation.acceptLoginRequest({ challenge: loginChallenge, identityProviderSessionId: session === null || session === void 0 ? void 0 : session.getHandle(), - subject: request.subject, + subject: loginRequest.subject, userContext, }); return { redirectTo: accept.redirectTo, setCookie }; - } else if (session && (!request.subject || session.getUserId() === request.subject)) { + } else if ( + session && + (["", undefined].includes(loginRequest.subject) || session.getUserId() === loginRequest.subject) && + (promptParam !== "login" || isDirectCall) && + (maxAgeParam === null || + (maxAgeParam === "0" && isDirectCall) || + Number.parseInt(maxAgeParam) * 1000 > Date.now() - sessionInfo.timeCreated) + ) { const accept = await recipeImplementation.acceptLoginRequest({ challenge: loginChallenge, subject: session.getUserId(), identityProviderSessionId: session.getHandle(), + remember: true, + rememberFor: 3600, userContext, }); return { redirectTo: accept.redirectTo, setCookie }; @@ -48,8 +64,17 @@ async function loginGET({ recipeImplementation, loginChallenge, session, setCook }) .getAsStringDangerous(); const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + const queryParamsForAuthPage = new URLSearchParams({ + loginChallenge, + }); + if ((_a = loginRequest.oidcContext) === null || _a === void 0 ? void 0 : _a.login_hint) { + queryParamsForAuthPage.set("hint", loginRequest.oidcContext.login_hint); + } + if (session !== undefined) { + queryParamsForAuthPage.set("forceFreshAuth", "true"); + } return { - redirectTo: websiteDomain + websiteBasePath + `?${queryParams.toString()}`, + redirectTo: websiteDomain + websiteBasePath + `?${queryParamsForAuthPage.toString()}`, setCookie, }; } @@ -81,7 +106,7 @@ function mergeSetCookieHeaders(setCookie1, setCookie2) { if (!setCookie2 || setCookie1 === setCookie2) { return setCookie1; } - return `${setCookie1};${setCookie2}`; + return `${setCookie1}, ${setCookie2}`; } function isInternalRedirect(redirectTo) { const { apiDomain, apiBasePath } = supertokens_1.default.getInstanceOrThrowError().appInfo; @@ -120,6 +145,7 @@ async function handleInternalRedirects({ response, recipeImplementation, session loginChallenge, session, setCookie: response.setCookie, + isDirectCall: false, userContext, }); response = { diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js index 1dff06e37..dab70e214 100644 --- a/lib/build/recipe/oauth2provider/recipeImplementation.js +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -201,6 +201,7 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, new normalisedURLPath_1.default(`/recipe/oauth2/pub/auth`), input.params, { + // TODO: if session is not set also clear the oauth2 cookie Cookie: `${input.cookies}`, }, input.userContext diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts index de87ea508..0c289312c 100644 --- a/lib/build/recipe/oauth2provider/types.d.ts +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -257,6 +257,7 @@ export declare type APIInterface = { }) => Promise< | { redirectTo: string; + setCookie: string | undefined; } | GeneralErrorResponse >); diff --git a/lib/ts/recipe/oauth2provider/api/implementation.ts b/lib/ts/recipe/oauth2provider/api/implementation.ts index 2ba89b59d..24a913346 100644 --- a/lib/ts/recipe/oauth2provider/api/implementation.ts +++ b/lib/ts/recipe/oauth2provider/api/implementation.ts @@ -23,6 +23,7 @@ export default function getAPIImplementation(): APIInterface { recipeImplementation: options.recipeImplementation, loginChallenge, session, + isDirectCall: true, userContext, }); return handleInternalRedirects({ diff --git a/lib/ts/recipe/oauth2provider/api/login.ts b/lib/ts/recipe/oauth2provider/api/login.ts index ef0c2cb9f..78ef67780 100644 --- a/lib/ts/recipe/oauth2provider/api/login.ts +++ b/lib/ts/recipe/oauth2provider/api/login.ts @@ -13,6 +13,7 @@ * under the License. */ +import setCookieParser from "set-cookie-parser"; import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import Session from "../../session"; @@ -54,6 +55,22 @@ export default async function login( if ("status" in response) { send200Response(options.res, response); } else { + if (response.setCookie) { + const cookieStr = setCookieParser.splitCookiesString(response.setCookie); + const cookies = setCookieParser.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires!).getTime(), + cookie.path || "/", + cookie.sameSite as any + ); + } + } options.res.original.redirect(response.redirectTo); } return true; diff --git a/lib/ts/recipe/oauth2provider/api/utils.ts b/lib/ts/recipe/oauth2provider/api/utils.ts index 3eba3cd15..a9be8ae42 100644 --- a/lib/ts/recipe/oauth2provider/api/utils.ts +++ b/lib/ts/recipe/oauth2provider/api/utils.ts @@ -1,5 +1,6 @@ import SuperTokens from "../../../supertokens"; import { UserContext } from "../../../types"; +import { getSessionInformation } from "../../session"; import { SessionContainerInterface } from "../../session/types"; import { AUTH_PATH, LOGIN_PATH } from "../constants"; import { RecipeInterface } from "../types"; @@ -12,6 +13,7 @@ export async function loginGET({ loginChallenge, session, setCookie, + isDirectCall, userContext, }: { recipeImplementation: RecipeInterface; @@ -19,34 +21,44 @@ export async function loginGET({ session?: SessionContainerInterface; setCookie?: string; userContext: UserContext; + isDirectCall: boolean; }) { - const request = await recipeImplementation.getLoginRequest({ + const loginRequest = await recipeImplementation.getLoginRequest({ challenge: loginChallenge, userContext, }); - const queryParams = new URLSearchParams({ - loginChallenge, - }); - - if (request.oidcContext?.login_hint) { - queryParams.set("hint", request.oidcContext.login_hint); + const sessionInfo = session !== undefined ? await getSessionInformation(session?.getHandle()) : undefined; + if (!sessionInfo) { + session = undefined; } - if (request.skip) { + const incomingAuthUrlQueryParams = new URLSearchParams(loginRequest.requestUrl.split("?")[1]); + const promptParam = incomingAuthUrlQueryParams.get("prompt"); + const maxAgeParam = incomingAuthUrlQueryParams.get("max_age"); + if (loginRequest.skip) { const accept = await recipeImplementation.acceptLoginRequest({ challenge: loginChallenge, identityProviderSessionId: session?.getHandle(), - subject: request.subject, + subject: loginRequest.subject, userContext, }); return { redirectTo: accept.redirectTo, setCookie }; - } else if (session && (!request.subject || session.getUserId() === request.subject)) { + } else if ( + session && + (["", undefined].includes(loginRequest.subject) || session.getUserId() === loginRequest.subject) && + (promptParam !== "login" || isDirectCall) && + (maxAgeParam === null || + (maxAgeParam === "0" && isDirectCall) || + Number.parseInt(maxAgeParam) * 1000 > Date.now() - sessionInfo!.timeCreated) + ) { const accept = await recipeImplementation.acceptLoginRequest({ challenge: loginChallenge, subject: session.getUserId(), identityProviderSessionId: session.getHandle(), + remember: true, + rememberFor: 3600, userContext, }); return { redirectTo: accept.redirectTo, setCookie }; @@ -60,8 +72,20 @@ export async function loginGET({ .getAsStringDangerous(); const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + const queryParamsForAuthPage = new URLSearchParams({ + loginChallenge, + }); + + if (loginRequest.oidcContext?.login_hint) { + queryParamsForAuthPage.set("hint", loginRequest.oidcContext.login_hint); + } + + if (session !== undefined) { + queryParamsForAuthPage.set("forceFreshAuth", "true"); + } + return { - redirectTo: websiteDomain + websiteBasePath + `?${queryParams.toString()}`, + redirectTo: websiteDomain + websiteBasePath + `?${queryParamsForAuthPage.toString()}`, setCookie, }; } @@ -98,7 +122,7 @@ function mergeSetCookieHeaders(setCookie1?: string, setCookie2?: string): string if (!setCookie2 || setCookie1 === setCookie2) { return setCookie1; } - return `${setCookie1};${setCookie2}`; + return `${setCookie1}, ${setCookie2}`; } function isInternalRedirect(redirectTo: string): boolean { @@ -154,6 +178,7 @@ export async function handleInternalRedirects({ loginChallenge, session, setCookie: response.setCookie, + isDirectCall: false, userContext, }); diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts index e9238fbc5..deba02e6d 100644 --- a/lib/ts/recipe/oauth2provider/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -180,6 +180,7 @@ export default function getRecipeInterface( new NormalisedURLPath(`/recipe/oauth2/pub/auth`), input.params, { + // TODO: if session is not set also clear the oauth2 cookie Cookie: `${input.cookies}`, }, input.userContext diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts index 5eeca4817..81d4f3a14 100644 --- a/lib/ts/recipe/oauth2provider/types.ts +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -368,7 +368,7 @@ export type APIInterface = { options: APIOptions; session?: SessionContainerInterface; userContext: UserContext; - }) => Promise<{ redirectTo: string } | GeneralErrorResponse>); + }) => Promise<{ redirectTo: string; setCookie: string | undefined } | GeneralErrorResponse>); authGET: | undefined