diff --git a/lib/build/combinedRemoteJWKSet.js b/lib/build/combinedRemoteJWKSet.js index 0adeefc15..6c565d6f5 100644 --- a/lib/build/combinedRemoteJWKSet.js +++ b/lib/build/combinedRemoteJWKSet.js @@ -15,10 +15,6 @@ function resetCombinedJWKS() { combinedJWKS = undefined; } exports.resetCombinedJWKS = resetCombinedJWKS; -// TODO: remove this after proper core support -const hydraJWKS = jose_1.createRemoteJWKSet(new URL("http://localhost:4444/.well-known/jwks.json"), { - cooldownDuration: constants_1.JWKCacheCooldownInMs, -}); /** The function returned by this getter fetches all JWKs from the first available core instance. This combines the other JWKS functions to become error resistant. @@ -36,18 +32,7 @@ function getCombinedJWKS() { }) ); combinedJWKS = async (...args) => { - var _a, _b, _c, _d; let lastError = undefined; - if ( - !((_b = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.kid) === null || _b === void 0 - ? void 0 - : _b.startsWith("s-")) && - !((_d = (_c = args[0]) === null || _c === void 0 ? void 0 : _c.kid) === null || _d === void 0 - ? void 0 - : _d.startsWith("d-")) - ) { - return hydraJWKS(...args); - } if (JWKS.length === 0) { throw Error( "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." diff --git a/lib/build/querier.d.ts b/lib/build/querier.d.ts index bc25a61fe..17d12763b 100644 --- a/lib/build/querier.d.ts +++ b/lib/build/querier.d.ts @@ -3,7 +3,6 @@ import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; import { UserContext } from "./types"; import { NetworkInterceptor } from "./types"; -export declare const hydraPubDomain: string; export declare class Querier { private static initCalled; private static hosts; diff --git a/lib/build/querier.js b/lib/build/querier.js index ee366ca91..20019496f 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -4,9 +4,8 @@ var __importDefault = function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; -var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Querier = exports.hydraPubDomain = void 0; +exports.Querier = void 0; /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. * * This software is licensed under the Apache License, Version 2.0 (the @@ -28,10 +27,6 @@ const processState_1 = require("./processState"); const constants_1 = require("./constants"); const logger_1 = require("./logger"); const supertokens_1 = __importDefault(require("./supertokens")); -exports.hydraPubDomain = (_a = process.env.HYDRA_PUB) !== null && _a !== void 0 ? _a : "http://localhost:4444"; // This will be used as a domain for paths starting with hydraPubPathPrefix -const hydraAdmDomain = (_b = process.env.HYDRA_ADM) !== null && _b !== void 0 ? _b : "http://localhost:4445"; // This will be used as a domain for paths starting with hydraAdmPathPrefix -const hydraPubPathPrefix = "/recipe/oauth2/pub"; // Replaced with "/oauth2" when sending the request (/recipe/oauth2/pub/token -> /oauth2/token) -const hydraAdmPathPrefix = "/recipe/oauth2/admin"; // Replaced with "/admin" when sending the request (/recipe/oauth2/admin/clients -> /admin/clients) class Querier { // we have rIdToCore so that recipes can force change the rId sent to core. This is a hack until the core is able // to support multiple rIds per API @@ -106,11 +101,6 @@ class Querier { this.sendPostRequest = async (path, body, userContext) => { var _a; this.invalidateCoreCallCache(userContext); - // TODO: remove FormData - const isForm = body !== undefined && body["$isFormData"]; - if (isForm) { - delete body["$isFormData"]; - } const { body: respBody } = await this.sendRequestHelper( path, "POST", @@ -119,11 +109,7 @@ class Querier { let headers = { "cdi-version": apiVersion, }; - if (isForm) { - headers["content-type"] = "application/x-www-form-urlencoded"; - } else { - headers["content-type"] = "application/json; charset=utf-8"; - } + 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"]; @@ -153,11 +139,7 @@ class Querier { } return utils_1.doFetch(url, { method: "POST", - body: isForm - ? new URLSearchParams(Object.entries(body)).toString() - : body !== undefined - ? JSON.stringify(body) - : undefined, + body: body !== undefined ? JSON.stringify(body) : undefined, headers, }); }, @@ -395,6 +377,12 @@ class Querier { Object.entries(params).filter(([_, value]) => value !== undefined) ); finalURL.search = searchParams.toString(); + console.log( + "finalURL", + finalURL.toString(), + "body", + body !== undefined ? JSON.stringify(body) : undefined + ); return utils_1.doFetch(finalURL.toString(), { method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined, @@ -471,17 +459,6 @@ class Querier { let currentDomain = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); let currentBasePath = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); let strPath = path.getAsStringDangerous(); - const isHydraAPICall = strPath.startsWith(hydraAdmPathPrefix) || strPath.startsWith(hydraPubPathPrefix); - if (strPath.startsWith(hydraPubPathPrefix)) { - currentDomain = exports.hydraPubDomain; - currentBasePath = ""; - strPath = strPath.replace(hydraPubPathPrefix, "/oauth2"); - } - if (strPath.startsWith(hydraAdmPathPrefix)) { - currentDomain = hydraAdmDomain; - currentBasePath = ""; - strPath = strPath.replace(hydraAdmPathPrefix, "/admin"); - } const url = currentDomain + currentBasePath + strPath; const maxRetries = 5; if (retryInfoMap === undefined) { @@ -501,10 +478,6 @@ class Querier { if (process.env.TEST_MODE === "testing") { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); } - // TODO: Temporary solution for handling Hydra API calls. Remove when Hydra is no longer called directly. - if (isHydraAPICall) { - return handleHydraAPICall(response); - } if (response.status !== 200) { throw response; } @@ -602,26 +575,3 @@ Querier.hostsAliveForTesting = new Set(); Querier.networkInterceptor = undefined; Querier.globalCacheTag = Date.now(); Querier.disableCache = false; -async function handleHydraAPICall(response) { - const contentType = response.headers.get("Content-Type"); - if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("application/json")) { - 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", - statusCode: response.status, - data: await response.clone().text(), - }, - headers: response.headers, - }; - } - return { body: { status: response.ok ? "OK" : "ERROR", statusCode: response.status }, headers: response.headers }; -} diff --git a/lib/build/recipe/jwt/api/implementation.js b/lib/build/recipe/jwt/api/implementation.js index cbef7fd16..e52174f38 100644 --- a/lib/build/recipe/jwt/api/implementation.js +++ b/lib/build/recipe/jwt/api/implementation.js @@ -21,15 +21,6 @@ function getAPIImplementation() { if (resp.validityInSeconds !== undefined) { options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false); } - const oauth2Provider = require("../../oauth2provider/recipe").default.getInstance(); - // TODO: dirty hack until we get core support - if (oauth2Provider !== undefined) { - const oauth2JWKSRes = await fetch("http://localhost:4444/.well-known/jwks.json"); - if (oauth2JWKSRes.ok) { - const oauth2RespBody = await oauth2JWKSRes.json(); - resp.keys = resp.keys.concat(oauth2RespBody.keys); - } - } return { keys: resp.keys, }; diff --git a/lib/build/recipe/oauth2client/api/implementation.js b/lib/build/recipe/oauth2client/api/implementation.js index 159016c57..5a5e1d56f 100644 --- a/lib/build/recipe/oauth2client/api/implementation.js +++ b/lib/build/recipe/oauth2client/api/implementation.js @@ -9,8 +9,20 @@ const session_1 = __importDefault(require("../../session")); function getAPIInterface() { return { signInPOST: async function (input) { - const { options, tenantId, userContext } = input; - const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext }); + 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({ diff --git a/lib/build/recipe/oauth2client/api/signin.js b/lib/build/recipe/oauth2client/api/signin.js index 52631ac60..fa3d2f69f 100644 --- a/lib/build/recipe/oauth2client/api/signin.js +++ b/lib/build/recipe/oauth2client/api/signin.js @@ -29,6 +29,12 @@ async function signInAPI(apiImplementation, tenantId, options, userContext) { 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({ @@ -59,6 +65,7 @@ async function signInAPI(apiImplementation, tenantId, options, userContext) { } let result = await apiImplementation.signInPOST({ tenantId, + clientId: bodyParams.clientId, redirectURIInfo, oAuthTokens, options, diff --git a/lib/build/recipe/oauth2client/index.d.ts b/lib/build/recipe/oauth2client/index.d.ts index fd09feb41..ff01c3827 100644 --- a/lib/build/recipe/oauth2client/index.d.ts +++ b/lib/build/recipe/oauth2client/index.d.ts @@ -9,6 +9,7 @@ export default class Wrapper { redirectURIQueryParams: any; pkceCodeVerifier?: string | undefined; }, + clientId?: string, userContext?: Record ): Promise; static getUserInfo( diff --git a/lib/build/recipe/oauth2client/index.js b/lib/build/recipe/oauth2client/index.js index c70a69f41..e24e2d323 100644 --- a/lib/build/recipe/oauth2client/index.js +++ b/lib/build/recipe/oauth2client/index.js @@ -21,12 +21,22 @@ var __importDefault = 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, userContext) { - const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + 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({ @@ -38,7 +48,12 @@ class Wrapper { 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({ diff --git a/lib/build/recipe/oauth2client/recipeImplementation.js b/lib/build/recipe/oauth2client/recipeImplementation.js index e935c9e60..bd552aa54 100644 --- a/lib/build/recipe/oauth2client/recipeImplementation.js +++ b/lib/build/recipe/oauth2client/recipeImplementation.js @@ -11,7 +11,7 @@ const __1 = require("../.."); const logger_1 = require("../../logger"); const jose_1 = require("jose"); function getRecipeImplementation(_querier, config) { - let providerConfigWithOIDCInfo = null; + let providerConfigsWithOIDCInfo = {}; return { signIn: async function ({ userId, tenantId, userContext, oAuthTokens, rawUserInfo }) { const user = await __1.getUser(userId, userContext); @@ -26,11 +26,16 @@ function getRecipeImplementation(_querier, config) { rawUserInfo, }; }, - getProviderConfig: async function () { - if (providerConfigWithOIDCInfo !== null) { - return providerConfigWithOIDCInfo; + getProviderConfig: async function ({ clientId }) { + if (providerConfigsWithOIDCInfo[clientId] !== null) { + return providerConfigsWithOIDCInfo[clientId]; } - const oidcInfo = await thirdpartyUtils_1.getOIDCDiscoveryInfo(config.providerConfig.oidcDiscoveryEndpoint); + const providerConfig = config.providerConfigs.find( + (providerConfig) => providerConfig.clientId === clientId + ); + const oidcInfo = await thirdpartyUtils_1.getOIDCDiscoveryInfo( + config.providerConfigs[0].oidcDiscoveryEndpoint + ); if (oidcInfo.authorization_endpoint === undefined) { throw new Error("Failed to authorization_endpoint from the oidcDiscoveryEndpoint."); } @@ -43,13 +48,13 @@ function getRecipeImplementation(_querier, config) { if (oidcInfo.jwks_uri === undefined) { throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); } - providerConfigWithOIDCInfo = Object.assign(Object.assign({}, config.providerConfig), { + providerConfigsWithOIDCInfo[clientId] = Object.assign(Object.assign({}, providerConfig), { authorizationEndpoint: oidcInfo.authorization_endpoint, tokenEndpoint: oidcInfo.token_endpoint, userInfoEndpoint: oidcInfo.userinfo_endpoint, jwksURI: oidcInfo.jwks_uri, }); - return providerConfigWithOIDCInfo; + return providerConfigsWithOIDCInfo[clientId]; }, exchangeAuthCodeForOAuthTokens: async function ({ providerConfig, redirectURIInfo }) { if (providerConfig.tokenEndpoint === undefined) { diff --git a/lib/build/recipe/oauth2client/types.d.ts b/lib/build/recipe/oauth2client/types.d.ts index 36b685837..84fab1847 100644 --- a/lib/build/recipe/oauth2client/types.d.ts +++ b/lib/build/recipe/oauth2client/types.d.ts @@ -40,7 +40,7 @@ export declare type OAuthTokenResponse = { token_type: string; }; export declare type TypeInput = { - providerConfig: ProviderConfigInput; + providerConfigs: ProviderConfigInput[]; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -50,7 +50,7 @@ export declare type TypeInput = { }; }; export declare type TypeNormalisedInput = { - providerConfig: ProviderConfigInput; + providerConfigs: ProviderConfigInput[]; override: { functions: ( originalImplementation: RecipeInterface, @@ -60,7 +60,7 @@ export declare type TypeNormalisedInput = { }; }; export declare type RecipeInterface = { - getProviderConfig(input: { userContext: UserContext }): Promise; + getProviderConfig(input: { clientId: string; userContext: UserContext }): Promise; signIn(input: { userId: string; oAuthTokens: OAuthTokens; @@ -116,6 +116,7 @@ export declare type APIInterface = { signInPOST: ( input: { tenantId: string; + clientId?: string; options: APIOptions; userContext: UserContext; } & ( diff --git a/lib/build/recipe/oauth2client/utils.js b/lib/build/recipe/oauth2client/utils.js index 0c5e2f39b..a540988d1 100644 --- a/lib/build/recipe/oauth2client/utils.js +++ b/lib/build/recipe/oauth2client/utils.js @@ -16,20 +16,19 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.validateAndNormaliseUserInput = void 0; function validateAndNormaliseUserInput(_appInfo, config) { - if (config === undefined || config.providerConfig === undefined) { - throw new Error("Please pass providerConfig argument in the OAuth2Client recipe."); + if (config === undefined || config.providerConfigs === undefined) { + throw new Error("Please pass providerConfigs argument in the OAuth2Client recipe."); } - if (config.providerConfig.clientId === undefined) { - throw new Error("Please pass clientId argument in the OAuth2Client providerConfig."); + if (config.providerConfigs.some((providerConfig) => providerConfig.clientId === undefined)) { + throw new Error("Please pass clientId for all providerConfigs."); } - // TODO: Decide on the prefix and also if we will allow users to customise clientIds - // if (!config.providerConfig.clientId.startsWith("supertokens_")) { - // throw new Error( - // `Only Supertokens OAuth ClientIds are supported in the OAuth2Client recipe. For any other OAuth Clients use the thirdparty recipe.` - // ); - // } - if (config.providerConfig.oidcDiscoveryEndpoint === undefined) { - throw new Error("Please pass oidcDiscoveryEndpoint argument in the OAuth2Client providerConfig."); + if (!config.providerConfigs.every((providerConfig) => providerConfig.clientId.startsWith("supertokens_"))) { + 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( { @@ -39,7 +38,7 @@ function validateAndNormaliseUserInput(_appInfo, config) { config === null || config === void 0 ? void 0 : config.override ); return { - providerConfig: config.providerConfig, + providerConfigs: config.providerConfigs, override, }; } diff --git a/lib/build/recipe/oauth2provider/api/auth.js b/lib/build/recipe/oauth2provider/api/auth.js index a064f9bb4..dbdb5b1fe 100644 --- a/lib/build/recipe/oauth2provider/api/auth.js +++ b/lib/build/recipe/oauth2provider/api/auth.js @@ -44,6 +44,7 @@ async function authGET(apiImplementation, options, userContext) { shouldTryRefresh = false; } } + console.log({ n: "authGET", shouldTryRefresh, origURL }); let response = await apiImplementation.authGET({ options, params: Object.fromEntries(params.entries()), @@ -52,6 +53,7 @@ async function authGET(apiImplementation, options, userContext) { shouldTryRefresh, userContext, }); + console.log({ n: "authGET", response }); if ("redirectTo" in response) { if (response.setCookie) { const cookieStr = set_cookie_parser_1.default.splitCookiesString(response.setCookie); diff --git a/lib/build/recipe/oauth2provider/api/token.js b/lib/build/recipe/oauth2provider/api/token.js index 3ff27158c..1c27c3cae 100644 --- a/lib/build/recipe/oauth2provider/api/token.js +++ b/lib/build/recipe/oauth2provider/api/token.js @@ -26,6 +26,7 @@ async function tokenPOST(apiImplementation, options, userContext) { body: await options.req.getBodyAsJSONOrFormData(), userContext, }); + console.log("response", response); if ("statusCode" in response && response.statusCode !== 200) { utils_1.sendNon200Response(options.res, response.statusCode, { error: response.error, diff --git a/lib/build/recipe/oauth2provider/api/utils.js b/lib/build/recipe/oauth2provider/api/utils.js index 531f2bf13..74873fd1e 100644 --- a/lib/build/recipe/oauth2provider/api/utils.js +++ b/lib/build/recipe/oauth2provider/api/utils.js @@ -42,6 +42,7 @@ async function loginGET({ ? _a : incomingAuthUrlQueryParams.get("st_prompt"); const maxAgeParam = incomingAuthUrlQueryParams.get("max_age"); + console.log({ n: "loginGET", shouldTryRefresh, loginRequest, sessionInfo }); if (maxAgeParam !== null) { try { const maxAgeParsed = Number.parseInt(maxAgeParam); @@ -96,6 +97,7 @@ async function loginGET({ }) .getAsStringDangerous(); const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + console.log({ n: "loginGET2", shouldTryRefresh }); if (shouldTryRefresh) { const websiteDomain = appInfo .getOrigin({ @@ -200,6 +202,12 @@ async function handleLoginInternalRedirects({ userContext, }) { var _a; + console.log({ + n: "handleLoginInternalRedirects", + response, + isLoginInternalRedirect: isLoginInternalRedirect(response.redirectTo), + shouldTryRefresh, + }); if (!isLoginInternalRedirect(response.redirectTo)) { return response; } @@ -208,6 +216,7 @@ async function handleLoginInternalRedirects({ const maxRedirects = 10; let redirectCount = 0; while (redirectCount < maxRedirects && isLoginInternalRedirect(response.redirectTo)) { + console.log({ n: "handleLoginInternalRedirects2", response, shouldTryRefresh }); cookie = getMergedCookies({ cookie, setCookie: response.setCookie }); const queryString = response.redirectTo.split("?")[1]; const params = new URLSearchParams(queryString); diff --git a/lib/build/recipe/oauth2provider/recipe.d.ts b/lib/build/recipe/oauth2provider/recipe.d.ts index 32241c801..d2739dbf7 100644 --- a/lib/build/recipe/oauth2provider/recipe.d.ts +++ b/lib/build/recipe/oauth2provider/recipe.d.ts @@ -32,7 +32,6 @@ export default class Recipe extends RecipeModule { addUserInfoBuilderFromOtherRecipe: (userInfoBuilderFn: UserInfoBuilderFunction) => void; addAccessTokenBuilderFromOtherRecipe: (accessTokenBuilders: PayloadBuilderFunction) => void; addIdTokenBuilderFromOtherRecipe: (idTokenBuilder: PayloadBuilderFunction) => void; - saveTokensForHook: (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => void; getAPIsHandled(): APIHandled[]; handleAPIRequest: ( id: string, diff --git a/lib/build/recipe/oauth2provider/recipe.js b/lib/build/recipe/oauth2provider/recipe.js index 171d58467..67529069c 100644 --- a/lib/build/recipe/oauth2provider/recipe.js +++ b/lib/build/recipe/oauth2provider/recipe.js @@ -39,8 +39,6 @@ const introspectToken_1 = __importDefault(require("./api/introspectToken")); const endSession_1 = require("./api/endSession"); const logout_1 = require("./api/logout"); const session_1 = require("../session"); -const utils_2 = require("../../utils"); -const tokenHookMap = new Map(); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); @@ -56,9 +54,6 @@ class Recipe extends recipeModule_1.default { this.addIdTokenBuilderFromOtherRecipe = (idTokenBuilder) => { this.idTokenBuilders.push(idTokenBuilder); }; - this.saveTokensForHook = (sessionHandle, idToken, accessToken) => { - tokenHookMap.set(sessionHandle, { idToken, accessToken }); - }; this.handleAPIRequest = async (id, tenantId, req, res, _path, method, userContext) => { let options = { config: this.config, @@ -98,23 +93,6 @@ class Recipe extends recipeModule_1.default { if (id === constants_1.LOGOUT_PATH && method === "post") { return logout_1.logoutPOST(this.apiImpl, options, userContext); } - if (id === "token-hook") { - const body = await options.req.getBodyAsJSONOrFormData(); - const sessionHandle = body.session.extra.sessionHandle; - const tokens = tokenHookMap.get(sessionHandle); - if (tokens !== undefined) { - const { idToken, accessToken } = tokens; - utils_2.send200Response(options.res, { - session: { - access_token: accessToken, - id_token: idToken, - }, - }); - } else { - utils_2.send200Response(options.res, {}); - } - return true; - } throw new Error("Should never come here: handleAPIRequest called with unknown id"); }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); @@ -127,8 +105,7 @@ class Recipe extends recipeModule_1.default { appInfo, this.getDefaultAccessTokenPayload.bind(this), this.getDefaultIdTokenPayload.bind(this), - this.getDefaultUserInfoPayload.bind(this), - this.saveTokensForHook.bind(this) + this.getDefaultUserInfoPayload.bind(this) ) ); this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); @@ -210,13 +187,6 @@ class Recipe extends recipeModule_1.default { id: constants_1.INTROSPECT_TOKEN_PATH, disabled: this.apiImpl.introspectTokenPOST === undefined, }, - { - // TODO: remove this once we get core support - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default("/oauth/token-hook"), - id: "token-hook", - disabled: false, - }, { method: "get", pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.END_SESSION_PATH), @@ -252,7 +222,6 @@ class Recipe extends recipeModule_1.default { throw new Error("Session not found"); } let payload = { - iss: this.appInfo.apiDomain.getAsStringDangerous() + this.appInfo.apiBasePath.getAsStringDangerous(), tId: sessionInfo.tenantId, rsub: sessionInfo.recipeUserId.getAsString(), sessionHandle: sessionHandle, diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.d.ts b/lib/build/recipe/oauth2provider/recipeImplementation.d.ts index 3b6fee065..4ecaeef69 100644 --- a/lib/build/recipe/oauth2provider/recipeImplementation.d.ts +++ b/lib/build/recipe/oauth2provider/recipeImplementation.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { Querier } from "../../querier"; -import { JSONObject, NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo } from "../../types"; import { RecipeInterface, TypeNormalisedInput, PayloadBuilderFunction, UserInfoBuilderFunction } from "./types"; export default function getRecipeInterface( querier: Querier, @@ -8,6 +8,5 @@ export default function getRecipeInterface( appInfo: NormalisedAppinfo, getDefaultAccessTokenPayload: PayloadBuilderFunction, getDefaultIdTokenPayload: PayloadBuilderFunction, - getDefaultUserInfoPayload: UserInfoBuilderFunction, - saveTokensForHook: (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => void + getDefaultUserInfoPayload: UserInfoBuilderFunction ): RecipeInterface; diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js index b513d77e2..8862ee189 100644 --- a/lib/build/recipe/oauth2provider/recipeImplementation.js +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -57,20 +57,22 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const jose = __importStar(require("jose")); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const querier_1 = require("../../querier"); -const utils_1 = require("../../utils"); const OAuth2Client_1 = require("./OAuth2Client"); const __1 = require("../.."); const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); -const session_1 = require("../session"); -// TODO: Remove this core changes are done function getUpdatedRedirectTo(appInfo, redirectTo) { - return redirectTo - .replace( - querier_1.hydraPubDomain, - appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() - ) - .replace("oauth2/", "oauth/"); + return redirectTo.replace( + "{apiDomain}", + appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() + ); + // .replace("oauth2/", "oauth/"); +} +function copyAndCleanRequestBodyInput(input) { + let result = Object.assign({}, input); + delete result.userContext; + delete result.tenantId; + delete result.session; + return result; } function getRecipeInterface( querier, @@ -78,149 +80,215 @@ function getRecipeInterface( appInfo, getDefaultAccessTokenPayload, getDefaultIdTokenPayload, - getDefaultUserInfoPayload, - saveTokensForHook + getDefaultUserInfoPayload ) { return { getLoginRequest: async function (input) { const resp = await querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/oauth2/admin/oauth2/auth/requests/login"), - { login_challenge: input.challenge }, + new normalisedURLPath_1.default("/recipe/oauth/auth/requests/login"), + { loginChallenge: input.challenge }, input.userContext ); return { - challenge: resp.data.challenge, - client: OAuth2Client_1.OAuth2Client.fromAPIResponse(resp.data.client), - oidcContext: resp.data.oidc_context, - requestUrl: resp.data.request_url, - requestedAccessTokenAudience: resp.data.requested_access_token_audience, - requestedScope: resp.data.requested_scope, - sessionId: resp.data.session_id, - skip: resp.data.skip, - subject: resp.data.subject, + challenge: resp.challenge, + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(resp.client), + oidcContext: resp.oidcContext, + requestUrl: resp.requestUrl, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + sessionId: resp.sessionId, + skip: resp.skip, + subject: resp.subject, }; }, acceptLoginRequest: async function (input) { const resp = await querier.sendPutRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/auth/requests/login/accept`), + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/login/accept`), { acr: input.acr, amr: input.amr, context: input.context, - extend_session_lifespan: input.extendSessionLifespan, - force_subject_identifier: input.forceSubjectIdentifier, - identity_provider_session_id: input.identityProviderSessionId, + extendSessionLifespan: input.extendSessionLifespan, + forceSubjectIdentifier: input.forceSubjectIdentifier, + identityProviderSessionId: input.identityProviderSessionId, remember: input.remember, - remember_for: input.rememberFor, + rememberFor: input.rememberFor, subject: input.subject, }, { - login_challenge: input.challenge, + loginChallenge: input.challenge, }, input.userContext ); + console.log("acceptLoginRequest resp", resp); return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), }; }, rejectLoginRequest: async function (input) { const resp = await querier.sendPutRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/auth/requests/login/reject`), - { - error: input.error.error, - error_description: input.error.errorDescription, - status_code: input.error.statusCode, - }, + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/login/reject`), + copyAndCleanRequestBodyInput(input), { - login_challenge: input.challenge, + loginChallenge: input.challenge, }, input.userContext ); return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), }; }, getConsentRequest: async function (input) { const resp = await querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/oauth2/admin/oauth2/auth/requests/consent"), - { consent_challenge: input.challenge }, + new normalisedURLPath_1.default("/recipe/oauth/auth/requests/consent"), + { consentChallenge: input.challenge }, input.userContext ); return { - acr: resp.data.acr, - amr: resp.data.amr, - challenge: resp.data.challenge, - client: OAuth2Client_1.OAuth2Client.fromAPIResponse(resp.data.client), - context: resp.data.context, - loginChallenge: resp.data.login_challenge, - loginSessionId: resp.data.login_session_id, - oidcContext: resp.data.oidc_context, - requestedAccessTokenAudience: resp.data.requested_access_token_audience, - requestedScope: resp.data.requested_scope, - skip: resp.data.skip, - subject: resp.data.subject, + acr: resp.acr, + amr: resp.amr, + challenge: resp.challenge, + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(resp.client), + context: resp.context, + loginChallenge: resp.loginChallenge, + loginSessionId: resp.loginSessionId, + oidcContext: resp.oidcContext, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + skip: resp.skip, + subject: resp.subject, }; }, acceptConsentRequest: async function (input) { const resp = await querier.sendPutRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/auth/requests/consent/accept`), + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/consent/accept`), { context: input.context, - grant_access_token_audience: input.grantAccessTokenAudience, - grant_scope: input.grantScope, - handled_at: input.handledAt, + grantAccessTokenAudience: input.grantAccessTokenAudience, + grantScope: input.grantScope, + handledAt: input.handledAt, remember: input.remember, - remember_for: input.rememberFor, - session: input.session, + rememberFor: input.rememberFor, + iss: appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(), + tId: input.tenantId, + rsub: input.rsub, + sessionHandle: input.sessionHandle, }, { - consent_challenge: input.challenge, + consentChallenge: input.challenge, }, input.userContext ); + console.log("acceptConsentRequest resp", { + body: { + context: input.context, + grantAccessTokenAudience: input.grantAccessTokenAudience, + grantScope: input.grantScope, + handledAt: input.handledAt, + remember: input.remember, + rememberFor: input.rememberFor, + iss: appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(), + tId: input.tenantId, + rsub: input.rsub, + sessionHandle: input.sessionHandle, + }, + queryParams: { + consentChallenge: input.challenge, + }, + resp, + }); return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), }; }, rejectConsentRequest: async function (input) { const resp = await querier.sendPutRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/auth/requests/consent/reject`), + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/consent/reject`), { error: input.error.error, - error_description: input.error.errorDescription, - status_code: input.error.statusCode, + errorDescription: input.error.errorDescription, + statusCode: input.error.statusCode, }, { - consent_challenge: input.challenge, + consentChallenge: input.challenge, }, input.userContext ); + console.log("rejectConsentRequest resp", resp); return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), }; }, authorization: async function (input) { - var _a, _b; + var _a, _b, _c, _d; if (input.session !== undefined) { if (input.params.prompt === "none") { input.params["st_prompt"] = "none"; delete input.params.prompt; } } - const resp = await querier.sendGetRequestWithResponseHeaders( - new normalisedURLPath_1.default(`/recipe/oauth2/pub/auth`), - input.params, + let payloads; + if (input.params.client_id === undefined) { + throw new Error("client_id is required"); + } + const responseTypes = + (_b = (_a = input.params.response_type) === null || _a === void 0 ? void 0 : _a.split(" ")) !== null && + _b !== void 0 + ? _b + : []; + if ( + input.session !== undefined && + (responseTypes.includes("token") || responseTypes.includes("id_token")) + ) { + const clientInfo = await this.getOAuth2Client({ + clientId: input.params.client_id, + userContext: input.userContext, + }); + if (clientInfo.status === "ERROR") { + throw new Error(clientInfo.error); + } + const client = clientInfo.client; + const user = await __1.getUser(input.session.getUserId()); + if (!user) { + throw new Error("User not found"); + } + const idToken = responseTypes.includes("id_token") + ? await this.buildIdTokenPayload({ + user, + client, + sessionHandle: input.session.getHandle(), + scopes: ((_c = input.params.scope) === null || _c === void 0 ? void 0 : _c.split(" ")) || [], + userContext: input.userContext, + }) + : undefined; + const accessToken = responseTypes.includes("token") + ? await this.buildAccessTokenPayload({ + user, + client, + sessionHandle: input.session.getHandle(), + scopes: ((_d = input.params.scope) === null || _d === void 0 ? void 0 : _d.split(" ")) || [], + userContext: input.userContext, + }) + : undefined; + payloads = { + idToken, + accessToken, + }; + } + const resp = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth`), { - // TODO: if session is not set also clear the oauth2 cookie - Cookie: `${input.cookies}`, + params: input.params, + cookies: input.cookies, + session: payloads, }, input.userContext ); - const redirectTo = getUpdatedRedirectTo(appInfo, resp.headers.get("Location")); + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); if (redirectTo === undefined) { throw new Error(resp.body); } @@ -231,68 +299,86 @@ function getRecipeInterface( challenge: consentChallenge, userContext: input.userContext, }); - const user = await __1.getUser(input.session.getUserId()); - if (!user) { - throw new Error("Should not happen"); - } - const idToken = await this.buildIdTokenPayload({ - user, - client: consentRequest.client, - sessionHandle: input.session.getHandle(), - scopes: consentRequest.requestedScope || [], + const consentRes = await this.acceptConsentRequest({ userContext: input.userContext, - }); - const accessTokenPayload = await this.buildAccessTokenPayload({ - user, - client: consentRequest.client, + challenge: consentRequest.challenge, + grantAccessTokenAudience: consentRequest.requestedAccessTokenAudience, + grantScope: consentRequest.requestedScope, + tenantId: input.session.getTenantId(), + rsub: input.session.getRecipeUserId().getAsString(), sessionHandle: input.session.getHandle(), - scopes: consentRequest.requestedScope || [], - userContext: input.userContext, }); - const sessionInfo = await session_1.getSessionInformation(input.session.getHandle()); - if (!sessionInfo) { - throw new Error("Session not found"); - } - const consentRes = await this.acceptConsentRequest( - Object.assign(Object.assign({}, input), { - challenge: consentRequest.challenge, - grantAccessTokenAudience: consentRequest.requestedAccessTokenAudience, - grantScope: consentRequest.requestedScope, - remember: true, - session: { - id_token: idToken, - access_token: accessTokenPayload, - }, - handledAt: new Date(sessionInfo.timeCreated).toISOString(), - }) - ); return { redirectTo: consentRes.redirectTo, - setCookie: (_a = resp.headers.get("set-cookie")) !== null && _a !== void 0 ? _a : undefined, + setCookie: resp.cookies, }; } - return { - redirectTo, - setCookie: (_b = resp.headers.get("set-cookie")) !== null && _b !== void 0 ? _b : undefined, - }; + return { redirectTo, setCookie: resp.cookies }; }, tokenExchange: async function (input) { - var _a, _b; - const body = { $isFormData: true }; // TODO: we ideally want to avoid using formdata, the core can do the translation - for (const key in input.body) { - body[key] = input.body[key]; - } - if (input.body.grant_type === "refresh_token") { + var _a, _b, _c, _d; + const body = { + inputBody: input.body, + authorizationHeader: input.authorizationHeader, + }; + body.iss = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + if (input.body.grant_type === "client_credentials") { + if (input.body.client_id === undefined) { + return { + statusCode: 400, + error: "invalid_request", + errorDescription: "client_id is required", + }; + } const scopes = (_b = (_a = input.body.scope) === null || _a === void 0 ? void 0 : _a.split(" ")) !== null && _b !== void 0 ? _b : []; + const clientInfo = await this.getOAuth2Client({ + clientId: input.body.client_id, + userContext: input.userContext, + }); + console.log("clientInfo", clientInfo); + if (clientInfo.status === "ERROR") { + return { + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorHint, + }; + } + const client = clientInfo.client; + const idToken = await this.buildIdTokenPayload({ + user: undefined, + client, + sessionHandle: undefined, + scopes, + userContext: input.userContext, + }); + const accessTokenPayload = await this.buildAccessTokenPayload({ + user: undefined, + client, + sessionHandle: undefined, + scopes, + userContext: input.userContext, + }); + body["session"] = { + idToken: idToken, + accessToken: accessTokenPayload, + }; + } + if (input.body.grant_type === "refresh_token") { + const scopes = + (_d = (_c = input.body.scope) === null || _c === void 0 ? void 0 : _c.split(" ")) !== null && + _d !== void 0 + ? _d + : []; const tokenInfo = await this.introspectToken({ token: input.body.refresh_token, scopes, userContext: input.userContext, }); + console.log("tokenInfo", input.body.refresh_token, tokenInfo); if (tokenInfo.active === true) { const sessionHandle = tokenInfo.ext.sessionHandle; const clientInfo = await this.getOAuth2Client({ @@ -314,7 +400,7 @@ function getRecipeInterface( const idToken = await this.buildIdTokenPayload({ user, client, - sessionHandle: sessionHandle, + sessionHandle, scopes, userContext: input.userContext, }); @@ -326,143 +412,109 @@ function getRecipeInterface( userContext: input.userContext, }); body["session"] = { - id_token: idToken, - access_token: accessTokenPayload, + idToken: idToken, + accessToken: accessTokenPayload, }; - saveTokensForHook(sessionHandle, idToken, accessTokenPayload); } } if (input.authorizationHeader) { body["authorizationHeader"] = input.authorizationHeader; } + console.log("/recipe/oauth/token", body); const res = await querier.sendPostRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/pub/token`), + new normalisedURLPath_1.default(`/recipe/oauth/token`), body, input.userContext ); if (res.status !== "OK") { return { statusCode: res.statusCode, - error: res.data.error, - errorDescription: res.data.error_description, + error: res.error, + errorDescription: res.error_description, }; } - return res.data; + return res; }, getOAuth2Clients: async function (input) { - var _a; let response = await querier.sendGetRequestWithResponseHeaders( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients`), - Object.assign(Object.assign({}, utils_1.transformObjectKeys(input, "snake-case")), { - page_token: input.paginationToken, + new normalisedURLPath_1.default(`/recipe/oauth/clients`), + Object.assign(Object.assign({}, copyAndCleanRequestBodyInput(input)), { + pageToken: input.paginationToken, }), {}, input.userContext ); if (response.body.status === "OK") { - // Pagination info is in the Link header, containing comma-separated links: - // "first", "next" (if applicable). - // Example: Link: ; rel="first", ; rel="next" - // We parse the nextPaginationToken from the Link header using RegExp - let nextPaginationToken; - const linkHeader = (_a = response.headers.get("link")) !== null && _a !== void 0 ? _a : ""; - const nextLinkMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/); - if (nextLinkMatch) { - const url = nextLinkMatch[1]; - const urlParams = new URLSearchParams(url.split("?")[1]); - nextPaginationToken = urlParams.get("page_token"); - } return { status: "OK", - clients: response.body.data.map((client) => OAuth2Client_1.OAuth2Client.fromAPIResponse(client)), - nextPaginationToken, + clients: response.body.clients.map((client) => OAuth2Client_1.OAuth2Client.fromAPIResponse(client)), + nextPaginationToken: response.body.nextPaginationToken, }; } else { return { status: "ERROR", - error: response.body.data.error, - errorHint: response.body.data.errorHint, + error: response.body.error, + errorHint: response.body.errorHint, }; } }, getOAuth2Client: async function (input) { let response = await querier.sendGetRequestWithResponseHeaders( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients/${input.clientId}`), - {}, + new normalisedURLPath_1.default(`/recipe/oauth/clients`), + { clientId: input.clientId }, {}, input.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, - }; - } + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.body), + }; + // return { + // status: "ERROR", + // error: response.body.error, + // errorHint: response.body.errorHint, + // }; }, createOAuth2Client: async function (input) { let response = await querier.sendPostRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients`), - Object.assign(Object.assign({}, utils_1.transformObjectKeys(input, "snake-case")), { - // TODO: these defaults should be set/enforced on the core side - access_token_strategy: "jwt", - skip_consent: true, - subject_type: "public", - }), + new normalisedURLPath_1.default(`/recipe/oauth/clients`), + copyAndCleanRequestBodyInput(input), input.userContext ); - if (response.status === "OK") { - return { - status: "OK", - client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.data), - }; - } else { - return { - status: "ERROR", - error: response.data.error, - errorHint: response.data.errorHint, - }; - } + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response), + }; + // return { + // status: "ERROR", + // error: response.error, + // errorHint: response.errorHint, + // }; }, updateOAuth2Client: async function (input) { - // We convert the input into an array of "replace" operations - const requestBody = Object.entries(input).reduce((result, [key, value]) => { - result.push({ - from: `/${utils_1.toSnakeCase(key)}`, - op: "replace", - path: `/${utils_1.toSnakeCase(key)}`, - value, - }); - return result; - }, []); - let response = await querier.sendPatchRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients/${input.clientId}`), - requestBody, + let response = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/clients`), + copyAndCleanRequestBodyInput(input), + { clientId: input.clientId }, input.userContext ); if (response.status === "OK") { return { status: "OK", - client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.data), + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response), }; } else { return { status: "ERROR", - error: response.data.error, - errorHint: response.data.errorHint, + error: response.error, + errorHint: response.errorHint, }; } }, deleteOAuth2Client: async function (input) { - let response = await querier.sendDeleteRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients/${input.clientId}`), - undefined, - undefined, + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/clients/remove`), + { clientId: input.clientId }, input.userContext ); if (response.status === "OK") { @@ -470,33 +522,32 @@ function getRecipeInterface( } else { return { status: "ERROR", - error: response.data.error, - errorHint: response.data.errorHint, + error: response.error, + errorHint: response.errorHint, }; } }, buildAccessTokenPayload: async function (input) { + if (input.user === undefined || input.sessionHandle === undefined) { + return {}; + } return getDefaultAccessTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); }, buildIdTokenPayload: async function (input) { + if (input.user === undefined || input.sessionHandle === undefined) { + return {}; + } return getDefaultIdTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); }, buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); }, validateOAuth2AccessToken: async function (input) { - var _a, _b, _c, _d, _e; + var _a, _b, _c, _d, _e, _f, _g, _h; const payload = (await jose.jwtVerify(input.token, combinedRemoteJWKSet_1.getCombinedJWKS())).payload; - // if (payload.stt !== 1) { - // throw new Error("Wrong token type"); - // } - // TODO: we should be able uncomment this after we get proper core support - // TODO: make this configurable? - // const expectedIssuer = - // appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); - // if (payload.iss !== expectedIssuer) { - // throw new Error("Issuer mismatch: this token was likely issued by another application or spoofed"); - // } + if (payload.stt !== 1) { + throw new Error("Wrong token type"); + } if ( ((_a = input.requirements) === null || _a === void 0 ? void 0 : _a.clientId) !== undefined && payload.client_id !== input.requirements.clientId @@ -524,52 +575,57 @@ function getRecipeInterface( } if (input.checkDatabase) { let response = await querier.sendPostRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/introspect`), + new normalisedURLPath_1.default(`/recipe/oauth/introspect`), { - $isFormData: true, token: input.token, + scope: + (_h = + (_g = (_f = input.requirements) === null || _f === void 0 ? void 0 : _f.scopes) === + null || _g === void 0 + ? void 0 + : _g.join(" ")) !== null && _h !== void 0 + ? _h + : undefined, }, input.userContext ); - // TODO: fix after the core interface is there - if (response.status !== "OK" || response.data.active !== true) { - throw new Error(response.data.error); + if (response.status !== "OK" || response.active !== true) { + throw new Error(response.error); } } return { status: "OK", payload: payload }; }, revokeToken: async function (input) { const requestBody = { - $isFormData: true, token: input.token, }; if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { requestBody.authorizationHeader = input.authorizationHeader; } else { if ("clientId" in input && input.clientId !== undefined) { - requestBody.client_id = input.clientId; + requestBody.clientId = input.clientId; } if ("clientSecret" in input && input.clientSecret !== undefined) { - requestBody.client_secret = input.clientSecret; + requestBody.clientSecret = input.clientSecret; } } const res = await querier.sendPostRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/pub/revoke`), + new normalisedURLPath_1.default(`/recipe/oauth/token/revoke`), requestBody, input.userContext ); if (res.status !== "OK") { return { statusCode: res.statusCode, - error: res.data.error, - errorDescription: res.data.error_description, + error: res.error, + errorDescription: res.error_description, }; } return { status: "OK" }; }, introspectToken: async function ({ token, scopes, userContext }) { - // Determine if the token is an access token by checking if it doesn't start with "ory_rt" - const isAccessToken = !token.startsWith("ory_rt"); + // Determine if the token is an access token by checking if it doesn't start with "st_rt" + const isAccessToken = !token.startsWith("st_rt"); // Attempt to validate the access token locally // If it fails, the token is not active, and we return early if (isAccessToken) { @@ -587,15 +643,14 @@ function getRecipeInterface( // For tokens that passed local validation or if it's a refresh token, // validate the token with the database by calling the core introspection endpoint const res = await querier.sendPostRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/introspect`), + new normalisedURLPath_1.default(`/recipe/oauth/introspect`), { - $isFormData: true, token, scope: scopes ? scopes.join(" ") : undefined, }, userContext ); - return res.data; + return res; }, endSession: async function (input) { /** @@ -611,7 +666,7 @@ function getRecipeInterface( * - Redirects to the `post_logout_redirect_uri` or the default logout fallback page. */ const resp = await querier.sendGetRequestWithResponseHeaders( - new normalisedURLPath_1.default(`/recipe/oauth2/pub/sessions/logout`), + new normalisedURLPath_1.default(`/recipe/oauth/sessions/logout`), input.params, {}, input.userContext @@ -653,27 +708,27 @@ function getRecipeInterface( }, acceptLogoutRequest: async function (input) { const resp = await querier.sendPutRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/auth/requests/logout/accept`), + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/logout/accept`), {}, { logout_challenge: input.challenge }, input.userContext ); return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to) + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirect_to) // NOTE: This renaming only applies to this endpoint, hence not part of the generic "getUpdatedRedirectTo" function. .replace("/sessions/logout", "/end_session"), }; }, rejectLogoutRequest: async function (input) { const resp = await querier.sendPutRequest( - new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/auth/requests/logout/reject`), + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/logout/reject`), {}, { logout_challenge: input.challenge }, input.userContext ); if (resp.status != "OK") { - throw new Error(resp.data.error); + throw new Error(resp.error); } return { status: "OK" }; }, diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts index 2165d097b..c4a8ce995 100644 --- a/lib/build/recipe/oauth2provider/types.d.ts +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -117,7 +117,9 @@ export declare type RecipeInterface = { handledAt?: string; remember?: boolean; rememberFor?: number; - session?: any; + tenantId: string; + rsub: string; + sessionHandle: string; userContext: UserContext; }): Promise<{ redirectTo: string; @@ -240,16 +242,16 @@ export declare type RecipeInterface = { payload: JSONObject; }>; buildAccessTokenPayload(input: { - user: User; + user: User | undefined; client: OAuth2Client; - sessionHandle: string; + sessionHandle: string | undefined; scopes: string[]; userContext: UserContext; }): Promise; buildIdTokenPayload(input: { - user: User; + user: User | undefined; client: OAuth2Client; - sessionHandle: string; + sessionHandle: string | undefined; scopes: string[]; userContext: UserContext; }): Promise; diff --git a/lib/ts/combinedRemoteJWKSet.ts b/lib/ts/combinedRemoteJWKSet.ts index 9da2b8b07..212e5d108 100644 --- a/lib/ts/combinedRemoteJWKSet.ts +++ b/lib/ts/combinedRemoteJWKSet.ts @@ -14,10 +14,6 @@ export function resetCombinedJWKS() { combinedJWKS = undefined; } -// TODO: remove this after proper core support -const hydraJWKS = createRemoteJWKSet(new URL("http://localhost:4444/.well-known/jwks.json"), { - cooldownDuration: JWKCacheCooldownInMs, -}); /** The function returned by this getter fetches all JWKs from the first available core instance. This combines the other JWKS functions to become error resistant. @@ -38,10 +34,6 @@ export function getCombinedJWKS() { combinedJWKS = async (...args) => { let lastError = undefined; - if (!args[0]?.kid?.startsWith("s-") && !args[0]?.kid?.startsWith("d-")) { - return hydraJWKS(...args); - } - if (JWKS.length === 0) { throw Error( "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index 895239ca9..7c40db94a 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -23,11 +23,6 @@ import { UserContext } from "./types"; import { NetworkInterceptor } from "./types"; import SuperTokens from "./supertokens"; -export const hydraPubDomain = process.env.HYDRA_PUB ?? "http://localhost:4444"; // This will be used as a domain for paths starting with hydraPubPathPrefix -const hydraAdmDomain = process.env.HYDRA_ADM ?? "http://localhost:4445"; // This will be used as a domain for paths starting with hydraAdmPathPrefix -const hydraPubPathPrefix = "/recipe/oauth2/pub"; // Replaced with "/oauth2" when sending the request (/recipe/oauth2/pub/token -> /oauth2/token) -const hydraAdmPathPrefix = "/recipe/oauth2/admin"; // Replaced with "/admin" when sending the request (/recipe/oauth2/admin/clients -> /admin/clients) - export class Querier { private static initCalled = false; private static hosts: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[] | undefined = undefined; @@ -160,11 +155,6 @@ export class Querier { // path should start with "/" sendPostRequest = async (path: NormalisedURLPath, body: any, userContext: UserContext): Promise => { this.invalidateCoreCallCache(userContext); - // TODO: remove FormData - const isForm = body !== undefined && body["$isFormData"]; - if (isForm) { - delete body["$isFormData"]; - } const { body: respBody } = await this.sendRequestHelper( path, @@ -174,11 +164,7 @@ export class Querier { let headers: any = { "cdi-version": apiVersion, }; - if (isForm) { - headers["content-type"] = "application/x-www-form-urlencoded"; - } else { - headers["content-type"] = "application/json; charset=utf-8"; - } + headers["content-type"] = "application/json; charset=utf-8"; // TODO: Remove this after core changes are done if (body !== undefined && body["authorizationHeader"]) { @@ -216,11 +202,7 @@ export class Querier { } return doFetch(url, { method: "POST", - body: isForm - ? new URLSearchParams(Object.entries(body)).toString() - : body !== undefined - ? JSON.stringify(body) - : undefined, + body: body !== undefined ? JSON.stringify(body) : undefined, headers, }); }, @@ -506,6 +488,12 @@ export class Querier { ); finalURL.search = searchParams.toString(); + console.log( + "finalURL", + finalURL.toString(), + "body", + body !== undefined ? JSON.stringify(body) : undefined + ); return doFetch(finalURL.toString(), { method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined, @@ -613,19 +601,6 @@ export class Querier { let currentBasePath: string = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); let strPath = path.getAsStringDangerous(); - const isHydraAPICall = strPath.startsWith(hydraAdmPathPrefix) || strPath.startsWith(hydraPubPathPrefix); - - if (strPath.startsWith(hydraPubPathPrefix)) { - currentDomain = hydraPubDomain; - currentBasePath = ""; - strPath = strPath.replace(hydraPubPathPrefix, "/oauth2"); - } - - if (strPath.startsWith(hydraAdmPathPrefix)) { - currentDomain = hydraAdmDomain; - currentBasePath = ""; - strPath = strPath.replace(hydraAdmPathPrefix, "/admin"); - } const url = currentDomain + currentBasePath + strPath; const maxRetries = 5; @@ -648,11 +623,6 @@ export class Querier { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); } - // TODO: Temporary solution for handling Hydra API calls. Remove when Hydra is no longer called directly. - if (isHydraAPICall) { - return handleHydraAPICall(response); - } - if (response.status !== 200) { throw response; } @@ -703,29 +673,3 @@ export class Querier { } }; } - -async function handleHydraAPICall(response: Response) { - const contentType = response.headers.get("Content-Type"); - - if (contentType?.startsWith("application/json")) { - 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", - statusCode: response.status, - data: await response.clone().text(), - }, - headers: response.headers, - }; - } - - return { body: { status: response.ok ? "OK" : "ERROR", statusCode: response.status }, headers: response.headers }; -} diff --git a/lib/ts/recipe/jwt/api/implementation.ts b/lib/ts/recipe/jwt/api/implementation.ts index 03bcab7ec..4308c7cf1 100644 --- a/lib/ts/recipe/jwt/api/implementation.ts +++ b/lib/ts/recipe/jwt/api/implementation.ts @@ -31,17 +31,6 @@ export default function getAPIImplementation(): APIInterface { options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false); } - const oauth2Provider = require("../../oauth2provider/recipe").default.getInstance(); - - // TODO: dirty hack until we get core support - if (oauth2Provider !== undefined) { - const oauth2JWKSRes = await fetch("http://localhost:4444/.well-known/jwks.json"); - if (oauth2JWKSRes.ok) { - const oauth2RespBody = await oauth2JWKSRes.json(); - resp.keys = resp.keys.concat(oauth2RespBody.keys); - } - } - return { keys: resp.keys, }; diff --git a/lib/ts/recipe/oauth2client/api/implementation.ts b/lib/ts/recipe/oauth2client/api/implementation.ts index 7e18629e0..613b81937 100644 --- a/lib/ts/recipe/oauth2client/api/implementation.ts +++ b/lib/ts/recipe/oauth2client/api/implementation.ts @@ -5,9 +5,22 @@ import { OAuthTokens } from "../types"; export default function getAPIInterface(): APIInterface { return { signInPOST: async function (input) { - const { options, tenantId, userContext } = input; + const { options, tenantId, userContext, clientId } = input; - const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext }); + 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 = {}; diff --git a/lib/ts/recipe/oauth2client/api/signin.ts b/lib/ts/recipe/oauth2client/api/signin.ts index 6e89436f9..345a56770 100644 --- a/lib/ts/recipe/oauth2client/api/signin.ts +++ b/lib/ts/recipe/oauth2client/api/signin.ts @@ -40,6 +40,13 @@ export default async function signInAPI( }; 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({ @@ -73,6 +80,7 @@ export default async function signInAPI( let result = await apiImplementation.signInPOST({ tenantId, + clientId: bodyParams.clientId, redirectURIInfo, oAuthTokens, options, diff --git a/lib/ts/recipe/oauth2client/index.ts b/lib/ts/recipe/oauth2client/index.ts index d2b2e2a02..9d175ef1c 100644 --- a/lib/ts/recipe/oauth2client/index.ts +++ b/lib/ts/recipe/oauth2client/index.ts @@ -14,6 +14,7 @@ */ import { getUserContext } from "../../utils"; +import { parseJWTWithoutSignatureVerification } from "../session/jwt"; import Recipe from "./recipe"; import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types"; @@ -26,11 +27,23 @@ export default class Wrapper { redirectURIQueryParams: any; pkceCodeVerifier?: string | undefined; }, + clientId?: string, userContext?: Record ) { - const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; + 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({ @@ -43,7 +56,12 @@ export default class Wrapper { 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({ diff --git a/lib/ts/recipe/oauth2client/recipeImplementation.ts b/lib/ts/recipe/oauth2client/recipeImplementation.ts index a5115c815..e951bae91 100644 --- a/lib/ts/recipe/oauth2client/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2client/recipeImplementation.ts @@ -20,7 +20,7 @@ import { logDebugMessage } from "../../logger"; import { JWTVerifyGetKey, createRemoteJWKSet } from "jose"; export default function getRecipeImplementation(_querier: Querier, config: TypeNormalisedInput): RecipeInterface { - let providerConfigWithOIDCInfo: ProviderConfigWithOIDCInfo | null = null; + let providerConfigsWithOIDCInfo: Record = {}; return { signIn: async function ({ @@ -53,11 +53,14 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN rawUserInfo, }; }, - getProviderConfig: async function () { - if (providerConfigWithOIDCInfo !== null) { - return providerConfigWithOIDCInfo; + getProviderConfig: async function ({ clientId }) { + if (providerConfigsWithOIDCInfo[clientId] !== null) { + return providerConfigsWithOIDCInfo[clientId]; } - const oidcInfo = await getOIDCDiscoveryInfo(config.providerConfig.oidcDiscoveryEndpoint); + const providerConfig = config.providerConfigs.find( + (providerConfig) => providerConfig.clientId === clientId + )!; + const oidcInfo = await getOIDCDiscoveryInfo(config.providerConfigs[0].oidcDiscoveryEndpoint); if (oidcInfo.authorization_endpoint === undefined) { throw new Error("Failed to authorization_endpoint from the oidcDiscoveryEndpoint."); @@ -72,14 +75,15 @@ export default function getRecipeImplementation(_querier: Querier, config: TypeN throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); } - providerConfigWithOIDCInfo = { - ...config.providerConfig, + providerConfigsWithOIDCInfo[clientId] = { + ...providerConfig, authorizationEndpoint: oidcInfo.authorization_endpoint, tokenEndpoint: oidcInfo.token_endpoint, userInfoEndpoint: oidcInfo.userinfo_endpoint, jwksURI: oidcInfo.jwks_uri, }; - return providerConfigWithOIDCInfo; + + return providerConfigsWithOIDCInfo[clientId]; }, exchangeAuthCodeForOAuthTokens: async function (this: RecipeInterface, { providerConfig, redirectURIInfo }) { if (providerConfig.tokenEndpoint === undefined) { diff --git a/lib/ts/recipe/oauth2client/types.ts b/lib/ts/recipe/oauth2client/types.ts index 740d00d16..795e39086 100644 --- a/lib/ts/recipe/oauth2client/types.ts +++ b/lib/ts/recipe/oauth2client/types.ts @@ -53,7 +53,7 @@ export type OAuthTokenResponse = { }; export type TypeInput = { - providerConfig: ProviderConfigInput; + providerConfigs: ProviderConfigInput[]; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -64,7 +64,7 @@ export type TypeInput = { }; export type TypeNormalisedInput = { - providerConfig: ProviderConfigInput; + providerConfigs: ProviderConfigInput[]; override: { functions: ( originalImplementation: RecipeInterface, @@ -75,7 +75,7 @@ export type TypeNormalisedInput = { }; export type RecipeInterface = { - getProviderConfig(input: { userContext: UserContext }): Promise; + getProviderConfig(input: { clientId: string; userContext: UserContext }): Promise; signIn(input: { userId: string; @@ -126,6 +126,7 @@ export type APIInterface = { signInPOST: ( input: { tenantId: string; + clientId?: string; options: APIOptions; userContext: UserContext; } & ( diff --git a/lib/ts/recipe/oauth2client/utils.ts b/lib/ts/recipe/oauth2client/utils.ts index 5996c2490..b19efc0f9 100644 --- a/lib/ts/recipe/oauth2client/utils.ts +++ b/lib/ts/recipe/oauth2client/utils.ts @@ -17,23 +17,22 @@ import { NormalisedAppinfo } from "../../types"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; export function validateAndNormaliseUserInput(_appInfo: NormalisedAppinfo, config: TypeInput): TypeNormalisedInput { - if (config === undefined || config.providerConfig === undefined) { - throw new Error("Please pass providerConfig argument in the OAuth2Client recipe."); + if (config === undefined || config.providerConfigs === undefined) { + throw new Error("Please pass providerConfigs argument in the OAuth2Client recipe."); } - if (config.providerConfig.clientId === undefined) { - throw new Error("Please pass clientId argument in the OAuth2Client providerConfig."); + if (config.providerConfigs.some((providerConfig) => providerConfig.clientId === undefined)) { + throw new Error("Please pass clientId for all providerConfigs."); } - // TODO: Decide on the prefix and also if we will allow users to customise clientIds - // if (!config.providerConfig.clientId.startsWith("supertokens_")) { - // 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.every((providerConfig) => providerConfig.clientId.startsWith("supertokens_"))) { + throw new Error( + `Only Supertokens OAuth ClientIds are supported in the OAuth2Client recipe. For any other OAuth Clients use the ThirdParty recipe.` + ); + } - if (config.providerConfig.oidcDiscoveryEndpoint === undefined) { - throw new Error("Please pass oidcDiscoveryEndpoint argument in the OAuth2Client providerConfig."); + if (config.providerConfigs.some((providerConfig) => providerConfig.oidcDiscoveryEndpoint === undefined)) { + throw new Error("Please pass oidcDiscoveryEndpoint for all providerConfigs."); } let override = { @@ -43,7 +42,7 @@ export function validateAndNormaliseUserInput(_appInfo: NormalisedAppinfo, confi }; return { - providerConfig: config.providerConfig, + providerConfigs: config.providerConfigs, override, }; } diff --git a/lib/ts/recipe/oauth2provider/api/auth.ts b/lib/ts/recipe/oauth2provider/api/auth.ts index c6fc92e10..c283f77b8 100644 --- a/lib/ts/recipe/oauth2provider/api/auth.ts +++ b/lib/ts/recipe/oauth2provider/api/auth.ts @@ -45,6 +45,7 @@ export default async function authGET( shouldTryRefresh = false; } } + console.log({ n: "authGET", shouldTryRefresh, origURL }); let response = await apiImplementation.authGET({ options, @@ -54,6 +55,7 @@ export default async function authGET( shouldTryRefresh, userContext, }); + console.log({ n: "authGET", response }); if ("redirectTo" in response) { if (response.setCookie) { const cookieStr = setCookieParser.splitCookiesString(response.setCookie); diff --git a/lib/ts/recipe/oauth2provider/api/token.ts b/lib/ts/recipe/oauth2provider/api/token.ts index 0ed290282..92cf44d11 100644 --- a/lib/ts/recipe/oauth2provider/api/token.ts +++ b/lib/ts/recipe/oauth2provider/api/token.ts @@ -35,6 +35,8 @@ export default async function tokenPOST( userContext, }); + console.log("response", response); + if ("statusCode" in response && response.statusCode !== 200) { sendNon200Response(options.res, response.statusCode!, { error: response.error, diff --git a/lib/ts/recipe/oauth2provider/api/utils.ts b/lib/ts/recipe/oauth2provider/api/utils.ts index b0d28001a..86624b829 100644 --- a/lib/ts/recipe/oauth2provider/api/utils.ts +++ b/lib/ts/recipe/oauth2provider/api/utils.ts @@ -39,6 +39,7 @@ export async function loginGET({ const incomingAuthUrlQueryParams = new URLSearchParams(loginRequest.requestUrl.split("?")[1]); const promptParam = incomingAuthUrlQueryParams.get("prompt") ?? incomingAuthUrlQueryParams.get("st_prompt"); const maxAgeParam = incomingAuthUrlQueryParams.get("max_age"); + console.log({ n: "loginGET", shouldTryRefresh, loginRequest, sessionInfo }); if (maxAgeParam !== null) { try { const maxAgeParsed = Number.parseInt(maxAgeParam); @@ -93,6 +94,7 @@ export async function loginGET({ }) .getAsStringDangerous(); const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + console.log({ n: "loginGET2", shouldTryRefresh }); if (shouldTryRefresh) { const websiteDomain = appInfo .getOrigin({ @@ -218,6 +220,12 @@ export async function handleLoginInternalRedirects({ cookie?: string; userContext: UserContext; }): Promise<{ redirectTo: string; setCookie?: string }> { + console.log({ + n: "handleLoginInternalRedirects", + response, + isLoginInternalRedirect: isLoginInternalRedirect(response.redirectTo), + shouldTryRefresh, + }); if (!isLoginInternalRedirect(response.redirectTo)) { return response; } @@ -228,6 +236,7 @@ export async function handleLoginInternalRedirects({ let redirectCount = 0; while (redirectCount < maxRedirects && isLoginInternalRedirect(response.redirectTo)) { + console.log({ n: "handleLoginInternalRedirects2", response, shouldTryRefresh }); cookie = getMergedCookies({ cookie, setCookie: response.setCookie }); const queryString = response.redirectTo.split("?")[1]; diff --git a/lib/ts/recipe/oauth2provider/recipe.ts b/lib/ts/recipe/oauth2provider/recipe.ts index 42fcc997f..6332ea471 100644 --- a/lib/ts/recipe/oauth2provider/recipe.ts +++ b/lib/ts/recipe/oauth2provider/recipe.ts @@ -56,9 +56,6 @@ import introspectTokenPOST from "./api/introspectToken"; import { endSessionGET, endSessionPOST } from "./api/endSession"; import { logoutPOST } from "./api/logout"; import { getSessionInformation } from "../session"; -import { send200Response } from "../../utils"; - -const tokenHookMap = new Map(); export default class Recipe extends RecipeModule { static RECIPE_ID = "oauth2provider"; @@ -85,8 +82,7 @@ export default class Recipe extends RecipeModule { appInfo, this.getDefaultAccessTokenPayload.bind(this), this.getDefaultIdTokenPayload.bind(this), - this.getDefaultUserInfoPayload.bind(this), - this.saveTokensForHook.bind(this) + this.getDefaultUserInfoPayload.bind(this) ) ); this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); @@ -137,9 +133,6 @@ export default class Recipe extends RecipeModule { addIdTokenBuilderFromOtherRecipe = (idTokenBuilder: PayloadBuilderFunction) => { this.idTokenBuilders.push(idTokenBuilder); }; - saveTokensForHook = (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => { - tokenHookMap.set(sessionHandle, { idToken, accessToken }); - }; /* RecipeModule functions */ @@ -187,13 +180,7 @@ export default class Recipe extends RecipeModule { id: INTROSPECT_TOKEN_PATH, disabled: this.apiImpl.introspectTokenPOST === undefined, }, - { - // TODO: remove this once we get core support - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/oauth/token-hook"), - id: "token-hook", - disabled: false, - }, + { method: "get", pathWithoutApiBasePath: new NormalisedURLPath(END_SESSION_PATH), @@ -263,24 +250,6 @@ export default class Recipe extends RecipeModule { if (id === LOGOUT_PATH && method === "post") { return logoutPOST(this.apiImpl, options, userContext); } - if (id === "token-hook") { - const body = await options.req.getBodyAsJSONOrFormData(); - const sessionHandle = body.session.extra.sessionHandle; - const tokens = tokenHookMap.get(sessionHandle); - - if (tokens !== undefined) { - const { idToken, accessToken } = tokens; - send200Response(options.res, { - session: { - access_token: accessToken, - id_token: idToken, - }, - }); - } else { - send200Response(options.res, {}); - } - return true; - } throw new Error("Should never come here: handleAPIRequest called with unknown id"); }; @@ -302,7 +271,6 @@ export default class Recipe extends RecipeModule { throw new Error("Session not found"); } let payload: JSONObject = { - iss: this.appInfo.apiDomain.getAsStringDangerous() + this.appInfo.apiBasePath.getAsStringDangerous(), tId: sessionInfo.tenantId, rsub: sessionInfo.recipeUserId.getAsString(), sessionHandle: sessionHandle, diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts index 463fcebc1..c84c70318 100644 --- a/lib/ts/recipe/oauth2provider/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -15,7 +15,7 @@ import * as jose from "jose"; import NormalisedURLPath from "../../normalisedURLPath"; -import { Querier, hydraPubDomain } from "../../querier"; +import { Querier } from "../../querier"; import { JSONObject, NormalisedAppinfo } from "../../types"; import { RecipeInterface, @@ -25,17 +25,27 @@ import { PayloadBuilderFunction, UserInfoBuilderFunction, } from "./types"; -import { toSnakeCase, transformObjectKeys } from "../../utils"; import { OAuth2Client } from "./OAuth2Client"; import { getUser } from "../.."; import { getCombinedJWKS } from "../../combinedRemoteJWKSet"; -import { getSessionInformation } from "../session"; -// TODO: Remove this core changes are done function getUpdatedRedirectTo(appInfo: NormalisedAppinfo, redirectTo: string) { - return redirectTo - .replace(hydraPubDomain, appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous()) - .replace("oauth2/", "oauth/"); + return redirectTo.replace( + "{apiDomain}", + appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() + ); + // .replace("oauth2/", "oauth/"); +} + +function copyAndCleanRequestBodyInput(input: any): any { + let result = { + ...input, + }; + delete result.userContext; + delete result.tenantId; + delete result.session; + + return result; } export default function getRecipeInterface( @@ -44,136 +54,154 @@ export default function getRecipeInterface( appInfo: NormalisedAppinfo, getDefaultAccessTokenPayload: PayloadBuilderFunction, getDefaultIdTokenPayload: PayloadBuilderFunction, - getDefaultUserInfoPayload: UserInfoBuilderFunction, - saveTokensForHook: (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => void + getDefaultUserInfoPayload: UserInfoBuilderFunction ): RecipeInterface { return { getLoginRequest: async function (this: RecipeInterface, input): Promise { const resp = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/oauth2/admin/oauth2/auth/requests/login"), - { login_challenge: input.challenge }, + new NormalisedURLPath("/recipe/oauth/auth/requests/login"), + { loginChallenge: input.challenge }, input.userContext ); return { - challenge: resp.data.challenge, - client: OAuth2Client.fromAPIResponse(resp.data.client), - oidcContext: resp.data.oidc_context, - requestUrl: resp.data.request_url, - requestedAccessTokenAudience: resp.data.requested_access_token_audience, - requestedScope: resp.data.requested_scope, - sessionId: resp.data.session_id, - skip: resp.data.skip, - subject: resp.data.subject, + challenge: resp.challenge, + client: OAuth2Client.fromAPIResponse(resp.client), + oidcContext: resp.oidcContext, + requestUrl: resp.requestUrl, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + sessionId: resp.sessionId, + skip: resp.skip, + subject: resp.subject, }; }, acceptLoginRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { const resp = await querier.sendPutRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/auth/requests/login/accept`), + new NormalisedURLPath(`/recipe/oauth/auth/requests/login/accept`), { acr: input.acr, amr: input.amr, context: input.context, - extend_session_lifespan: input.extendSessionLifespan, - force_subject_identifier: input.forceSubjectIdentifier, - identity_provider_session_id: input.identityProviderSessionId, + extendSessionLifespan: input.extendSessionLifespan, + forceSubjectIdentifier: input.forceSubjectIdentifier, + identityProviderSessionId: input.identityProviderSessionId, remember: input.remember, - remember_for: input.rememberFor, + rememberFor: input.rememberFor, subject: input.subject, }, { - login_challenge: input.challenge, + loginChallenge: input.challenge, }, input.userContext ); + console.log("acceptLoginRequest resp", resp); + return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), }; }, rejectLoginRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { const resp = await querier.sendPutRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/auth/requests/login/reject`), - { - error: input.error.error, - error_description: input.error.errorDescription, - status_code: input.error.statusCode, - }, + new NormalisedURLPath(`/recipe/oauth/auth/requests/login/reject`), + copyAndCleanRequestBodyInput(input), { - login_challenge: input.challenge, + loginChallenge: input.challenge, }, input.userContext ); return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), }; }, getConsentRequest: async function (this: RecipeInterface, input): Promise { const resp = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/oauth2/admin/oauth2/auth/requests/consent"), - { consent_challenge: input.challenge }, + new NormalisedURLPath("/recipe/oauth/auth/requests/consent"), + { consentChallenge: input.challenge }, input.userContext ); return { - acr: resp.data.acr, - amr: resp.data.amr, - challenge: resp.data.challenge, - client: OAuth2Client.fromAPIResponse(resp.data.client), - context: resp.data.context, - loginChallenge: resp.data.login_challenge, - loginSessionId: resp.data.login_session_id, - oidcContext: resp.data.oidc_context, - requestedAccessTokenAudience: resp.data.requested_access_token_audience, - requestedScope: resp.data.requested_scope, - skip: resp.data.skip, - subject: resp.data.subject, + acr: resp.acr, + amr: resp.amr, + challenge: resp.challenge, + client: OAuth2Client.fromAPIResponse(resp.client), + context: resp.context, + loginChallenge: resp.loginChallenge, + loginSessionId: resp.loginSessionId, + oidcContext: resp.oidcContext, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + skip: resp.skip, + subject: resp.subject, }; }, acceptConsentRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { const resp = await querier.sendPutRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/auth/requests/consent/accept`), + new NormalisedURLPath(`/recipe/oauth/auth/requests/consent/accept`), { context: input.context, - grant_access_token_audience: input.grantAccessTokenAudience, - grant_scope: input.grantScope, - handled_at: input.handledAt, + grantAccessTokenAudience: input.grantAccessTokenAudience, + grantScope: input.grantScope, + handledAt: input.handledAt, remember: input.remember, - remember_for: input.rememberFor, - session: input.session, + rememberFor: input.rememberFor, + iss: appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(), + tId: input.tenantId, + rsub: input.rsub, + sessionHandle: input.sessionHandle, }, { - consent_challenge: input.challenge, + consentChallenge: input.challenge, }, input.userContext ); + console.log("acceptConsentRequest resp", { + body: { + context: input.context, + grantAccessTokenAudience: input.grantAccessTokenAudience, + grantScope: input.grantScope, + handledAt: input.handledAt, + remember: input.remember, + rememberFor: input.rememberFor, + iss: appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(), + tId: input.tenantId, + rsub: input.rsub, + sessionHandle: input.sessionHandle, + }, + queryParams: { + consentChallenge: input.challenge, + }, + resp, + }); return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), }; }, rejectConsentRequest: async function (this: RecipeInterface, input) { const resp = await querier.sendPutRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/auth/requests/consent/reject`), + new NormalisedURLPath(`/recipe/oauth/auth/requests/consent/reject`), { error: input.error.error, - error_description: input.error.errorDescription, - status_code: input.error.statusCode, + errorDescription: input.error.errorDescription, + statusCode: input.error.statusCode, }, { - consent_challenge: input.challenge, + consentChallenge: input.challenge, }, input.userContext ); - + console.log("rejectConsentRequest resp", resp); return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to), + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), }; }, authorization: async function (this: RecipeInterface, input) { @@ -184,17 +212,67 @@ export default function getRecipeInterface( } } - const resp = await querier.sendGetRequestWithResponseHeaders( - new NormalisedURLPath(`/recipe/oauth2/pub/auth`), - input.params, + let payloads: { idToken: JSONObject | undefined; accessToken: JSONObject | undefined } | undefined; + + if (input.params.client_id === undefined) { + throw new Error("client_id is required"); + } + + const responseTypes = input.params.response_type?.split(" ") ?? []; + if ( + input.session !== undefined && + (responseTypes.includes("token") || responseTypes.includes("id_token")) + ) { + const clientInfo = await this.getOAuth2Client({ + clientId: input.params.client_id as string, + userContext: input.userContext, + }); + + if (clientInfo.status === "ERROR") { + throw new Error(clientInfo.error); + } + const client = clientInfo.client; + + const user = await getUser(input.session.getUserId()); + if (!user) { + throw new Error("User not found"); + } + + const idToken = responseTypes.includes("id_token") + ? await this.buildIdTokenPayload({ + user, + client, + sessionHandle: input.session.getHandle(), + scopes: input.params.scope?.split(" ") || [], + userContext: input.userContext, + }) + : undefined; + const accessToken = responseTypes.includes("token") + ? await this.buildAccessTokenPayload({ + user, + client, + sessionHandle: input.session.getHandle(), + scopes: input.params.scope?.split(" ") || [], + userContext: input.userContext, + }) + : undefined; + payloads = { + idToken, + accessToken, + }; + } + + const resp = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/auth`), { - // TODO: if session is not set also clear the oauth2 cookie - Cookie: `${input.cookies}`, + params: input.params, + cookies: input.cookies, + session: payloads, }, input.userContext ); - const redirectTo = getUpdatedRedirectTo(appInfo, resp.headers.get("Location")!); + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); if (redirectTo === undefined) { throw new Error(resp.body); } @@ -206,55 +284,73 @@ export default function getRecipeInterface( userContext: input.userContext, }); - const user = await getUser(input.session.getUserId()); - if (!user) { - throw new Error("Should not happen"); - } - const idToken = await this.buildIdTokenPayload({ - user, - client: consentRequest.client!, - sessionHandle: input.session.getHandle(), - scopes: consentRequest.requestedScope || [], - userContext: input.userContext, - }); - const accessTokenPayload = await this.buildAccessTokenPayload({ - user, - client: consentRequest.client!, - sessionHandle: input.session.getHandle(), - scopes: consentRequest.requestedScope || [], - userContext: input.userContext, - }); - - const sessionInfo = await getSessionInformation(input.session.getHandle()); - if (!sessionInfo) { - throw new Error("Session not found"); - } - const consentRes = await this.acceptConsentRequest({ - ...input, + userContext: input.userContext, challenge: consentRequest.challenge, grantAccessTokenAudience: consentRequest.requestedAccessTokenAudience, grantScope: consentRequest.requestedScope, - remember: true, // TODO: verify that we need this - session: { - id_token: idToken, - access_token: accessTokenPayload, - }, - handledAt: new Date(sessionInfo.timeCreated).toISOString(), + tenantId: input.session.getTenantId(), + rsub: input.session.getRecipeUserId().getAsString(), + sessionHandle: input.session.getHandle(), }); return { redirectTo: consentRes.redirectTo, - setCookie: resp.headers.get("set-cookie") ?? undefined, + setCookie: resp.cookies, }; } - return { redirectTo, setCookie: resp.headers.get("set-cookie") ?? undefined }; + return { redirectTo, setCookie: resp.cookies }; }, tokenExchange: async function (this: RecipeInterface, input) { - const body: any = { $isFormData: true }; // TODO: we ideally want to avoid using formdata, the core can do the translation - for (const key in input.body) { - body[key] = input.body[key]; + const body: any = { + inputBody: input.body, + authorizationHeader: input.authorizationHeader, + }; + + body.iss = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + + if (input.body.grant_type === "client_credentials") { + if (input.body.client_id === undefined) { + return { + statusCode: 400, + error: "invalid_request", + errorDescription: "client_id is required", + }; + } + + const scopes = input.body.scope?.split(" ") ?? []; + const clientInfo = await this.getOAuth2Client({ + clientId: input.body.client_id as string, + userContext: input.userContext, + }); + console.log("clientInfo", clientInfo); + if (clientInfo.status === "ERROR") { + return { + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorHint, + }; + } + const client = clientInfo.client; + const idToken = await this.buildIdTokenPayload({ + user: undefined, + client, + sessionHandle: undefined, + scopes, + userContext: input.userContext, + }); + const accessTokenPayload = await this.buildAccessTokenPayload({ + user: undefined, + client, + sessionHandle: undefined, + scopes, + userContext: input.userContext, + }); + body["session"] = { + idToken: idToken, + accessToken: accessTokenPayload, + }; } if (input.body.grant_type === "refresh_token") { @@ -264,6 +360,7 @@ export default function getRecipeInterface( scopes, userContext: input.userContext, }); + console.log("tokenInfo", input.body.refresh_token, tokenInfo); if (tokenInfo.active === true) { const sessionHandle = (tokenInfo.ext as any).sessionHandle as string; @@ -287,7 +384,7 @@ export default function getRecipeInterface( const idToken = await this.buildIdTokenPayload({ user, client, - sessionHandle: sessionHandle, + sessionHandle, scopes, userContext: input.userContext, }); @@ -299,11 +396,9 @@ export default function getRecipeInterface( userContext: input.userContext, }); body["session"] = { - id_token: idToken, - access_token: accessTokenPayload, + idToken: idToken, + accessToken: accessTokenPayload, }; - - saveTokensForHook(sessionHandle, idToken, accessTokenPayload); } } @@ -311,8 +406,9 @@ export default function getRecipeInterface( body["authorizationHeader"] = input.authorizationHeader; } + console.log("/recipe/oauth/token", body); const res = await querier.sendPostRequest( - new NormalisedURLPath(`/recipe/oauth2/pub/token`), + new NormalisedURLPath(`/recipe/oauth/token`), body, input.userContext ); @@ -320,138 +416,98 @@ export default function getRecipeInterface( if (res.status !== "OK") { return { statusCode: res.statusCode, - error: res.data.error, - errorDescription: res.data.error_description, + error: res.error, + errorDescription: res.error_description, }; } - return res.data; + return res; }, getOAuth2Clients: async function (input) { let response = await querier.sendGetRequestWithResponseHeaders( - new NormalisedURLPath(`/recipe/oauth2/admin/clients`), + new NormalisedURLPath(`/recipe/oauth/clients`), { - ...transformObjectKeys(input, "snake-case"), - page_token: input.paginationToken, + ...copyAndCleanRequestBodyInput(input), + pageToken: input.paginationToken, }, {}, input.userContext ); if (response.body.status === "OK") { - // Pagination info is in the Link header, containing comma-separated links: - // "first", "next" (if applicable). - // Example: Link: ; rel="first", ; rel="next" - - // We parse the nextPaginationToken from the Link header using RegExp - let nextPaginationToken: string | undefined; - const linkHeader = response.headers.get("link") ?? ""; - - const nextLinkMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/); - if (nextLinkMatch) { - const url = nextLinkMatch[1]; - const urlParams = new URLSearchParams(url.split("?")[1]); - nextPaginationToken = urlParams.get("page_token") as string; - } - return { status: "OK", - clients: response.body.data.map((client: any) => OAuth2Client.fromAPIResponse(client)), - nextPaginationToken, + clients: response.body.clients.map((client: any) => OAuth2Client.fromAPIResponse(client)), + nextPaginationToken: response.body.nextPaginationToken, }; } else { return { status: "ERROR", - error: response.body.data.error, - errorHint: response.body.data.errorHint, + error: response.body.error, + errorHint: response.body.errorHint, }; } }, getOAuth2Client: async function (input) { let response = await querier.sendGetRequestWithResponseHeaders( - new NormalisedURLPath(`/recipe/oauth2/admin/clients/${input.clientId}`), - {}, + new NormalisedURLPath(`/recipe/oauth/clients`), + { clientId: input.clientId }, {}, input.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, - }; - } + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response.body), + }; + // return { + // status: "ERROR", + // error: response.body.error, + // errorHint: response.body.errorHint, + // }; }, createOAuth2Client: async function (input) { let response = await querier.sendPostRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/clients`), - { - ...transformObjectKeys(input, "snake-case"), - // TODO: these defaults should be set/enforced on the core side - access_token_strategy: "jwt", - skip_consent: true, - subject_type: "public", - }, + new NormalisedURLPath(`/recipe/oauth/clients`), + copyAndCleanRequestBodyInput(input), input.userContext ); - if (response.status === "OK") { - return { - status: "OK", - client: OAuth2Client.fromAPIResponse(response.data), - }; - } else { - return { - status: "ERROR", - error: response.data.error, - errorHint: response.data.errorHint, - }; - } + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response), + }; + // return { + // status: "ERROR", + // error: response.error, + // errorHint: response.errorHint, + // }; }, updateOAuth2Client: async function (input) { - // We convert the input into an array of "replace" operations - const requestBody = Object.entries(input).reduce< - Array<{ from: string; op: "replace"; path: string; value: any }> - >((result, [key, value]) => { - result.push({ - from: `/${toSnakeCase(key)}`, - op: "replace", - path: `/${toSnakeCase(key)}`, - value, - }); - return result; - }, []); - - let response = await querier.sendPatchRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/clients/${input.clientId}`), - requestBody, + let response = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/clients`), + copyAndCleanRequestBodyInput(input), + { clientId: input.clientId }, input.userContext ); if (response.status === "OK") { return { status: "OK", - client: OAuth2Client.fromAPIResponse(response.data), + client: OAuth2Client.fromAPIResponse(response), }; } else { return { status: "ERROR", - error: response.data.error, - errorHint: response.data.errorHint, + error: response.error, + errorHint: response.errorHint, }; } }, deleteOAuth2Client: async function (input) { - let response = await querier.sendDeleteRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/clients/${input.clientId}`), - undefined, - undefined, + let response = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/clients/remove`), + { clientId: input.clientId }, input.userContext ); @@ -460,15 +516,21 @@ export default function getRecipeInterface( } else { return { status: "ERROR", - error: response.data.error, - errorHint: response.data.errorHint, + error: response.error, + errorHint: response.errorHint, }; } }, buildAccessTokenPayload: async function (input) { + if (input.user === undefined || input.sessionHandle === undefined) { + return {}; + } return getDefaultAccessTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); }, buildIdTokenPayload: async function (input) { + if (input.user === undefined || input.sessionHandle === undefined) { + return {}; + } return getDefaultIdTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); }, buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { @@ -477,17 +539,9 @@ export default function getRecipeInterface( validateOAuth2AccessToken: async function (input) { const payload = (await jose.jwtVerify(input.token, getCombinedJWKS())).payload; - // if (payload.stt !== 1) { - // throw new Error("Wrong token type"); - // } - - // TODO: we should be able uncomment this after we get proper core support - // TODO: make this configurable? - // const expectedIssuer = - // appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); - // if (payload.iss !== expectedIssuer) { - // throw new Error("Issuer mismatch: this token was likely issued by another application or spoofed"); - // } + if (payload.stt !== 1) { + throw new Error("Wrong token type"); + } if (input.requirements?.clientId !== undefined && payload.client_id !== input.requirements.clientId) { throw new Error("The token doesn't belong to the specified client"); @@ -507,24 +561,22 @@ export default function getRecipeInterface( if (input.checkDatabase) { let response = await querier.sendPostRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/introspect`), + new NormalisedURLPath(`/recipe/oauth/introspect`), { - $isFormData: true, token: input.token, + scope: input.requirements?.scopes?.join(" ") ?? undefined, }, input.userContext ); - // TODO: fix after the core interface is there - if (response.status !== "OK" || response.data.active !== true) { - throw new Error(response.data.error); + if (response.status !== "OK" || response.active !== true) { + throw new Error(response.error); } } return { status: "OK", payload: payload as JSONObject }; }, revokeToken: async function (this: RecipeInterface, input) { const requestBody: Record = { - $isFormData: true, token: input.token, }; @@ -532,15 +584,15 @@ export default function getRecipeInterface( requestBody.authorizationHeader = input.authorizationHeader; } else { if ("clientId" in input && input.clientId !== undefined) { - requestBody.client_id = input.clientId; + requestBody.clientId = input.clientId; } if ("clientSecret" in input && input.clientSecret !== undefined) { - requestBody.client_secret = input.clientSecret; + requestBody.clientSecret = input.clientSecret; } } const res = await querier.sendPostRequest( - new NormalisedURLPath(`/recipe/oauth2/pub/revoke`), + new NormalisedURLPath(`/recipe/oauth/token/revoke`), requestBody, input.userContext ); @@ -548,8 +600,8 @@ export default function getRecipeInterface( if (res.status !== "OK") { return { statusCode: res.statusCode, - error: res.data.error, - errorDescription: res.data.error_description, + error: res.error, + errorDescription: res.error_description, }; } @@ -557,8 +609,8 @@ export default function getRecipeInterface( }, introspectToken: async function (this: RecipeInterface, { token, scopes, userContext }) { - // Determine if the token is an access token by checking if it doesn't start with "ory_rt" - const isAccessToken = !token.startsWith("ory_rt"); + // Determine if the token is an access token by checking if it doesn't start with "st_rt" + const isAccessToken = !token.startsWith("st_rt"); // Attempt to validate the access token locally // If it fails, the token is not active, and we return early @@ -578,16 +630,15 @@ export default function getRecipeInterface( // For tokens that passed local validation or if it's a refresh token, // validate the token with the database by calling the core introspection endpoint const res = await querier.sendPostRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/introspect`), + new NormalisedURLPath(`/recipe/oauth/introspect`), { - $isFormData: true, token, scope: scopes ? scopes.join(" ") : undefined, }, userContext ); - return res.data; + return res; }, endSession: async function (this: RecipeInterface, input) { @@ -605,7 +656,7 @@ export default function getRecipeInterface( */ const resp = await querier.sendGetRequestWithResponseHeaders( - new NormalisedURLPath(`/recipe/oauth2/pub/sessions/logout`), + new NormalisedURLPath(`/recipe/oauth/sessions/logout`), input.params, {}, input.userContext @@ -654,7 +705,7 @@ export default function getRecipeInterface( }, acceptLogoutRequest: async function (this: RecipeInterface, input) { const resp = await querier.sendPutRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/auth/requests/logout/accept`), + new NormalisedURLPath(`/recipe/oauth/auth/requests/logout/accept`), {}, { logout_challenge: input.challenge }, input.userContext @@ -662,21 +713,21 @@ export default function getRecipeInterface( return { // TODO: FIXME!!! - redirectTo: getUpdatedRedirectTo(appInfo, resp.data.redirect_to) + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirect_to) // NOTE: This renaming only applies to this endpoint, hence not part of the generic "getUpdatedRedirectTo" function. .replace("/sessions/logout", "/end_session"), }; }, rejectLogoutRequest: async function (this: RecipeInterface, input) { const resp = await querier.sendPutRequest( - new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/auth/requests/logout/reject`), + new NormalisedURLPath(`/recipe/oauth/auth/requests/logout/reject`), {}, { logout_challenge: input.challenge }, input.userContext ); if (resp.status != "OK") { - throw new Error(resp.data.error); + throw new Error(resp.error); } return { status: "OK" }; diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts index ee81d7868..7cd4d708d 100644 --- a/lib/ts/recipe/oauth2provider/types.ts +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -21,7 +21,6 @@ import { OAuth2Client } from "./OAuth2Client"; import { User } from "../../user"; export type TypeInput = { - // TODO: issuer? override?: { functions?: ( originalImplementation: RecipeInterface, @@ -204,8 +203,10 @@ export type RecipeInterface = { // RememberFor sets how long the consent authorization should be remembered for in seconds. If set to 0, the authorization will be remembered indefinitely. integer rememberFor?: number; - // object (Pass session data to a consent request.) - session?: any; + tenantId: string; + rsub: string; + sessionHandle: string; + userContext: UserContext; }): Promise<{ redirectTo: string }>; @@ -352,16 +353,16 @@ export type RecipeInterface = { }): Promise<{ status: "OK"; payload: JSONObject }>; buildAccessTokenPayload(input: { - user: User; + user: User | undefined; client: OAuth2Client; - sessionHandle: string; + sessionHandle: string | undefined; scopes: string[]; userContext: UserContext; }): Promise; buildIdTokenPayload(input: { - user: User; + user: User | undefined; client: OAuth2Client; - sessionHandle: string; + sessionHandle: string | undefined; scopes: string[]; userContext: UserContext; }): Promise;