Skip to content

Commit

Permalink
feat: Add userInfoGET endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
anku255 committed Jul 21, 2024
1 parent c5e6988 commit 64bb9c7
Show file tree
Hide file tree
Showing 17 changed files with 376 additions and 19 deletions.
12 changes: 12 additions & 0 deletions lib/build/recipe/oauth2/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ function getAPIImplementation() {
},
};
},
userInfoGET: async ({ accessTokenPayload, user, scopes, options, userContext }) => {
const userInfo = await options.recipeImplementation.buildUserInfo({
user,
accessTokenPayload,
scopes,
userContext,
});
return {
status: "OK",
info: userInfo,
};
},
};
}
exports.default = getAPIImplementation;
8 changes: 8 additions & 0 deletions lib/build/recipe/oauth2/api/userInfo.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 userInfoGET(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
68 changes: 68 additions & 0 deletions lib/build/recipe/oauth2/api/userInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"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");
const __1 = require("../../..");
// TODO: Replace stub implementation by the actual implementation
async function validateOAuth2AccessToken(accessToken) {
const resp = await fetch(`http://localhost:4445/admin/oauth2/introspect`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ token: accessToken }),
});
return await resp.json();
}
async function userInfoGET(apiImplementation, options, userContext) {
var _a;
if (apiImplementation.userInfoGET === undefined) {
return false;
}
const authHeader = options.req.getHeaderValue("authorization") || options.req.getHeaderValue("Authorization");
if (authHeader === undefined || !authHeader.startsWith("Bearer ")) {
utils_1.sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401);
return true;
}
const accessToken = authHeader.replace(/^Bearer /, "").trim();
let accessTokenPayload;
try {
accessTokenPayload = await validateOAuth2AccessToken(accessToken);
} catch (error) {
utils_1.sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token!", 401);
return true;
}
const userId = accessTokenPayload.sub;
const user = await __1.getUser(userId, userContext);
if (user === undefined) {
utils_1.sendNon200ResponseWithMessage(
options.res,
"Couldn't find any user associated with the access token",
401
);
return true;
}
const response = await apiImplementation.userInfoGET({
accessTokenPayload,
user,
scopes: ((_a = accessTokenPayload.scope) !== null && _a !== void 0 ? _a : "").split(" "),
options,
userContext,
});
utils_1.send200Response(options.res, response);
return true;
}
exports.default = userInfoGET;
1 change: 1 addition & 0 deletions lib/build/recipe/oauth2/constants.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export declare const CONSENT_PATH = "/oauth2/consent";
export declare const AUTH_PATH = "/oauth2/auth";
export declare const TOKEN_PATH = "/oauth2/token";
export declare const LOGIN_INFO_PATH = "/oauth2/login/info";
export declare const USER_INFO_PATH = "/oauth2/userinfo";
3 changes: 2 additions & 1 deletion lib/build/recipe/oauth2/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.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.CONSENT_PATH = exports.LOGOUT_PATH = exports.LOGIN_PATH = exports.OAUTH2_BASE_PATH = void 0;
exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.CONSENT_PATH = exports.LOGOUT_PATH = exports.LOGIN_PATH = exports.OAUTH2_BASE_PATH = void 0;
exports.OAUTH2_BASE_PATH = "/oauth2/";
exports.LOGIN_PATH = "/oauth2/login";
exports.LOGOUT_PATH = "/oauth2/logout";
exports.CONSENT_PATH = "/oauth2/consent";
exports.AUTH_PATH = "/oauth2/auth";
exports.TOKEN_PATH = "/oauth2/token";
exports.LOGIN_INFO_PATH = "/oauth2/login/info";
exports.USER_INFO_PATH = "/oauth2/userinfo";
17 changes: 16 additions & 1 deletion lib/build/recipe/oauth2/recipe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import type { BaseRequest, BaseResponse } from "../../framework";
import NormalisedURLPath from "../../normalisedURLPath";
import RecipeModule from "../../recipeModule";
import { APIHandled, HTTPMethod, JSONObject, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types";
import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types";
import {
APIInterface,
RecipeInterface,
TypeInput,
TypeNormalisedInput,
UserInfo,
UserInfoBuilderFunction,
} from "./types";
import { User } from "../../user";
export default class Recipe extends RecipeModule {
static RECIPE_ID: string;
private static instance;
private idTokenBuilders;
private userInfoBuilders;
config: TypeNormalisedInput;
recipeInterfaceImpl: RecipeInterface;
apiImpl: APIInterface;
Expand All @@ -19,6 +27,7 @@ export default class Recipe extends RecipeModule {
static getInstanceOrThrowError(): Recipe;
static init(config?: TypeInput): RecipeListFunction;
static reset(): void;
addUserInfoBuilderFromOtherRecipe: (userInfoBuilderFn: UserInfoBuilderFunction) => void;
getAPIsHandled(): APIHandled[];
handleAPIRequest: (
id: string,
Expand All @@ -33,4 +42,10 @@ export default class Recipe extends RecipeModule {
getAllCORSHeaders(): string[];
isErrorFromThisRecipe(err: any): err is error;
getDefaultIdTokenPayload(user: User, scopes: string[], userContext: UserContext): Promise<JSONObject>;
getDefaultUserInfoPayload(
user: User,
accessTokenPayload: JSONObject,
scopes: string[],
userContext: UserContext
): Promise<UserInfo>;
}
43 changes: 42 additions & 1 deletion lib/build/recipe/oauth2/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ const constants_1 = require("./constants");
const recipeImplementation_1 = __importDefault(require("./recipeImplementation"));
const utils_1 = require("./utils");
const supertokens_js_override_1 = __importDefault(require("supertokens-js-override"));
const userInfo_1 = __importDefault(require("./api/userInfo"));
class Recipe extends recipeModule_1.default {
constructor(recipeId, appInfo, isInServerlessEnv, config) {
super(recipeId, appInfo);
this.idTokenBuilders = [];
this.userInfoBuilders = [];
this.addUserInfoBuilderFromOtherRecipe = (userInfoBuilderFn) => {
this.userInfoBuilders.push(userInfoBuilderFn);
};
this.handleAPIRequest = async (id, _tenantId, req, res, _path, _method, userContext) => {
let options = {
config: this.config,
Expand Down Expand Up @@ -65,6 +70,9 @@ class Recipe extends recipeModule_1.default {
if (id === constants_1.LOGIN_INFO_PATH) {
return loginInfo_1.default(this.apiImpl, options, userContext);
}
if (id === constants_1.USER_INFO_PATH) {
return userInfo_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 All @@ -75,7 +83,8 @@ class Recipe extends recipeModule_1.default {
querier_1.Querier.getNewInstanceOrThrowError(recipeId),
this.config,
appInfo,
this.getDefaultIdTokenPayload.bind(this)
this.getDefaultIdTokenPayload.bind(this),
this.getDefaultUserInfoPayload.bind(this)
)
);
this.recipeInterfaceImpl = builder.override(this.config.override.functions).build();
Expand Down Expand Up @@ -168,6 +177,12 @@ class Recipe extends recipeModule_1.default {
id: constants_1.LOGIN_INFO_PATH,
disabled: this.apiImpl.loginInfoGET === undefined,
},
{
method: "get",
pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.USER_INFO_PATH),
id: constants_1.USER_INFO_PATH,
disabled: this.apiImpl.userInfoGET === undefined,
},
];
}
handleError(error, _, __, _userContext) {
Expand Down Expand Up @@ -200,6 +215,32 @@ class Recipe extends recipeModule_1.default {
}
return payload;
}
async getDefaultUserInfoPayload(user, accessTokenPayload, scopes, userContext) {
let payload = {
sub: accessTokenPayload.sub,
};
if (scopes.includes("email")) {
payload.email = user === null || user === void 0 ? void 0 : user.emails[0];
payload.email_verified = user.loginMethods.some(
(lm) => lm.hasSameEmailAs(user === null || user === void 0 ? void 0 : user.emails[0]) && lm.verified
);
}
if (scopes.includes("phoneNumber")) {
payload.phoneNumber = user === null || user === void 0 ? void 0 : user.phoneNumbers[0];
payload.phoneNumber_verified = user.loginMethods.some(
(lm) =>
lm.hasSamePhoneNumberAs(user === null || user === void 0 ? void 0 : user.phoneNumbers[0]) &&
lm.verified
);
}
for (const fn of this.userInfoBuilders) {
payload = Object.assign(
Object.assign({}, payload),
await fn(user, accessTokenPayload, scopes, userContext)
);
}
return payload;
}
}
exports.default = Recipe;
Recipe.RECIPE_ID = "oauth2";
Expand Down
5 changes: 3 additions & 2 deletions lib/build/recipe/oauth2/recipeImplementation.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @ts-nocheck
import { Querier } from "../../querier";
import { NormalisedAppinfo } from "../../types";
import { RecipeInterface, TypeNormalisedInput, PayloadBuilderFunction } from "./types";
import { RecipeInterface, TypeNormalisedInput, PayloadBuilderFunction, UserInfoBuilderFunction } from "./types";
export default function getRecipeInterface(
querier: Querier,
_config: TypeNormalisedInput,
_appInfo: NormalisedAppinfo,
getDefaultIdTokenPayload: PayloadBuilderFunction
getDefaultIdTokenPayload: PayloadBuilderFunction,
getDefaultUserInfoPayload: UserInfoBuilderFunction
): RecipeInterface;
6 changes: 3 additions & 3 deletions lib/build/recipe/oauth2/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const querier_1 = require("../../querier");
const utils_1 = require("../../utils");
const OAuth2Client_1 = require("./OAuth2Client");
const __1 = require("../..");
function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload) {
function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload, getDefaultUserInfoPayload) {
return {
getLoginRequest: async function (input) {
const resp = await querier.sendGetRequest(
Expand Down Expand Up @@ -394,8 +394,8 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
buildIdTokenPayload: async function (input) {
return input.defaultPayload;
},
buildUserInfo: async function (input) {
return input.user.toJson(); // Proper impl
buildUserInfo: async function ({ user, accessTokenPayload, scopes, userContext }) {
return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, userContext);
},
};
}
Expand Down
30 changes: 29 additions & 1 deletion lib/build/recipe/oauth2/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ export declare type LoginInfo = {
logoUri: string;
metadata?: Record<string, any> | null;
};
export declare type UserInfo = {
sub: string;
email?: string;
email_verified?: boolean;
phoneNumber?: string;
phoneNumber_verified?: boolean;
[key: string]: any;
};
export declare type RecipeInterface = {
authorization(input: {
params: any;
Expand Down Expand Up @@ -224,7 +232,6 @@ export declare type RecipeInterface = {
user: User;
accessTokenPayload: JSONObject;
scopes: string[];
defaultInfo: JSONObject;
userContext: UserContext;
}): Promise<JSONObject>;
};
Expand Down Expand Up @@ -344,6 +351,21 @@ export declare type APIInterface = {
}
| GeneralErrorResponse
>);
userInfoGET:
| undefined
| ((input: {
accessTokenPayload: JSONObject;
user: User;
scopes: string[];
options: APIOptions;
userContext: UserContext;
}) => Promise<
| {
status: "OK";
info: JSONObject;
}
| GeneralErrorResponse
>);
};
export declare type OAuth2ClientOptions = {
clientId: string;
Expand Down Expand Up @@ -445,3 +467,9 @@ export declare type PayloadBuilderFunction = (
scopes: string[],
userContext: UserContext
) => Promise<JSONObject>;
export declare type UserInfoBuilderFunction = (
user: User,
accessTokenPayload: JSONObject,
scopes: string[],
userContext: UserContext
) => Promise<UserInfo>;
8 changes: 5 additions & 3 deletions lib/build/recipe/oauth2client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ class Wrapper {
static async getUserInfo(oAuthTokens, userContext) {
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const providerConfig = await recipeInterfaceImpl.getProviderConfig({ userContext });
return await recipe_1.default
.getInstanceOrThrowError()
.recipeInterfaceImpl.getUserInfo({ providerConfig, oAuthTokens, userContext });
return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({
providerConfig,
oAuthTokens,
userContext,
});
}
}
exports.default = Wrapper;
Expand Down
13 changes: 13 additions & 0 deletions lib/ts/recipe/oauth2/api/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,18 @@ export default function getAPIImplementation(): APIInterface {
},
};
},
userInfoGET: async ({ accessTokenPayload, user, scopes, options, userContext }) => {
const userInfo = await options.recipeImplementation.buildUserInfo({
user,
accessTokenPayload,
scopes,
userContext,
});

return {
status: "OK",
info: userInfo,
};
},
};
}
Loading

0 comments on commit 64bb9c7

Please sign in to comment.