diff --git a/lib/build/querier.js b/lib/build/querier.js index 9cc9680bd..ee366ca91 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -124,6 +124,11 @@ class Querier { } else { headers["content-type"] = "application/json; charset=utf-8"; } + // TODO: Remove this after core changes are done + if (body !== undefined && body["authorizationHeader"]) { + headers["authorization"] = body["authorizationHeader"]; + delete body["authorizationHeader"]; + } if (Querier.apiKey !== undefined) { headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); } @@ -603,15 +608,20 @@ async function handleHydraAPICall(response) { return { body: { status: response.ok ? "OK" : "ERROR", + statusCode: response.status, data: await response.clone().json(), }, headers: response.headers, }; } else if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("text/plain")) { return { - body: { status: response.ok ? "OK" : "ERROR", data: await response.clone().text() }, + body: { + status: response.ok ? "OK" : "ERROR", + statusCode: response.status, + data: await response.clone().text(), + }, headers: response.headers, }; } - return { body: { status: response.ok ? "OK" : "ERROR" }, headers: response.headers }; + return { body: { status: response.ok ? "OK" : "ERROR", statusCode: response.status }, headers: response.headers }; } diff --git a/lib/build/recipe/oauth2client/api/authorisationUrl.js b/lib/build/recipe/oauth2client/api/authorisationUrl.js deleted file mode 100644 index e4544b31c..000000000 --- a/lib/build/recipe/oauth2client/api/authorisationUrl.js +++ /dev/null @@ -1,44 +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 utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../../../error")); -async function authorisationUrlAPI(apiImplementation, _tenantId, options, userContext) { - if (apiImplementation.authorisationUrlGET === undefined) { - return false; - } - // TODO: Check if we can rename `redirectURIOnProviderDashboard` to a more suitable name - const redirectURIOnProviderDashboard = options.req.getKeyValueFromQuery("redirectURIOnProviderDashboard"); - if (redirectURIOnProviderDashboard === undefined || typeof redirectURIOnProviderDashboard !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the redirectURIOnProviderDashboard as a GET param", - }); - } - let result = await apiImplementation.authorisationUrlGET({ - redirectURIOnProviderDashboard, - options, - userContext, - }); - utils_1.send200Response(options.res, result); - return true; -} -exports.default = authorisationUrlAPI; diff --git a/lib/build/recipe/oauth2client/api/implementation.js b/lib/build/recipe/oauth2client/api/implementation.js index 5b642629a..159016c57 100644 --- a/lib/build/recipe/oauth2client/api/implementation.js +++ b/lib/build/recipe/oauth2client/api/implementation.js @@ -8,15 +8,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); const session_1 = __importDefault(require("../../session")); function getAPIInterface() { return { - authorisationUrlGET: async function ({ options, redirectURIOnProviderDashboard, userContext }) { - const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext }); - const authUrl = await options.recipeImplementation.getAuthorisationRedirectURL({ - providerConfig, - redirectURIOnProviderDashboard, - userContext, - }); - return Object.assign({ status: "OK" }, authUrl); - }, signInPOST: async function (input) { const { options, tenantId, userContext } = input; const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext }); @@ -32,7 +23,7 @@ function getAPIInterface() { } else { throw Error("should never come here"); } - const { userId, rawUserInfoFromProvider } = await options.recipeImplementation.getUserInfo({ + const { userId, rawUserInfo } = await options.recipeImplementation.getUserInfo({ providerConfig, oAuthTokens: oAuthTokensToUse, userContext, @@ -40,7 +31,7 @@ function getAPIInterface() { const { user, recipeUserId } = await options.recipeImplementation.signIn({ userId, tenantId, - rawUserInfoFromProvider, + rawUserInfo, oAuthTokens: oAuthTokensToUse, userContext, }); @@ -58,7 +49,7 @@ function getAPIInterface() { user, session, oAuthTokens: oAuthTokensToUse, - rawUserInfoFromProvider, + rawUserInfo, }; }, }; diff --git a/lib/build/recipe/oauth2client/api/signin.js b/lib/build/recipe/oauth2client/api/signin.js index 0a0fadc45..52631ac60 100644 --- a/lib/build/recipe/oauth2client/api/signin.js +++ b/lib/build/recipe/oauth2client/api/signin.js @@ -30,10 +30,10 @@ async function signInAPI(apiImplementation, tenantId, options, userContext) { let redirectURIInfo; let oAuthTokens; if (bodyParams.redirectURIInfo !== undefined) { - if (bodyParams.redirectURIInfo.redirectURIOnProviderDashboard === undefined) { + if (bodyParams.redirectURIInfo.redirectURI === undefined) { throw new error_1.default({ type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the redirectURIOnProviderDashboard in request body", + message: "Please provide the redirectURI in request body", }); } redirectURIInfo = bodyParams.redirectURIInfo; @@ -61,7 +61,6 @@ async function signInAPI(apiImplementation, tenantId, options, userContext) { tenantId, redirectURIInfo, oAuthTokens, - session, options, userContext, }); diff --git a/lib/build/recipe/oauth2client/constants.d.ts b/lib/build/recipe/oauth2client/constants.d.ts index fd2bef07d..1fb91e760 100644 --- a/lib/build/recipe/oauth2client/constants.d.ts +++ b/lib/build/recipe/oauth2client/constants.d.ts @@ -1,3 +1,2 @@ // @ts-nocheck -export declare const AUTHORISATION_API = "/oauth2client/authorisationurl"; -export declare const SIGN_IN_API = "/oauth2client/signin"; +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 index e8156ec89..4f42a6cb4 100644 --- a/lib/build/recipe/oauth2client/constants.js +++ b/lib/build/recipe/oauth2client/constants.js @@ -14,6 +14,5 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.SIGN_IN_API = exports.AUTHORISATION_API = void 0; -exports.AUTHORISATION_API = "/oauth2client/authorisationurl"; -exports.SIGN_IN_API = "/oauth2client/signin"; +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 index a16f0bb05..fd09feb41 100644 --- a/lib/build/recipe/oauth2client/index.d.ts +++ b/lib/build/recipe/oauth2client/index.d.ts @@ -3,16 +3,9 @@ import Recipe from "./recipe"; import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types"; export default class Wrapper { static init: typeof Recipe.init; - static getAuthorisationRedirectURL( - redirectURIOnProviderDashboard: string, - userContext?: Record - ): Promise<{ - urlWithQueryParams: string; - pkceCodeVerifier?: string | undefined; - }>; static exchangeAuthCodeForOAuthTokens( redirectURIInfo: { - redirectURIOnProviderDashboard: string; + redirectURI: string; redirectURIQueryParams: any; pkceCodeVerifier?: string | undefined; }, @@ -24,7 +17,6 @@ export default class Wrapper { ): Promise; } export declare let init: typeof Recipe.init; -export declare let getAuthorisationRedirectURL: typeof Wrapper.getAuthorisationRedirectURL; 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 index cdf968aaf..c70a69f41 100644 --- a/lib/build/recipe/oauth2client/index.js +++ b/lib/build/recipe/oauth2client/index.js @@ -19,22 +19,10 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getUserInfo = exports.exchangeAuthCodeForOAuthTokens = exports.getAuthorisationRedirectURL = exports.init = void 0; +exports.getUserInfo = exports.exchangeAuthCodeForOAuthTokens = exports.init = void 0; const utils_1 = require("../../utils"); const recipe_1 = __importDefault(require("./recipe")); class Wrapper { - static async getAuthorisationRedirectURL(redirectURIOnProviderDashboard, userContext) { - const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; - const normalisedUserContext = utils_1.getUserContext(userContext); - const providerConfig = await recipeInterfaceImpl.getProviderConfig({ - userContext: normalisedUserContext, - }); - return await recipeInterfaceImpl.getAuthorisationRedirectURL({ - providerConfig, - redirectURIOnProviderDashboard, - userContext: normalisedUserContext, - }); - } static async exchangeAuthCodeForOAuthTokens(redirectURIInfo, userContext) { const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; const normalisedUserContext = utils_1.getUserContext(userContext); @@ -63,6 +51,5 @@ class Wrapper { exports.default = Wrapper; Wrapper.init = recipe_1.default.init; exports.init = Wrapper.init; -exports.getAuthorisationRedirectURL = Wrapper.getAuthorisationRedirectURL; exports.exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens; exports.getUserInfo = Wrapper.getUserInfo; diff --git a/lib/build/recipe/oauth2client/recipe.js b/lib/build/recipe/oauth2client/recipe.js index d3ad7c29a..daf6b07f3 100644 --- a/lib/build/recipe/oauth2client/recipe.js +++ b/lib/build/recipe/oauth2client/recipe.js @@ -25,7 +25,6 @@ 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 authorisationUrl_1 = __importDefault(require("./api/authorisationUrl")); const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); const implementation_1 = __importDefault(require("./api/implementation")); const querier_1 = require("../../querier"); @@ -41,12 +40,6 @@ class Recipe extends recipeModule_1.default { id: constants_1.SIGN_IN_API, disabled: this.apiImpl.signInPOST === undefined, }, - { - method: "get", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.AUTHORISATION_API), - id: constants_1.AUTHORISATION_API, - disabled: this.apiImpl.authorisationUrlGET === undefined, - }, ]; }; this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { @@ -61,8 +54,6 @@ class Recipe extends recipeModule_1.default { }; if (id === constants_1.SIGN_IN_API) { return await signin_1.default(this.apiImpl, tenantId, options, userContext); - } else if (id === constants_1.AUTHORISATION_API) { - return await authorisationUrl_1.default(this.apiImpl, tenantId, options, userContext); } return false; }; diff --git a/lib/build/recipe/oauth2client/recipeImplementation.js b/lib/build/recipe/oauth2client/recipeImplementation.js index 3ea769a72..e935c9e60 100644 --- a/lib/build/recipe/oauth2client/recipeImplementation.js +++ b/lib/build/recipe/oauth2client/recipeImplementation.js @@ -7,39 +7,13 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const thirdpartyUtils_1 = require("../../thirdpartyUtils"); -const pkce_challenge_1 = __importDefault(require("pkce-challenge")); const __1 = require("../.."); const logger_1 = require("../../logger"); const jose_1 = require("jose"); function getRecipeImplementation(_querier, config) { let providerConfigWithOIDCInfo = null; return { - getAuthorisationRedirectURL: async function ({ providerConfig, redirectURIOnProviderDashboard }) { - const queryParams = { - client_id: providerConfig.clientId, - redirect_uri: redirectURIOnProviderDashboard, - response_type: "code", - }; - if (providerConfig.scope !== undefined) { - queryParams.scope = providerConfig.scope.join(" "); - } - let pkceCodeVerifier = undefined; - if (providerConfig.clientSecret === undefined || providerConfig.forcePKCE) { - const { code_challenge, code_verifier } = pkce_challenge_1.default(64); // According to https://www.rfc-editor.org/rfc/rfc7636, length must be between 43 and 128 - queryParams["code_challenge"] = code_challenge; - queryParams["code_challenge_method"] = "S256"; - pkceCodeVerifier = code_verifier; - } - const urlObj = new URL(providerConfig.authorizationEndpoint); - for (const [key, value] of Object.entries(queryParams)) { - urlObj.searchParams.set(key, value); - } - return { - urlWithQueryParams: urlObj.toString(), - pkceCodeVerifier: pkceCodeVerifier, - }; - }, - signIn: async function ({ userId, tenantId, userContext, oAuthTokens, rawUserInfoFromProvider }) { + 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`); @@ -49,7 +23,7 @@ function getRecipeImplementation(_querier, config) { user, recipeUserId: new recipeUserId_1.default(userId), oAuthTokens, - rawUserInfoFromProvider, + rawUserInfo, }; }, getProviderConfig: async function () { @@ -84,7 +58,7 @@ function getRecipeImplementation(_querier, config) { const tokenAPIURL = providerConfig.tokenEndpoint; const accessTokenAPIParams = { client_id: providerConfig.clientId, - redirect_uri: redirectURIInfo.redirectURIOnProviderDashboard, + redirect_uri: redirectURIInfo.redirectURI, code: redirectURIInfo.redirectURIQueryParams["code"], grant_type: "authorization_code", }; @@ -110,7 +84,7 @@ function getRecipeImplementation(_querier, config) { let jwks; const accessToken = oAuthTokens["access_token"]; const idToken = oAuthTokens["id_token"]; - let rawUserInfoFromProvider = { + let rawUserInfo = { fromUserInfoAPI: {}, fromIdTokenPayload: {}, }; @@ -118,7 +92,7 @@ function getRecipeImplementation(_querier, config) { if (jwks === undefined) { jwks = jose_1.createRemoteJWKSet(new URL(providerConfig.jwksURI)); } - rawUserInfoFromProvider.fromIdTokenPayload = await thirdpartyUtils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( + rawUserInfo.fromIdTokenPayload = await thirdpartyUtils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( idToken, jwks, { @@ -144,26 +118,20 @@ function getRecipeImplementation(_querier, config) { `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` ); } - rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; + rawUserInfo.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; } let userId = undefined; - if ( - ((_a = rawUserInfoFromProvider.fromIdTokenPayload) === null || _a === void 0 ? void 0 : _a.sub) !== - undefined - ) { - userId = rawUserInfoFromProvider.fromIdTokenPayload["sub"]; - } else if ( - ((_b = rawUserInfoFromProvider.fromUserInfoAPI) === null || _b === void 0 ? void 0 : _b.sub) !== - undefined - ) { - userId = rawUserInfoFromProvider.fromUserInfoAPI["sub"]; + 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, - rawUserInfoFromProvider, + rawUserInfo, }; }, }; diff --git a/lib/build/recipe/oauth2client/types.d.ts b/lib/build/recipe/oauth2client/types.d.ts index d00a4a069..c7b43e2f3 100644 --- a/lib/build/recipe/oauth2client/types.d.ts +++ b/lib/build/recipe/oauth2client/types.d.ts @@ -7,7 +7,7 @@ import { GeneralErrorResponse, User } from "../../types"; import RecipeUserId from "../../recipeUserId"; export declare type UserInfo = { userId: string; - rawUserInfoFromProvider: { + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any; }; @@ -19,12 +19,7 @@ export declare type UserInfo = { export declare type ProviderConfigInput = { clientId: string; clientSecret: string; - authorizationEndpointQueryParams?: { - [key: string]: string | null; - }; oidcDiscoveryEndpoint: string; - scope?: string[]; - forcePKCE?: boolean; }; export declare type ProviderConfigWithOIDCInfo = ProviderConfigInput & { authorizationEndpoint: string; @@ -65,19 +60,11 @@ export declare type TypeNormalisedInput = { }; }; export declare type RecipeInterface = { - getAuthorisationRedirectURL(input: { - providerConfig: ProviderConfigWithOIDCInfo; - redirectURIOnProviderDashboard: string; - userContext: UserContext; - }): Promise<{ - urlWithQueryParams: string; - pkceCodeVerifier?: string; - }>; getProviderConfig(input: { userContext: UserContext }): Promise; signIn(input: { userId: string; oAuthTokens: OAuthTokens; - rawUserInfoFromProvider: { + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any; }; @@ -92,7 +79,7 @@ export declare type RecipeInterface = { recipeUserId: RecipeUserId; user: User; oAuthTokens: OAuthTokens; - rawUserInfoFromProvider: { + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any; }; @@ -104,7 +91,7 @@ export declare type RecipeInterface = { exchangeAuthCodeForOAuthTokens(input: { providerConfig: ProviderConfigWithOIDCInfo; redirectURIInfo: { - redirectURIOnProviderDashboard: string; + redirectURI: string; redirectURIQueryParams: any; pkceCodeVerifier?: string | undefined; }; @@ -126,30 +113,15 @@ export declare type APIOptions = { appInfo: NormalisedAppinfo; }; export declare type APIInterface = { - authorisationUrlGET: - | undefined - | ((input: { - redirectURIOnProviderDashboard: string; - options: APIOptions; - userContext: UserContext; - }) => Promise< - | { - status: "OK"; - urlWithQueryParams: string; - pkceCodeVerifier?: string; - } - | GeneralErrorResponse - >); signInPOST: ( input: { tenantId: string; - session: SessionContainerInterface | undefined; options: APIOptions; userContext: UserContext; } & ( | { redirectURIInfo: { - redirectURIOnProviderDashboard: string; + redirectURI: string; redirectURIQueryParams: any; pkceCodeVerifier?: string; }; @@ -168,7 +140,7 @@ export declare type APIInterface = { oAuthTokens: { [key: string]: any; }; - rawUserInfoFromProvider: { + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any; }; diff --git a/lib/build/recipe/oauth2provider/api/implementation.js b/lib/build/recipe/oauth2provider/api/implementation.js index cee232fec..95ae499c4 100644 --- a/lib/build/recipe/oauth2provider/api/implementation.js +++ b/lib/build/recipe/oauth2provider/api/implementation.js @@ -80,6 +80,22 @@ function getAPIImplementation() { userContext, }); }, + revokeTokenPOST: async (input) => { + if ("authorizationHeader" in input) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + authorizationHeader: input.authorizationHeader, + userContext: input.userContext, + }); + } else { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + clientId: input.clientId, + clientSecret: input.clientSecret, + userContext: input.userContext, + }); + } + }, }; } exports.default = getAPIImplementation; diff --git a/lib/build/recipe/oauth2client/api/authorisationUrl.d.ts b/lib/build/recipe/oauth2provider/api/revokeToken.d.ts similarity index 59% rename from lib/build/recipe/oauth2client/api/authorisationUrl.d.ts rename to lib/build/recipe/oauth2provider/api/revokeToken.d.ts index 61b231e1a..902e734d5 100644 --- a/lib/build/recipe/oauth2client/api/authorisationUrl.d.ts +++ b/lib/build/recipe/oauth2provider/api/revokeToken.d.ts @@ -1,9 +1,8 @@ // @ts-nocheck -import { APIInterface, APIOptions } from "../"; +import { APIInterface, APIOptions } from ".."; import { UserContext } from "../../../types"; -export default function authorisationUrlAPI( +export default function revokeTokenPOST( apiImplementation: APIInterface, - _tenantId: string, options: APIOptions, userContext: UserContext ): Promise; diff --git a/lib/build/recipe/oauth2provider/api/revokeToken.js b/lib/build/recipe/oauth2provider/api/revokeToken.js new file mode 100644 index 000000000..c36975c41 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/revokeToken.js @@ -0,0 +1,51 @@ +"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 }); +const utils_1 = require("../../../utils"); +async function revokeTokenPOST(apiImplementation, options, userContext) { + if (apiImplementation.revokeTokenPOST === undefined) { + return false; + } + const body = await options.req.getFormData(); + if (body.token === undefined) { + utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + const authorizationHeader = options.req.getHeaderValue("authorization"); + if (authorizationHeader !== undefined && (body.client_id !== undefined || body.client_secret !== undefined)) { + utils_1.sendNon200ResponseWithMessage( + options.res, + "Only one of authorization header or client_id and client_secret can be provided", + 400 + ); + return true; + } + let response = await apiImplementation.revokeTokenPOST({ + options, + authorizationHeader, + token: body.token, + clientId: body.client_id, + clientSecret: body.client_secret, + userContext, + }); + if ("statusCode" in response && response.statusCode !== 200) { + utils_1.sendNon200Response(options.res, response.statusCode, response); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.default = revokeTokenPOST; diff --git a/lib/build/recipe/oauth2provider/api/userInfo.js b/lib/build/recipe/oauth2provider/api/userInfo.js index ff38a82b3..cfa8f704b 100644 --- a/lib/build/recipe/oauth2provider/api/userInfo.js +++ b/lib/build/recipe/oauth2provider/api/userInfo.js @@ -26,7 +26,7 @@ async function userInfoGET(apiImplementation, tenantId, options, userContext) { if (apiImplementation.userInfoGET === undefined) { return false; } - const authHeader = options.req.getHeaderValue("authorization") || options.req.getHeaderValue("Authorization"); + const authHeader = options.req.getHeaderValue("authorization"); if (authHeader === undefined || !authHeader.startsWith("Bearer ")) { utils_1.sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401); return true; diff --git a/lib/build/recipe/oauth2provider/constants.d.ts b/lib/build/recipe/oauth2provider/constants.d.ts index c609db866..0ddf7e65f 100644 --- a/lib/build/recipe/oauth2provider/constants.d.ts +++ b/lib/build/recipe/oauth2provider/constants.d.ts @@ -5,3 +5,4 @@ export declare const AUTH_PATH = "/oauth/auth"; export declare const TOKEN_PATH = "/oauth/token"; export declare const LOGIN_INFO_PATH = "/oauth/login/info"; export declare const USER_INFO_PATH = "/oauth/userinfo"; +export declare const REVOKE_TOKEN_PATH = "/oauth/revoke"; diff --git a/lib/build/recipe/oauth2provider/constants.js b/lib/build/recipe/oauth2provider/constants.js index 66872296b..be31a261b 100644 --- a/lib/build/recipe/oauth2provider/constants.js +++ b/lib/build/recipe/oauth2provider/constants.js @@ -14,10 +14,11 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.LOGIN_PATH = exports.OAUTH2_BASE_PATH = void 0; +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.LOGIN_PATH = "/oauth/login"; exports.AUTH_PATH = "/oauth/auth"; exports.TOKEN_PATH = "/oauth/token"; exports.LOGIN_INFO_PATH = "/oauth/login/info"; exports.USER_INFO_PATH = "/oauth/userinfo"; +exports.REVOKE_TOKEN_PATH = "/oauth/revoke"; diff --git a/lib/build/recipe/oauth2provider/index.d.ts b/lib/build/recipe/oauth2provider/index.d.ts index f749615ec..8e967f21f 100644 --- a/lib/build/recipe/oauth2provider/index.d.ts +++ b/lib/build/recipe/oauth2provider/index.d.ts @@ -11,6 +11,20 @@ import { } from "./types"; export default class Wrapper { static init: typeof Recipe.init; + static getOAuth2Client( + clientId: string, + userContext?: Record + ): Promise< + | { + status: "OK"; + client: import("./OAuth2Client").OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; static getOAuth2Clients( input: GetOAuth2ClientsInput, userContext?: Record @@ -87,6 +101,17 @@ export default class Wrapper { audience?: string, userContext?: Record ): Promise; + static revokeToken( + token: string, + clientId: string, + clientSecret?: string, + userContext?: Record + ): Promise< + | import("./types").ErrorOAuth2 + | { + status: "OK"; + } + >; } export declare let init: typeof Recipe.init; export declare let getOAuth2Clients: typeof Wrapper.getOAuth2Clients; @@ -95,4 +120,5 @@ export declare let updateOAuth2Client: typeof Wrapper.updateOAuth2Client; export declare let deleteOAuth2Client: typeof Wrapper.deleteOAuth2Client; export declare let validateOAuth2AccessToken: typeof Wrapper.validateOAuth2AccessToken; export declare let createTokenForClientCredentials: typeof Wrapper.createTokenForClientCredentials; +export declare let revokeToken: typeof Wrapper.revokeToken; export type { APIInterface, APIOptions, RecipeInterface }; diff --git a/lib/build/recipe/oauth2provider/index.js b/lib/build/recipe/oauth2provider/index.js index f06c7ad6c..1166425d1 100644 --- a/lib/build/recipe/oauth2provider/index.js +++ b/lib/build/recipe/oauth2provider/index.js @@ -19,10 +19,15 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.createTokenForClientCredentials = exports.validateOAuth2AccessToken = exports.deleteOAuth2Client = exports.updateOAuth2Client = exports.createOAuth2Client = exports.getOAuth2Clients = exports.init = void 0; +exports.revokeToken = exports.createTokenForClientCredentials = exports.validateOAuth2AccessToken = exports.deleteOAuth2Client = exports.updateOAuth2Client = exports.createOAuth2Client = exports.getOAuth2Clients = exports.init = void 0; const utils_1 = require("../../utils"); const recipe_1 = __importDefault(require("./recipe")); class Wrapper { + static async getOAuth2Client(clientId, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.getOAuth2Client({ clientId }, utils_1.getUserContext(userContext)); + } static async getOAuth2Clients(input, userContext) { return await recipe_1.default .getInstanceOrThrowError() @@ -64,6 +69,34 @@ class Wrapper { userContext: utils_1.getUserContext(userContext), }); } + static async revokeToken(token, clientId, clientSecret, userContext) { + let authorizationHeader = undefined; + const normalisedUserContext = utils_1.getUserContext(userContext); + const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + const res = await recipeInterfaceImpl.getOAuth2Client({ clientId }, normalisedUserContext); + if (res.status !== "OK") { + throw new Error(`Failed to get OAuth2 client with id ${clientId}: ${res.error}`); + } + const { tokenEndpointAuthMethod } = res.client; + if (tokenEndpointAuthMethod === "none") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":").toString("base64"); + } else if (tokenEndpointAuthMethod === "client_secret_basic") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"); + } + if (authorizationHeader !== undefined) { + return await recipeInterfaceImpl.revokeToken({ + token, + authorizationHeader, + userContext: normalisedUserContext, + }); + } + return await recipeInterfaceImpl.revokeToken({ + token, + clientId, + clientSecret, + userContext: normalisedUserContext, + }); + } } exports.default = Wrapper; Wrapper.init = recipe_1.default.init; @@ -74,3 +107,4 @@ exports.updateOAuth2Client = Wrapper.updateOAuth2Client; exports.deleteOAuth2Client = Wrapper.deleteOAuth2Client; exports.validateOAuth2AccessToken = Wrapper.validateOAuth2AccessToken; exports.createTokenForClientCredentials = Wrapper.createTokenForClientCredentials; +exports.revokeToken = Wrapper.revokeToken; diff --git a/lib/build/recipe/oauth2provider/recipe.js b/lib/build/recipe/oauth2provider/recipe.js index 95a3de853..d523000c0 100644 --- a/lib/build/recipe/oauth2provider/recipe.js +++ b/lib/build/recipe/oauth2provider/recipe.js @@ -34,6 +34,7 @@ const utils_1 = require("./utils"); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const userInfo_1 = __importDefault(require("./api/userInfo")); const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); +const revokeToken_1 = __importDefault(require("./api/revokeToken")); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); @@ -66,6 +67,9 @@ class Recipe extends recipeModule_1.default { if (id === constants_1.USER_INFO_PATH) { return userInfo_1.default(this.apiImpl, tenantId, options, userContext); } + if (id === constants_1.REVOKE_TOKEN_PATH) { + return revokeToken_1.default(this.apiImpl, options, userContext); + } throw new Error("Should never come here: handleAPIRequest called with unknown id"); }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); @@ -147,6 +151,12 @@ class Recipe extends recipeModule_1.default { id: constants_1.USER_INFO_PATH, disabled: this.apiImpl.userInfoGET === undefined, }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.REVOKE_TOKEN_PATH), + id: constants_1.REVOKE_TOKEN_PATH, + disabled: this.apiImpl.revokeTokenPOST === undefined, + }, ]; } handleError(error, _, __, _userContext) { diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js index f436eaf2c..7debcd947 100644 --- a/lib/build/recipe/oauth2provider/recipeImplementation.js +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -278,6 +278,13 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, body, input.userContext ); + if (res.status !== "OK") { + return { + statusCode: res.statusCode, + error: res.data.error, + errorDescription: res.data.error_description, + }; + } return res.data; }, getOAuth2Clients: async function (input, userContext) { @@ -316,6 +323,26 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, }; } }, + getOAuth2Client: async function (input, userContext) { + let response = await querier.sendGetRequestWithResponseHeaders( + new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients/${input.clientId}`), + {}, + {}, + userContext + ); + if (response.body.status === "OK") { + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.body.data), + }; + } else { + return { + status: "ERROR", + error: response.body.data.error, + errorHint: response.body.data.errorHint, + }; + } + }, createOAuth2Client: async function (input, userContext) { let response = await querier.sendPostRequest( new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients`), @@ -460,6 +487,35 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload, } return { status: "OK", payload: payload }; }, + revokeToken: async function (input) { + const requestBody = { + $isFormData: true, + token: input.token, + }; + if ("authorizationHeader" in input) { + requestBody.authorizationHeader = input.authorizationHeader; + } else { + if ("clientId" in input) { + requestBody.client_id = input.clientId; + } + if ("clientSecret" in input) { + requestBody.client_secret = input.clientSecret; + } + } + const res = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth2/pub/revoke`), + requestBody, + input.userContext + ); + if (res.status !== "OK") { + return { + statusCode: res.statusCode, + error: res.data.error, + errorDescription: res.data.error_description, + }; + } + return { status: "OK" }; + }, }; } exports.default = getRecipeInterface; diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts index 514d8bbc9..ca4887e39 100644 --- a/lib/build/recipe/oauth2provider/types.d.ts +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -143,6 +143,20 @@ export declare type RecipeInterface = { }): Promise<{ redirectTo: string; }>; + getOAuth2Client( + input: Pick, + userContext: UserContext + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; getOAuth2Clients( input: GetOAuth2ClientsInput, userContext: UserContext @@ -233,6 +247,25 @@ export declare type RecipeInterface = { tenantId: string; userContext: UserContext; }): Promise; + revokeToken( + input: { + token: string; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { + clientId: string; + clientSecret?: string; + } + ) + ): Promise< + | { + status: "OK"; + } + | ErrorOAuth2 + >; }; export declare type APIInterface = { loginGET: @@ -295,6 +328,28 @@ export declare type APIInterface = { options: APIOptions; userContext: UserContext; }) => Promise); + revokeTokenPOST: + | undefined + | (( + input: { + token: string; + options: APIOptions; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { + clientId: string; + clientSecret?: string; + } + ) + ) => Promise< + | { + status: "OK"; + } + | ErrorOAuth2 + >); }; export declare type OAuth2ClientOptions = { clientId: string; diff --git a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js index 6cd6f8ec5..227295247 100644 --- a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js +++ b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js @@ -17,6 +17,7 @@ async function getOpenIdDiscoveryConfiguration(apiImplementation, options, userC authorization_endpoint: result.authorization_endpoint, token_endpoint: result.token_endpoint, userinfo_endpoint: result.userinfo_endpoint, + revocation_endpoint: result.revocation_endpoint, subject_types_supported: result.subject_types_supported, id_token_signing_alg_values_supported: result.id_token_signing_alg_values_supported, response_types_supported: result.response_types_supported, diff --git a/lib/build/recipe/openid/index.d.ts b/lib/build/recipe/openid/index.d.ts index 6c78c266e..4b3c30cea 100644 --- a/lib/build/recipe/openid/index.d.ts +++ b/lib/build/recipe/openid/index.d.ts @@ -11,6 +11,7 @@ export default class OpenIdRecipeWrapper { authorization_endpoint: string; token_endpoint: string; userinfo_endpoint: string; + revocation_endpoint: string; subject_types_supported: string[]; id_token_signing_alg_values_supported: string[]; response_types_supported: string[]; diff --git a/lib/build/recipe/openid/recipeImplementation.js b/lib/build/recipe/openid/recipeImplementation.js index 61726ce5d..70de44644 100644 --- a/lib/build/recipe/openid/recipeImplementation.js +++ b/lib/build/recipe/openid/recipeImplementation.js @@ -25,6 +25,7 @@ function getRecipeInterface(config, jwtRecipeImplementation, appInfo) { authorization_endpoint: apiBasePath + constants_2.AUTH_PATH, token_endpoint: apiBasePath + constants_2.TOKEN_PATH, userinfo_endpoint: apiBasePath + constants_2.USER_INFO_PATH, + revocation_endpoint: apiBasePath + constants_2.REVOKE_TOKEN_PATH, subject_types_supported: ["public"], id_token_signing_alg_values_supported: ["RS256"], response_types_supported: ["code", "id_token", "id_token token"], diff --git a/lib/build/recipe/openid/types.d.ts b/lib/build/recipe/openid/types.d.ts index 246c57ac5..24dc22887 100644 --- a/lib/build/recipe/openid/types.d.ts +++ b/lib/build/recipe/openid/types.d.ts @@ -69,6 +69,7 @@ export declare type APIInterface = { authorization_endpoint: string; token_endpoint: string; userinfo_endpoint: string; + revocation_endpoint: string; subject_types_supported: string[]; id_token_signing_alg_values_supported: string[]; response_types_supported: string[]; @@ -86,6 +87,7 @@ export declare type RecipeInterface = { authorization_endpoint: string; token_endpoint: string; userinfo_endpoint: string; + revocation_endpoint: string; subject_types_supported: string[]; id_token_signing_alg_values_supported: string[]; response_types_supported: string[]; diff --git a/lib/build/recipe/session/index.d.ts b/lib/build/recipe/session/index.d.ts index 73a99d24e..7c397dc28 100644 --- a/lib/build/recipe/session/index.d.ts +++ b/lib/build/recipe/session/index.d.ts @@ -180,6 +180,7 @@ export default class SessionWrapper { authorization_endpoint: string; token_endpoint: string; userinfo_endpoint: string; + revocation_endpoint: string; subject_types_supported: string[]; id_token_signing_alg_values_supported: string[]; response_types_supported: string[]; diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index 666eaf6fc..895239ca9 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -179,6 +179,13 @@ export class Querier { } else { headers["content-type"] = "application/json; charset=utf-8"; } + + // TODO: Remove this after core changes are done + if (body !== undefined && body["authorizationHeader"]) { + headers["authorization"] = body["authorizationHeader"]; + delete body["authorizationHeader"]; + } + if (Querier.apiKey !== undefined) { headers = { ...headers, @@ -704,16 +711,21 @@ async function handleHydraAPICall(response: Response) { return { body: { status: response.ok ? "OK" : "ERROR", + statusCode: response.status, data: await response.clone().json(), }, headers: response.headers, }; } else if (contentType?.startsWith("text/plain")) { return { - body: { status: response.ok ? "OK" : "ERROR", data: await response.clone().text() }, + body: { + status: response.ok ? "OK" : "ERROR", + statusCode: response.status, + data: await response.clone().text(), + }, headers: response.headers, }; } - return { body: { status: response.ok ? "OK" : "ERROR" }, headers: response.headers }; + return { body: { status: response.ok ? "OK" : "ERROR", statusCode: response.status }, headers: response.headers }; } diff --git a/lib/ts/recipe/oauth2client/api/authorisationUrl.ts b/lib/ts/recipe/oauth2client/api/authorisationUrl.ts deleted file mode 100644 index 80d43138c..000000000 --- a/lib/ts/recipe/oauth2client/api/authorisationUrl.ts +++ /dev/null @@ -1,49 +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 { send200Response } from "../../../utils"; -import STError from "../../../error"; -import { APIInterface, APIOptions } from "../"; -import { UserContext } from "../../../types"; - -export default async function authorisationUrlAPI( - apiImplementation: APIInterface, - _tenantId: string, - options: APIOptions, - userContext: UserContext -): Promise { - if (apiImplementation.authorisationUrlGET === undefined) { - return false; - } - - // TODO: Check if we can rename `redirectURIOnProviderDashboard` to a more suitable name - const redirectURIOnProviderDashboard = options.req.getKeyValueFromQuery("redirectURIOnProviderDashboard"); - - if (redirectURIOnProviderDashboard === undefined || typeof redirectURIOnProviderDashboard !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the redirectURIOnProviderDashboard as a GET param", - }); - } - - let result = await apiImplementation.authorisationUrlGET({ - redirectURIOnProviderDashboard, - options, - userContext, - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/oauth2client/api/implementation.ts b/lib/ts/recipe/oauth2client/api/implementation.ts index a6f14e0cb..7e18629e0 100644 --- a/lib/ts/recipe/oauth2client/api/implementation.ts +++ b/lib/ts/recipe/oauth2client/api/implementation.ts @@ -4,19 +4,6 @@ import { OAuthTokens } from "../types"; export default function getAPIInterface(): APIInterface { return { - authorisationUrlGET: async function ({ options, redirectURIOnProviderDashboard, userContext }) { - const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext }); - - const authUrl = await options.recipeImplementation.getAuthorisationRedirectURL({ - providerConfig, - redirectURIOnProviderDashboard, - userContext, - }); - return { - status: "OK", - ...authUrl, - }; - }, signInPOST: async function (input) { const { options, tenantId, userContext } = input; @@ -36,7 +23,7 @@ export default function getAPIInterface(): APIInterface { throw Error("should never come here"); } - const { userId, rawUserInfoFromProvider } = await options.recipeImplementation.getUserInfo({ + const { userId, rawUserInfo } = await options.recipeImplementation.getUserInfo({ providerConfig, oAuthTokens: oAuthTokensToUse, userContext, @@ -45,7 +32,7 @@ export default function getAPIInterface(): APIInterface { const { user, recipeUserId } = await options.recipeImplementation.signIn({ userId, tenantId, - rawUserInfoFromProvider, + rawUserInfo, oAuthTokens: oAuthTokensToUse, userContext, }); @@ -65,7 +52,7 @@ export default function getAPIInterface(): APIInterface { user, session, oAuthTokens: oAuthTokensToUse, - rawUserInfoFromProvider, + rawUserInfo, }; }, }; diff --git a/lib/ts/recipe/oauth2client/api/signin.ts b/lib/ts/recipe/oauth2client/api/signin.ts index 663af60e4..6e89436f9 100644 --- a/lib/ts/recipe/oauth2client/api/signin.ts +++ b/lib/ts/recipe/oauth2client/api/signin.ts @@ -34,17 +34,17 @@ export default async function signInAPI( let redirectURIInfo: | undefined | { - redirectURIOnProviderDashboard: string; + redirectURI: string; redirectURIQueryParams: any; pkceCodeVerifier?: string; }; let oAuthTokens: any; if (bodyParams.redirectURIInfo !== undefined) { - if (bodyParams.redirectURIInfo.redirectURIOnProviderDashboard === undefined) { + if (bodyParams.redirectURIInfo.redirectURI === undefined) { throw new STError({ type: STError.BAD_INPUT_ERROR, - message: "Please provide the redirectURIOnProviderDashboard in request body", + message: "Please provide the redirectURI in request body", }); } redirectURIInfo = bodyParams.redirectURIInfo; @@ -75,7 +75,6 @@ export default async function signInAPI( tenantId, redirectURIInfo, oAuthTokens, - session, options, userContext, }); diff --git a/lib/ts/recipe/oauth2client/constants.ts b/lib/ts/recipe/oauth2client/constants.ts index 545ef08f1..8e45f0567 100644 --- a/lib/ts/recipe/oauth2client/constants.ts +++ b/lib/ts/recipe/oauth2client/constants.ts @@ -13,6 +13,4 @@ * under the License. */ -export const AUTHORISATION_API = "/oauth2client/authorisationurl"; - -export const SIGN_IN_API = "/oauth2client/signin"; +export const SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/ts/recipe/oauth2client/index.ts b/lib/ts/recipe/oauth2client/index.ts index 0ae16a19d..d2b2e2a02 100644 --- a/lib/ts/recipe/oauth2client/index.ts +++ b/lib/ts/recipe/oauth2client/index.ts @@ -20,25 +20,9 @@ import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types" export default class Wrapper { static init = Recipe.init; - static async getAuthorisationRedirectURL( - redirectURIOnProviderDashboard: string, - userContext?: Record - ) { - const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; - const normalisedUserContext = getUserContext(userContext); - const providerConfig = await recipeInterfaceImpl.getProviderConfig({ - userContext: normalisedUserContext, - }); - return await recipeInterfaceImpl.getAuthorisationRedirectURL({ - providerConfig, - redirectURIOnProviderDashboard, - userContext: normalisedUserContext, - }); - } - static async exchangeAuthCodeForOAuthTokens( redirectURIInfo: { - redirectURIOnProviderDashboard: string; + redirectURI: string; redirectURIQueryParams: any; pkceCodeVerifier?: string | undefined; }, @@ -72,8 +56,6 @@ export default class Wrapper { export let init = Wrapper.init; -export let getAuthorisationRedirectURL = Wrapper.getAuthorisationRedirectURL; - export let exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens; export let getUserInfo = Wrapper.getUserInfo; diff --git a/lib/ts/recipe/oauth2client/recipe.ts b/lib/ts/recipe/oauth2client/recipe.ts index 9dfc98a90..7da475c5c 100644 --- a/lib/ts/recipe/oauth2client/recipe.ts +++ b/lib/ts/recipe/oauth2client/recipe.ts @@ -18,10 +18,9 @@ import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserCont import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; import { validateAndNormaliseUserInput } from "./utils"; import STError from "../../error"; -import { SIGN_IN_API, AUTHORISATION_API } from "./constants"; +import { SIGN_IN_API } from "./constants"; import NormalisedURLPath from "../../normalisedURLPath"; import signInAPI from "./api/signin"; -import authorisationUrlAPI from "./api/authorisationUrl"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; import { Querier } from "../../querier"; @@ -97,12 +96,6 @@ export default class Recipe extends RecipeModule { id: SIGN_IN_API, disabled: this.apiImpl.signInPOST === undefined, }, - { - method: "get", - pathWithoutApiBasePath: new NormalisedURLPath(AUTHORISATION_API), - id: AUTHORISATION_API, - disabled: this.apiImpl.authorisationUrlGET === undefined, - }, ]; }; @@ -126,8 +119,6 @@ export default class Recipe extends RecipeModule { }; if (id === SIGN_IN_API) { return await signInAPI(this.apiImpl, tenantId, options, userContext); - } else if (id === AUTHORISATION_API) { - return await authorisationUrlAPI(this.apiImpl, tenantId, options, userContext); } return false; }; diff --git a/lib/ts/recipe/oauth2client/recipeImplementation.ts b/lib/ts/recipe/oauth2client/recipeImplementation.ts index 11b277965..a5115c815 100644 --- a/lib/ts/recipe/oauth2client/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2client/recipeImplementation.ts @@ -15,7 +15,6 @@ import { getOIDCDiscoveryInfo, verifyIdTokenFromJWKSEndpointAndGetPayload, } from "../../thirdpartyUtils"; -import pkceChallenge from "pkce-challenge"; import { getUser } from "../.."; import { logDebugMessage } from "../../logger"; import { JWTVerifyGetKey, createRemoteJWKSet } from "jose"; @@ -24,52 +23,18 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN let providerConfigWithOIDCInfo: ProviderConfigWithOIDCInfo | null = null; return { - getAuthorisationRedirectURL: async function ( - this: RecipeInterface, - { providerConfig, redirectURIOnProviderDashboard } - ) { - const queryParams: { [key: string]: string } = { - client_id: providerConfig.clientId, - redirect_uri: redirectURIOnProviderDashboard, - response_type: "code", - }; - - if (providerConfig.scope !== undefined) { - queryParams.scope = providerConfig.scope.join(" "); - } - - let pkceCodeVerifier: string | undefined = undefined; - - if (providerConfig.clientSecret === undefined || providerConfig.forcePKCE) { - const { code_challenge, code_verifier } = pkceChallenge(64); // According to https://www.rfc-editor.org/rfc/rfc7636, length must be between 43 and 128 - queryParams["code_challenge"] = code_challenge; - queryParams["code_challenge_method"] = "S256"; - pkceCodeVerifier = code_verifier; - } - - const urlObj = new URL(providerConfig.authorizationEndpoint); - - for (const [key, value] of Object.entries(queryParams)) { - urlObj.searchParams.set(key, value); - } - - return { - urlWithQueryParams: urlObj.toString(), - pkceCodeVerifier: pkceCodeVerifier, - }; - }, signIn: async function ({ userId, tenantId, userContext, oAuthTokens, - rawUserInfoFromProvider, + rawUserInfo, }): Promise<{ status: "OK"; user: UserType; recipeUserId: RecipeUserId; oAuthTokens: OAuthTokens; - rawUserInfoFromProvider: { + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; @@ -85,7 +50,7 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN user, recipeUserId: new RecipeUserId(userId), oAuthTokens, - rawUserInfoFromProvider, + rawUserInfo, }; }, getProviderConfig: async function () { @@ -123,7 +88,7 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN const tokenAPIURL = providerConfig.tokenEndpoint; const accessTokenAPIParams: { [key: string]: string } = { client_id: providerConfig.clientId, - redirect_uri: redirectURIInfo.redirectURIOnProviderDashboard, + redirect_uri: redirectURIInfo.redirectURI, code: redirectURIInfo.redirectURIQueryParams["code"], grant_type: "authorization_code", }; @@ -153,7 +118,7 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN const accessToken = oAuthTokens["access_token"]; const idToken = oAuthTokens["id_token"]; - let rawUserInfoFromProvider: { + let rawUserInfo: { fromUserInfoAPI: any; fromIdTokenPayload: any; } = { @@ -166,13 +131,9 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN jwks = createRemoteJWKSet(new URL(providerConfig.jwksURI)); } - rawUserInfoFromProvider.fromIdTokenPayload = await verifyIdTokenFromJWKSEndpointAndGetPayload( - idToken, - jwks, - { - audience: providerConfig.clientId, - } - ); + rawUserInfo.fromIdTokenPayload = await verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, { + audience: providerConfig.clientId, + }); } if (accessToken && providerConfig.userInfoEndpoint !== undefined) { @@ -196,15 +157,15 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN ); } - rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; + rawUserInfo.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; } let userId: string | undefined = undefined; - if (rawUserInfoFromProvider.fromIdTokenPayload?.sub !== undefined) { - userId = rawUserInfoFromProvider.fromIdTokenPayload["sub"]; - } else if (rawUserInfoFromProvider.fromUserInfoAPI?.sub !== undefined) { - userId = rawUserInfoFromProvider.fromUserInfoAPI["sub"]; + if (rawUserInfo.fromIdTokenPayload?.sub !== undefined) { + userId = rawUserInfo.fromIdTokenPayload["sub"]; + } else if (rawUserInfo.fromUserInfoAPI?.sub !== undefined) { + userId = rawUserInfo.fromUserInfoAPI["sub"]; } if (userId === undefined) { @@ -213,7 +174,7 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN return { userId, - rawUserInfoFromProvider, + rawUserInfo, }; }, }; diff --git a/lib/ts/recipe/oauth2client/types.ts b/lib/ts/recipe/oauth2client/types.ts index 75d3a55c2..0c40c9bdc 100644 --- a/lib/ts/recipe/oauth2client/types.ts +++ b/lib/ts/recipe/oauth2client/types.ts @@ -22,16 +22,13 @@ import RecipeUserId from "../../recipeUserId"; export type UserInfo = { userId: string; - rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any } }; + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any } }; }; export type ProviderConfigInput = { clientId: string; clientSecret: string; - authorizationEndpointQueryParams?: { [key: string]: string | null }; oidcDiscoveryEndpoint: string; - scope?: string[]; - forcePKCE?: boolean; }; export type ProviderConfigWithOIDCInfo = ProviderConfigInput & { @@ -78,20 +75,12 @@ export type TypeNormalisedInput = { }; export type RecipeInterface = { - getAuthorisationRedirectURL(input: { - providerConfig: ProviderConfigWithOIDCInfo; - redirectURIOnProviderDashboard: string; - userContext: UserContext; - }): Promise<{ - urlWithQueryParams: string; - pkceCodeVerifier?: string; - }>; getProviderConfig(input: { userContext: UserContext }): Promise; signIn(input: { userId: string; oAuthTokens: OAuthTokens; - rawUserInfoFromProvider: { + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; @@ -102,7 +91,7 @@ export type RecipeInterface = { recipeUserId: RecipeUserId; user: User; oAuthTokens: OAuthTokens; - rawUserInfoFromProvider: { + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; @@ -110,7 +99,7 @@ export type RecipeInterface = { exchangeAuthCodeForOAuthTokens(input: { providerConfig: ProviderConfigWithOIDCInfo; redirectURIInfo: { - redirectURIOnProviderDashboard: string; + redirectURI: string; redirectURIQueryParams: any; pkceCodeVerifier?: string | undefined; }; @@ -134,31 +123,15 @@ export type APIOptions = { }; export type APIInterface = { - authorisationUrlGET: - | undefined - | ((input: { - redirectURIOnProviderDashboard: string; - options: APIOptions; - userContext: UserContext; - }) => Promise< - | { - status: "OK"; - urlWithQueryParams: string; - pkceCodeVerifier?: string; - } - | GeneralErrorResponse - >); - signInPOST: ( input: { tenantId: string; - session: SessionContainerInterface | undefined; options: APIOptions; userContext: UserContext; } & ( | { redirectURIInfo: { - redirectURIOnProviderDashboard: string; + redirectURI: string; redirectURIQueryParams: any; pkceCodeVerifier?: string; }; @@ -173,7 +146,7 @@ export type APIInterface = { user: User; session: SessionContainerInterface; oAuthTokens: { [key: string]: any }; - rawUserInfoFromProvider: { + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; diff --git a/lib/ts/recipe/oauth2provider/api/implementation.ts b/lib/ts/recipe/oauth2provider/api/implementation.ts index 24a913346..7a0a06add 100644 --- a/lib/ts/recipe/oauth2provider/api/implementation.ts +++ b/lib/ts/recipe/oauth2provider/api/implementation.ts @@ -84,5 +84,21 @@ export default function getAPIImplementation(): APIInterface { userContext, }); }, + revokeTokenPOST: async (input) => { + if ("authorizationHeader" in input) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + authorizationHeader: input.authorizationHeader, + userContext: input.userContext, + }); + } else { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + clientId: input.clientId, + clientSecret: input.clientSecret, + userContext: input.userContext, + }); + } + }, }; } diff --git a/lib/ts/recipe/oauth2provider/api/revokeToken.ts b/lib/ts/recipe/oauth2provider/api/revokeToken.ts new file mode 100644 index 000000000..c81eb21c5 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/revokeToken.ts @@ -0,0 +1,62 @@ +/* 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 { send200Response, sendNon200Response, sendNon200ResponseWithMessage } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; + +export default async function revokeTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.revokeTokenPOST === undefined) { + return false; + } + + const body = await options.req.getFormData(); + + if (body.token === undefined) { + sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + + const authorizationHeader = options.req.getHeaderValue("authorization"); + + if (authorizationHeader !== undefined && (body.client_id !== undefined || body.client_secret !== undefined)) { + sendNon200ResponseWithMessage( + options.res, + "Only one of authorization header or client_id and client_secret can be provided", + 400 + ); + return true; + } + + let response = await apiImplementation.revokeTokenPOST({ + options, + authorizationHeader, + token: body.token, + clientId: body.client_id, + clientSecret: body.client_secret, + userContext, + }); + + if ("statusCode" in response && response.statusCode !== 200) { + sendNon200Response(options.res, response.statusCode!, response); + } else { + send200Response(options.res, response); + } + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/userInfo.ts b/lib/ts/recipe/oauth2provider/api/userInfo.ts index 897d57672..3e661068d 100644 --- a/lib/ts/recipe/oauth2provider/api/userInfo.ts +++ b/lib/ts/recipe/oauth2provider/api/userInfo.ts @@ -29,7 +29,7 @@ export default async function userInfoGET( return false; } - const authHeader = options.req.getHeaderValue("authorization") || options.req.getHeaderValue("Authorization"); + const authHeader = options.req.getHeaderValue("authorization"); if (authHeader === undefined || !authHeader.startsWith("Bearer ")) { sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401); diff --git a/lib/ts/recipe/oauth2provider/constants.ts b/lib/ts/recipe/oauth2provider/constants.ts index a4e3ec85b..4a58eb3b4 100644 --- a/lib/ts/recipe/oauth2provider/constants.ts +++ b/lib/ts/recipe/oauth2provider/constants.ts @@ -20,3 +20,4 @@ export const AUTH_PATH = "/oauth/auth"; export const TOKEN_PATH = "/oauth/token"; export const LOGIN_INFO_PATH = "/oauth/login/info"; export const USER_INFO_PATH = "/oauth/userinfo"; +export const REVOKE_TOKEN_PATH = "/oauth/revoke"; diff --git a/lib/ts/recipe/oauth2provider/index.ts b/lib/ts/recipe/oauth2provider/index.ts index 8c9223d36..77e925c8d 100644 --- a/lib/ts/recipe/oauth2provider/index.ts +++ b/lib/ts/recipe/oauth2provider/index.ts @@ -28,6 +28,12 @@ import { export default class Wrapper { static init = Recipe.init; + static async getOAuth2Client(clientId: string, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Client( + { clientId }, + getUserContext(userContext) + ); + } static async getOAuth2Clients(input: GetOAuth2ClientsInput, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Clients( input, @@ -90,6 +96,47 @@ export default class Wrapper { userContext: getUserContext(userContext), }); } + + static async revokeToken( + token: string, + clientId: string, + clientSecret?: string, + userContext?: Record + ) { + let authorizationHeader: string | undefined = undefined; + + const normalisedUserContext = getUserContext(userContext); + const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; + + const res = await recipeInterfaceImpl.getOAuth2Client({ clientId }, normalisedUserContext); + + if (res.status !== "OK") { + throw new Error(`Failed to get OAuth2 client with id ${clientId}: ${res.error}`); + } + + const { tokenEndpointAuthMethod } = res.client; + + if (tokenEndpointAuthMethod === "none") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":").toString("base64"); + } else if (tokenEndpointAuthMethod === "client_secret_basic") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"); + } + + if (authorizationHeader !== undefined) { + return await recipeInterfaceImpl.revokeToken({ + token, + authorizationHeader, + userContext: normalisedUserContext, + }); + } + + return await recipeInterfaceImpl.revokeToken({ + token, + clientId, + clientSecret, + userContext: normalisedUserContext, + }); + } } export let init = Wrapper.init; @@ -106,4 +153,6 @@ export let validateOAuth2AccessToken = Wrapper.validateOAuth2AccessToken; export let createTokenForClientCredentials = Wrapper.createTokenForClientCredentials; +export let revokeToken = Wrapper.revokeToken; + export type { APIInterface, APIOptions, RecipeInterface }; diff --git a/lib/ts/recipe/oauth2provider/recipe.ts b/lib/ts/recipe/oauth2provider/recipe.ts index 2c3b8e9cc..e60665bc6 100644 --- a/lib/ts/recipe/oauth2provider/recipe.ts +++ b/lib/ts/recipe/oauth2provider/recipe.ts @@ -25,7 +25,7 @@ import APIImplementation from "./api/implementation"; import loginAPI from "./api/login"; import tokenPOST from "./api/token"; import loginInfoGET from "./api/loginInfo"; -import { AUTH_PATH, LOGIN_INFO_PATH, LOGIN_PATH, TOKEN_PATH, USER_INFO_PATH } from "./constants"; +import { AUTH_PATH, LOGIN_INFO_PATH, LOGIN_PATH, REVOKE_TOKEN_PATH, TOKEN_PATH, USER_INFO_PATH } from "./constants"; import RecipeImplementation from "./recipeImplementation"; import { APIInterface, @@ -41,6 +41,7 @@ import OverrideableBuilder from "supertokens-js-override"; import { User } from "../../user"; import userInfoGET from "./api/userInfo"; import { resetCombinedJWKS } from "../../combinedRemoteJWKSet"; +import revokeTokenPOST from "./api/revokeToken"; export default class Recipe extends RecipeModule { static RECIPE_ID = "oauth2provider"; @@ -145,6 +146,12 @@ export default class Recipe extends RecipeModule { id: USER_INFO_PATH, disabled: this.apiImpl.userInfoGET === undefined, }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(REVOKE_TOKEN_PATH), + id: REVOKE_TOKEN_PATH, + disabled: this.apiImpl.revokeTokenPOST === undefined, + }, ]; } @@ -181,6 +188,9 @@ export default class Recipe extends RecipeModule { if (id === USER_INFO_PATH) { return userInfoGET(this.apiImpl, tenantId, options, userContext); } + if (id === REVOKE_TOKEN_PATH) { + return revokeTokenPOST(this.apiImpl, options, userContext); + } throw new Error("Should never come here: handleAPIRequest called with unknown id"); }; diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts index b6febdf7d..9a0d06b60 100644 --- a/lib/ts/recipe/oauth2provider/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -262,6 +262,13 @@ export default function getRecipeInterface( input.userContext ); + if (res.status !== "OK") { + return { + statusCode: res.statusCode, + error: res.data.error, + errorDescription: res.data.error_description, + }; + } return res.data; }, @@ -305,6 +312,27 @@ export default function getRecipeInterface( }; } }, + getOAuth2Client: async function (input, userContext) { + let response = await querier.sendGetRequestWithResponseHeaders( + new NormalisedURLPath(`/recipe/oauth2/admin/clients/${input.clientId}`), + {}, + {}, + userContext + ); + + if (response.body.status === "OK") { + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response.body.data), + }; + } else { + return { + status: "ERROR", + error: response.body.data.error, + errorHint: response.body.data.errorHint, + }; + } + }, createOAuth2Client: async function (input, userContext) { let response = await querier.sendPostRequest( new NormalisedURLPath(`/recipe/oauth2/admin/clients`), @@ -450,5 +478,38 @@ export default function getRecipeInterface( } return { status: "OK", payload: payload as JSONObject }; }, + revokeToken: async function (this: RecipeInterface, input) { + const requestBody: Record = { + $isFormData: true, + token: input.token, + }; + + if ("authorizationHeader" in input) { + requestBody.authorizationHeader = input.authorizationHeader; + } else { + if ("clientId" in input) { + requestBody.client_id = input.clientId; + } + if ("clientSecret" in input) { + requestBody.client_secret = input.clientSecret; + } + } + + const res = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth2/pub/revoke`), + requestBody, + input.userContext + ); + + if (res.status !== "OK") { + return { + statusCode: res.statusCode, + error: res.data.error, + errorDescription: res.data.error_description, + }; + } + + return { status: "OK" }; + }, }; } diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts index e531bcbc4..dd2e9ccca 100644 --- a/lib/ts/recipe/oauth2provider/types.ts +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -256,6 +256,21 @@ export type RecipeInterface = { userContext: UserContext; }): Promise<{ redirectTo: string }>; + getOAuth2Client( + input: Pick, + userContext: UserContext + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + // TODO: Define specific error types once requirements are clearer + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; getOAuth2Clients( input: GetOAuth2ClientsInput, userContext: UserContext @@ -349,6 +364,17 @@ export type RecipeInterface = { tenantId: string; userContext: UserContext; }): Promise; + revokeToken( + input: { + token: string; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { clientId: string; clientSecret?: string } + ) + ): Promise<{ status: "OK" } | ErrorOAuth2>; }; export type APIInterface = { @@ -394,6 +420,15 @@ export type APIInterface = { options: APIOptions; userContext: UserContext; }) => Promise); + revokeTokenPOST: + | undefined + | (( + input: { + token: string; + options: APIOptions; + userContext: UserContext; + } & ({ authorizationHeader: string } | { clientId: string; clientSecret?: string }) + ) => Promise<{ status: "OK" } | ErrorOAuth2>); }; export type OAuth2ClientOptions = { diff --git a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts index 2dfe7a2bd..8bd134a78 100644 --- a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts +++ b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts @@ -37,6 +37,7 @@ export default async function getOpenIdDiscoveryConfiguration( authorization_endpoint: result.authorization_endpoint, token_endpoint: result.token_endpoint, userinfo_endpoint: result.userinfo_endpoint, + revocation_endpoint: result.revocation_endpoint, subject_types_supported: result.subject_types_supported, id_token_signing_alg_values_supported: result.id_token_signing_alg_values_supported, response_types_supported: result.response_types_supported, diff --git a/lib/ts/recipe/openid/recipeImplementation.ts b/lib/ts/recipe/openid/recipeImplementation.ts index a01377cbc..c8b302788 100644 --- a/lib/ts/recipe/openid/recipeImplementation.ts +++ b/lib/ts/recipe/openid/recipeImplementation.ts @@ -17,7 +17,7 @@ import { RecipeInterface as JWTRecipeInterface, JsonWebKey } from "../jwt/types" import NormalisedURLPath from "../../normalisedURLPath"; import { GET_JWKS_API } from "../jwt/constants"; import { NormalisedAppinfo, UserContext } from "../../types"; -import { AUTH_PATH, TOKEN_PATH, USER_INFO_PATH } from "../oauth2provider/constants"; +import { AUTH_PATH, REVOKE_TOKEN_PATH, TOKEN_PATH, USER_INFO_PATH } from "../oauth2provider/constants"; export default function getRecipeInterface( config: TypeNormalisedInput, @@ -39,6 +39,7 @@ export default function getRecipeInterface( authorization_endpoint: apiBasePath + AUTH_PATH, token_endpoint: apiBasePath + TOKEN_PATH, userinfo_endpoint: apiBasePath + USER_INFO_PATH, + revocation_endpoint: apiBasePath + REVOKE_TOKEN_PATH, subject_types_supported: ["public"], id_token_signing_alg_values_supported: ["RS256"], response_types_supported: ["code", "id_token", "id_token token"], diff --git a/lib/ts/recipe/openid/types.ts b/lib/ts/recipe/openid/types.ts index 32f3bc958..a798e1eaa 100644 --- a/lib/ts/recipe/openid/types.ts +++ b/lib/ts/recipe/openid/types.ts @@ -86,6 +86,7 @@ export type APIInterface = { authorization_endpoint: string; token_endpoint: string; userinfo_endpoint: string; + revocation_endpoint: string; subject_types_supported: string[]; id_token_signing_alg_values_supported: string[]; response_types_supported: string[]; @@ -104,6 +105,7 @@ export type RecipeInterface = { authorization_endpoint: string; token_endpoint: string; userinfo_endpoint: string; + revocation_endpoint: string; subject_types_supported: string[]; id_token_signing_alg_values_supported: string[]; response_types_supported: string[]; diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index 54b4e9185..0003ebe28 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -1597,6 +1597,7 @@ Session.init({ token_endpoint: "http://localhost:3000/auth/oauth2/token", authorization_endpoint: "http://localhost:3000/auth/oauth2/auth", userinfo_endpoint: "http://localhost:3000/auth/oauth2/userinfo", + revocation_endpoint: "http://localhost:3000/auth/oauth2/revoke", id_token_signing_alg_values_supported: [], response_types_supported: [], subject_types_supported: [],