Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add token revocation endpoint #902

Merged
merged 9 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions lib/build/querier.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions lib/build/recipe/oauth2provider/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ function getAPIImplementation() {
userContext,
});
},
revokeTokenPOST: async (input) => {
if ("authorizationHeader" in input) {
return input.options.recipeImplementation.revokeToken({
token: input.token,
authorizationHeader: input.authorizationHeader,
userContext: input.userContext,
});
} else {
return input.options.recipeImplementation.revokeToken({
token: input.token,
clientId: input.clientId,
clientSecret: input.clientSecret,
userContext: input.userContext,
});
}
},
};
}
exports.default = getAPIImplementation;
8 changes: 8 additions & 0 deletions lib/build/recipe/oauth2provider/api/revokeToken.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @ts-nocheck
import { APIInterface, APIOptions } from "..";
import { UserContext } from "../../../types";
export default function revokeTokenPOST(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
51 changes: 51 additions & 0 deletions lib/build/recipe/oauth2provider/api/revokeToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use strict";
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../../../utils");
async function revokeTokenPOST(apiImplementation, options, userContext) {
if (apiImplementation.revokeTokenPOST === undefined) {
return false;
}
const body = await options.req.getFormData();
if (body.token === undefined) {
utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400);
return true;
}
const authorizationHeader = options.req.getHeaderValue("authorization");
if (authorizationHeader !== undefined && (body.client_id !== undefined || body.client_secret !== undefined)) {
utils_1.sendNon200ResponseWithMessage(
options.res,
"Only one of authorization header or client_id and client_secret can be provided",
400
);
return true;
}
let response = await apiImplementation.revokeTokenPOST({
options,
authorizationHeader,
token: body.token,
clientId: body.client_id,
clientSecret: body.client_secret,
userContext,
});
if ("statusCode" in response && response.statusCode !== 200) {
utils_1.sendNon200Response(options.res, response.statusCode, response);
} else {
utils_1.send200Response(options.res, response);
}
return true;
}
exports.default = revokeTokenPOST;
2 changes: 1 addition & 1 deletion lib/build/recipe/oauth2provider/api/userInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async function userInfoGET(apiImplementation, tenantId, options, userContext) {
if (apiImplementation.userInfoGET === undefined) {
return false;
}
const authHeader = options.req.getHeaderValue("authorization") || options.req.getHeaderValue("Authorization");
const authHeader = options.req.getHeaderValue("authorization");
if (authHeader === undefined || !authHeader.startsWith("Bearer ")) {
utils_1.sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401);
return true;
Expand Down
1 change: 1 addition & 0 deletions lib/build/recipe/oauth2provider/constants.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export declare const AUTH_PATH = "/oauth/auth";
export declare const TOKEN_PATH = "/oauth/token";
export declare const LOGIN_INFO_PATH = "/oauth/login/info";
export declare const USER_INFO_PATH = "/oauth/userinfo";
export declare const REVOKE_TOKEN_PATH = "/oauth/revoke";
3 changes: 2 additions & 1 deletion lib/build/recipe/oauth2provider/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
* under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.LOGIN_PATH = exports.OAUTH2_BASE_PATH = void 0;
exports.REVOKE_TOKEN_PATH = exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.LOGIN_PATH = exports.OAUTH2_BASE_PATH = void 0;
exports.OAUTH2_BASE_PATH = "/oauth/";
exports.LOGIN_PATH = "/oauth/login";
exports.AUTH_PATH = "/oauth/auth";
exports.TOKEN_PATH = "/oauth/token";
exports.LOGIN_INFO_PATH = "/oauth/login/info";
exports.USER_INFO_PATH = "/oauth/userinfo";
exports.REVOKE_TOKEN_PATH = "/oauth/revoke";
26 changes: 26 additions & 0 deletions lib/build/recipe/oauth2provider/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ import {
} from "./types";
export default class Wrapper {
static init: typeof Recipe.init;
static getOAuth2Client(
clientId: string,
userContext?: Record<string, any>
): Promise<
| {
status: "OK";
client: import("./OAuth2Client").OAuth2Client;
}
| {
status: "ERROR";
error: string;
errorHint: string;
}
>;
static getOAuth2Clients(
input: GetOAuth2ClientsInput,
userContext?: Record<string, any>
Expand Down Expand Up @@ -99,6 +113,17 @@ export default class Wrapper {
audience?: string,
userContext?: Record<string, any>
): Promise<import("./types").ErrorOAuth2 | import("./types").TokenInfo>;
static revokeToken(
token: string,
clientId: string,
clientSecret?: string,
userContext?: Record<string, any>
): Promise<
| import("./types").ErrorOAuth2
| {
status: "OK";
}
>;
}
export declare let init: typeof Recipe.init;
export declare let getOAuth2Clients: typeof Wrapper.getOAuth2Clients;
Expand All @@ -108,4 +133,5 @@ export declare let deleteOAuth2Client: typeof Wrapper.deleteOAuth2Client;
export declare let validateOAuth2AccessToken: typeof Wrapper.validateOAuth2AccessToken;
export declare let validateOAuth2IdToken: typeof Wrapper.validateOAuth2IdToken;
export declare let createTokenForClientCredentials: typeof Wrapper.createTokenForClientCredentials;
export declare let revokeToken: typeof Wrapper.revokeToken;
export type { APIInterface, APIOptions, RecipeInterface };
36 changes: 35 additions & 1 deletion lib/build/recipe/oauth2provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ var __importDefault =
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTokenForClientCredentials = exports.validateOAuth2IdToken = exports.validateOAuth2AccessToken = exports.deleteOAuth2Client = exports.updateOAuth2Client = exports.createOAuth2Client = exports.getOAuth2Clients = exports.init = void 0;
exports.revokeToken = exports.createTokenForClientCredentials = exports.validateOAuth2IdToken = exports.validateOAuth2AccessToken = exports.deleteOAuth2Client = exports.updateOAuth2Client = exports.createOAuth2Client = exports.getOAuth2Clients = exports.init = void 0;
const utils_1 = require("../../utils");
const recipe_1 = __importDefault(require("./recipe"));
class Wrapper {
static async getOAuth2Client(clientId, userContext) {
return await recipe_1.default
.getInstanceOrThrowError()
.recipeInterfaceImpl.getOAuth2Client({ clientId }, utils_1.getUserContext(userContext));
}
static async getOAuth2Clients(input, userContext) {
return await recipe_1.default
.getInstanceOrThrowError()
Expand Down Expand Up @@ -71,6 +76,34 @@ class Wrapper {
userContext: utils_1.getUserContext(userContext),
});
}
static async revokeToken(token, clientId, clientSecret, userContext) {
let authorizationHeader = undefined;
const normalisedUserContext = utils_1.getUserContext(userContext);
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const res = await recipeInterfaceImpl.getOAuth2Client({ clientId }, normalisedUserContext);
if (res.status !== "OK") {
throw new Error(`Failed to get OAuth2 client with id ${clientId}: ${res.error}`);
}
const { tokenEndpointAuthMethod } = res.client;
if (tokenEndpointAuthMethod === "none") {
authorizationHeader = "Basic " + Buffer.from(clientId + ":").toString("base64");
} else if (tokenEndpointAuthMethod === "client_secret_basic") {
authorizationHeader = "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64");
}
if (authorizationHeader !== undefined) {
return await recipeInterfaceImpl.revokeToken({
token,
authorizationHeader,
userContext: normalisedUserContext,
});
}
return await recipeInterfaceImpl.revokeToken({
token,
clientId,
clientSecret,
userContext: normalisedUserContext,
});
}
}
exports.default = Wrapper;
Wrapper.init = recipe_1.default.init;
Expand All @@ -82,3 +115,4 @@ exports.deleteOAuth2Client = Wrapper.deleteOAuth2Client;
exports.validateOAuth2AccessToken = Wrapper.validateOAuth2AccessToken;
exports.validateOAuth2IdToken = Wrapper.validateOAuth2IdToken;
exports.createTokenForClientCredentials = Wrapper.createTokenForClientCredentials;
exports.revokeToken = Wrapper.revokeToken;
10 changes: 10 additions & 0 deletions lib/build/recipe/oauth2provider/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const utils_1 = require("./utils");
const supertokens_js_override_1 = __importDefault(require("supertokens-js-override"));
const userInfo_1 = __importDefault(require("./api/userInfo"));
const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet");
const revokeToken_1 = __importDefault(require("./api/revokeToken"));
class Recipe extends recipeModule_1.default {
constructor(recipeId, appInfo, isInServerlessEnv, config) {
super(recipeId, appInfo);
Expand Down Expand Up @@ -66,6 +67,9 @@ class Recipe extends recipeModule_1.default {
if (id === constants_1.USER_INFO_PATH) {
return userInfo_1.default(this.apiImpl, tenantId, options, userContext);
}
if (id === constants_1.REVOKE_TOKEN_PATH) {
return revokeToken_1.default(this.apiImpl, options, userContext);
}
throw new Error("Should never come here: handleAPIRequest called with unknown id");
};
this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config);
Expand Down Expand Up @@ -147,6 +151,12 @@ class Recipe extends recipeModule_1.default {
id: constants_1.USER_INFO_PATH,
disabled: this.apiImpl.userInfoGET === undefined,
},
{
method: "post",
pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.REVOKE_TOKEN_PATH),
id: constants_1.REVOKE_TOKEN_PATH,
disabled: this.apiImpl.revokeTokenPOST === undefined,
},
];
}
handleError(error, _, __, _userContext) {
Expand Down
56 changes: 56 additions & 0 deletions lib/build/recipe/oauth2provider/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload,
body,
input.userContext
);
if (res.status !== "OK") {
return {
statusCode: res.statusCode,
error: res.data.error,
errorDescription: res.data.error_description,
};
}
return res.data;
},
getOAuth2Clients: async function (input, userContext) {
Expand Down Expand Up @@ -308,6 +315,26 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload,
};
}
},
getOAuth2Client: async function (input, userContext) {
let response = await querier.sendGetRequestWithResponseHeaders(
new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients/${input.clientId}`),
{},
{},
userContext
);
if (response.body.status === "OK") {
return {
status: "OK",
client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.body.data),
};
} else {
return {
status: "ERROR",
error: response.body.data.error,
errorHint: response.body.data.errorHint,
};
}
},
createOAuth2Client: async function (input, userContext) {
let response = await querier.sendPostRequest(
new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients`),
Expand Down Expand Up @@ -492,6 +519,35 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload,
}
return { status: "OK", payload: payload };
},
revokeToken: async function (input) {
const requestBody = {
$isFormData: true,
token: input.token,
};
if ("authorizationHeader" in input) {
requestBody.authorizationHeader = input.authorizationHeader;
} else {
if ("clientId" in input) {
requestBody.client_id = input.clientId;
}
if ("clientSecret" in input) {
requestBody.client_secret = input.clientSecret;
}
}
const res = await querier.sendPostRequest(
new normalisedURLPath_1.default(`/recipe/oauth2/pub/revoke`),
requestBody,
input.userContext
);
if (res.status !== "OK") {
return {
statusCode: res.statusCode,
error: res.data.error,
errorDescription: res.data.error_description,
};
}
return { status: "OK" };
},
};
}
exports.default = getRecipeInterface;
Loading
Loading