Skip to content

Commit

Permalink
feat: Add token introspection endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
anku255 committed Aug 6, 2024
1 parent a558b99 commit 788d81d
Show file tree
Hide file tree
Showing 24 changed files with 326 additions and 6 deletions.
1 change: 1 addition & 0 deletions lib/build/framework/request.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export declare abstract class BaseRequest {
abstract getOriginalURL: () => string;
getFormData: () => Promise<any>;
getJSONBody: () => Promise<any>;
getBodyAsJSONOrFormData: () => Promise<any>;
}
20 changes: 20 additions & 0 deletions lib/build/framework/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ class BaseRequest {
}
return this.parsedJSONBody;
};
this.getBodyAsJSONOrFormData = async () => {
const contentType = this.getHeaderValue("content-type") || this.getHeaderValue("Content-Type");
if (contentType) {
if (contentType.includes("application/json")) {
return await this.getJSONBody();
} else if (contentType.includes("application/x-www-form-urlencoded")) {
return await this.getFormData();
}
} else {
try {
return await this.getJSONBody();
} catch (_a) {
try {
return await this.getFormData();
} catch (_b) {
throw new Error("Unable to parse body as JSON or Form Data.");
}
}
}
};
this.wrapperUsed = true;
this.parsedJSONBody = undefined;
this.parsedUrlEncodedFormData = undefined;
Expand Down
8 changes: 8 additions & 0 deletions lib/build/recipe/oauth2provider/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function getAPIImplementation() {
},
tokenPOST: async (input) => {
return input.options.recipeImplementation.tokenExchange({
authorizationHeader: input.authorizationHeader,
body: input.body,
userContext: input.userContext,
});
Expand Down Expand Up @@ -88,6 +89,13 @@ function getAPIImplementation() {
userContext: input.userContext,
});
},
introspectTokenPOST: async (input) => {
return input.options.recipeImplementation.introspectToken({
token: input.token,
scopes: input.scopes,
userContext: input.userContext,
});
},
};
}
exports.default = getAPIImplementation;
8 changes: 8 additions & 0 deletions lib/build/recipe/oauth2provider/api/introspectToken.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 introspectTokenPOST(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
37 changes: 37 additions & 0 deletions lib/build/recipe/oauth2provider/api/introspectToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"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 introspectTokenPOST(apiImplementation, options, userContext) {
if (apiImplementation.introspectTokenPOST === undefined) {
return false;
}
const body = await options.req.getBodyAsJSONOrFormData();
if (body.token === undefined) {
utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400);
return true;
}
const scopes = body.scope ? body.scope.split(" ") : [];
let response = await apiImplementation.introspectTokenPOST({
options,
token: body.token,
scopes,
userContext,
});
utils_1.send200Response(options.res, response);
return true;
}
exports.default = introspectTokenPOST;
2 changes: 1 addition & 1 deletion lib/build/recipe/oauth2provider/api/revokeToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async function revokeTokenPOST(apiImplementation, options, userContext) {
if (apiImplementation.revokeTokenPOST === undefined) {
return false;
}
const body = await options.req.getFormData();
const body = await options.req.getBodyAsJSONOrFormData();
if (body.token === undefined) {
utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400);
return true;
Expand Down
5 changes: 4 additions & 1 deletion lib/build/recipe/oauth2provider/api/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ async function tokenPOST(apiImplementation, options, userContext) {
if (apiImplementation.tokenPOST === undefined) {
return false;
}
const authorizationHeader =
options.req.getHeaderValue("authorization") || options.req.getHeaderValue("Authorization");
let response = await apiImplementation.tokenPOST({
authorizationHeader,
options,
body: await options.req.getFormData(),
body: await options.req.getBodyAsJSONOrFormData(),
userContext,
});
if ("statusCode" in response && response.statusCode !== 200) {
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 @@ -6,3 +6,4 @@ 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";
export declare const INTROSPECT_TOKEN_PATH = "/oauth/introspect";
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,11 +14,12 @@
* under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
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.INTROSPECT_TOKEN_PATH = 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";
exports.INTROSPECT_TOKEN_PATH = "/oauth/introspect";
5 changes: 5 additions & 0 deletions lib/build/recipe/oauth2provider/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ export default class Wrapper {
): Promise<{
status: "OK";
}>;
static validateOAuth2RefreshToken(
token: string,
scopes?: string[],
userContext?: Record<string, any>
): Promise<import("./types").InstrospectTokenResponse>;
}
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 @@ -84,6 +84,13 @@ class Wrapper {
userContext: utils_1.getUserContext(userContext),
});
}
static validateOAuth2RefreshToken(token, scopes, userContext) {
return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.introspectToken({
token,
scopes,
userContext: utils_1.getUserContext(userContext),
});
}
}
exports.default = Wrapper;
Wrapper.init = recipe_1.default.init;
Expand Down
10 changes: 10 additions & 0 deletions lib/build/recipe/oauth2provider/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const supertokens_js_override_1 = __importDefault(require("supertokens-js-overri
const userInfo_1 = __importDefault(require("./api/userInfo"));
const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet");
const revokeToken_1 = __importDefault(require("./api/revokeToken"));
const introspectToken_1 = __importDefault(require("./api/introspectToken"));
class Recipe extends recipeModule_1.default {
constructor(recipeId, appInfo, isInServerlessEnv, config) {
super(recipeId, appInfo);
Expand Down Expand Up @@ -70,6 +71,9 @@ class Recipe extends recipeModule_1.default {
if (id === constants_1.REVOKE_TOKEN_PATH) {
return revokeToken_1.default(this.apiImpl, options, userContext);
}
if (id === constants_1.INTROSPECT_TOKEN_PATH) {
return introspectToken_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 @@ -157,6 +161,12 @@ class Recipe extends recipeModule_1.default {
id: constants_1.REVOKE_TOKEN_PATH,
disabled: this.apiImpl.revokeTokenPOST === undefined,
},
{
method: "post",
pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.INTROSPECT_TOKEN_PATH),
id: constants_1.INTROSPECT_TOKEN_PATH,
disabled: this.apiImpl.introspectTokenPOST === undefined,
},
];
}
handleError(error, _, __, _userContext) {
Expand Down
33 changes: 33 additions & 0 deletions lib/build/recipe/oauth2provider/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload,
for (const key in input.body) {
body[key] = input.body[key];
}
if (input.authorizationHeader) {
body["authorizationHeader"] = input.authorizationHeader;
}
const res = await querier.sendPostRequest(
new normalisedURLPath_1.default(`/recipe/oauth2/pub/token`),
body,
Expand Down Expand Up @@ -506,6 +509,36 @@ function getRecipeInterface(querier, _config, appInfo, getDefaultIdTokenPayload,
);
return res.data;
},
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");
// 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 };
}
}
// 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`),
{
$isFormData: true,
token,
scope: scopes ? scopes.join(" ") : undefined,
},
userContext
);
return res.data;
},
};
}
exports.default = getRecipeInterface;
22 changes: 22 additions & 0 deletions lib/build/recipe/oauth2provider/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export declare type UserInfo = {
phoneNumber_verified?: boolean;
[key: string]: JSONValue;
};
export declare type InstrospectTokenResponse =
| {
active: false;
}
| ({
active: true;
} & JSONObject);
export declare type RecipeInterface = {
authorization(input: {
params: Record<string, string>;
Expand All @@ -96,6 +103,7 @@ export declare type RecipeInterface = {
setCookie: string | undefined;
}>;
tokenExchange(input: {
authorizationHeader?: string;
body: Record<string, string | undefined>;
userContext: UserContext;
}): Promise<TokenInfo | ErrorOAuth2>;
Expand Down Expand Up @@ -254,6 +262,11 @@ export declare type RecipeInterface = {
}): Promise<{
status: "OK";
}>;
introspectToken(input: {
token: string;
scopes?: string[];
userContext: UserContext;
}): Promise<InstrospectTokenResponse>;
};
export declare type APIInterface = {
loginGET:
Expand Down Expand Up @@ -288,6 +301,7 @@ export declare type APIInterface = {
tokenPOST:
| undefined
| ((input: {
authorizationHeader?: string;
body: any;
options: APIOptions;
userContext: UserContext;
Expand Down Expand Up @@ -330,6 +344,14 @@ export declare type APIInterface = {
}
| GeneralErrorResponse
>);
introspectTokenPOST:
| undefined
| ((input: {
token: string;
scopes?: string[];
options: APIOptions;
userContext: UserContext;
}) => Promise<InstrospectTokenResponse | GeneralErrorResponse>);
};
export declare type OAuth2ClientOptions = {
clientId: string;
Expand Down
22 changes: 22 additions & 0 deletions lib/ts/framework/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,26 @@ export abstract class BaseRequest {
}
return this.parsedJSONBody;
};

getBodyAsJSONOrFormData = async (): Promise<any> => {
const contentType = this.getHeaderValue("content-type") || this.getHeaderValue("Content-Type");

if (contentType) {
if (contentType.includes("application/json")) {
return await this.getJSONBody();
} else if (contentType.includes("application/x-www-form-urlencoded")) {
return await this.getFormData();
}
} else {
try {
return await this.getJSONBody();
} catch {
try {
return await this.getFormData();
} catch {
throw new Error("Unable to parse body as JSON or Form Data.");
}
}
}
};
}
8 changes: 8 additions & 0 deletions lib/ts/recipe/oauth2provider/api/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default function getAPIImplementation(): APIInterface {
},
tokenPOST: async (input) => {
return input.options.recipeImplementation.tokenExchange({
authorizationHeader: input.authorizationHeader,
body: input.body,
userContext: input.userContext,
});
Expand Down Expand Up @@ -92,5 +93,12 @@ export default function getAPIImplementation(): APIInterface {
userContext: input.userContext,
});
},
introspectTokenPOST: async (input) => {
return input.options.recipeImplementation.introspectToken({
token: input.token,
scopes: input.scopes,
userContext: input.userContext,
});
},
};
}
Loading

0 comments on commit 788d81d

Please sign in to comment.