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 1 commit
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
7 changes: 7 additions & 0 deletions lib/build/recipe/oauth2provider/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ function getAPIImplementation() {
userContext,
});
},
revokeTokenPOST: async (input) => {
return input.options.recipeImplementation.revokeToken({
token: input.body.token,
tokenTypeHint: input.body.tokenTypeHint,
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>;
38 changes: 38 additions & 0 deletions lib/build/recipe/oauth2provider/api/revokeToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"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.getJSONBody();
if (body.token === undefined) {
utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400);
return true;
}
let response = await apiImplementation.revokeTokenPOST({
options,
body: {
token: body.token,
tokenTypeHint: body.token_type_hint,
},
userContext,
});
utils_1.send200Response(options.res, response);
return true;
}
exports.default = revokeTokenPOST;
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 = "/oauth2provider/auth";
export declare const TOKEN_PATH = "/oauth2provider/token";
export declare const LOGIN_INFO_PATH = "/oauth2provider/login/info";
export declare const USER_INFO_PATH = "/oauth2provider/userinfo";
export declare const REVOKE_TOKEN_PATH = "/oauth2provider/token/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 = "/oauth2provider/";
exports.LOGIN_PATH = "/oauth2provider/login";
exports.AUTH_PATH = "/oauth2provider/auth";
exports.TOKEN_PATH = "/oauth2provider/token";
exports.LOGIN_INFO_PATH = "/oauth2provider/login/info";
exports.USER_INFO_PATH = "/oauth2provider/userinfo";
exports.REVOKE_TOKEN_PATH = "/oauth2provider/token/revoke";
10 changes: 10 additions & 0 deletions lib/build/recipe/oauth2provider/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ export default class Wrapper {
status: "OK";
payload: import("../usermetadata").JSONObject;
}>;
static revokeToken(
token: string,
tokenTypeHint?: "access_token" | "refresh_token",
userContext?: Record<string, any>
): Promise<
| import("../../types").GeneralErrorResponse
| {
status: "OK";
}
>;
}
export declare let init: typeof Recipe.init;
export declare let getOAuth2Clients: typeof Wrapper.getOAuth2Clients;
Expand Down
7 changes: 7 additions & 0 deletions lib/build/recipe/oauth2provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ class Wrapper {
userContext: utils_1.getUserContext(userContext),
});
}
static revokeToken(token, tokenTypeHint, userContext) {
return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeToken({
token,
tokenTypeHint,
userContext: utils_1.getUserContext(userContext),
});
}
}
exports.default = Wrapper;
Wrapper.init = recipe_1.default.init;
Expand Down
12 changes: 12 additions & 0 deletions lib/build/recipe/oauth2provider/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,18 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload,
// TODO: add a check to make sure this is the right token type as they can be signed with the same key
return { status: "OK", payload: payload };
},
revokeToken: async function ({ token, tokenTypeHint, userContext }) {
// TODO: Update the endpoint to the correct token revocation endpoint
const res = await querier.sendPostRequest(
new normalisedURLPath_1.default(`/recipe/oauth2/revoke/token`),
{
token,
tokenTypeHint,
},
userContext
);
return res.data;
},
};
}
exports.default = getRecipeInterface;
25 changes: 25 additions & 0 deletions lib/build/recipe/oauth2provider/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,16 @@ export declare type RecipeInterface = {
tenantId: string;
userContext: UserContext;
}): Promise<JSONObject>;
revokeToken(input: {
token: string;
tokenTypeHint?: "access_token" | "refresh_token";
userContext: UserContext;
}): Promise<
| {
status: "OK";
}
| GeneralErrorResponse
>;
};
export declare type APIInterface = {
loginGET:
Expand Down Expand Up @@ -309,6 +319,21 @@ export declare type APIInterface = {
options: APIOptions;
userContext: UserContext;
}) => Promise<JSONObject | GeneralErrorResponse>);
revokeTokenPOST:
| undefined
| ((input: {
body: {
token: string;
tokenTypeHint?: "access_token" | "refresh_token";
};
options: APIOptions;
userContext: UserContext;
}) => Promise<
| {
status: "OK";
}
| GeneralErrorResponse
>);
};
export declare type OAuth2ClientOptions = {
clientId: string;
Expand Down
7 changes: 7 additions & 0 deletions lib/ts/recipe/oauth2provider/api/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,12 @@ export default function getAPIImplementation(): APIInterface {
userContext,
});
},
revokeTokenPOST: async (input) => {
return input.options.recipeImplementation.revokeToken({
token: input.body.token,
tokenTypeHint: input.body.tokenTypeHint,
userContext: input.userContext,
});
},
};
}
47 changes: 47 additions & 0 deletions lib/ts/recipe/oauth2provider/api/revokeToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

