Skip to content

Commit

Permalink
feat: Add APIs for rp-initiated logout
Browse files Browse the repository at this point in the history
  • Loading branch information
anku255 committed Aug 15, 2024
1 parent 905b5cd commit a4f6de7
Show file tree
Hide file tree
Showing 33 changed files with 950 additions and 16 deletions.
6 changes: 6 additions & 0 deletions lib/build/recipe/oauth2provider/OAuth2Client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export declare class OAuth2Client {
* StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.
*/
redirectUris: string[] | null;
/**
* Array of post logout redirect URIs
* StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.
*/
postLogoutRedirectUris: string[] | null;
/**
* Authorization Code Grant Access Token Lifespan
* NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$
Expand Down Expand Up @@ -144,6 +149,7 @@ export declare class OAuth2Client {
clientName,
scope,
redirectUris,
postLogoutRedirectUris,
authorizationCodeGrantAccessTokenLifespan,
authorizationCodeGrantIdTokenLifespan,
authorizationCodeGrantRefreshTokenLifespan,
Expand Down
2 changes: 2 additions & 0 deletions lib/build/recipe/oauth2provider/OAuth2Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class OAuth2Client {
clientName,
scope,
redirectUris = null,
postLogoutRedirectUris = null,
authorizationCodeGrantAccessTokenLifespan = null,
authorizationCodeGrantIdTokenLifespan = null,
authorizationCodeGrantRefreshTokenLifespan = null,
Expand Down Expand Up @@ -55,6 +56,7 @@ class OAuth2Client {
this.clientName = clientName;
this.scope = scope;
this.redirectUris = redirectUris;
this.postLogoutRedirectUris = postLogoutRedirectUris;
this.authorizationCodeGrantAccessTokenLifespan = authorizationCodeGrantAccessTokenLifespan;
this.authorizationCodeGrantIdTokenLifespan = authorizationCodeGrantIdTokenLifespan;
this.authorizationCodeGrantRefreshTokenLifespan = authorizationCodeGrantRefreshTokenLifespan;
Expand Down
13 changes: 13 additions & 0 deletions lib/build/recipe/oauth2provider/api/endSession.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-nocheck
import { APIInterface, APIOptions } from "..";
import { UserContext } from "../../../types";
export declare function endSessionGET(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
export declare function endSessionPOST(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
81 changes: 81 additions & 0 deletions lib/build/recipe/oauth2provider/api/endSession.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use strict";
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.endSessionPOST = exports.endSessionGET = void 0;
const utils_1 = require("../../../utils");
const session_1 = __importDefault(require("../../session"));
async function endSessionGET(apiImplementation, options, userContext) {
if (apiImplementation.endSessionGET === undefined) {
return false;
}
const origURL = options.req.getOriginalURL();
const splitURL = origURL.split("?");
const params = new URLSearchParams(splitURL[1]);
return endSessionCommon(
Object.fromEntries(params.entries()),
apiImplementation.endSessionGET,
options,
userContext
);
}
exports.endSessionGET = endSessionGET;
async function endSessionPOST(apiImplementation, options, userContext) {
if (apiImplementation.endSessionPOST === undefined) {
return false;
}
const params = await options.req.getBodyAsJSONOrFormData();
return endSessionCommon(params, apiImplementation.endSessionPOST, options, userContext);
}
exports.endSessionPOST = endSessionPOST;
async function endSessionCommon(params, apiImplementation, options, userContext) {
if (apiImplementation === undefined) {
return false;
}
// TODO: Validate client_id if passed
let session;
try {
session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext);
} catch (_a) {
session = undefined;
}
let response = await apiImplementation({
options,
params,
session,
userContext,
});
if ("redirectTo" in response) {
// TODO: Fix
if (response.redirectTo.includes("/oauth/fallbacks/error")) {
const redirectToUrlObj = new URL(response.redirectTo);
const res = {
error: redirectToUrlObj.searchParams.get("error"),
errorDescription: redirectToUrlObj.searchParams.get("error_description"),
};
utils_1.sendNon200Response(options.res, 400, res);
} else {
options.res.original.redirect(response.redirectTo);
}
} else {
utils_1.send200Response(options.res, response);
}
return true;
}
50 changes: 50 additions & 0 deletions lib/build/recipe/oauth2provider/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,56 @@ function getAPIImplementation() {
userContext: input.userContext,
});
},
endSessionGET: async ({ options, params, session, userContext }) => {
const response = await options.recipeImplementation.endSession({
params,
userContext,
});
return utils_1.handleInternalRedirects({
response,
session,
recipeImplementation: options.recipeImplementation,
userContext,
});
},
endSessionPOST: async ({ options, params, session, userContext }) => {
const response = await options.recipeImplementation.endSession({
params,
userContext,
});
return utils_1.handleInternalRedirects({
response,
session,
recipeImplementation: options.recipeImplementation,
userContext,
});
},
logoutGET: async ({ logoutChallenge, options, session, userContext }) => {
const response = await utils_1.logoutGET({
logoutChallenge,
recipeImplementation: options.recipeImplementation,
session,
userContext,
});
return utils_1.handleInternalRedirects({
response,
recipeImplementation: options.recipeImplementation,
userContext,
});
},
logoutPOST: async ({ logoutChallenge, options, session, userContext }) => {
const response = await utils_1.logoutPOST({
logoutChallenge,
recipeImplementation: options.recipeImplementation,
session,
userContext,
});
return utils_1.handleInternalRedirects({
response,
recipeImplementation: options.recipeImplementation,
userContext,
});
},
};
}
exports.default = getAPIImplementation;
13 changes: 13 additions & 0 deletions lib/build/recipe/oauth2provider/api/logout.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-nocheck
import { APIInterface, APIOptions } from "..";
import { UserContext } from "../../../types";
export declare function logoutGET(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
export declare function logoutPOST(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
87 changes: 87 additions & 0 deletions lib/build/recipe/oauth2provider/api/logout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use strict";
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.logoutPOST = exports.logoutGET = void 0;
const utils_1 = require("../../../utils");
const session_1 = __importDefault(require("../../session"));
const error_1 = __importDefault(require("../../../error"));
async function logoutGET(apiImplementation, options, userContext) {
var _a;
if (apiImplementation.logoutGET === undefined) {
return false;
}
let session;
try {
session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext);
} catch (_b) {
session = undefined;
}
const logoutChallenge =
(_a = options.req.getKeyValueFromQuery("logout_challenge")) !== null && _a !== void 0
? _a
: options.req.getKeyValueFromQuery("logoutChallenge");
if (logoutChallenge === undefined) {
throw new error_1.default({
type: error_1.default.BAD_INPUT_ERROR,
message: "Missing input param: logoutChallenge",
});
}
let response = await apiImplementation.logoutGET({
options,
logoutChallenge,
session,
userContext,
});
if ("redirectTo" in response) {
options.res.original.redirect(response.redirectTo);
} else {
utils_1.send200Response(options.res, response);
}
return true;
}
exports.logoutGET = logoutGET;
async function logoutPOST(apiImplementation, options, userContext) {
if (apiImplementation.logoutPOST === undefined) {
return false;
}
let session;
try {
session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext);
} catch (_a) {
session = undefined;
}
const body = await options.req.getBodyAsJSONOrFormData();
if (body.logoutChallenge === undefined) {
throw new error_1.default({
type: error_1.default.BAD_INPUT_ERROR,
message: "Missing body param: logoutChallenge",
});
}
let response = await apiImplementation.logoutPOST({
options,
logoutChallenge: body.logoutChallenge,
session,
userContext,
});
utils_1.send200Response(options.res, response);
return true;
}
exports.logoutPOST = logoutPOST;
30 changes: 28 additions & 2 deletions lib/build/recipe/oauth2provider/api/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,32 @@ export declare function loginGET({
redirectTo: string;
setCookie: string | undefined;
}>;
export declare function logoutGET({
logoutChallenge,
recipeImplementation,
session,
userContext,
}: {
logoutChallenge: string;
recipeImplementation: RecipeInterface;
session?: SessionContainerInterface;
userContext: UserContext;
}): Promise<{
redirectTo: string;
}>;
export declare function logoutPOST({
logoutChallenge,
session,
recipeImplementation,
userContext,
}: {
logoutChallenge: string;
recipeImplementation: RecipeInterface;
session?: SessionContainerInterface;
userContext: UserContext;
}): Promise<{
redirectTo: string;
}>;
export declare function handleInternalRedirects({
response,
recipeImplementation,
Expand All @@ -29,13 +55,13 @@ export declare function handleInternalRedirects({
}: {
response: {
redirectTo: string;
setCookie: string | undefined;
setCookie?: string;
};
recipeImplementation: RecipeInterface;
session?: SessionContainerInterface;
cookie?: string;
userContext: UserContext;
}): Promise<{
redirectTo: string;
setCookie: string | undefined;
setCookie?: string;
}>;
Loading

0 comments on commit a4f6de7

Please sign in to comment.