From 9506071e9cdf402fde0ef9968dfcf6be863e8b04 Mon Sep 17 00:00:00 2001 From: Mihaly Lengyel Date: Wed, 25 Sep 2024 23:06:40 +0200 Subject: [PATCH] feat: make loginGET return the redirection link as a JSON response instead --- lib/build/querier.js | 2 ++ lib/build/recipe/oauth2provider/OAuth2Client.d.ts | 6 ------ lib/build/recipe/oauth2provider/OAuth2Client.js | 2 -- .../recipe/oauth2provider/api/implementation.js | 9 ++++++++- lib/build/recipe/oauth2provider/api/login.js | 6 ++++-- lib/build/recipe/oauth2provider/recipe.js | 3 +++ .../recipe/oauth2provider/recipeImplementation.js | 15 +++++++++------ lib/build/recipe/oauth2provider/types.d.ts | 7 +------ lib/ts/querier.ts | 2 ++ lib/ts/recipe/oauth2provider/OAuth2Client.ts | 8 -------- .../recipe/oauth2provider/api/implementation.ts | 11 ++++++++++- lib/ts/recipe/oauth2provider/api/login.ts | 6 ++++-- lib/ts/recipe/oauth2provider/recipe.ts | 3 +++ .../recipe/oauth2provider/recipeImplementation.ts | 15 +++++++++------ lib/ts/recipe/oauth2provider/types.ts | 8 +------- test/auth-react-server/index.js | 12 ++++++++++++ 16 files changed, 68 insertions(+), 47 deletions(-) diff --git a/lib/build/querier.js b/lib/build/querier.js index 9ee1f364f..7dba9e348 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -262,6 +262,7 @@ class Querier { Object.entries(params).filter(([_, value]) => value !== undefined) ); finalURL.search = searchParams.toString(); + console.log("finalURL", finalURL.toString()); // Update cache and return let response = await utils_1.doFetch(finalURL.toString(), { method: "GET", @@ -467,6 +468,7 @@ class Querier { if (utils_1.isTestEnv()) { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); } + console.log("response", { url, body: await response.clone().text(), headers: response.headers }); if (response.status !== 200) { throw response; } diff --git a/lib/build/recipe/oauth2provider/OAuth2Client.d.ts b/lib/build/recipe/oauth2provider/OAuth2Client.d.ts index d61e16556..b58e933fb 100644 --- a/lib/build/recipe/oauth2provider/OAuth2Client.d.ts +++ b/lib/build/recipe/oauth2provider/OAuth2Client.d.ts @@ -94,11 +94,6 @@ export declare class OAuth2Client { * ClientURI is a URL string of a web page providing information about the client. */ clientUri: string; - /** - * Array of allowed CORS origins - * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. - */ - allowedCorsOrigins: string[]; /** * Array of audiences * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. @@ -167,7 +162,6 @@ export declare class OAuth2Client { refreshTokenGrantRefreshTokenLifespan, tokenEndpointAuthMethod, clientUri, - allowedCorsOrigins, audience, grantTypes, responseTypes, diff --git a/lib/build/recipe/oauth2provider/OAuth2Client.js b/lib/build/recipe/oauth2provider/OAuth2Client.js index b174839e1..4c700f04f 100644 --- a/lib/build/recipe/oauth2provider/OAuth2Client.js +++ b/lib/build/recipe/oauth2provider/OAuth2Client.js @@ -35,7 +35,6 @@ class OAuth2Client { refreshTokenGrantRefreshTokenLifespan = null, tokenEndpointAuthMethod, clientUri = "", - allowedCorsOrigins = [], audience = [], grantTypes = null, responseTypes = null, @@ -68,7 +67,6 @@ class OAuth2Client { this.refreshTokenGrantRefreshTokenLifespan = refreshTokenGrantRefreshTokenLifespan; this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; this.clientUri = clientUri; - this.allowedCorsOrigins = allowedCorsOrigins; this.audience = audience; this.grantTypes = grantTypes; this.responseTypes = responseTypes; diff --git a/lib/build/recipe/oauth2provider/api/implementation.js b/lib/build/recipe/oauth2provider/api/implementation.js index 986c23590..5baf073ea 100644 --- a/lib/build/recipe/oauth2provider/api/implementation.js +++ b/lib/build/recipe/oauth2provider/api/implementation.js @@ -26,7 +26,7 @@ function getAPIImplementation() { isDirectCall: true, userContext, }); - return utils_1.handleLoginInternalRedirects({ + const respAfterInternalRedirects = await utils_1.handleLoginInternalRedirects({ response, cookie: options.req.getHeaderValue("cookie"), recipeImplementation: options.recipeImplementation, @@ -34,6 +34,13 @@ function getAPIImplementation() { shouldTryRefresh, userContext, }); + if ("error" in respAfterInternalRedirects) { + return respAfterInternalRedirects; + } + return { + frontendRedirectTo: respAfterInternalRedirects.redirectTo, + setCookie: respAfterInternalRedirects.setCookie, + }; }, authGET: async ({ options, params, cookie, session, shouldTryRefresh, userContext }) => { const response = await options.recipeImplementation.authorization({ diff --git a/lib/build/recipe/oauth2provider/api/login.js b/lib/build/recipe/oauth2provider/api/login.js index af7a36837..547399479 100644 --- a/lib/build/recipe/oauth2provider/api/login.js +++ b/lib/build/recipe/oauth2provider/api/login.js @@ -60,7 +60,7 @@ async function login(apiImplementation, options, userContext) { shouldTryRefresh, userContext, }); - if ("redirectTo" in response) { + if ("frontendRedirectTo" in response) { if (response.setCookie) { const cookieStr = set_cookie_parser_1.default.splitCookiesString(response.setCookie); const cookies = set_cookie_parser_1.default.parse(cookieStr); @@ -77,7 +77,9 @@ async function login(apiImplementation, options, userContext) { ); } } - options.res.original.redirect(response.redirectTo); + utils_1.send200Response(options.res, { + frontendRedirectTo: response.frontendRedirectTo, + }); } else if ("statusCode" in response) { utils_1.sendNon200ResponseWithMessage( options.res, diff --git a/lib/build/recipe/oauth2provider/recipe.js b/lib/build/recipe/oauth2provider/recipe.js index c0265a40f..d52a62f5a 100644 --- a/lib/build/recipe/oauth2provider/recipe.js +++ b/lib/build/recipe/oauth2provider/recipe.js @@ -253,10 +253,12 @@ class Recipe extends recipeModule_1.default { sub: accessTokenPayload.sub, }; if (scopes.includes("email")) { + // TODO: try and get the email based on the user id of the entire user object payload.email = user === null || user === void 0 ? void 0 : user.emails[0]; payload.email_verified = user.loginMethods.some( (lm) => lm.hasSameEmailAs(user === null || user === void 0 ? void 0 : user.emails[0]) && lm.verified ); + payload.emails = user.emails; } if (scopes.includes("phoneNumber")) { payload.phoneNumber = user === null || user === void 0 ? void 0 : user.phoneNumbers[0]; @@ -265,6 +267,7 @@ class Recipe extends recipeModule_1.default { lm.hasSamePhoneNumberAs(user === null || user === void 0 ? void 0 : user.phoneNumbers[0]) && lm.verified ); + payload.phoneNumbers = user.phoneNumbers; } for (const fn of this.userInfoBuilders) { payload = Object.assign( diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js index 3031e5f43..e8a1e613b 100644 --- a/lib/build/recipe/oauth2provider/recipeImplementation.js +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -431,7 +431,6 @@ function getRecipeInterface( { pageSize: input.pageSize, clientName: input.clientName, - owner: input.owner, pageToken: input.paginationToken, }, {}, @@ -704,16 +703,20 @@ function getRecipeInterface( * CASE 3: `end_session` request with a `logout_verifier` (after accepting the logout request) * - Redirects to the `post_logout_redirect_uri` or the default logout fallback page. */ - const resp = await querier.sendGetRequestWithResponseHeaders( + console.log("input", input.params); + const resp = await querier.sendGetRequest( new normalisedURLPath_1.default(`/recipe/oauth/sessions/logout`), input.params, - {}, input.userContext ); - const redirectTo = getUpdatedRedirectTo(appInfo, resp.headers.get("Location")); - if (redirectTo === undefined) { - throw new Error(resp.body); + if ("error" in resp) { + return { + statusCode: resp.statusCode, + error: resp.error, + errorDescription: resp.errorDescription, + }; } + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); const redirectToURL = new URL(redirectTo); const logoutChallenge = redirectToURL.searchParams.get("logout_challenge"); // CASE 1 (See above notes) diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts index 7f42a024d..d7a229e61 100644 --- a/lib/build/recipe/oauth2provider/types.d.ts +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -371,7 +371,7 @@ export declare type APIInterface = { userContext: UserContext; }) => Promise< | { - redirectTo: string; + frontendRedirectTo: string; setCookie?: string; } | ErrorOAuth2 @@ -510,7 +510,6 @@ export declare type OAuth2ClientOptions = { scope: string; redirectUris?: string[] | null; postLogoutRedirectUris?: string[]; - allowedCorsOrigins?: string[]; authorizationCodeGrantAccessTokenLifespan?: string | null; authorizationCodeGrantIdTokenLifespan?: string | null; authorizationCodeGrantRefreshTokenLifespan?: string | null; @@ -543,10 +542,6 @@ export declare type GetOAuth2ClientsInput = { * The name of the clients to filter by. */ clientName?: string; - /** - * The owner of the clients to filter by. - */ - owner?: string; }; export declare type CreateOAuth2ClientInput = Partial< Omit diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index 1c8af9b2e..ec8e08bf4 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -344,6 +344,7 @@ export class Querier { ); finalURL.search = searchParams.toString(); + console.log("finalURL", finalURL.toString()); // Update cache and return let response = await doFetch(finalURL.toString(), { method: "GET", @@ -611,6 +612,7 @@ export class Querier { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); } + console.log("response", { url, body: await response.clone().text(), headers: response.headers }); if (response.status !== 200) { throw response; } diff --git a/lib/ts/recipe/oauth2provider/OAuth2Client.ts b/lib/ts/recipe/oauth2provider/OAuth2Client.ts index 3541a05a8..e0bdca1ed 100644 --- a/lib/ts/recipe/oauth2provider/OAuth2Client.ts +++ b/lib/ts/recipe/oauth2provider/OAuth2Client.ts @@ -127,12 +127,6 @@ export class OAuth2Client { */ clientUri: string; - /** - * Array of allowed CORS origins - * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. - */ - allowedCorsOrigins: string[]; - /** * Array of audiences * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. @@ -210,7 +204,6 @@ export class OAuth2Client { refreshTokenGrantRefreshTokenLifespan = null, tokenEndpointAuthMethod, clientUri = "", - allowedCorsOrigins = [], audience = [], grantTypes = null, responseTypes = null, @@ -238,7 +231,6 @@ export class OAuth2Client { this.refreshTokenGrantRefreshTokenLifespan = refreshTokenGrantRefreshTokenLifespan; this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; this.clientUri = clientUri; - this.allowedCorsOrigins = allowedCorsOrigins; this.audience = audience; this.grantTypes = grantTypes; this.responseTypes = responseTypes; diff --git a/lib/ts/recipe/oauth2provider/api/implementation.ts b/lib/ts/recipe/oauth2provider/api/implementation.ts index abf4d2ce2..dd5e3e1fe 100644 --- a/lib/ts/recipe/oauth2provider/api/implementation.ts +++ b/lib/ts/recipe/oauth2provider/api/implementation.ts @@ -27,7 +27,7 @@ export default function getAPIImplementation(): APIInterface { isDirectCall: true, userContext, }); - return handleLoginInternalRedirects({ + const respAfterInternalRedirects = await handleLoginInternalRedirects({ response, cookie: options.req.getHeaderValue("cookie"), recipeImplementation: options.recipeImplementation, @@ -35,6 +35,15 @@ export default function getAPIImplementation(): APIInterface { shouldTryRefresh, userContext, }); + + if ("error" in respAfterInternalRedirects) { + return respAfterInternalRedirects; + } + + return { + frontendRedirectTo: respAfterInternalRedirects.redirectTo, + setCookie: respAfterInternalRedirects.setCookie, + }; }, authGET: async ({ options, params, cookie, session, shouldTryRefresh, userContext }) => { diff --git a/lib/ts/recipe/oauth2provider/api/login.ts b/lib/ts/recipe/oauth2provider/api/login.ts index 5a8041a01..e15a37afd 100644 --- a/lib/ts/recipe/oauth2provider/api/login.ts +++ b/lib/ts/recipe/oauth2provider/api/login.ts @@ -61,7 +61,7 @@ export default async function login( userContext, }); - if ("redirectTo" in response) { + if ("frontendRedirectTo" in response) { if (response.setCookie) { const cookieStr = setCookieParser.splitCookiesString(response.setCookie); const cookies = setCookieParser.parse(cookieStr); @@ -78,7 +78,9 @@ export default async function login( ); } } - options.res.original.redirect(response.redirectTo); + send200Response(options.res, { + frontendRedirectTo: response.frontendRedirectTo, + }); } else if ("statusCode" in response) { sendNon200ResponseWithMessage( options.res, diff --git a/lib/ts/recipe/oauth2provider/recipe.ts b/lib/ts/recipe/oauth2provider/recipe.ts index 6f63b0b1c..07d28e8f4 100644 --- a/lib/ts/recipe/oauth2provider/recipe.ts +++ b/lib/ts/recipe/oauth2provider/recipe.ts @@ -315,14 +315,17 @@ export default class Recipe extends RecipeModule { sub: accessTokenPayload.sub, }; if (scopes.includes("email")) { + // TODO: try and get the email based on the user id of the entire user object payload.email = user?.emails[0]; payload.email_verified = user.loginMethods.some((lm) => lm.hasSameEmailAs(user?.emails[0]) && lm.verified); + payload.emails = user.emails; } if (scopes.includes("phoneNumber")) { payload.phoneNumber = user?.phoneNumbers[0]; payload.phoneNumber_verified = user.loginMethods.some( (lm) => lm.hasSamePhoneNumberAs(user?.phoneNumbers[0]) && lm.verified ); + payload.phoneNumbers = user.phoneNumbers; } for (const fn of this.userInfoBuilders) { diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts index bbf9abd92..739a0f76f 100644 --- a/lib/ts/recipe/oauth2provider/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -426,7 +426,6 @@ export default function getRecipeInterface( { pageSize: input.pageSize, clientName: input.clientName, - owner: input.owner, pageToken: input.paginationToken, }, {}, @@ -709,17 +708,21 @@ export default function getRecipeInterface( * - Redirects to the `post_logout_redirect_uri` or the default logout fallback page. */ - const resp = await querier.sendGetRequestWithResponseHeaders( + console.log("input", input.params); + const resp = await querier.sendGetRequest( new NormalisedURLPath(`/recipe/oauth/sessions/logout`), input.params, - {}, input.userContext ); - const redirectTo = getUpdatedRedirectTo(appInfo, resp.headers.get("Location")!); - if (redirectTo === undefined) { - throw new Error(resp.body); + if ("error" in resp) { + return { + statusCode: resp.statusCode, + error: resp.error, + errorDescription: resp.errorDescription, + }; } + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); const redirectToURL = new URL(redirectTo); const logoutChallenge = redirectToURL.searchParams.get("logout_challenge"); diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts index f7b4ce91c..f123ef164 100644 --- a/lib/ts/recipe/oauth2provider/types.ts +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -440,7 +440,7 @@ export type APIInterface = { session?: SessionContainerInterface; shouldTryRefresh: boolean; userContext: UserContext; - }) => Promise<{ redirectTo: string; setCookie?: string } | ErrorOAuth2 | GeneralErrorResponse>); + }) => Promise<{ frontendRedirectTo: string; setCookie?: string } | ErrorOAuth2 | GeneralErrorResponse>); authGET: | undefined @@ -533,7 +533,6 @@ export type OAuth2ClientOptions = { scope: string; redirectUris?: string[] | null; postLogoutRedirectUris?: string[]; - allowedCorsOrigins?: string[]; authorizationCodeGrantAccessTokenLifespan?: string | null; authorizationCodeGrantIdTokenLifespan?: string | null; @@ -573,11 +572,6 @@ export type GetOAuth2ClientsInput = { * The name of the clients to filter by. */ clientName?: string; - - /** - * The owner of the clients to filter by. - */ - owner?: string; }; export type CreateOAuth2ClientInput = Partial< diff --git a/test/auth-react-server/index.js b/test/auth-react-server/index.js index 3b552a1a9..a4c7a1f9b 100644 --- a/test/auth-react-server/index.js +++ b/test/auth-react-server/index.js @@ -51,6 +51,9 @@ const TOTPRaw = require("../../lib/build/recipe/totp/recipe").default; const TOTP = require("../../recipe/totp"); const OTPAuth = require("otpauth"); +const OAuth2ProviderRaw = require("../../lib/build/recipe/oauth2provider/recipe").default; +const OAuth2Provider = require("../../recipe/oauth2provider"); + let { startST, killAllST, @@ -502,6 +505,11 @@ app.post("/test/getTOTPCode", (req, res) => { res.send(JSON.stringify({ totp: new OTPAuth.TOTP({ secret: req.body.secret, digits: 6, period: 1 }).generate() })); }); +app.post("/test/create-oauth2-client", async (req, res) => { + const { client } = await OAuth2Provider.createOAuth2Client(req.body); + res.send({ client }); +}); + app.get("/test/featureFlags", (req, res) => { const available = []; @@ -515,6 +523,7 @@ app.get("/test/featureFlags", (req, res) => { available.push("mfa"); available.push("recipeConfig"); available.push("accountlinking-fixes"); // this is related to 19.0 release in which we fixed a bunch of issues with account linking, including changing error codes. + available.push("oauth2"); res.send({ available, @@ -568,6 +577,7 @@ function initST({ passwordlessConfig } = {}) { UserMetadataRaw.reset(); MultiFactorAuthRaw.reset(); TOTPRaw.reset(); + OAuth2ProviderRaw.reset(); SuperTokensRaw.reset(); passwordlessConfig = { @@ -937,6 +947,8 @@ function initST({ passwordlessConfig } = {}) { }), ]); + recipeList.push(["oauth2", OAuth2Provider.init()]); + SuperTokens.init({ appInfo: { appName: "SuperTokens",