import { send200Response, sendNon200ResponseWithMessage } from "../../../utils";
import { APIInterface, APIOptions } from "..";
import { UserContext } from "../../../types";

export default async function revokeTokenPOST(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean> {
if (apiImplementation.revokeTokenPOST === undefined) {
return false;
}

const body = await options.req.getJSONBody();

if (body.token === undefined) {
porcellus marked this conversation as resolved.
Show resolved Hide resolved
sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400);
return true;
}

let response = await apiImplementation.revokeTokenPOST({
porcellus marked this conversation as resolved.
Show resolved Hide resolved
options,
body: {
token: body.token,
tokenTypeHint: body.token_type_hint,
},
userContext,
});

send200Response(options.res, response);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to set the WWW-Authorize header in the response? What happens if the auth params are missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided not to set it as couldn't find it anywhere in spec.

return true;
}
1 change: 1 addition & 0 deletions lib/ts/recipe/oauth2provider/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export const AUTH_PATH = "/oauth2provider/auth";
export const TOKEN_PATH = "/oauth2provider/token";
export const LOGIN_INFO_PATH = "/oauth2provider/login/info";
export const USER_INFO_PATH = "/oauth2provider/userinfo";
export const REVOKE_TOKEN_PATH = "/oauth2provider/token/revoke";
12 changes: 12 additions & 0 deletions lib/ts/recipe/oauth2provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ export default class Wrapper {
userContext: getUserContext(userContext),
});
}

static revokeToken(
token: string,
tokenTypeHint?: "access_token" | "refresh_token",
porcellus marked this conversation as resolved.
Show resolved Hide resolved
userContext?: Record<string, any>
) {
return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeToken({
token,
tokenTypeHint,
userContext: getUserContext(userContext),
});
}
}

export let init = Wrapper.init;
Expand Down
13 changes: 13 additions & 0 deletions lib/ts/recipe/oauth2provider/recipeImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,5 +428,18 @@ export default function getRecipeInterface(

return { status: "OK", payload: payload as JSONObject };
},

revokeToken: async function (this: RecipeInterface, { token, tokenTypeHint, userContext }) {
// TODO: Update the endpoint to the correct token revocation endpoint
const res = await querier.sendPostRequest(
porcellus marked this conversation as resolved.
Show resolved Hide resolved
new NormalisedURLPath(`/recipe/oauth2/revoke/token`),
{
token,
tokenTypeHint,
},
userContext
);
return res.data;
},
};
}
15 changes: 15 additions & 0 deletions lib/ts/recipe/oauth2provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ export type RecipeInterface = {
tenantId: string;
userContext: UserContext;
}): Promise<JSONObject>;
revokeToken(input: {
token: string;
tokenTypeHint?: "access_token" | "refresh_token";
userContext: UserContext;
}): Promise<{ status: "OK" } | GeneralErrorResponse>;
};

export type APIInterface = {
Expand Down Expand Up @@ -404,6 +409,16 @@ export type APIInterface = {
options: APIOptions;
userContext: UserContext;
}) => Promise<JSONObject | GeneralErrorResponse>);
revokeTokenPOST:
| undefined
| ((input: {
body: {
token: string;
tokenTypeHint?: "access_token" | "refresh_token";
};
options: APIOptions;
userContext: UserContext;
}) => Promise<{ status: "OK" } | GeneralErrorResponse>);
};

export type OAuth2ClientOptions = {
Expand Down
Loading