diff --git a/.circleci/config_continue.yml b/.circleci/config_continue.yml index 11ac5d92d..b250913d9 100644 --- a/.circleci/config_continue.yml +++ b/.circleci/config_continue.yml @@ -5,6 +5,26 @@ orbs: slack: circleci/slack@3.4.2 jq: circleci/jq@2.2.0 +parameters: + force: + type: boolean + default: false + cdi-core-map: + type: string + default: "{}" + cdi-plugin-interface-map: + type: string + default: "{}" + fdi-node-map: + type: string + default: "{}" + fdi-auth-react-map: + type: string + default: "{}" + fdi-website-map: + type: string + default: "{}" + jobs: test-dev-tag-as-not-passed: docker: diff --git a/.circleci/forceRunCI.sh b/.circleci/forceRunCI.sh index e0a05f6d9..41e2a62e9 100755 --- a/.circleci/forceRunCI.sh +++ b/.circleci/forceRunCI.sh @@ -2,11 +2,11 @@ PAT=`cat .pat` auth=`echo "${PAT}:" | tr -d '\n' | base64 --wrap=0` branch=`git rev-parse --abbrev-ref HEAD` -cdiCoreMap='{ "5.2": "feat/oauth-provider-base" }' -cdiPluginInterfaceMap='{ "5.2": "feat/oauth-provider-base" }' -fdiNodeMap='{ "3.1": "21.0", "4.0": "21.0" }' +cdiCoreMap='{ "5.2": "feat/oauth/remaining-changes" }' +cdiPluginInterfaceMap='{ "5.2": "feat/oauth/remaining-changes" }' +fdiNodeMap='{ "3.1": "feat/add_clientId_secret_and_refreshTokenRotation_settings", "4.0": "feat/add_clientId_secret_and_refreshTokenRotation_settings" }' fdiWebsiteMap='{ "1.17": "20.1", "1.18": "20.1", "1.19": "20.1", "2.0": "20.1", "3.0": "20.1", "3.1": "20.1", "4.0": "20.1" }' -fdiAuthReactMap='{ "3.1": "0.49", "4.0": "0.49" }' +fdiAuthReactMap='{ "3.1": "0.48", "4.0": "0.48" }' data=`jq -cn --arg branch "$branch" \ --arg cdiCoreMap "$cdiCoreMap" \ diff --git a/.circleci/setupAndTestBackendSDKWithFreeCore.sh b/.circleci/setupAndTestBackendSDKWithFreeCore.sh index b139658fb..0870c659a 100755 --- a/.circleci/setupAndTestBackendSDKWithFreeCore.sh +++ b/.circleci/setupAndTestBackendSDKWithFreeCore.sh @@ -77,6 +77,7 @@ git checkout $coreTag sed -i 's/# oauth_provider_public_service_url:/oauth_provider_public_service_url: "http:\/\/localhost:4444"/' devConfig.yaml sed -i 's/# oauth_provider_admin_service_url:/oauth_provider_admin_service_url: "http:\/\/localhost:4445"/' devConfig.yaml sed -i 's/# oauth_provider_consent_login_base_url:/oauth_provider_consent_login_base_url: "http:\/\/localhost:3001\/auth"/' devConfig.yaml +sed -i 's/# oauth_client_secret_encryption_key:/oauth_client_secret_encryption_key: "asdfasdfasdfasdfasdf"/' devConfig.yaml cd ../supertokens-plugin-interface git checkout $pluginInterfaceTag diff --git a/.circleci/setupAndTestWithAuthReact.sh b/.circleci/setupAndTestWithAuthReact.sh index 61ae3f2af..2622c5bbd 100755 --- a/.circleci/setupAndTestWithAuthReact.sh +++ b/.circleci/setupAndTestWithAuthReact.sh @@ -73,6 +73,7 @@ git checkout $coreTag sed -i 's/# oauth_provider_public_service_url:/oauth_provider_public_service_url: "http:\/\/localhost:4444"/' devConfig.yaml sed -i 's/# oauth_provider_admin_service_url:/oauth_provider_admin_service_url: "http:\/\/localhost:4445"/' devConfig.yaml sed -i 's/# oauth_provider_consent_login_base_url:/oauth_provider_consent_login_base_url: "http:\/\/localhost:3001\/auth"/' devConfig.yaml +sed -i 's/# oauth_client_secret_encryption_key:/oauth_client_secret_encryption_key: "asdfasdfasdfasdfasdf"/' devConfig.yaml cd ../supertokens-plugin-interface git checkout $pluginInterfaceTag diff --git a/.circleci/setupAndTestWithFreeCore.sh b/.circleci/setupAndTestWithFreeCore.sh index bda9aa416..100d4ae92 100755 --- a/.circleci/setupAndTestWithFreeCore.sh +++ b/.circleci/setupAndTestWithFreeCore.sh @@ -76,6 +76,7 @@ git checkout $coreTag sed -i 's/# oauth_provider_public_service_url:/oauth_provider_public_service_url: "http:\/\/localhost:4444"/' devConfig.yaml sed -i 's/# oauth_provider_admin_service_url:/oauth_provider_admin_service_url: "http:\/\/localhost:4445"/' devConfig.yaml sed -i 's/# oauth_provider_consent_login_base_url:/oauth_provider_consent_login_base_url: "http:\/\/localhost:3001\/auth"/' devConfig.yaml +sed -i 's/# oauth_client_secret_encryption_key:/oauth_client_secret_encryption_key: "asdfasdfasdfasdfasdf"/' devConfig.yaml cd ../supertokens-plugin-interface git checkout $pluginInterfaceTag diff --git a/.circleci/setupAndTestWithFrontend.sh b/.circleci/setupAndTestWithFrontend.sh index 9721c7a6e..937faf787 100755 --- a/.circleci/setupAndTestWithFrontend.sh +++ b/.circleci/setupAndTestWithFrontend.sh @@ -73,6 +73,7 @@ git checkout $coreTag sed -i 's/# oauth_provider_public_service_url:/oauth_provider_public_service_url: "http:\/\/localhost:4444"/' devConfig.yaml sed -i 's/# oauth_provider_admin_service_url:/oauth_provider_admin_service_url: "http:\/\/localhost:4445"/' devConfig.yaml sed -i 's/# oauth_provider_consent_login_base_url:/oauth_provider_consent_login_base_url: "http:\/\/localhost:3001\/auth"/' devConfig.yaml +sed -i 's/# oauth_client_secret_encryption_key:/oauth_client_secret_encryption_key: "asdfasdfasdfasdfasdf"/' devConfig.yaml cd ../supertokens-plugin-interface git checkout $pluginInterfaceTag diff --git a/CHANGELOG.md b/CHANGELOG.md index 6829b8a84..9dc9751d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,16 +28,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - By setting this to true you can enable MFA flows (trying to connect to the session user) - If set to false, the sign-in/up will be considered a first-factor - Changed APIs: + - `EmailPassword.signInPOST` + - `EmailPassword.signUpPOST` - `ThirdParty.signInUpPOST` - `Passwordless.createCodePOST` - `Passwordless.consumeCodePOST` - - `Passwordless.consumeCodePOST` + - `Passwordless.resendCodePOST` - Changed functions: + - `EmailPassword.signIn` + - `EmailPassword.signUp` - `ThirdParty.signInUp` - `ThirdPary.manuallyCreateOrUpdateUser` - `Passwordless.createCode` - `Passwordless.consumeCode` -- We no longer try to load the session if `shouldTryLinkingWithSessionUser` is set to false and overwriteSessionDuringSignInUp is set to true or left as the default value. +- We no longer try to load the session if `shouldTryLinkingWithSessionUser` is set to false. - Changed the return type of `getOpenIdConfiguration` and `getOpenIdDiscoveryConfigurationGET`, and added the following props: - authorization_endpoint - token_endpoint diff --git a/lib/build/recipe/oauth2client/api/implementation.d.ts b/lib/build/recipe/oauth2client/api/implementation.d.ts deleted file mode 100644 index dd40e7025..000000000 --- a/lib/build/recipe/oauth2client/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../"; -export default function getAPIInterface(): APIInterface; diff --git a/lib/build/recipe/oauth2client/api/implementation.js b/lib/build/recipe/oauth2client/api/implementation.js deleted file mode 100644 index 5a5e1d56f..000000000 --- a/lib/build/recipe/oauth2client/api/implementation.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const session_1 = __importDefault(require("../../session")); -function getAPIInterface() { - return { - signInPOST: async function (input) { - const { options, tenantId, userContext, clientId } = input; - let normalisedClientId = clientId; - if (normalisedClientId === undefined) { - if (options.config.providerConfigs.length > 1) { - throw new Error( - "Should never come here: clientId is undefined and there are multiple providerConfigs" - ); - } - normalisedClientId = options.config.providerConfigs[0].clientId; - } - const providerConfig = await options.recipeImplementation.getProviderConfig({ - clientId: normalisedClientId, - userContext, - }); - let oAuthTokensToUse = {}; - if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) { - oAuthTokensToUse = await options.recipeImplementation.exchangeAuthCodeForOAuthTokens({ - providerConfig, - redirectURIInfo: input.redirectURIInfo, - userContext, - }); - } else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) { - oAuthTokensToUse = input.oAuthTokens; - } else { - throw Error("should never come here"); - } - const { userId, rawUserInfo } = await options.recipeImplementation.getUserInfo({ - providerConfig, - oAuthTokens: oAuthTokensToUse, - userContext, - }); - const { user, recipeUserId } = await options.recipeImplementation.signIn({ - userId, - tenantId, - rawUserInfo, - oAuthTokens: oAuthTokensToUse, - userContext, - }); - const session = await session_1.default.createNewSession( - options.req, - options.res, - tenantId, - recipeUserId, - undefined, - undefined, - userContext - ); - return { - status: "OK", - user, - session, - oAuthTokens: oAuthTokensToUse, - rawUserInfo, - }; - }, - }; -} -exports.default = getAPIInterface; diff --git a/lib/build/recipe/oauth2client/api/signin.d.ts b/lib/build/recipe/oauth2client/api/signin.d.ts deleted file mode 100644 index 72cd6e46b..000000000 --- a/lib/build/recipe/oauth2client/api/signin.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from ".."; -import { UserContext } from "../../../types"; -export default function signInAPI( - apiImplementation: APIInterface, - tenantId: string, - options: APIOptions, - userContext: UserContext -): Promise; diff --git a/lib/build/recipe/oauth2client/api/signin.js b/lib/build/recipe/oauth2client/api/signin.js deleted file mode 100644 index 57155dc81..000000000 --- a/lib/build/recipe/oauth2client/api/signin.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../../error")); -const utils_1 = require("../../../utils"); -async function signInAPI(apiImplementation, tenantId, options, userContext) { - if (apiImplementation.signInPOST === undefined) { - return false; - } - const bodyParams = await options.req.getJSONBody(); - let redirectURIInfo; - let oAuthTokens; - if (bodyParams.clientId === undefined && options.config.providerConfigs.length > 1) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the clientId in request body", - }); - } - if (bodyParams.redirectURIInfo !== undefined) { - if (bodyParams.redirectURIInfo.redirectURI === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the redirectURI in request body", - }); - } - redirectURIInfo = bodyParams.redirectURIInfo; - } else if (bodyParams.oAuthTokens !== undefined) { - oAuthTokens = bodyParams.oAuthTokens; - } else { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide one of redirectURIInfo or oAuthTokens in the request body", - }); - } - let result = await apiImplementation.signInPOST({ - tenantId, - clientId: bodyParams.clientId, - redirectURIInfo, - oAuthTokens, - options, - userContext, - }); - if (result.status === "OK") { - utils_1.send200Response( - options.res, - Object.assign( - { status: result.status }, - utils_1.getBackwardsCompatibleUserInfo(options.req, result, userContext) - ) - ); - } else { - utils_1.send200Response(options.res, result); - } - return true; -} -exports.default = signInAPI; diff --git a/lib/build/recipe/oauth2client/constants.d.ts b/lib/build/recipe/oauth2client/constants.d.ts deleted file mode 100644 index 1fb91e760..000000000 --- a/lib/build/recipe/oauth2client/constants.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @ts-nocheck -export declare const SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/build/recipe/oauth2client/constants.js b/lib/build/recipe/oauth2client/constants.js deleted file mode 100644 index 4f42a6cb4..000000000 --- a/lib/build/recipe/oauth2client/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SIGN_IN_API = void 0; -exports.SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/build/recipe/oauth2client/index.d.ts b/lib/build/recipe/oauth2client/index.d.ts deleted file mode 100644 index ff01c3827..000000000 --- a/lib/build/recipe/oauth2client/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; - static exchangeAuthCodeForOAuthTokens( - redirectURIInfo: { - redirectURI: string; - redirectURIQueryParams: any; - pkceCodeVerifier?: string | undefined; - }, - clientId?: string, - userContext?: Record - ): Promise; - static getUserInfo( - oAuthTokens: OAuthTokens, - userContext?: Record - ): Promise; -} -export declare let init: typeof Recipe.init; -export declare let exchangeAuthCodeForOAuthTokens: typeof Wrapper.exchangeAuthCodeForOAuthTokens; -export declare let getUserInfo: typeof Wrapper.getUserInfo; -export type { RecipeInterface, APIInterface, APIOptions }; diff --git a/lib/build/recipe/oauth2client/index.js b/lib/build/recipe/oauth2client/index.js deleted file mode 100644 index e24e2d323..000000000 --- a/lib/build/recipe/oauth2client/index.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getUserInfo = exports.exchangeAuthCodeForOAuthTokens = exports.init = void 0; -const utils_1 = require("../../utils"); -const jwt_1 = require("../session/jwt"); -const recipe_1 = __importDefault(require("./recipe")); -class Wrapper { - static async exchangeAuthCodeForOAuthTokens(redirectURIInfo, clientId, userContext) { - let normalisedClientId = clientId; - const instance = recipe_1.default.getInstanceOrThrowError(); - const recipeInterfaceImpl = instance.recipeInterfaceImpl; - const normalisedUserContext = utils_1.getUserContext(userContext); - if (normalisedClientId === undefined) { - if (instance.config.providerConfigs.length > 1) { - throw new Error("clientId is required if there are more than one provider configs defined"); - } - normalisedClientId = instance.config.providerConfigs[0].clientId; - } - const providerConfig = await recipeInterfaceImpl.getProviderConfig({ - clientId: normalisedClientId, - userContext: normalisedUserContext, - }); - return await recipeInterfaceImpl.exchangeAuthCodeForOAuthTokens({ - providerConfig, - redirectURIInfo, - userContext: normalisedUserContext, - }); - } - static async getUserInfo(oAuthTokens, userContext) { - const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; - const normalisedUserContext = utils_1.getUserContext(userContext); - if (oAuthTokens.access_token === undefined) { - throw new Error("access_token is required to get user info"); - } - const preparseJWTInfo = jwt_1.parseJWTWithoutSignatureVerification(oAuthTokens.access_token); - const providerConfig = await recipeInterfaceImpl.getProviderConfig({ - clientId: preparseJWTInfo.payload.client_id, - userContext: normalisedUserContext, - }); - return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({ - providerConfig, - oAuthTokens, - userContext: normalisedUserContext, - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -exports.init = Wrapper.init; -exports.exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens; -exports.getUserInfo = Wrapper.getUserInfo; diff --git a/lib/build/recipe/oauth2client/recipe.d.ts b/lib/build/recipe/oauth2client/recipe.d.ts deleted file mode 100644 index 180227169..000000000 --- a/lib/build/recipe/oauth2client/recipe.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import STError from "../../error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import type { BaseRequest, BaseResponse } from "../../framework"; -export default class Recipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - _recipes: {} - ); - static init(config: TypeInput): RecipeListFunction; - static getInstanceOrThrowError(): Recipe; - static reset(): void; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - tenantId: string, - req: BaseRequest, - res: BaseResponse, - _path: NormalisedURLPath, - _method: HTTPMethod, - userContext: UserContext - ) => Promise; - handleError: (err: STError, _request: BaseRequest, _response: BaseResponse) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; -} diff --git a/lib/build/recipe/oauth2client/recipe.js b/lib/build/recipe/oauth2client/recipe.js deleted file mode 100644 index daf6b07f3..000000000 --- a/lib/build/recipe/oauth2client/recipe.js +++ /dev/null @@ -1,107 +0,0 @@ -"use strict"; -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const utils_1 = require("./utils"); -const error_1 = __importDefault(require("../../error")); -const constants_1 = require("./constants"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const signin_1 = __importDefault(require("./api/signin")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const querier_1 = require("../../querier"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config, _recipes) { - super(recipeId, appInfo); - this.getAPIsHandled = () => { - return [ - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGN_IN_API), - id: constants_1.SIGN_IN_API, - disabled: this.apiImpl.signInPOST === undefined, - }, - ]; - }; - this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.SIGN_IN_API) { - return await signin_1.default(this.apiImpl, tenantId, options, userContext); - } - return false; - }; - this.handleError = async (err, _request, _response) => { - throw err; - }; - this.getAllCORSHeaders = () => { - return []; - }; - this.isErrorFromThisRecipe = (err) => { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId), this.config) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, {}); - return Recipe.instance; - } else { - throw new Error("OAuth2Client recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the OAuth2Client.init function?"); - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } -} -exports.default = Recipe; -Recipe.instance = undefined; -Recipe.RECIPE_ID = "oauth2client"; diff --git a/lib/build/recipe/oauth2client/recipeImplementation.d.ts b/lib/build/recipe/oauth2client/recipeImplementation.d.ts deleted file mode 100644 index 24599a2c7..000000000 --- a/lib/build/recipe/oauth2client/recipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface, TypeNormalisedInput } from "./types"; -import { Querier } from "../../querier"; -export default function getRecipeImplementation(_querier: Querier, config: TypeNormalisedInput): RecipeInterface; diff --git a/lib/build/recipe/oauth2client/recipeImplementation.js b/lib/build/recipe/oauth2client/recipeImplementation.js deleted file mode 100644 index 351fa3a45..000000000 --- a/lib/build/recipe/oauth2client/recipeImplementation.js +++ /dev/null @@ -1,142 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeUserId_1 = __importDefault(require("../../recipeUserId")); -const thirdpartyUtils_1 = require("../../thirdpartyUtils"); -const __1 = require("../.."); -const logger_1 = require("../../logger"); -const jose_1 = require("jose"); -function getRecipeImplementation(_querier, config) { - let providerConfigsWithOIDCInfo = {}; - return { - signIn: async function ({ userId, tenantId, userContext, oAuthTokens, rawUserInfo }) { - const user = await __1.getUser(userId, userContext); - if (user === undefined) { - throw new Error(`Failed to getUser from the userId ${userId} in the ${tenantId} tenant`); - } - return { - status: "OK", - user, - recipeUserId: new recipeUserId_1.default(userId), - oAuthTokens, - rawUserInfo, - }; - }, - getProviderConfig: async function ({ clientId }) { - if (providerConfigsWithOIDCInfo[clientId] !== undefined) { - return providerConfigsWithOIDCInfo[clientId]; - } - const providerConfig = config.providerConfigs.find( - (providerConfig) => providerConfig.clientId === clientId - ); - const oidcInfo = await thirdpartyUtils_1.getOIDCDiscoveryInfo(providerConfig.oidcDiscoveryEndpoint); - if (oidcInfo.authorization_endpoint === undefined) { - throw new Error("Failed to authorization_endpoint from the oidcDiscoveryEndpoint."); - } - if (oidcInfo.token_endpoint === undefined) { - throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint."); - } - if (oidcInfo.userinfo_endpoint === undefined) { - throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); - } - if (oidcInfo.jwks_uri === undefined) { - throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); - } - providerConfigsWithOIDCInfo[clientId] = Object.assign(Object.assign({}, providerConfig), { - authorizationEndpoint: oidcInfo.authorization_endpoint, - tokenEndpoint: oidcInfo.token_endpoint, - userInfoEndpoint: oidcInfo.userinfo_endpoint, - jwksURI: oidcInfo.jwks_uri, - }); - return providerConfigsWithOIDCInfo[clientId]; - }, - exchangeAuthCodeForOAuthTokens: async function ({ providerConfig, redirectURIInfo }) { - if (providerConfig.tokenEndpoint === undefined) { - throw new Error("OAuth2Client provider's tokenEndpoint is not configured."); - } - const tokenAPIURL = providerConfig.tokenEndpoint; - const accessTokenAPIParams = { - client_id: providerConfig.clientId, - redirect_uri: redirectURIInfo.redirectURI, - code: redirectURIInfo.redirectURIQueryParams["code"], - grant_type: "authorization_code", - }; - if (providerConfig.clientSecret !== undefined) { - accessTokenAPIParams["client_secret"] = providerConfig.clientSecret; - } - if (redirectURIInfo.pkceCodeVerifier !== undefined) { - accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; - } - const tokenResponse = await thirdpartyUtils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); - if (tokenResponse.status >= 400) { - logger_1.logDebugMessage( - `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` - ); - throw new Error( - `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` - ); - } - return tokenResponse.jsonResponse; - }, - getUserInfo: async function ({ providerConfig, oAuthTokens }) { - var _a, _b; - let jwks; - const accessToken = oAuthTokens["access_token"]; - const idToken = oAuthTokens["id_token"]; - let rawUserInfo = { - fromUserInfoAPI: {}, - fromIdTokenPayload: {}, - }; - if (idToken && providerConfig.jwksURI !== undefined) { - if (jwks === undefined) { - jwks = jose_1.createRemoteJWKSet(new URL(providerConfig.jwksURI)); - } - rawUserInfo.fromIdTokenPayload = await thirdpartyUtils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( - idToken, - jwks, - { - audience: providerConfig.clientId, - } - ); - } - if (accessToken && providerConfig.userInfoEndpoint !== undefined) { - const headers = { - Authorization: "Bearer " + accessToken, - }; - const queryParams = {}; - const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( - providerConfig.userInfoEndpoint, - queryParams, - headers - ); - if (userInfoFromAccessToken.status >= 400) { - logger_1.logDebugMessage( - `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` - ); - throw new Error( - `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` - ); - } - rawUserInfo.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; - } - let userId = undefined; - if (((_a = rawUserInfo.fromIdTokenPayload) === null || _a === void 0 ? void 0 : _a.sub) !== undefined) { - userId = rawUserInfo.fromIdTokenPayload["sub"]; - } else if (((_b = rawUserInfo.fromUserInfoAPI) === null || _b === void 0 ? void 0 : _b.sub) !== undefined) { - userId = rawUserInfo.fromUserInfoAPI["sub"]; - } - if (userId === undefined) { - throw new Error(`Failed to get userId from both the idToken and userInfo endpoint.`); - } - return { - userId, - rawUserInfo, - }; - }, - }; -} -exports.default = getRecipeImplementation; diff --git a/lib/build/recipe/oauth2client/types.d.ts b/lib/build/recipe/oauth2client/types.d.ts deleted file mode 100644 index 84fab1847..000000000 --- a/lib/build/recipe/oauth2client/types.d.ts +++ /dev/null @@ -1,155 +0,0 @@ -// @ts-nocheck -import type { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo, UserContext } from "../../types"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { GeneralErrorResponse, User } from "../../types"; -import RecipeUserId from "../../recipeUserId"; -export declare type UserInfo = { - userId: string; - rawUserInfo: { - fromIdTokenPayload?: { - [key: string]: any; - }; - fromUserInfoAPI?: { - [key: string]: any; - }; - }; -}; -export declare type ProviderConfigInput = { - clientId: string; - clientSecret?: string; - oidcDiscoveryEndpoint: string; -}; -export declare type ProviderConfigWithOIDCInfo = ProviderConfigInput & { - authorizationEndpoint: string; - tokenEndpoint: string; - userInfoEndpoint: string; - jwksURI: string; -}; -export declare type OAuthTokens = { - access_token?: string; - id_token?: string; -}; -export declare type OAuthTokenResponse = { - access_token: string; - id_token?: string; - refresh_token?: string; - expires_in: number; - scope?: string; - token_type: string; -}; -export declare type TypeInput = { - providerConfigs: ProviderConfigInput[]; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = { - providerConfigs: ProviderConfigInput[]; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type RecipeInterface = { - getProviderConfig(input: { clientId: string; userContext: UserContext }): Promise; - signIn(input: { - userId: string; - oAuthTokens: OAuthTokens; - rawUserInfo: { - fromIdTokenPayload?: { - [key: string]: any; - }; - fromUserInfoAPI?: { - [key: string]: any; - }; - }; - tenantId: string; - userContext: UserContext; - }): Promise<{ - status: "OK"; - recipeUserId: RecipeUserId; - user: User; - oAuthTokens: OAuthTokens; - rawUserInfo: { - fromIdTokenPayload?: { - [key: string]: any; - }; - fromUserInfoAPI?: { - [key: string]: any; - }; - }; - }>; - exchangeAuthCodeForOAuthTokens(input: { - providerConfig: ProviderConfigWithOIDCInfo; - redirectURIInfo: { - redirectURI: string; - redirectURIQueryParams: any; - pkceCodeVerifier?: string | undefined; - }; - userContext: UserContext; - }): Promise; - getUserInfo(input: { - providerConfig: ProviderConfigWithOIDCInfo; - oAuthTokens: OAuthTokens; - userContext: UserContext; - }): Promise; -}; -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; - appInfo: NormalisedAppinfo; -}; -export declare type APIInterface = { - signInPOST: ( - input: { - tenantId: string; - clientId?: string; - options: APIOptions; - userContext: UserContext; - } & ( - | { - redirectURIInfo: { - redirectURI: string; - redirectURIQueryParams: any; - pkceCodeVerifier?: string; - }; - } - | { - oAuthTokens: { - [key: string]: any; - }; - } - ) - ) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - oAuthTokens: { - [key: string]: any; - }; - rawUserInfo: { - fromIdTokenPayload?: { - [key: string]: any; - }; - fromUserInfoAPI?: { - [key: string]: any; - }; - }; - } - | GeneralErrorResponse - >; -}; diff --git a/lib/build/recipe/oauth2client/types.js b/lib/build/recipe/oauth2client/types.js deleted file mode 100644 index 9f1237319..000000000 --- a/lib/build/recipe/oauth2client/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/oauth2client/utils.d.ts b/lib/build/recipe/oauth2client/utils.d.ts deleted file mode 100644 index 6a930e641..000000000 --- a/lib/build/recipe/oauth2client/utils.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { NormalisedAppinfo } from "../../types"; -import { TypeInput, TypeNormalisedInput } from "./types"; -export declare function validateAndNormaliseUserInput( - _appInfo: NormalisedAppinfo, - config: TypeInput -): TypeNormalisedInput; diff --git a/lib/build/recipe/oauth2client/utils.js b/lib/build/recipe/oauth2client/utils.js deleted file mode 100644 index 25e759254..000000000 --- a/lib/build/recipe/oauth2client/utils.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; -function validateAndNormaliseUserInput(_appInfo, config) { - if (config === undefined || config.providerConfigs === undefined) { - throw new Error("Please pass providerConfigs argument in the OAuth2Client recipe."); - } - if (config.providerConfigs.some((providerConfig) => providerConfig.clientId === undefined)) { - throw new Error("Please pass clientId for all providerConfigs."); - } - if (!config.providerConfigs.every((providerConfig) => providerConfig.clientId.startsWith("stcl_"))) { - throw new Error( - `Only Supertokens OAuth ClientIds are supported in the OAuth2Client recipe. For any other OAuth Clients use the ThirdParty recipe.` - ); - } - if (config.providerConfigs.some((providerConfig) => providerConfig.oidcDiscoveryEndpoint === undefined)) { - throw new Error("Please pass oidcDiscoveryEndpoint for all providerConfigs."); - } - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - return { - providerConfigs: config.providerConfigs, - override, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/oauth2provider/OAuth2Client.d.ts b/lib/build/recipe/oauth2provider/OAuth2Client.d.ts index 7ffba30b1..8d5ab3324 100644 --- a/lib/build/recipe/oauth2provider/OAuth2Client.d.ts +++ b/lib/build/recipe/oauth2provider/OAuth2Client.d.ts @@ -8,8 +8,6 @@ export declare class OAuth2Client { clientId: string; /** * OAuth 2.0 Client Secret - * The secret will be included in the create request as cleartext, and then - * never again. The secret is kept in hashed format and is not recoverable once lost. */ clientSecret?: string; /** @@ -136,9 +134,10 @@ export declare class OAuth2Client { updatedAt: string; /** * Metadata - JSON object - * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. */ metadata: Record; + /** This flag is set to true if refresh tokens are updated upon use */ + enableRefreshTokenRotation: boolean; constructor({ clientId, clientSecret, @@ -166,6 +165,7 @@ export declare class OAuth2Client { createdAt, updatedAt, metadata, + enableRefreshTokenRotation, }: OAuth2ClientOptions); static fromAPIResponse(response: any): OAuth2Client; } diff --git a/lib/build/recipe/oauth2provider/OAuth2Client.js b/lib/build/recipe/oauth2provider/OAuth2Client.js index 4c700f04f..14622abd3 100644 --- a/lib/build/recipe/oauth2provider/OAuth2Client.js +++ b/lib/build/recipe/oauth2provider/OAuth2Client.js @@ -44,10 +44,10 @@ class OAuth2Client { createdAt, updatedAt, metadata = {}, + enableRefreshTokenRotation = false, }) { /** * Metadata - JSON object - * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. */ this.metadata = {}; this.clientId = clientId; @@ -76,6 +76,7 @@ class OAuth2Client { this.createdAt = createdAt; this.updatedAt = updatedAt; this.metadata = metadata; + this.enableRefreshTokenRotation = enableRefreshTokenRotation; } static fromAPIResponse(response) { return new OAuth2Client(utils_1.transformObjectKeys(response, "camelCase")); diff --git a/lib/build/recipe/oauth2provider/api/login.js b/lib/build/recipe/oauth2provider/api/login.js index 186337139..abce54cb7 100644 --- a/lib/build/recipe/oauth2provider/api/login.js +++ b/lib/build/recipe/oauth2provider/api/login.js @@ -81,6 +81,10 @@ async function login(apiImplementation, options, userContext) { frontendRedirectTo: response.frontendRedirectTo, }); } else if ("statusCode" in response) { + // We want to avoid returning a 401 to the frontend, as it may trigger a refresh loop + if (response.statusCode === 401) { + response.statusCode = 400; + } utils_1.sendNon200Response(options.res, (_b = response.statusCode) !== null && _b !== void 0 ? _b : 400, { error: response.error, error_description: response.errorDescription, diff --git a/lib/build/recipe/oauth2provider/api/loginInfo.js b/lib/build/recipe/oauth2provider/api/loginInfo.js index 15b9da808..8e7d26889 100644 --- a/lib/build/recipe/oauth2provider/api/loginInfo.js +++ b/lib/build/recipe/oauth2provider/api/loginInfo.js @@ -22,7 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../../../error")); async function loginInfoGET(apiImplementation, options, userContext) { - var _a; + var _a, _b; if (apiImplementation.loginInfoGET === undefined) { return false; } @@ -41,7 +41,18 @@ async function loginInfoGET(apiImplementation, options, userContext) { loginChallenge, userContext, }); - utils_1.send200Response(options.res, response); + if ("error" in response) { + // We want to avoid returning a 401 to the frontend, as it may trigger a refresh loop + if (response.statusCode === 401) { + response.statusCode = 400; + } + utils_1.sendNon200Response(options.res, (_b = response.statusCode) !== null && _b !== void 0 ? _b : 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + utils_1.send200Response(options.res, response); + } return true; } exports.default = loginInfoGET; diff --git a/lib/build/recipe/oauth2provider/api/logout.js b/lib/build/recipe/oauth2provider/api/logout.js index 16a51b511..7a1d372b3 100644 --- a/lib/build/recipe/oauth2provider/api/logout.js +++ b/lib/build/recipe/oauth2provider/api/logout.js @@ -50,6 +50,10 @@ async function logoutPOST(apiImplementation, options, userContext) { if ("status" in response && response.status === "OK") { utils_1.send200Response(options.res, response); } else if ("statusCode" in response) { + // We want to avoid returning a 401 to the frontend, as it may trigger a refresh loop + if (response.statusCode === 401) { + response.statusCode = 400; + } utils_1.sendNon200Response(options.res, (_a = response.statusCode) !== null && _a !== void 0 ? _a : 400, { error: response.error, error_description: response.errorDescription, diff --git a/lib/build/recipe/oauth2provider/api/revokeToken.js b/lib/build/recipe/oauth2provider/api/revokeToken.js index 7a190ae5d..dd4eb922a 100644 --- a/lib/build/recipe/oauth2provider/api/revokeToken.js +++ b/lib/build/recipe/oauth2provider/api/revokeToken.js @@ -43,6 +43,7 @@ async function revokeTokenPOST(apiImplementation, options, userContext) { userContext, }); if ("statusCode" in response && response.statusCode !== 200) { + // We do not need to normalize as this is not expected to be called by frontends where interception is enabled utils_1.sendNon200Response(options.res, (_a = response.statusCode) !== null && _a !== void 0 ? _a : 400, { error: response.error, error_description: response.errorDescription, diff --git a/lib/build/recipe/oauth2provider/api/token.js b/lib/build/recipe/oauth2provider/api/token.js index 9aaa0bd38..55710e111 100644 --- a/lib/build/recipe/oauth2provider/api/token.js +++ b/lib/build/recipe/oauth2provider/api/token.js @@ -28,6 +28,7 @@ async function tokenPOST(apiImplementation, options, userContext) { userContext, }); if ("error" in response) { + // We do not need to normalize as this is not expected to be called by frontends where interception is enabled utils_1.sendNon200Response(options.res, (_a = response.statusCode) !== null && _a !== void 0 ? _a : 400, { error: response.error, error_description: response.errorDescription, diff --git a/lib/build/recipe/oauth2provider/constants.d.ts b/lib/build/recipe/oauth2provider/constants.d.ts index 56069cece..19ab5d31f 100644 --- a/lib/build/recipe/oauth2provider/constants.d.ts +++ b/lib/build/recipe/oauth2provider/constants.d.ts @@ -1,5 +1,4 @@ // @ts-nocheck -export declare const OAUTH2_BASE_PATH = "/oauth/"; export declare const LOGIN_PATH = "/oauth/login"; export declare const AUTH_PATH = "/oauth/auth"; export declare const TOKEN_PATH = "/oauth/token"; diff --git a/lib/build/recipe/oauth2provider/constants.js b/lib/build/recipe/oauth2provider/constants.js index 13cc9ad7f..260e9eb50 100644 --- a/lib/build/recipe/oauth2provider/constants.js +++ b/lib/build/recipe/oauth2provider/constants.js @@ -14,8 +14,7 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.LOGOUT_PATH = exports.END_SESSION_PATH = exports.INTROSPECT_TOKEN_PATH = exports.REVOKE_TOKEN_PATH = exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.LOGIN_PATH = exports.OAUTH2_BASE_PATH = void 0; -exports.OAUTH2_BASE_PATH = "/oauth/"; +exports.LOGOUT_PATH = exports.END_SESSION_PATH = exports.INTROSPECT_TOKEN_PATH = exports.REVOKE_TOKEN_PATH = exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.LOGIN_PATH = void 0; exports.LOGIN_PATH = "/oauth/login"; exports.AUTH_PATH = "/oauth/auth"; exports.TOKEN_PATH = "/oauth/token"; diff --git a/lib/build/recipe/oauth2provider/recipe.js b/lib/build/recipe/oauth2provider/recipe.js index 4c31ae8cc..9a70bae90 100644 --- a/lib/build/recipe/oauth2provider/recipe.js +++ b/lib/build/recipe/oauth2provider/recipe.js @@ -217,6 +217,22 @@ class Recipe extends recipeModule_1.default { } async getDefaultAccessTokenPayload(user, scopes, sessionHandle, userContext) { let payload = {}; + if (scopes.includes("email")) { + 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]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => + lm.hasSamePhoneNumberAs(user === null || user === void 0 ? void 0 : user.phoneNumbers[0]) && + lm.verified + ); + payload.phoneNumbers = user.phoneNumbers; + } for (const fn of this.accessTokenBuilders) { payload = Object.assign(Object.assign({}, payload), await fn(user, scopes, sessionHandle, userContext)); } diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js index cde274874..d1f440e85 100644 --- a/lib/build/recipe/oauth2provider/recipeImplementation.js +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -63,6 +63,7 @@ const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); const recipe_1 = __importDefault(require("../session/recipe")); const recipe_2 = __importDefault(require("../openid/recipe")); const constants_1 = require("../multitenancy/constants"); +const utils_1 = require("../../utils"); function getUpdatedRedirectTo(appInfo, redirectTo) { return redirectTo.replace( "{apiDomain}", @@ -293,6 +294,7 @@ function getRecipeInterface( new normalisedURLPath_1.default(`/recipe/oauth/auth`), { params: Object.assign(Object.assign({}, input.params), { scope: scopes.join(" ") }), + iss: await recipe_2.default.getIssuer(input.userContext), cookies: input.cookies, session: payloads, }, @@ -359,7 +361,11 @@ function getRecipeInterface( }; } if (input.body.grant_type === "client_credentials") { - if (input.body.client_id === undefined) { + let clientId = + input.authorizationHeader !== undefined + ? utils_1.decodeBase64(input.authorizationHeader.replace(/^Basic /, "").trim()).split(":")[0] + : input.body.client_id; + if (clientId === undefined) { return { status: "ERROR", statusCode: 400, @@ -373,7 +379,7 @@ function getRecipeInterface( ? _b : []; const clientInfo = await this.getOAuth2Client({ - clientId: input.body.client_id, + clientId, userContext: input.userContext, }); if (clientInfo.status === "ERROR") { @@ -459,6 +465,14 @@ function getRecipeInterface( body, input.userContext ); + if (res.status === "CLIENT_NOT_FOUND_ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "client_id not found", + }; + } if (res.status !== "OK") { return { status: "ERROR", diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts index 7d3987138..8c4f49b7e 100644 --- a/lib/build/recipe/oauth2provider/types.d.ts +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -533,6 +533,7 @@ export declare type OAuth2ClientOptions = { policyUri?: string; tosUri?: string; metadata?: Record; + enableRefreshTokenRotation?: boolean; }; export declare type GetOAuth2ClientsInput = { /** @@ -548,9 +549,7 @@ export declare type GetOAuth2ClientsInput = { */ clientName?: string; }; -export declare type CreateOAuth2ClientInput = Partial< - Omit ->; +export declare type CreateOAuth2ClientInput = Partial>; export declare type UpdateOAuth2ClientInput = NonNullableProperties< Omit > & { diff --git a/lib/ts/recipe/oauth2client/api/implementation.ts b/lib/ts/recipe/oauth2client/api/implementation.ts deleted file mode 100644 index 613b81937..000000000 --- a/lib/ts/recipe/oauth2client/api/implementation.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { APIInterface } from "../"; -import Session from "../../session"; -import { OAuthTokens } from "../types"; - -export default function getAPIInterface(): APIInterface { - return { - signInPOST: async function (input) { - const { options, tenantId, userContext, clientId } = input; - - let normalisedClientId = clientId; - if (normalisedClientId === undefined) { - if (options.config.providerConfigs.length > 1) { - throw new Error( - "Should never come here: clientId is undefined and there are multiple providerConfigs" - ); - } - - normalisedClientId = options.config.providerConfigs[0].clientId!; - } - const providerConfig = await options.recipeImplementation.getProviderConfig({ - clientId: normalisedClientId, - userContext, - }); - - let oAuthTokensToUse: OAuthTokens = {}; - - if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) { - oAuthTokensToUse = await options.recipeImplementation.exchangeAuthCodeForOAuthTokens({ - providerConfig, - redirectURIInfo: input.redirectURIInfo, - userContext, - }); - } else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) { - oAuthTokensToUse = input.oAuthTokens; - } else { - throw Error("should never come here"); - } - - const { userId, rawUserInfo } = await options.recipeImplementation.getUserInfo({ - providerConfig, - oAuthTokens: oAuthTokensToUse, - userContext, - }); - - const { user, recipeUserId } = await options.recipeImplementation.signIn({ - userId, - tenantId, - rawUserInfo, - oAuthTokens: oAuthTokensToUse, - userContext, - }); - - const session = await Session.createNewSession( - options.req, - options.res, - tenantId, - recipeUserId, - undefined, - undefined, - userContext - ); - - return { - status: "OK", - user, - session, - oAuthTokens: oAuthTokensToUse, - rawUserInfo, - }; - }, - }; -} diff --git a/lib/ts/recipe/oauth2client/api/signin.ts b/lib/ts/recipe/oauth2client/api/signin.ts deleted file mode 100644 index a4662cf27..000000000 --- a/lib/ts/recipe/oauth2client/api/signin.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../../error"; -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; -import { APIInterface, APIOptions } from ".."; -import { UserContext } from "../../../types"; - -export default async function signInAPI( - apiImplementation: APIInterface, - tenantId: string, - options: APIOptions, - userContext: UserContext -): Promise { - if (apiImplementation.signInPOST === undefined) { - return false; - } - - const bodyParams = await options.req.getJSONBody(); - - let redirectURIInfo: - | undefined - | { - redirectURI: string; - redirectURIQueryParams: any; - pkceCodeVerifier?: string; - }; - let oAuthTokens: any; - - if (bodyParams.clientId === undefined && options.config.providerConfigs.length > 1) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the clientId in request body", - }); - } - - if (bodyParams.redirectURIInfo !== undefined) { - if (bodyParams.redirectURIInfo.redirectURI === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the redirectURI in request body", - }); - } - redirectURIInfo = bodyParams.redirectURIInfo; - } else if (bodyParams.oAuthTokens !== undefined) { - oAuthTokens = bodyParams.oAuthTokens; - } else { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide one of redirectURIInfo or oAuthTokens in the request body", - }); - } - - let result = await apiImplementation.signInPOST({ - tenantId, - clientId: bodyParams.clientId, - redirectURIInfo, - oAuthTokens, - options, - userContext, - }); - - if (result.status === "OK") { - send200Response(options.res, { - status: result.status, - ...getBackwardsCompatibleUserInfo(options.req, result, userContext), - }); - } else { - send200Response(options.res, result); - } - return true; -} diff --git a/lib/ts/recipe/oauth2client/constants.ts b/lib/ts/recipe/oauth2client/constants.ts deleted file mode 100644 index 8e45f0567..000000000 --- a/lib/ts/recipe/oauth2client/constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/ts/recipe/oauth2client/index.ts b/lib/ts/recipe/oauth2client/index.ts deleted file mode 100644 index 9d175ef1c..000000000 --- a/lib/ts/recipe/oauth2client/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { getUserContext } from "../../utils"; -import { parseJWTWithoutSignatureVerification } from "../session/jwt"; -import Recipe from "./recipe"; -import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types"; - -export default class Wrapper { - static init = Recipe.init; - - static async exchangeAuthCodeForOAuthTokens( - redirectURIInfo: { - redirectURI: string; - redirectURIQueryParams: any; - pkceCodeVerifier?: string | undefined; - }, - clientId?: string, - userContext?: Record - ) { - let normalisedClientId = clientId; - const instance = Recipe.getInstanceOrThrowError(); - const recipeInterfaceImpl = instance.recipeInterfaceImpl; - const normalisedUserContext = getUserContext(userContext); - if (normalisedClientId === undefined) { - if (instance.config.providerConfigs.length > 1) { - throw new Error("clientId is required if there are more than one provider configs defined"); - } - - normalisedClientId = instance.config.providerConfigs[0].clientId!; - } - - const providerConfig = await recipeInterfaceImpl.getProviderConfig({ - clientId: normalisedClientId, - userContext: normalisedUserContext, - }); - return await recipeInterfaceImpl.exchangeAuthCodeForOAuthTokens({ - providerConfig, - redirectURIInfo, - userContext: normalisedUserContext, - }); - } - - static async getUserInfo(oAuthTokens: OAuthTokens, userContext?: Record) { - const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; - const normalisedUserContext = getUserContext(userContext); - if (oAuthTokens.access_token === undefined) { - throw new Error("access_token is required to get user info"); - } - const preparseJWTInfo = parseJWTWithoutSignatureVerification(oAuthTokens.access_token!); - const providerConfig = await recipeInterfaceImpl.getProviderConfig({ - clientId: preparseJWTInfo.payload.client_id, - userContext: normalisedUserContext, - }); - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({ - providerConfig, - oAuthTokens, - userContext: normalisedUserContext, - }); - } -} - -export let init = Wrapper.init; - -export let exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens; - -export let getUserInfo = Wrapper.getUserInfo; - -export type { RecipeInterface, APIInterface, APIOptions }; diff --git a/lib/ts/recipe/oauth2client/recipe.ts b/lib/ts/recipe/oauth2client/recipe.ts deleted file mode 100644 index 7da475c5c..000000000 --- a/lib/ts/recipe/oauth2client/recipe.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; -import STError from "../../error"; -import { SIGN_IN_API } from "./constants"; -import NormalisedURLPath from "../../normalisedURLPath"; -import signInAPI from "./api/signin"; -import RecipeImplementation from "./recipeImplementation"; -import APIImplementation from "./api/implementation"; -import { Querier } from "../../querier"; -import type { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; - -export default class Recipe extends RecipeModule { - private static instance: Recipe | undefined = undefined; - static RECIPE_ID = "oauth2client"; - - config: TypeNormalisedInput; - - recipeInterfaceImpl: RecipeInterface; - - apiImpl: APIInterface; - - isInServerlessEnv: boolean; - - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - _recipes: {} - ) { - super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - - { - let builder = new OverrideableBuilder( - RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId), this.config) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - } - - static init(config: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, {}); - - return Recipe.instance; - } else { - throw new Error("OAuth2Client recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the OAuth2Client.init function?"); - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - getAPIsHandled = (): APIHandled[] => { - return [ - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(SIGN_IN_API), - id: SIGN_IN_API, - disabled: this.apiImpl.signInPOST === undefined, - }, - ]; - }; - - handleAPIRequest = async ( - id: string, - tenantId: string, - req: BaseRequest, - res: BaseResponse, - _path: NormalisedURLPath, - _method: HTTPMethod, - userContext: UserContext - ): Promise => { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - appInfo: this.getAppInfo(), - }; - if (id === SIGN_IN_API) { - return await signInAPI(this.apiImpl, tenantId, options, userContext); - } - return false; - }; - - handleError = async (err: STError, _request: BaseRequest, _response: BaseResponse): Promise => { - throw err; - }; - - getAllCORSHeaders = (): string[] => { - return []; - }; - - isErrorFromThisRecipe = (err: any): err is STError => { - return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; -} diff --git a/lib/ts/recipe/oauth2client/recipeImplementation.ts b/lib/ts/recipe/oauth2client/recipeImplementation.ts deleted file mode 100644 index 59e90967b..000000000 --- a/lib/ts/recipe/oauth2client/recipeImplementation.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { - OAuthTokenResponse, - OAuthTokens, - ProviderConfigWithOIDCInfo, - RecipeInterface, - TypeNormalisedInput, - UserInfo, -} from "./types"; -import { Querier } from "../../querier"; -import RecipeUserId from "../../recipeUserId"; -import { User as UserType } from "../../types"; -import { - doGetRequest, - doPostRequest, - getOIDCDiscoveryInfo, - verifyIdTokenFromJWKSEndpointAndGetPayload, -} from "../../thirdpartyUtils"; -import { getUser } from "../.."; -import { logDebugMessage } from "../../logger"; -import { JWTVerifyGetKey, createRemoteJWKSet } from "jose"; - -export default function getRecipeImplementation(_querier: Querier, config: TypeNormalisedInput): RecipeInterface { - let providerConfigsWithOIDCInfo: Record = {}; - - return { - signIn: async function ({ - userId, - tenantId, - userContext, - oAuthTokens, - rawUserInfo, - }): Promise<{ - status: "OK"; - user: UserType; - recipeUserId: RecipeUserId; - oAuthTokens: OAuthTokens; - rawUserInfo: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }> { - const user = await getUser(userId, userContext); - - if (user === undefined) { - throw new Error(`Failed to getUser from the userId ${userId} in the ${tenantId} tenant`); - } - - return { - status: "OK", - user, - recipeUserId: new RecipeUserId(userId), - oAuthTokens, - rawUserInfo, - }; - }, - getProviderConfig: async function ({ clientId }) { - if (providerConfigsWithOIDCInfo[clientId] !== undefined) { - return providerConfigsWithOIDCInfo[clientId]; - } - const providerConfig = config.providerConfigs.find( - (providerConfig) => providerConfig.clientId === clientId - )!; - const oidcInfo = await getOIDCDiscoveryInfo(providerConfig.oidcDiscoveryEndpoint); - - if (oidcInfo.authorization_endpoint === undefined) { - throw new Error("Failed to authorization_endpoint from the oidcDiscoveryEndpoint."); - } - if (oidcInfo.token_endpoint === undefined) { - throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint."); - } - if (oidcInfo.userinfo_endpoint === undefined) { - throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); - } - if (oidcInfo.jwks_uri === undefined) { - throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); - } - - providerConfigsWithOIDCInfo[clientId] = { - ...providerConfig, - authorizationEndpoint: oidcInfo.authorization_endpoint, - tokenEndpoint: oidcInfo.token_endpoint, - userInfoEndpoint: oidcInfo.userinfo_endpoint, - jwksURI: oidcInfo.jwks_uri, - }; - - return providerConfigsWithOIDCInfo[clientId]; - }, - exchangeAuthCodeForOAuthTokens: async function (this: RecipeInterface, { providerConfig, redirectURIInfo }) { - if (providerConfig.tokenEndpoint === undefined) { - throw new Error("OAuth2Client provider's tokenEndpoint is not configured."); - } - const tokenAPIURL = providerConfig.tokenEndpoint; - const accessTokenAPIParams: { [key: string]: string } = { - client_id: providerConfig.clientId, - redirect_uri: redirectURIInfo.redirectURI, - code: redirectURIInfo.redirectURIQueryParams["code"], - grant_type: "authorization_code", - }; - if (providerConfig.clientSecret !== undefined) { - accessTokenAPIParams["client_secret"] = providerConfig.clientSecret; - } - if (redirectURIInfo.pkceCodeVerifier !== undefined) { - accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; - } - - const tokenResponse = await doPostRequest(tokenAPIURL, accessTokenAPIParams); - - if (tokenResponse.status >= 400) { - logDebugMessage( - `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` - ); - throw new Error( - `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` - ); - } - - return tokenResponse.jsonResponse as OAuthTokenResponse; - }, - getUserInfo: async function ({ providerConfig, oAuthTokens }): Promise { - let jwks: JWTVerifyGetKey | undefined; - - const accessToken = oAuthTokens["access_token"]; - const idToken = oAuthTokens["id_token"]; - - let rawUserInfo: { - fromUserInfoAPI: any; - fromIdTokenPayload: any; - } = { - fromUserInfoAPI: {}, - fromIdTokenPayload: {}, - }; - - if (idToken && providerConfig.jwksURI !== undefined) { - if (jwks === undefined) { - jwks = createRemoteJWKSet(new URL(providerConfig.jwksURI)); - } - - rawUserInfo.fromIdTokenPayload = await verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, { - audience: providerConfig.clientId, - }); - } - - if (accessToken && providerConfig.userInfoEndpoint !== undefined) { - const headers: { [key: string]: string } = { - Authorization: "Bearer " + accessToken, - }; - const queryParams: { [key: string]: string } = {}; - - const userInfoFromAccessToken = await doGetRequest( - providerConfig.userInfoEndpoint, - queryParams, - headers - ); - - if (userInfoFromAccessToken.status >= 400) { - logDebugMessage( - `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` - ); - throw new Error( - `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` - ); - } - - rawUserInfo.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; - } - - let userId: string | undefined = undefined; - - if (rawUserInfo.fromIdTokenPayload?.sub !== undefined) { - userId = rawUserInfo.fromIdTokenPayload["sub"]; - } else if (rawUserInfo.fromUserInfoAPI?.sub !== undefined) { - userId = rawUserInfo.fromUserInfoAPI["sub"]; - } - - if (userId === undefined) { - throw new Error(`Failed to get userId from both the idToken and userInfo endpoint.`); - } - - return { - userId, - rawUserInfo, - }; - }, - }; -} diff --git a/lib/ts/recipe/oauth2client/types.ts b/lib/ts/recipe/oauth2client/types.ts deleted file mode 100644 index 795e39086..000000000 --- a/lib/ts/recipe/oauth2client/types.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import type { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo, UserContext } from "../../types"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { GeneralErrorResponse, User } from "../../types"; -import RecipeUserId from "../../recipeUserId"; - -export type UserInfo = { - userId: string; - rawUserInfo: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any } }; -}; - -export type ProviderConfigInput = { - clientId: string; - clientSecret?: string; - oidcDiscoveryEndpoint: string; -}; - -export type ProviderConfigWithOIDCInfo = ProviderConfigInput & { - authorizationEndpoint: string; - tokenEndpoint: string; - userInfoEndpoint: string; - jwksURI: string; -}; - -export type OAuthTokens = { - access_token?: string; - id_token?: string; -}; - -export type OAuthTokenResponse = { - access_token: string; - id_token?: string; - refresh_token?: string; - expires_in: number; - scope?: string; - token_type: string; -}; - -export type TypeInput = { - providerConfigs: ProviderConfigInput[]; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = { - providerConfigs: ProviderConfigInput[]; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type RecipeInterface = { - getProviderConfig(input: { clientId: string; userContext: UserContext }): Promise; - - signIn(input: { - userId: string; - oAuthTokens: OAuthTokens; - rawUserInfo: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - tenantId: string; - userContext: UserContext; - }): Promise<{ - status: "OK"; - recipeUserId: RecipeUserId; - user: User; - oAuthTokens: OAuthTokens; - rawUserInfo: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - }>; - exchangeAuthCodeForOAuthTokens(input: { - providerConfig: ProviderConfigWithOIDCInfo; - redirectURIInfo: { - redirectURI: string; - redirectURIQueryParams: any; - pkceCodeVerifier?: string | undefined; - }; - userContext: UserContext; - }): Promise; - getUserInfo(input: { - providerConfig: ProviderConfigWithOIDCInfo; - oAuthTokens: OAuthTokens; - userContext: UserContext; - }): Promise; -}; - -export type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; - appInfo: NormalisedAppinfo; -}; - -export type APIInterface = { - signInPOST: ( - input: { - tenantId: string; - clientId?: string; - options: APIOptions; - userContext: UserContext; - } & ( - | { - redirectURIInfo: { - redirectURI: string; - redirectURIQueryParams: any; - pkceCodeVerifier?: string; - }; - } - | { - oAuthTokens: { [key: string]: any }; - } - ) - ) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - oAuthTokens: { [key: string]: any }; - rawUserInfo: { - fromIdTokenPayload?: { [key: string]: any }; - fromUserInfoAPI?: { [key: string]: any }; - }; - } - | GeneralErrorResponse - >; -}; diff --git a/lib/ts/recipe/oauth2client/utils.ts b/lib/ts/recipe/oauth2client/utils.ts deleted file mode 100644 index 54c990144..000000000 --- a/lib/ts/recipe/oauth2client/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { NormalisedAppinfo } from "../../types"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; - -export function validateAndNormaliseUserInput(_appInfo: NormalisedAppinfo, config: TypeInput): TypeNormalisedInput { - if (config === undefined || config.providerConfigs === undefined) { - throw new Error("Please pass providerConfigs argument in the OAuth2Client recipe."); - } - - if (config.providerConfigs.some((providerConfig) => providerConfig.clientId === undefined)) { - throw new Error("Please pass clientId for all providerConfigs."); - } - - if (!config.providerConfigs.every((providerConfig) => providerConfig.clientId.startsWith("stcl_"))) { - throw new Error( - `Only Supertokens OAuth ClientIds are supported in the OAuth2Client recipe. For any other OAuth Clients use the ThirdParty recipe.` - ); - } - - if (config.providerConfigs.some((providerConfig) => providerConfig.oidcDiscoveryEndpoint === undefined)) { - throw new Error("Please pass oidcDiscoveryEndpoint for all providerConfigs."); - } - - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - return { - providerConfigs: config.providerConfigs, - override, - }; -} diff --git a/lib/ts/recipe/oauth2provider/OAuth2Client.ts b/lib/ts/recipe/oauth2provider/OAuth2Client.ts index 92546443b..f24de0a4d 100644 --- a/lib/ts/recipe/oauth2provider/OAuth2Client.ts +++ b/lib/ts/recipe/oauth2provider/OAuth2Client.ts @@ -25,8 +25,6 @@ export class OAuth2Client { /** * OAuth 2.0 Client Secret - * The secret will be included in the create request as cleartext, and then - * never again. The secret is kept in hashed format and is not recoverable once lost. */ clientSecret?: string; @@ -177,10 +175,12 @@ export class OAuth2Client { /** * Metadata - JSON object - * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. */ metadata: Record = {}; + /** This flag is set to true if refresh tokens are updated upon use */ + enableRefreshTokenRotation: boolean; + constructor({ clientId, clientSecret, @@ -208,6 +208,7 @@ export class OAuth2Client { createdAt, updatedAt, metadata = {}, + enableRefreshTokenRotation = false, }: OAuth2ClientOptions) { this.clientId = clientId; this.clientSecret = clientSecret; @@ -235,6 +236,7 @@ export class OAuth2Client { this.createdAt = createdAt; this.updatedAt = updatedAt; this.metadata = metadata; + this.enableRefreshTokenRotation = enableRefreshTokenRotation; } static fromAPIResponse(response: any): OAuth2Client { diff --git a/lib/ts/recipe/oauth2provider/api/login.ts b/lib/ts/recipe/oauth2provider/api/login.ts index e399b2877..d20728cf2 100644 --- a/lib/ts/recipe/oauth2provider/api/login.ts +++ b/lib/ts/recipe/oauth2provider/api/login.ts @@ -82,6 +82,11 @@ export default async function login( frontendRedirectTo: response.frontendRedirectTo, }); } else if ("statusCode" in response) { + // We want to avoid returning a 401 to the frontend, as it may trigger a refresh loop + if (response.statusCode === 401) { + response.statusCode = 400; + } + sendNon200Response(options.res, response.statusCode ?? 400, { error: response.error, error_description: response.errorDescription, diff --git a/lib/ts/recipe/oauth2provider/api/loginInfo.ts b/lib/ts/recipe/oauth2provider/api/loginInfo.ts index 2c13ddad0..0ab9ec61c 100644 --- a/lib/ts/recipe/oauth2provider/api/loginInfo.ts +++ b/lib/ts/recipe/oauth2provider/api/loginInfo.ts @@ -13,7 +13,7 @@ * under the License. */ -import { send200Response } from "../../../utils"; +import { send200Response, sendNon200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import { UserContext } from "../../../types"; import SuperTokensError from "../../../error"; @@ -43,6 +43,18 @@ export default async function loginInfoGET( userContext, }); - send200Response(options.res, response); + if ("error" in response) { + // We want to avoid returning a 401 to the frontend, as it may trigger a refresh loop + if (response.statusCode === 401) { + response.statusCode = 400; + } + sendNon200Response(options.res, response.statusCode ?? 400, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + send200Response(options.res, response); + } + return true; } diff --git a/lib/ts/recipe/oauth2provider/api/logout.ts b/lib/ts/recipe/oauth2provider/api/logout.ts index 55c1c83ce..68ea83441 100644 --- a/lib/ts/recipe/oauth2provider/api/logout.ts +++ b/lib/ts/recipe/oauth2provider/api/logout.ts @@ -54,6 +54,11 @@ export async function logoutPOST( if ("status" in response && response.status === "OK") { send200Response(options.res, response); } else if ("statusCode" in response) { + // We want to avoid returning a 401 to the frontend, as it may trigger a refresh loop + if (response.statusCode === 401) { + response.statusCode = 400; + } + sendNon200Response(options.res, response.statusCode ?? 400, { error: response.error, error_description: response.errorDescription, diff --git a/lib/ts/recipe/oauth2provider/api/revokeToken.ts b/lib/ts/recipe/oauth2provider/api/revokeToken.ts index 46bb65692..654fd1765 100644 --- a/lib/ts/recipe/oauth2provider/api/revokeToken.ts +++ b/lib/ts/recipe/oauth2provider/api/revokeToken.ts @@ -54,6 +54,7 @@ export default async function revokeTokenPOST( }); if ("statusCode" in response && response.statusCode !== 200) { + // We do not need to normalize as this is not expected to be called by frontends where interception is enabled sendNon200Response(options.res, response.statusCode ?? 400, { error: response.error, error_description: response.errorDescription, diff --git a/lib/ts/recipe/oauth2provider/api/token.ts b/lib/ts/recipe/oauth2provider/api/token.ts index cd546567c..6b1b0ea2a 100644 --- a/lib/ts/recipe/oauth2provider/api/token.ts +++ b/lib/ts/recipe/oauth2provider/api/token.ts @@ -36,6 +36,7 @@ export default async function tokenPOST( }); if ("error" in response) { + // We do not need to normalize as this is not expected to be called by frontends where interception is enabled sendNon200Response(options.res, response.statusCode ?? 400, { error: response.error, error_description: response.errorDescription, diff --git a/lib/ts/recipe/oauth2provider/constants.ts b/lib/ts/recipe/oauth2provider/constants.ts index f6c696b66..42d56e63a 100644 --- a/lib/ts/recipe/oauth2provider/constants.ts +++ b/lib/ts/recipe/oauth2provider/constants.ts @@ -13,8 +13,6 @@ * under the License. */ -export const OAUTH2_BASE_PATH = "/oauth/"; - export const LOGIN_PATH = "/oauth/login"; export const AUTH_PATH = "/oauth/auth"; export const TOKEN_PATH = "/oauth/token"; diff --git a/lib/ts/recipe/oauth2provider/recipe.ts b/lib/ts/recipe/oauth2provider/recipe.ts index bdd4e0e64..d76d8a7bd 100644 --- a/lib/ts/recipe/oauth2provider/recipe.ts +++ b/lib/ts/recipe/oauth2provider/recipe.ts @@ -267,6 +267,20 @@ export default class Recipe extends RecipeModule { async getDefaultAccessTokenPayload(user: User, scopes: string[], sessionHandle: string, userContext: UserContext) { let payload: JSONObject = {}; + if (scopes.includes("email")) { + 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.accessTokenBuilders) { payload = { ...payload, diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts index 2e7a62e43..1af9d5611 100644 --- a/lib/ts/recipe/oauth2provider/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -30,6 +30,7 @@ import { getCombinedJWKS } from "../../combinedRemoteJWKSet"; import SessionRecipe from "../session/recipe"; import OpenIdRecipe from "../openid/recipe"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; +import { decodeBase64 } from "../../utils"; function getUpdatedRedirectTo(appInfo: NormalisedAppinfo, redirectTo: string) { return redirectTo.replace( @@ -279,6 +280,7 @@ export default function getRecipeInterface( ...input.params, scope: scopes.join(" "), }, + iss: await OpenIdRecipe.getIssuer(input.userContext), cookies: input.cookies, session: payloads, }, @@ -353,7 +355,11 @@ export default function getRecipeInterface( } if (input.body.grant_type === "client_credentials") { - if (input.body.client_id === undefined) { + let clientId = + input.authorizationHeader !== undefined + ? decodeBase64(input.authorizationHeader.replace(/^Basic /, "").trim()).split(":")[0] + : input.body.client_id; + if (clientId === undefined) { return { status: "ERROR", statusCode: 400, @@ -363,8 +369,9 @@ export default function getRecipeInterface( } const scopes = input.body.scope?.split(" ") ?? []; + const clientInfo = await this.getOAuth2Client({ - clientId: input.body.client_id as string, + clientId, userContext: input.userContext, }); @@ -453,6 +460,15 @@ export default function getRecipeInterface( input.userContext ); + if (res.status === "CLIENT_NOT_FOUND_ERROR") { + return { + status: "ERROR", + statusCode: 400, + error: "invalid_request", + errorDescription: "client_id not found", + }; + } + if (res.status !== "OK") { return { status: "ERROR", diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts index c964f7ce0..e73880482 100644 --- a/lib/ts/recipe/oauth2provider/types.ts +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -136,8 +136,7 @@ export type LoginRequest = { export type TokenInfo = { // The access token issued by the authorization server. access_token?: string; - // The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. - // integer + // The lifetime in seconds of the access token (integer). For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. expires_in: number; // To retrieve a refresh token request the id_token scope. id_token?: string; @@ -541,6 +540,7 @@ export type OAuth2ClientOptions = { policyUri?: string; tosUri?: string; metadata?: Record; + enableRefreshTokenRotation?: boolean; }; export type GetOAuth2ClientsInput = { @@ -560,9 +560,7 @@ export type GetOAuth2ClientsInput = { clientName?: string; }; -export type CreateOAuth2ClientInput = Partial< - Omit ->; +export type CreateOAuth2ClientInput = Partial>; export type UpdateOAuth2ClientInput = NonNullableProperties< Omit diff --git a/package.json b/package.json index 335447feb..885295016 100644 --- a/package.json +++ b/package.json @@ -65,15 +65,15 @@ "types": "./nextjs/index.d.ts", "default": "./nextjs/index.js" }, - "./customframework": { + "./custom": { "types": "./custom/index.d.ts", "default": "./custom/index.js" }, - "./customframework/index": { + "./custom/index": { "types": "./custom/index.d.ts", "default": "./custom/index.js" }, - "./customframework/index.js": { + "./custom/index.js": { "types": "./custom/index.d.ts", "default": "./custom/index.js" }, diff --git a/recipe/oauth2client/index.d.ts b/recipe/oauth2client/index.d.ts deleted file mode 100644 index 89f4241f8..000000000 --- a/recipe/oauth2client/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/oauth2client"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/oauth2client"; -export default _default; diff --git a/recipe/oauth2client/index.js b/recipe/oauth2client/index.js deleted file mode 100644 index f1b31d6db..000000000 --- a/recipe/oauth2client/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/oauth2client")); diff --git a/recipe/oauth2client/types/index.d.ts b/recipe/oauth2client/types/index.d.ts deleted file mode 100644 index e475d4576..000000000 --- a/recipe/oauth2client/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/oauth2client/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/oauth2client/types"; -export default _default; diff --git a/recipe/oauth2client/types/index.js b/recipe/oauth2client/types/index.js deleted file mode 100644 index 01b5c40c6..000000000 --- a/recipe/oauth2client/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/oauth2client/types")); diff --git a/test/auth-react-server/index.js b/test/auth-react-server/index.js index 51ad8fa28..ee3692521 100644 --- a/test/auth-react-server/index.js +++ b/test/auth-react-server/index.js @@ -761,7 +761,6 @@ function initST({ passwordlessConfig } = {}) { [ "session", Session.init({ - overwriteSessionDuringSignIn: true, override: { apis: function (originalImplementation) { return { diff --git a/test/test-server/src/index.ts b/test/test-server/src/index.ts index 3aa41da73..b2097452f 100644 --- a/test/test-server/src/index.ts +++ b/test/test-server/src/index.ts @@ -21,8 +21,6 @@ import { TypeInput as MFATypeInput } from "../../../lib/build/recipe/multifactor import TOTPRecipe from "../../../lib/build/recipe/totp/recipe"; import OAuth2ProviderRecipe from "../../../lib/build/recipe/oauth2provider/recipe"; import { TypeInput as OAuth2ProviderTypeInput } from "../../../lib/build/recipe/oauth2provider/types"; -import OAuth2ClientRecipe from "../../../lib/build/recipe/oauth2client/recipe"; -import { TypeInput as OAuth2ClientTypeInput } from "../../../lib/build/recipe/oauth2client/types"; import { TypeInput as OpenIdRecipeTypeInput } from "../../../lib/build/recipe/openid/types"; import UserMetadataRecipe from "../../../lib/build/recipe/usermetadata/recipe"; import SuperTokensRecipe from "../../../lib/build/supertokens"; @@ -38,7 +36,6 @@ import { verifySession } from "../../../recipe/session/framework/express"; import ThirdParty from "../../../recipe/thirdparty"; import TOTP from "../../../recipe/totp"; import OAuth2Provider from "../../../recipe/oauth2provider"; -import OAuth2Client from "../../../recipe/oauth2client"; import accountlinkingRoutes from "./accountlinking"; import emailpasswordRoutes from "./emailpassword"; import emailverificationRoutes from "./emailverification"; @@ -100,7 +97,6 @@ function STReset() { MultiFactorAuthRecipe.reset(); TOTPRecipe.reset(); OAuth2ProviderRecipe.reset(); - OAuth2ClientRecipe.reset(); SuperTokensRecipe.reset(); DashboardRecipe.reset(); } @@ -308,40 +304,18 @@ function initST(config: any) { ); } if (recipe.recipeId === "oauth2provider") { - let initConfig: OAuth2ProviderTypeInput = { - ...config, - }; - if (initConfig.override?.functions) { - initConfig.override = { - ...initConfig.override, - functions: getFunc(`${initConfig.override.functions}`), - }; - } - if (initConfig.override?.apis) { - initConfig.override = { - ...initConfig.override, - apis: getFunc(`${initConfig.override.apis}`), - }; - } - recipeList.push(OAuth2Provider.init(initConfig)); - } - if (recipe.recipeId === "oauth2client") { - let initConfig: OAuth2ClientTypeInput = { - ...config, - }; - if (initConfig.override?.functions) { - initConfig.override = { - ...initConfig.override, - functions: getFunc(`${initConfig.override.functions}`), - }; - } - if (initConfig.override?.apis) { - initConfig.override = { - ...initConfig.override, - apis: getFunc(`${initConfig.override.apis}`), - }; - } - recipeList.push(OAuth2Client.init(initConfig)); + recipeList.push( + OAuth2Provider.init({ + ...config, + override: { + apis: overrideBuilderWithLogging("OAuth2Provider.override.apis", config?.override?.apis), + functions: overrideBuilderWithLogging( + "OAuth2Provider.override.functions", + config?.override?.functions + ), + }, + }) + ); } }); diff --git a/test/test-server/src/oauth2provider.ts b/test/test-server/src/oauth2provider.ts index 1da664e3c..82e273e8d 100644 --- a/test/test-server/src/oauth2provider.ts +++ b/test/test-server/src/oauth2provider.ts @@ -56,6 +56,19 @@ const router = Router() next(e); } }) + .post("/validateoauth2refreshtoken", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:validateOAuth2RefreshToken %j", req.body); + const response = await OAuth2Provider.validateOAuth2RefreshToken( + req.body.token, + req.body.scopes, + req.body.userContext + ); + res.json(response); + } catch (e) { + next(e); + } + }) .post("/createtokenforclientcredentials", async (req, res, next) => { try { logDebugMessage("OAuth2Provider:createTokenForClientCredentials %j", req.body);