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: oauth2 core integration #916

Open
wants to merge 38 commits into
base: 21.0
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
03f3914
feat: add boilerplate for oauth2 recipe
porcellus Jun 9, 2024
5a215b9
feat: add a temporary solution to query hydra (until core impl) from …
porcellus Jun 12, 2024
7a29a7a
fix: fix temp solution for hydra calls
porcellus Jun 13, 2024
16e9631
feat: Add a recipe function to create OAuth2Client (#859)
anku255 Jun 17, 2024
fb51f36
feat: Add recipe functions to update/delete OAuth2Client (#863)
anku255 Jun 21, 2024
568a2f3
feat: Add recipe functions to get OAuth2Clients (#865)
anku255 Jun 21, 2024
05b6fb2
Merge remote-tracking branch 'origin/19.0' into feat/oauth2/base
porcellus Jun 26, 2024
2fd8ef4
feat: add initial oauth2 client apis (#866)
porcellus Jul 14, 2024
e84eb49
feat: Add OAuth2Client recipe (#877)
anku255 Jul 23, 2024
9c7a22d
fix: Remove internal redirects in the OAuth2 flow (#896)
anku255 Jul 26, 2024
0b39ad9
fix: Prefer exact api path match in the middleware (#892)
anku255 Jul 26, 2024
92121af
feat: Add userInfoGET endpoint (#890)
anku255 Jul 26, 2024
4ab2410
feat: add functions to validate oauth2 tokens
porcellus Jul 28, 2024
e0cdae5
feat: rename OAuth2 to OAuth2Provider
porcellus Jul 28, 2024
a463b65
feat: expose token validation functions
porcellus Jul 28, 2024
b0984c1
test: update tests
porcellus Jul 28, 2024
a29ffbe
fix: add userinfo_endpoint properly
porcellus Jul 28, 2024
6950da7
feat: removed unnecessary props
porcellus Jul 28, 2024
6974420
fix: add workaround to validate access/idtokens
porcellus Jul 29, 2024
a351c0a
fix: OAuth2 fixes (#900)
anku255 Jul 29, 2024
3736358
Merge remote-tracking branch 'origin/20.0' into feat/oauth2/base
porcellus Aug 1, 2024
aac74df
Merge branch 'feat/oauth2/base' of github.com:supertokens/supertokens…
porcellus Aug 1, 2024
6bab7f5
feat: review fixes
porcellus Aug 1, 2024
be263bd
feat: remove accessTokenStrategy
porcellus Aug 1, 2024
f53853c
test: update tests
porcellus Aug 1, 2024
1271be1
feat: OAuth2Client interface changes (#904)
anku255 Aug 6, 2024
a7a2b87
feat: Add token revocation endpoint (#902)
anku255 Aug 8, 2024
611d860
feat: Add token introspection endpoint (#906)
anku255 Aug 8, 2024
4830f0a
fix: make clientSecret optional (#908)
anku255 Aug 9, 2024
6f45c5f
fix: revokeToken input check
anku255 Aug 9, 2024
905b5cd
feat: add shouldTryLinkingWithSessionUser flag to auth apis and make …
porcellus Aug 11, 2024
9f7866c
feat: add shouldTryRefresh plus self-review and test related fixes
porcellus Aug 18, 2024
6f09926
fix: auth and token endpoint integration
sattvikc Aug 21, 2024
93c3790
fix: token endpoint
sattvikc Aug 23, 2024
2ec4784
fix: refactor issuer
sattvikc Aug 27, 2024
cc9d278
fix: consent integration
sattvikc Sep 5, 2024
cba1ce5
fix: login integration
sattvikc Sep 6, 2024
471e282
fix: minor updates
sattvikc Sep 11, 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
Prev Previous commit
Next Next commit
feat: Add userInfoGET endpoint (#890)
* feat: add initial oauth2 client apis

* feat: Add an api to get login info

* fix: merge issues and FE path

* fix: WIP fix for CSRF and redirection issues

* fix: OAuth2 fixes and test-server updates (#871)

* feat: update oauth2 login info endpoint types to match our general patterns

* fix: make login flow work

* fix: circular dependency

* feat: Add OAuth2Client recipe

* fix: PR changes

* fix: PR changes

* fix: PR changes

* feat: Add userInfoGET endpoint

* fix: PR changes

* fix: PR changes

* fix: PR changes

---------

Co-authored-by: Mihaly Lengyel <mihaly@lengyel.tech>
anku255 and porcellus authored Jul 26, 2024
commit 92121af3278d32eb891c23c15df77d65fdd93315
13 changes: 13 additions & 0 deletions lib/build/recipe/oauth2/api/implementation.js
Original file line number Diff line number Diff line change
@@ -166,6 +166,19 @@ function getAPIImplementation() {
},
};
},
userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => {
const userInfo = await options.recipeImplementation.buildUserInfo({
user,
accessTokenPayload,
scopes,
tenantId,
userContext,
});
return {
status: "OK",
info: userInfo,
};
},
};
}
exports.default = getAPIImplementation;
9 changes: 9 additions & 0 deletions lib/build/recipe/oauth2/api/userInfo.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 userInfoGET(
apiImplementation: APIInterface,
tenantId: string,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
82 changes: 82 additions & 0 deletions lib/build/recipe/oauth2/api/userInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"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, 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 ")) {
// 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) {
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",
400
);
return true;
}
const response = await apiImplementation.userInfoGET({
accessTokenPayload,
user,
tenantId,
scopes: accessTokenPayload.scope.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
@@ -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
@@ -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";
20 changes: 18 additions & 2 deletions lib/build/recipe/oauth2/recipe.d.ts
Original file line number Diff line number Diff line change
@@ -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;
@@ -19,10 +27,11 @@ 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,
_tenantId: string | undefined,
tenantId: string,
req: BaseRequest,
res: BaseResponse,
_path: NormalisedURLPath,
@@ -33,4 +42,11 @@ 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[],
tenantId: string,
userContext: UserContext
): Promise<UserInfo>;
}
45 changes: 43 additions & 2 deletions lib/build/recipe/oauth2/recipe.js
Original file line number Diff line number Diff line change
@@ -34,11 +34,16 @@ 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.handleAPIRequest = async (id, _tenantId, req, res, _path, _method, userContext) => {
this.userInfoBuilders = [];
this.addUserInfoBuilderFromOtherRecipe = (userInfoBuilderFn) => {
this.userInfoBuilders.push(userInfoBuilderFn);
};
this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => {
let options = {
config: this.config,
recipeId: this.getRecipeId(),
@@ -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, tenantId, options, userContext);
}
throw new Error("Should never come here: handleAPIRequest called with unknown id");
};
this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config);
@@ -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();
@@ -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) {
@@ -200,6 +215,32 @@ class Recipe extends recipeModule_1.default {
}
return payload;
}
async getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, 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, tenantId, userContext)
);
}
return payload;
}
}
exports.default = Recipe;
Recipe.RECIPE_ID = "oauth2";
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
@@ -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(
@@ -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, tenantId, userContext }) {
return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext);
},
};
}
35 changes: 33 additions & 2 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";
@@ -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]: JSONValue;
};
export declare type RecipeInterface = {
authorization(input: {
params: any;
@@ -224,7 +232,7 @@ export declare type RecipeInterface = {
user: User;
accessTokenPayload: JSONObject;
scopes: string[];
defaultInfo: JSONObject;
tenantId: string;
userContext: UserContext;
}): Promise<JSONObject>;
};
@@ -344,6 +352,22 @@ export declare type APIInterface = {
}
| GeneralErrorResponse
>);
userInfoGET:
| undefined
| ((input: {
accessTokenPayload: JSONObject;
user: User;
scopes: string[];
tenantId: string;
options: APIOptions;
userContext: UserContext;
}) => Promise<
| {
status: "OK";
info: JSONObject;
}
| GeneralErrorResponse
>);
};
export declare type OAuth2ClientOptions = {
clientId: string;
@@ -445,3 +469,10 @@ export declare type PayloadBuilderFunction = (
scopes: string[],
userContext: UserContext
) => Promise<JSONObject>;
export declare type UserInfoBuilderFunction = (
user: User,
accessTokenPayload: JSONObject,
scopes: string[],
tenantId: string,
userContext: UserContext
) => Promise<JSONObject>;
Loading