supertokens-node
Version:
NodeJS driver for SuperTokens core
789 lines (788 loc) • 37.4 kB
JavaScript
;
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = getRecipeInterface;
const jose = __importStar(require("jose"));
const OAuth2Client_1 = require("./OAuth2Client");
const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet");
const constants_1 = require("../multitenancy/constants");
const utils_1 = require("../../utils");
function getUpdatedRedirectTo(appInfo, redirectTo) {
return redirectTo.replace("{apiDomain}", appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous());
}
function copyAndCleanRequestBodyInput(input) {
let result = Object.assign({}, input);
delete result.userContext;
delete result.tenantId;
delete result.session;
return result;
}
function getRecipeInterface(stInstance, querier, _config, appInfo, getDefaultAccessTokenPayload, getDefaultIdTokenPayload, getDefaultUserInfoPayload) {
return {
getLoginRequest: async function (input) {
const resp = await querier.sendGetRequest("/recipe/oauth/auth/requests/login", { loginChallenge: input.challenge }, input.userContext);
if (resp.status !== "OK") {
return {
status: "ERROR",
statusCode: resp.statusCode,
error: resp.error,
errorDescription: resp.errorDescription,
};
}
return {
status: "OK",
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("/recipe/oauth/auth/requests/login/accept", {
acr: input.acr,
amr: input.amr,
context: input.context,
extendSessionLifespan: input.extendSessionLifespan,
identityProviderSessionId: input.identityProviderSessionId,
subject: input.subject,
}, {
loginChallenge: input.challenge,
}, input.userContext);
if (resp.status === "OAUTH_ERROR") {
return Object.assign(Object.assign({}, resp), { status: "ERROR" });
}
return {
redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo),
status: "OK",
};
},
rejectLoginRequest: async function (input) {
const resp = await querier.sendPutRequest("/recipe/oauth/auth/requests/login/reject", {
error: input.error.error,
errorDescription: input.error.errorDescription,
statusCode: input.error.statusCode,
}, {
login_challenge: input.challenge,
}, input.userContext);
if (resp.status === "OAUTH_ERROR") {
return Object.assign(Object.assign({}, resp), { status: "ERROR" });
}
return {
redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo),
status: "OK",
};
},
getConsentRequest: async function (input) {
const resp = await querier.sendGetRequest("/recipe/oauth/auth/requests/consent", { consentChallenge: input.challenge }, input.userContext);
if (resp.status === "OAUTH_ERROR") {
return Object.assign(Object.assign({}, resp), { status: "ERROR" });
}
return {
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("/recipe/oauth/auth/requests/consent/accept", {
context: input.context,
grantAccessTokenAudience: input.grantAccessTokenAudience,
grantScope: input.grantScope,
handledAt: input.handledAt,
iss: await stInstance.getRecipeInstanceOrThrow("openid").getIssuer(input.userContext),
tId: input.tenantId,
rsub: input.rsub,
sessionHandle: input.sessionHandle,
initialAccessTokenPayload: input.initialAccessTokenPayload,
initialIdTokenPayload: input.initialIdTokenPayload,
}, {
consentChallenge: input.challenge,
}, input.userContext);
if (resp.status === "OAUTH_ERROR") {
return Object.assign(Object.assign({}, resp), { status: "ERROR" });
}
return {
redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo),
status: "OK",
};
},
rejectConsentRequest: async function (input) {
const resp = await querier.sendPutRequest("/recipe/oauth/auth/requests/consent/reject", {
error: input.error.error,
errorDescription: input.error.errorDescription,
statusCode: input.error.statusCode,
}, {
consentChallenge: input.challenge,
}, input.userContext);
if (resp.status === "OAUTH_ERROR") {
return Object.assign(Object.assign({}, resp), { status: "ERROR" });
}
return {
redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo),
status: "OK",
};
},
authorization: async function (input) {
var _a, _b, _c, _d, _e;
// we handle this in the backend SDK level
if (input.params.prompt === "none") {
input.params["st_prompt"] = "none";
delete input.params.prompt;
}
let payloads;
if (input.params.client_id === undefined || typeof input.params.client_id !== "string") {
return {
status: "ERROR",
statusCode: 400,
error: "invalid_request",
errorDescription: "client_id is required and must be a string",
};
}
const scopes = await this.getRequestedScopes({
scopeParam: ((_a = input.params.scope) === null || _a === void 0 ? void 0 : _a.split(" ")) || [],
clientId: input.params.client_id,
recipeUserId: (_b = input.session) === null || _b === void 0 ? void 0 : _b.getRecipeUserId(),
sessionHandle: (_c = input.session) === null || _c === void 0 ? void 0 : _c.getHandle(),
userContext: input.userContext,
});
const responseTypes = (_e = (_d = input.params.response_type) === null || _d === void 0 ? void 0 : _d.split(" ")) !== null && _e !== void 0 ? _e : [];
if (input.session !== undefined) {
const clientInfo = await this.getOAuth2Client({
clientId: input.params.client_id,
userContext: input.userContext,
});
if (clientInfo.status === "ERROR") {
return {
status: "ERROR",
statusCode: 400,
error: clientInfo.error,
errorDescription: clientInfo.errorDescription,
};
}
const client = clientInfo.client;
const user = await stInstance
.getRecipeInstanceOrThrow("accountlinking")
.recipeInterfaceImpl.getUser({ userId: input.session.getUserId(), userContext: input.userContext });
if (!user) {
return {
status: "ERROR",
statusCode: 400,
error: "invalid_request",
errorDescription: "User deleted",
};
}
// These default to an empty objects, because we want to keep them as a required input
// but they'll not be actually used in the flows where we are not building them.
const idToken = scopes.includes("openid") && (responseTypes.includes("id_token") || responseTypes.includes("code"))
? await this.buildIdTokenPayload({
user,
client,
sessionHandle: input.session.getHandle(),
scopes,
userContext: input.userContext,
})
: {};
const accessToken = responseTypes.includes("token") || responseTypes.includes("code")
? await this.buildAccessTokenPayload({
user,
client,
sessionHandle: input.session.getHandle(),
scopes,
userContext: input.userContext,
})
: {};
payloads = {
idToken,
accessToken,
};
}
const resp = await querier.sendPostRequest("/recipe/oauth/auth", {
params: Object.assign(Object.assign({}, input.params), { scope: scopes.join(" ") }),
iss: await stInstance.getRecipeInstanceOrThrow("openid").getIssuer(input.userContext),
cookies: input.cookies,
session: payloads,
}, input.userContext);
if (resp.status === "CLIENT_NOT_FOUND_ERROR") {
return {
status: "ERROR",
statusCode: 400,
error: "invalid_request",
errorDescription: "The provided client_id is not valid",
};
}
if (resp.status !== "OK") {
return {
status: "ERROR",
statusCode: resp.statusCode,
error: resp.error,
errorDescription: resp.errorDescription,
};
}
const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo);
if (redirectTo === undefined) {
throw new Error("Got redirectTo as undefined, cannot continue");
}
const redirectToURL = new URL(redirectTo);
const consentChallenge = redirectToURL.searchParams.get("consent_challenge");
if (consentChallenge !== null && input.session !== undefined) {
const consentRequest = await this.getConsentRequest({
challenge: consentChallenge,
userContext: input.userContext,
});
if ("error" in consentRequest) {
return consentRequest;
}
const consentRes = await this.acceptConsentRequest({
userContext: input.userContext,
challenge: consentRequest.challenge,
grantAccessTokenAudience: consentRequest.requestedAccessTokenAudience,
grantScope: consentRequest.requestedScope,
tenantId: input.session.getTenantId(),
rsub: input.session.getRecipeUserId().getAsString(),
sessionHandle: input.session.getHandle(),
initialAccessTokenPayload: payloads === null || payloads === void 0 ? void 0 : payloads.accessToken,
initialIdTokenPayload: payloads === null || payloads === void 0 ? void 0 : payloads.idToken,
});
if ("error" in consentRes) {
return consentRes;
}
return {
redirectTo: consentRes.redirectTo,
cookies: resp.cookies,
};
}
return { redirectTo, cookies: resp.cookies };
},
tokenExchange: async function (input) {
var _a, _b, _c, _d;
const body = {
inputBody: input.body,
authorizationHeader: input.authorizationHeader,
};
body.iss = await stInstance.getRecipeInstanceOrThrow("openid").getIssuer(input.userContext);
if (input.body.grant_type === "password") {
return {
status: "ERROR",
statusCode: 400,
error: "invalid_request",
errorDescription: "Unsupported grant type: password",
};
}
if (input.body.grant_type === "client_credentials") {
let clientId = input.authorizationHeader !== undefined
? (0, utils_1.decodeBase64)(input.authorizationHeader.replace(/^Basic /, "").trim()).split(":")[0]
: input.body.client_id;
if (clientId === undefined) {
return {
status: "ERROR",
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,
userContext: input.userContext,
});
if (clientInfo.status === "ERROR") {
return {
status: "ERROR",
statusCode: 400,
error: clientInfo.error,
errorDescription: clientInfo.errorDescription,
};
}
const client = clientInfo.client;
body["id_token"] = await this.buildIdTokenPayload({
user: undefined,
client,
sessionHandle: undefined,
scopes,
userContext: input.userContext,
});
body["access_token"] = await this.buildAccessTokenPayload({
user: undefined,
client,
sessionHandle: undefined,
scopes,
userContext: input.userContext,
});
}
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,
});
if (tokenInfo.status === "ERROR") {
return tokenInfo;
}
if (!tokenInfo.active) {
return {
status: "ERROR",
statusCode: 400,
error: "invalid_grant",
errorDescription: "The provided refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.",
};
}
if (tokenInfo.active === true) {
const sessionHandle = tokenInfo.sessionHandle;
const clientInfo = await this.getOAuth2Client({
clientId: tokenInfo.client_id,
userContext: input.userContext,
});
if (clientInfo.status === "ERROR") {
return {
status: "ERROR",
statusCode: 400,
error: clientInfo.error,
errorDescription: clientInfo.errorDescription,
};
}
const client = clientInfo.client;
const user = await stInstance
.getRecipeInstanceOrThrow("accountlinking")
.recipeInterfaceImpl.getUser({
userId: tokenInfo.sub,
userContext: input.userContext,
});
if (!user) {
return {
status: "ERROR",
statusCode: 400,
error: "invalid_request",
errorDescription: "User not found",
};
}
body["id_token"] = await this.buildIdTokenPayload({
user,
client,
sessionHandle,
scopes,
userContext: input.userContext,
});
body["access_token"] = await this.buildAccessTokenPayload({
user,
client,
sessionHandle: sessionHandle,
scopes,
userContext: input.userContext,
});
}
}
if (input.authorizationHeader) {
body["authorizationHeader"] = input.authorizationHeader;
}
const res = await querier.sendPostRequest("/recipe/oauth/token", body, input.userContext);
if (res.status === "CLIENT_NOT_FOUND_ERROR") {
return {
status: "ERROR",
statusCode: 400,
error: "invalid_request",
errorDescription: "client_id not found",
};
}
if (res.status !== "OK") {
return {
status: "ERROR",
statusCode: res.statusCode,
error: res.error,
errorDescription: res.errorDescription,
};
}
return res;
},
getOAuth2Clients: async function (input) {
let response = await querier.sendGetRequestWithResponseHeaders("/recipe/oauth/clients/list", {
pageSize: input.pageSize,
clientName: input.clientName,
pageToken: input.paginationToken,
}, {}, input.userContext);
if (response.body.status === "OK") {
return {
status: "OK",
clients: response.body.clients.map((client) => OAuth2Client_1.OAuth2Client.fromAPIResponse(client)),
nextPaginationToken: response.body.nextPaginationToken,
};
}
else {
return {
status: "ERROR",
error: response.body.error,
errorDescription: response.body.errorDescription,
};
}
},
getOAuth2Client: async function (input) {
let response = await querier.sendGetRequestWithResponseHeaders("/recipe/oauth/clients", { clientId: input.clientId }, {}, input.userContext);
if (response.body.status === "OK") {
return {
status: "OK",
client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.body),
};
}
else if (response.body.status === "CLIENT_NOT_FOUND_ERROR") {
return {
status: "ERROR",
error: "invalid_request",
errorDescription: "The provided client_id is not valid or unknown",
};
}
else {
return {
status: "ERROR",
error: response.body.error,
errorDescription: response.body.errorDescription,
};
}
},
createOAuth2Client: async function (input) {
let response = await querier.sendPostRequest("/recipe/oauth/clients", copyAndCleanRequestBodyInput(input), input.userContext);
if (response.status === "OK") {
return {
status: "OK",
client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response),
};
}
else {
return {
status: "ERROR",
error: response.error,
errorDescription: response.errorDescription,
};
}
},
updateOAuth2Client: async function (input) {
let response = await querier.sendPutRequest("/recipe/oauth/clients", copyAndCleanRequestBodyInput(input), { clientId: input.clientId }, input.userContext);
if (response.status === "OK") {
return {
status: "OK",
client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response),
};
}
else {
return {
status: "ERROR",
error: response.error,
errorDescription: response.errorDescription,
};
}
},
deleteOAuth2Client: async function (input) {
let response = await querier.sendPostRequest("/recipe/oauth/clients/remove", { clientId: input.clientId }, input.userContext);
if (response.status === "OK") {
return { status: "OK" };
}
else {
return {
status: "ERROR",
error: response.error,
errorDescription: response.errorDescription,
};
}
},
getRequestedScopes: async function (input) {
return input.scopeParam;
},
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);
},
getFrontendRedirectionURL: async function (input) {
const websiteDomain = appInfo
.getOrigin({ request: undefined, userContext: input.userContext })
.getAsStringDangerous();
const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous();
if (input.type === "login") {
const queryParams = new URLSearchParams({
loginChallenge: input.loginChallenge,
});
if (input.tenantId !== undefined && input.tenantId !== constants_1.DEFAULT_TENANT_ID) {
queryParams.set("tenantId", input.tenantId);
}
if (input.hint !== undefined) {
queryParams.set("hint", input.hint);
}
if (input.forceFreshAuth) {
queryParams.set("forceFreshAuth", "true");
}
return `${websiteDomain}${websiteBasePath}?${queryParams.toString()}`;
}
else if (input.type === "try-refresh") {
return `${websiteDomain}${websiteBasePath}/try-refresh?loginChallenge=${input.loginChallenge}`;
}
else if (input.type === "post-logout-fallback") {
return `${websiteDomain}${websiteBasePath}`;
}
else if (input.type === "logout-confirmation") {
return `${websiteDomain}${websiteBasePath}/oauth/logout?logoutChallenge=${input.logoutChallenge}`;
}
throw new Error("This should never happen: invalid type passed to getFrontendRedirectionURL");
},
validateOAuth2AccessToken: async function (input) {
var _a, _b, _c;
const payload = (await jose.jwtVerify(input.token, (0, combinedRemoteJWKSet_1.getCombinedJWKS)(querier, stInstance.getRecipeInstanceOrThrow("session").config))).payload;
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) {
throw new Error(`The token doesn't belong to the specified client (${input.requirements.clientId} !== ${payload.client_id})`);
}
if (((_b = input.requirements) === null || _b === void 0 ? void 0 : _b.scopes) !== undefined &&
input.requirements.scopes.some((scope) => !payload.scp.includes(scope))) {
throw new Error("The token is missing some required scopes");
}
const aud = payload.aud instanceof Array ? payload.aud : [payload.aud];
if (((_c = input.requirements) === null || _c === void 0 ? void 0 : _c.audience) !== undefined && !aud.includes(input.requirements.audience)) {
throw new Error("The token doesn't belong to the specified audience");
}
if (input.checkDatabase) {
let response = await querier.sendPostRequest("/recipe/oauth/introspect", {
token: input.token,
}, input.userContext);
if (response.status === "OAUTH_ERROR") {
throw new Error("The token is expired, invalid or has been revoked");
}
if (response.active !== true) {
throw new Error("The token is expired, invalid or has been revoked");
}
}
return { status: "OK", payload: payload };
},
revokeToken: async function (input) {
const requestBody = {
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;
}
if ("clientSecret" in input && input.clientSecret !== undefined) {
requestBody.client_secret = input.clientSecret;
}
}
const res = await querier.sendPostRequest("/recipe/oauth/token/revoke", requestBody, input.userContext);
if (res.status !== "OK") {
return {
status: "ERROR",
statusCode: res.statusCode,
error: res.error,
errorDescription: res.errorDescription,
};
}
return { status: "OK" };
},
revokeTokensBySessionHandle: async function (input) {
await querier.sendPostRequest("/recipe/oauth/session/revoke", { sessionHandle: input.sessionHandle }, input.userContext);
return { status: "OK" };
},
revokeTokensByClientId: async function (input) {
await querier.sendPostRequest("/recipe/oauth/tokens/revoke", { client_id: input.clientId }, input.userContext);
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 "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) {
try {
await this.validateOAuth2AccessToken({
token,
requirements: { scopes },
checkDatabase: false,
userContext,
});
}
catch (error) {
return {
active: false,
status: "OK",
};
}
}
// 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("/recipe/oauth/introspect", {
token,
scope: scopes ? scopes.join(" ") : undefined,
}, userContext);
if (res.status === "OAUTH_ERROR") {
return Object.assign(Object.assign({}, res), { status: "ERROR" });
}
return Object.assign(Object.assign({}, res), { status: "OK" });
},
endSession: async function (input) {
/**
* NOTE: The API response has 3 possible cases:
*
* CASE 1: `end_session` request with a valid `id_token_hint`
* - Redirects to `/oauth/logout` with a `logout_challenge`.
*
* CASE 2: `end_session` request with an already logged out `id_token_hint`
* - Redirects to the `post_logout_redirect_uri` or the default logout fallback page.
*
* CASE 3: `end_session` request with a `logout_verifier` (after accepting the logout request)
* - Redirects to the `post_logout_redirect_uri` or the default logout fallback page.
*/
const resp = await querier.sendGetRequest("/recipe/oauth/sessions/logout", {
clientId: input.params.client_id,
idTokenHint: input.params.id_token_hint,
postLogoutRedirectUri: input.params.post_logout_redirect_uri,
state: input.params.state,
logoutVerifier: input.params.logout_verifier,
}, input.userContext);
if ("error" in resp) {
return {
status: "ERROR",
statusCode: resp.statusCode,
error: resp.error,
errorDescription: resp.errorDescription,
};
}
let redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo);
const initialRedirectToURL = new URL(redirectTo);
const logoutChallenge = initialRedirectToURL.searchParams.get("logout_challenge");
// CASE 1 (See above notes)
if (logoutChallenge !== null) {
// Redirect to the frontend to ask for logout confirmation if there is a valid or expired supertokens session
if (input.session !== undefined || input.shouldTryRefresh) {
return {
redirectTo: await this.getFrontendRedirectionURL({
type: "logout-confirmation",
logoutChallenge,
userContext: input.userContext,
}),
};
}
else {
// Accept the logout challenge immediately as there is no supertokens session
const acceptLogoutResponse = await this.acceptLogoutRequest({
challenge: logoutChallenge,
userContext: input.userContext,
});
if ("error" in acceptLogoutResponse) {
return acceptLogoutResponse;
}
return { redirectTo: acceptLogoutResponse.redirectTo };
}
}
// CASE 2 or 3 (See above notes)
// NOTE: If no post_logout_redirect_uri is provided, Hydra redirects to a fallback page.
// In this case, we redirect the user to the /auth page.
if (redirectTo.endsWith("/fallbacks/logout/callback")) {
return {
redirectTo: await this.getFrontendRedirectionURL({
type: "post-logout-fallback",
userContext: input.userContext,
}),
};
}
return { redirectTo };
},
acceptLogoutRequest: async function (input) {
const resp = await querier.sendPutRequest("/recipe/oauth/auth/requests/logout/accept", { challenge: input.challenge }, {}, input.userContext);
if (resp.status !== "OK") {
return {
status: "ERROR",
statusCode: resp.statusCode,
error: resp.error,
errorDescription: resp.errorDescription,
};
}
const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo);
if (redirectTo.endsWith("/fallbacks/logout/callback")) {
return {
redirectTo: await this.getFrontendRedirectionURL({
type: "post-logout-fallback",
userContext: input.userContext,
}),
};
}
return { redirectTo };
},
rejectLogoutRequest: async function (input) {
const resp = await querier.sendPutRequest("/recipe/oauth/auth/requests/logout/reject", {}, { challenge: input.challenge }, input.userContext);
if (resp.status != "OK") {
throw new Error(resp.error);
}
return { status: "OK" };
},
};
}