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 2 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
3 changes: 2 additions & 1 deletion lib/build/recipe/oauth2/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,12 @@ function getAPIImplementation() {
},
};
},
userInfoGET: async ({ accessTokenPayload, user, scopes, options, userContext }) => {
userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => {
const userInfo = await options.recipeImplementation.buildUserInfo({
user,
accessTokenPayload,
scopes,
tenantId,
userContext,
});
return {
Expand Down
1 change: 1 addition & 0 deletions lib/build/recipe/oauth2/api/userInfo.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { APIInterface, APIOptions } from "..";
import { UserContext } from "../../../types";
export default function userInfoGET(
apiImplementation: APIInterface,
tenantId: string,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
26 changes: 20 additions & 6 deletions lib/build/recipe/oauth2/api/userInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,52 @@ async function validateOAuth2AccessToken(accessToken) {
});
return await resp.json();
}
async function userInfoGET(apiImplementation, options, userContext) {
var _a;
async function userInfoGET(apiImplementation, tenantId, options, userContext) {
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);
// TODO: Returning a 400 instead of a 401 to prevent a potential refresh loop in the client SDK.
// When addressing this TODO, review other response codes in this function as well.
utils_1.sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 400);
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);
options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false);
utils_1.sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 400);
return true;
}
if (
accessTokenPayload === null ||
typeof accessTokenPayload !== "object" ||
typeof accessTokenPayload.sub !== "string" ||
typeof accessTokenPayload.scope !== "string"
) {
options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false);
utils_1.sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 400);
return true;
}
const userId = accessTokenPayload.sub;
const user = await __1.getUser(userId, userContext);
if (user === undefined) {
options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false);
utils_1.sendNon200ResponseWithMessage(
options.res,
"Couldn't find any user associated with the access token",
401
400
);
return true;
}
const response = await apiImplementation.userInfoGET({
accessTokenPayload,
user,
scopes: ((_a = accessTokenPayload.scope) !== null && _a !== void 0 ? _a : "").split(" "),
tenantId,
scopes: accessTokenPayload.scope.split(" "),
options,
userContext,
});
Expand Down
3 changes: 2 additions & 1 deletion lib/build/recipe/oauth2/recipe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class Recipe extends RecipeModule {
getAPIsHandled(): APIHandled[];
handleAPIRequest: (
id: string,
_tenantId: string | undefined,
tenantId: string,
req: BaseRequest,
res: BaseResponse,
_path: NormalisedURLPath,
Expand All @@ -46,6 +46,7 @@ export default class Recipe extends RecipeModule {
user: User,
accessTokenPayload: JSONObject,
scopes: string[],
tenantId: string,
userContext: UserContext
): Promise<UserInfo>;
}
8 changes: 4 additions & 4 deletions lib/build/recipe/oauth2/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Recipe extends recipeModule_1.default {
this.addUserInfoBuilderFromOtherRecipe = (userInfoBuilderFn) => {
this.userInfoBuilders.push(userInfoBuilderFn);
};
this.handleAPIRequest = async (id, _tenantId, req, res, _path, _method, userContext) => {
this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => {
let options = {
config: this.config,
recipeId: this.getRecipeId(),
Expand Down Expand Up @@ -71,7 +71,7 @@ class Recipe extends recipeModule_1.default {
return loginInfo_1.default(this.apiImpl, options, userContext);
}
if (id === constants_1.USER_INFO_PATH) {
return userInfo_1.default(this.apiImpl, options, userContext);
return userInfo_1.default(this.apiImpl, tenantId, options, userContext);
}
throw new Error("Should never come here: handleAPIRequest called with unknown id");
};
Expand Down Expand Up @@ -215,7 +215,7 @@ class Recipe extends recipeModule_1.default {
}
return payload;
}
async getDefaultUserInfoPayload(user, accessTokenPayload, scopes, userContext) {
async getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext) {
let payload = {
sub: accessTokenPayload.sub,
};
Expand All @@ -236,7 +236,7 @@ class Recipe extends recipeModule_1.default {
for (const fn of this.userInfoBuilders) {
payload = Object.assign(
Object.assign({}, payload),
await fn(user, accessTokenPayload, scopes, userContext)
await fn(user, accessTokenPayload, scopes, tenantId, userContext)
);
}
return payload;
Expand Down
4 changes: 2 additions & 2 deletions lib/build/recipe/oauth2/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,8 @@ function getRecipeInterface(querier, _config, _appInfo, getDefaultIdTokenPayload
buildIdTokenPayload: async function (input) {
return input.defaultPayload;
},
buildUserInfo: async function ({ user, accessTokenPayload, scopes, userContext }) {
return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, userContext);
buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) {
return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext);
},
};
}
Expand Down
9 changes: 6 additions & 3 deletions lib/build/recipe/oauth2/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-nocheck
import type { BaseRequest, BaseResponse } from "../../framework";
import OverrideableBuilder from "supertokens-js-override";
import { GeneralErrorResponse, JSONObject, NonNullableProperties, UserContext } from "../../types";
import { GeneralErrorResponse, JSONObject, JSONValue, NonNullableProperties, UserContext } from "../../types";
import { SessionContainerInterface } from "../session/types";
import { OAuth2Client } from "./OAuth2Client";
import { User } from "../../user";
Expand Down Expand Up @@ -93,7 +93,7 @@ export declare type UserInfo = {
email_verified?: boolean;
phoneNumber?: string;
phoneNumber_verified?: boolean;
[key: string]: any;
[key: string]: JSONValue;
};
export declare type RecipeInterface = {
authorization(input: {
Expand Down Expand Up @@ -232,6 +232,7 @@ export declare type RecipeInterface = {
user: User;
accessTokenPayload: JSONObject;
scopes: string[];
tenantId: string;
userContext: UserContext;
}): Promise<JSONObject>;
};
Expand Down Expand Up @@ -357,6 +358,7 @@ export declare type APIInterface = {
accessTokenPayload: JSONObject;
user: User;
scopes: string[];
tenantId: string;
options: APIOptions;
userContext: UserContext;
}) => Promise<
Expand Down Expand Up @@ -471,5 +473,6 @@ export declare type UserInfoBuilderFunction = (
user: User,
accessTokenPayload: JSONObject,
scopes: string[],
tenantId: string,
userContext: UserContext
) => Promise<UserInfo>;
) => Promise<JSONObject>;
10 changes: 6 additions & 4 deletions lib/build/recipe/oauth2client/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// @ts-nocheck
import { UserContext } from "../../types";
import Recipe from "./recipe";
import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types";
export default class Wrapper {
static init: typeof Recipe.init;
static getAuthorisationRedirectURL(
redirectURIOnProviderDashboard: string,
userContext: UserContext
userContext?: Record<string, any>
): Promise<{
urlWithQueryParams: string;
pkceCodeVerifier?: string | undefined;
Expand All @@ -17,9 +16,12 @@ export default class Wrapper {
redirectURIQueryParams: any;
pkceCodeVerifier?: string | undefined;
},
userContext: UserContext
userContext?: Record<string, any>
): Promise<import("./types").OAuthTokenResponse>;
static getUserInfo(oAuthTokens: OAuthTokens, userContext: UserContext): Promise<import("./types").UserInfo>;
static getUserInfo(
oAuthTokens: OAuthTokens,
userContext?: Record<string, any>
): Promise<import("./types").UserInfo>;
}
export declare let init: typeof Recipe.init;
export declare let getAuthorisationRedirectURL: typeof Wrapper.getAuthorisationRedirectURL;
Expand Down
19 changes: 13 additions & 6 deletions lib/build/recipe/oauth2client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,40 @@ var __importDefault =
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUserInfo = exports.exchangeAuthCodeForOAuthTokens = exports.getAuthorisationRedirectURL = exports.init = void 0;
const utils_1 = require("../../utils");
const recipe_1 = __importDefault(require("./recipe"));
class Wrapper {
static async getAuthorisationRedirectURL(redirectURIOnProviderDashboard, userContext) {
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const providerConfig = await recipeInterfaceImpl.getProviderConfig({ userContext });
const providerConfig = await recipeInterfaceImpl.getProviderConfig({
userContext: utils_1.getUserContext(userContext),
});
return await recipeInterfaceImpl.getAuthorisationRedirectURL({
providerConfig,
redirectURIOnProviderDashboard,
userContext,
userContext: utils_1.getUserContext(userContext),
});
}
static async exchangeAuthCodeForOAuthTokens(redirectURIInfo, userContext) {
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const providerConfig = await recipeInterfaceImpl.getProviderConfig({ userContext });
const providerConfig = await recipeInterfaceImpl.getProviderConfig({
userContext: utils_1.getUserContext(userContext),
});
return await recipeInterfaceImpl.exchangeAuthCodeForOAuthTokens({
providerConfig,
redirectURIInfo,
userContext,
userContext: utils_1.getUserContext(userContext),
});
}
static async getUserInfo(oAuthTokens, userContext) {
const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl;
const providerConfig = await recipeInterfaceImpl.getProviderConfig({ userContext });
const providerConfig = await recipeInterfaceImpl.getProviderConfig({
userContext: utils_1.getUserContext(userContext),
});
return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({
providerConfig,
oAuthTokens,
userContext,
userContext: utils_1.getUserContext(userContext),
});
}
}
Expand Down
7 changes: 3 additions & 4 deletions lib/build/recipe/oauth2client/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,9 @@ function getRecipeImplementation(_querier, config) {
if (oidcInfo.token_endpoint === undefined) {
throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint.");
}
// TODO: We currently don't have this
// if (oidcInfo.userinfo_endpoint === undefined) {
// throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint.");
// }
if (oidcInfo.userinfo_endpoint === undefined) {
throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint.");
}
if (oidcInfo.jwks_uri === undefined) {
throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint.");
}
Expand Down
1 change: 1 addition & 0 deletions lib/build/recipe/openid/recipeImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function getRecipeInterface(config, jwtRecipeImplementation, appInfo) {
jwks_uri,
authorization_endpoint: apiBasePath + constants_2.AUTH_PATH,
token_endpoint: apiBasePath + constants_2.TOKEN_PATH,
userinfo_endpoint: apiBasePath + constants_2.USER_INFO_PATH,
subject_types_supported: ["public"],
id_token_signing_alg_values_supported: ["RS256"],
response_types_supported: ["code", "id_token", "id_token token"],
Expand Down
34 changes: 34 additions & 0 deletions lib/build/recipe/userroles/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const utils_1 = require("./utils");
const supertokens_js_override_1 = __importDefault(require("supertokens-js-override"));
const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks");
const recipe_1 = __importDefault(require("../session/recipe"));
const recipe_2 = __importDefault(require("../oauth2/recipe"));
const userRoleClaim_1 = require("./userRoleClaim");
const permissionClaim_1 = require("./permissionClaim");
class Recipe extends recipeModule_1.default {
Expand All @@ -51,6 +52,39 @@ class Recipe extends recipeModule_1.default {
if (!this.config.skipAddingPermissionsToAccessToken) {
recipe_1.default.getInstanceOrThrowError().addClaimFromOtherRecipe(permissionClaim_1.PermissionClaim);
}
recipe_2.default
.getInstanceOrThrowError()
.addUserInfoBuilderFromOtherRecipe(async (user, _accessTokenPayload, scopes, tenantId, userContext) => {
let userInfo = {};
if (scopes.includes("roles")) {
const res = await this.recipeInterfaceImpl.getRolesForUser({
userId: user.id,
tenantId,
userContext,
});
if (res.status !== "OK") {
throw new Error("Failed to fetch roles for the user");
}
userInfo.roles = res.roles;
if (scopes.includes("permissions")) {
const userPermissions = new Set();
for (const role of userInfo.roles) {
const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({
role,
userContext,
});
if (rolePermissions.status !== "OK") {
throw new Error("Failed to fetch permissions for the role");
}
for (const perm of rolePermissions.permissions) {
userPermissions.add(perm);
}
}
userInfo.permissons = Array.from(userPermissions);
}
}
return userInfo;
});
});
}
/* Init functions */
Expand Down
3 changes: 2 additions & 1 deletion lib/ts/recipe/oauth2/api/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,12 @@ export default function getAPIImplementation(): APIInterface {
},
};
},
userInfoGET: async ({ accessTokenPayload, user, scopes, options, userContext }) => {
userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => {
const userInfo = await options.recipeImplementation.buildUserInfo({
user,
accessTokenPayload,
scopes,
tenantId,
userContext,
});

Expand Down
Loading
Loading