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 userInfoGET endpoint #890

Merged
merged 22 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d292b0e
feat: add initial oauth2 client apis
porcellus Jun 13, 2024
bf7f178
Merge remote-tracking branch 'origin/feat/oauth2/base' into feat/oaut…
porcellus Jun 21, 2024
508bfad
feat: Add an api to get login info
anku255 Jun 21, 2024
ab707b4
Merge remote-tracking branch 'origin/feat/oauth2/base' into feat/oaut…
porcellus Jun 26, 2024
849c654
fix: merge issues and FE path
porcellus Jun 26, 2024
5bcedd6
fix: WIP fix for CSRF and redirection issues
porcellus Jun 27, 2024
112e5ee
Merge remote-tracking branch 'origin/feat/oauth2/initial_apis' into f…
anku255 Jun 27, 2024
a4636e2
Merge pull request #867 from supertokens/feat/login-info-api
anku255 Jun 27, 2024
deb0392
fix: OAuth2 fixes and test-server updates (#871)
anku255 Jun 27, 2024
3af555e
feat: update oauth2 login info endpoint types to match our general pa…
porcellus Jun 27, 2024
ff2135b
fix: make login flow work
porcellus Jun 27, 2024
8c977f4
fix: circular dependency
porcellus Jul 4, 2024
769a049
feat: Add OAuth2Client recipe
anku255 Jul 8, 2024
7cd08a1
fix: PR changes
anku255 Jul 10, 2024
efa789b
fix: PR changes
anku255 Jul 12, 2024
51f4c9f
Merge remote-tracking branch 'origin/feat/oauth2/base' into feat/oaut…
porcellus Jul 14, 2024
c5e6988
fix: PR changes
anku255 Jul 15, 2024
64bb9c7
feat: Add userInfoGET endpoint
anku255 Jul 21, 2024
e4506de
Merge remote-tracking branch 'origin/feat/oauth2/base' into feat/oaut…
anku255 Jul 24, 2024
08183ce
fix: PR changes
anku255 Jul 24, 2024
b0f62cb
fix: PR changes
anku255 Jul 26, 2024
09035a6
fix: PR changes
anku255 Jul 26, 2024
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
2 changes: 1 addition & 1 deletion lib/build/recipe/jwt/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function getAPIImplementation() {
if (resp.validityInSeconds !== undefined) {
options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false);
}
const oauth2 = require("../../oauth2").getInstance();
const oauth2 = require("../../oauth2/recipe").default.getInstance();
// TODO: dirty hack until we get core support
if (oauth2 !== undefined) {
const oauth2JWKSRes = await fetch("http://localhost:4444/.well-known/jwks.json");
Expand Down
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
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>;
9 changes: 9 additions & 0 deletions lib/build/recipe/oauth2client/api/authorisationUrl.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @ts-nocheck
import { APIInterface, APIOptions } from "../";
import { UserContext } from "../../../types";
export default function authorisationUrlAPI(
apiImplementation: APIInterface,
_tenantId: string,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
44 changes: 44 additions & 0 deletions lib/build/recipe/oauth2client/api/authorisationUrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"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 });
const utils_1 = require("../../../utils");
const error_1 = __importDefault(require("../../../error"));
async function authorisationUrlAPI(apiImplementation, _tenantId, options, userContext) {
if (apiImplementation.authorisationUrlGET === undefined) {
return false;
}
// TODO: Check if we can rename `redirectURIOnProviderDashboard` to a more suitable name
const redirectURIOnProviderDashboard = options.req.getKeyValueFromQuery("redirectURIOnProviderDashboard");
if (redirectURIOnProviderDashboard === undefined || typeof redirectURIOnProviderDashboard !== "string") {
throw new error_1.default({
type: error_1.default.BAD_INPUT_ERROR,
message: "Please provide the redirectURIOnProviderDashboard as a GET param",
});
}
let result = await apiImplementation.authorisationUrlGET({
redirectURIOnProviderDashboard,
options,
userContext,
});
utils_1.send200Response(options.res, result);
return true;
}
exports.default = authorisationUrlAPI;
3 changes: 3 additions & 0 deletions lib/build/recipe/oauth2client/api/implementation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @ts-nocheck
import { APIInterface } from "../";
export default function getAPIInterface(): APIInterface;
Loading
Loading