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: rename OAuth2 to OAuth2Provider
porcellus committed Jul 28, 2024
commit e0cdae54a4dc8c72ac6769e9a4d51750bdb94e13
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## UNRELEASED

- Added OAuth2 recipe
- Added OAuth2Provider recipe

## [19.0.0] - 2024-06-10

2 changes: 1 addition & 1 deletion lib/build/combinedRemoteJWKSet.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/combinedRemoteJWKSet.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions lib/build/recipe/jwt/api/implementation.js
Original file line number Diff line number Diff line change
@@ -21,9 +21,9 @@ function getAPIImplementation() {
if (resp.validityInSeconds !== undefined) {
options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false);
}
const oauth2 = require("../../oauth2/recipe").default.getInstance();
const oauth2Provider = require("../../oauth2provider/recipe").default.getInstance();
// TODO: dirty hack until we get core support
if (oauth2 !== undefined) {
if (oauth2Provider !== undefined) {
const oauth2JWKSRes = await fetch("http://localhost:4444/.well-known/jwks.json");
if (oauth2JWKSRes.ok) {
const oauth2RespBody = await oauth2JWKSRes.json();
9 changes: 0 additions & 9 deletions lib/build/recipe/oauth2/constants.d.ts

This file was deleted.

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -13,20 +13,15 @@
* 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 recipe_1 = __importDefault(require("../recipe"));
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;
@@ -41,7 +36,13 @@ async function userInfoGET(apiImplementation, tenantId, options, userContext) {
const accessToken = authHeader.replace(/^Bearer /, "").trim();
let accessTokenPayload;
try {
accessTokenPayload = await validateOAuth2AccessToken(accessToken);
accessTokenPayload = await recipe_1.default
.getInstanceOrThrowError()
.recipeInterfaceImpl.validateOAuth2AccessToken({
token: accessToken,
// TODO: expectedAudience?
userContext,
});
} catch (error) {
options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false);
utils_1.sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 400);
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -94,7 +94,7 @@ function isInternalRedirect(redirectTo) {
}
// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip.
// If an internal redirect is identified, it's handled directly by this function.
// Currently, we only need to handle redirects to /oauth2/login and /oauth2/auth endpoints.
// Currently, we only need to handle redirects to /oauth2provider/login and /oauth2provider/auth endpoints.
async function handleInternalRedirects({ response, recipeImplementation, session, cookie = "", userContext }) {
var _a;
if (!isInternalRedirect(response.redirectTo)) {
9 changes: 9 additions & 0 deletions lib/build/recipe/oauth2provider/constants.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @ts-nocheck
export declare const OAUTH2_BASE_PATH = "/oauth2provider/";
export declare const LOGIN_PATH = "/oauth2provider/login";
export declare const LOGOUT_PATH = "/oauth2provider/logout";
export declare const CONSENT_PATH = "/oauth2provider/consent";
export declare const AUTH_PATH = "/oauth2provider/auth";
export declare const TOKEN_PATH = "/oauth2provider/token";
export declare const LOGIN_INFO_PATH = "/oauth2provider/login/info";
export declare const USER_INFO_PATH = "/oauth2provider/userinfo";
Original file line number Diff line number Diff line change
@@ -15,11 +15,11 @@
*/
Object.defineProperty(exports, "__esModule", { value: true });
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";
exports.OAUTH2_BASE_PATH = "/oauth2provider/";
exports.LOGIN_PATH = "/oauth2provider/login";
exports.LOGOUT_PATH = "/oauth2provider/logout";
exports.CONSENT_PATH = "/oauth2provider/consent";
exports.AUTH_PATH = "/oauth2provider/auth";
exports.TOKEN_PATH = "/oauth2provider/token";
exports.LOGIN_INFO_PATH = "/oauth2provider/login/info";
exports.USER_INFO_PATH = "/oauth2provider/userinfo";
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -111,7 +111,7 @@ class Recipe extends recipeModule_1.default {
Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config);
return Recipe.instance;
} else {
throw new Error("OAuth2 recipe has already been initialised. Please check your code for bugs.");
throw new Error("OAuth2Provider recipe has already been initialised. Please check your code for bugs.");
}
};
}
@@ -245,5 +245,5 @@ class Recipe extends recipeModule_1.default {
}
}
exports.default = Recipe;
Recipe.RECIPE_ID = "oauth2";
Recipe.RECIPE_ID = "oauth2provider";
Recipe.instance = undefined;
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/build/recipe/openid/recipeImplementation.js
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ var __importDefault =
Object.defineProperty(exports, "__esModule", { value: true });
const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath"));
const constants_1 = require("../jwt/constants");
const constants_2 = require("../oauth2/constants");
const constants_2 = require("../oauth2provider/constants");
function getRecipeInterface(config, jwtRecipeImplementation, appInfo) {
return {
getOpenIdDiscoveryConfiguration: async function () {
2 changes: 1 addition & 1 deletion lib/build/recipe/userroles/recipe.js
Original file line number Diff line number Diff line change
@@ -27,7 +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 recipe_2 = __importDefault(require("../oauth2provider/recipe"));
const userRoleClaim_1 = require("./userRoleClaim");
const permissionClaim_1 = require("./permissionClaim");
class Recipe extends recipeModule_1.default {
2 changes: 1 addition & 1 deletion lib/build/supertokens.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/ts/combinedRemoteJWKSet.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ let combinedJWKS: ReturnType<typeof createRemoteJWKSet> | undefined;
/**
* We need this to reset the combinedJWKS in tests because we need to create a new instance of the combinedJWKS
* for each test to avoid caching issues.
* This is called when the session recipe is reset and when the oauth2 recipe is reset.
* This is called when the session recipe is reset and when the oauth2provider recipe is reset.
* Calling this multiple times doesn't cause an issue.
*/
export function resetCombinedJWKS() {
4 changes: 2 additions & 2 deletions lib/ts/recipe/jwt/api/implementation.ts
Original file line number Diff line number Diff line change
@@ -31,10 +31,10 @@ export default function getAPIImplementation(): APIInterface {
options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false);
}

const oauth2 = require("../../oauth2/recipe").default.getInstance();
const oauth2Provider = require("../../oauth2provider/recipe").default.getInstance();

// TODO: dirty hack until we get core support
if (oauth2 !== undefined) {
if (oauth2Provider !== undefined) {
const oauth2JWKSRes = await fetch("http://localhost:4444/.well-known/jwks.json");
if (oauth2JWKSRes.ok) {
const oauth2RespBody = await oauth2JWKSRes.json();
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -13,23 +13,12 @@
* under the License.
*/

import OAuth2ProviderRecipe from "../recipe";
import { send200Response, sendNon200ResponseWithMessage } from "../../../utils";
import { APIInterface, APIOptions } from "..";
import { JSONObject, UserContext } from "../../../types";
import { getUser } from "../../..";

// TODO: Replace stub implementation by the actual implementation
async function validateOAuth2AccessToken(accessToken: string) {
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();
}

export default async function userInfoGET(
apiImplementation: APIInterface,
tenantId: string,
@@ -54,7 +43,13 @@ export default async function userInfoGET(
let accessTokenPayload: JSONObject;

try {
accessTokenPayload = await validateOAuth2AccessToken(accessToken);
accessTokenPayload = await OAuth2ProviderRecipe.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken(
{
token: accessToken,
// TODO: expectedAudience?
userContext,
}
);
} catch (error) {
options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false);
sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 400);
Original file line number Diff line number Diff line change
@@ -107,7 +107,7 @@ function isInternalRedirect(redirectTo: string): boolean {

// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip.
// If an internal redirect is identified, it's handled directly by this function.
// Currently, we only need to handle redirects to /oauth2/login and /oauth2/auth endpoints.
// Currently, we only need to handle redirects to /oauth2provider/login and /oauth2provider/auth endpoints.
export async function handleInternalRedirects({
response,
recipeImplementation,
Original file line number Diff line number Diff line change
@@ -13,12 +13,12 @@
* under the License.
*/

export const OAUTH2_BASE_PATH = "/oauth2/";
export const OAUTH2_BASE_PATH = "/oauth2provider/";

export const LOGIN_PATH = "/oauth2/login";
export const LOGOUT_PATH = "/oauth2/logout";
export const CONSENT_PATH = "/oauth2/consent";
export const AUTH_PATH = "/oauth2/auth";
export const TOKEN_PATH = "/oauth2/token";
export const LOGIN_INFO_PATH = "/oauth2/login/info";
export const USER_INFO_PATH = "/oauth2/userinfo";
export const LOGIN_PATH = "/oauth2provider/login";
export const LOGOUT_PATH = "/oauth2provider/logout";
export const CONSENT_PATH = "/oauth2provider/consent";
export const AUTH_PATH = "/oauth2provider/auth";
export const TOKEN_PATH = "/oauth2provider/token";
export const LOGIN_INFO_PATH = "/oauth2provider/login/info";
export const USER_INFO_PATH = "/oauth2provider/userinfo";
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ import userInfoGET from "./api/userInfo";
import { resetCombinedJWKS } from "../../combinedRemoteJWKSet";

export default class Recipe extends RecipeModule {
static RECIPE_ID = "oauth2";
static RECIPE_ID = "oauth2provider";
private static instance: Recipe | undefined = undefined;
private idTokenBuilders: PayloadBuilderFunction[] = [];
private userInfoBuilders: UserInfoBuilderFunction[] = [];
@@ -104,7 +104,7 @@ export default class Recipe extends RecipeModule {
Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config);
return Recipe.instance;
} else {
throw new Error("OAuth2 recipe has already been initialised. Please check your code for bugs.");
throw new Error("OAuth2Provider recipe has already been initialised. Please check your code for bugs.");
}
};
}
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/ts/recipe/openid/recipeImplementation.ts
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ import { RecipeInterface as JWTRecipeInterface, JsonWebKey } from "../jwt/types"
import NormalisedURLPath from "../../normalisedURLPath";
import { GET_JWKS_API } from "../jwt/constants";
import { NormalisedAppinfo, UserContext } from "../../types";
import { AUTH_PATH, TOKEN_PATH, USER_INFO_PATH } from "../oauth2/constants";
import { AUTH_PATH, TOKEN_PATH, USER_INFO_PATH } from "../oauth2provider/constants";

export default function getRecipeInterface(
config: TypeNormalisedInput,
2 changes: 1 addition & 1 deletion lib/ts/recipe/userroles/recipe.ts
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ import { validateAndNormaliseUserInput } from "./utils";
import OverrideableBuilder from "supertokens-js-override";
import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks";
import SessionRecipe from "../session/recipe";
import OAuth2Recipe from "../oauth2/recipe";
import OAuth2Recipe from "../oauth2provider/recipe";
import { UserRoleClaim } from "./userRoleClaim";
import { PermissionClaim } from "./permissionClaim";

2 changes: 1 addition & 1 deletion lib/ts/supertokens.ts
Original file line number Diff line number Diff line change
@@ -112,7 +112,7 @@ export default class SuperTokens {
let UserMetadataRecipe = require("./recipe/usermetadata/recipe").default;
let MultiFactorAuthRecipe = require("./recipe/multifactorauth/recipe").default;
let TotpRecipe = require("./recipe/totp/recipe").default;
let OAuth2ProviderRecipe = require("./recipe/oauth2/recipe").default;
let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default;

this.recipeModules = config.recipeList.map((func) => {
const recipeModule = func(this.appInfo, this.isInServerlessEnv);
4 changes: 2 additions & 2 deletions recipe/oauth2/index.d.ts → recipe/oauth2provider/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export * from "../../lib/build/recipe/oauth2";
export * from "../../lib/build/recipe/oauth2provider";
/**
* 'export *' does not re-export a default.
* import NextJS from "supertokens-node/nextjs";
* the above import statement won't be possible unless either
* - user add "esModuleInterop": true in their tsconfig.json file
* - we do the following change:
*/
import * as _default from "../../lib/build/recipe/oauth2";
import * as _default from "../../lib/build/recipe/oauth2provider";
export default _default;
2 changes: 1 addition & 1 deletion recipe/oauth2/index.js → recipe/oauth2provider/index.js
Original file line number Diff line number Diff line change
@@ -3,4 +3,4 @@ function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
exports.__esModule = true;
__export(require("../../lib/build/recipe/oauth2"));
__export(require("../../lib/build/recipe/oauth2provider"));
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export * from "../../../lib/build/recipe/oauth2/types";
export * from "../../../lib/build/recipe/oauth2provider/types";
/**
* 'export *' does not re-export a default.
* import NextJS from "supertokens-node/nextjs";
* the above import statement won't be possible unless either
* - user add "esModuleInterop": true in their tsconfig.json file
* - we do the following change:
*/
import * as _default from "../../../lib/build/recipe/oauth2/types";
import * as _default from "../../../lib/build/recipe/oauth2provider/types";
export default _default;
Original file line number Diff line number Diff line change
@@ -3,4 +3,4 @@ function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
exports.__esModule = true;
__export(require("../../../lib/build/recipe/oauth2/types"));
__export(require("../../../lib/build/recipe/oauth2provider/types"));
2 changes: 1 addition & 1 deletion test/oauth2/oauth2client.test.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ let assert = require("assert");
const { printPath, setupST, startST, killAllST, cleanST } = require("../utils");
let { ProcessState } = require("../../lib/build/processState");
let STExpress = require("../../");
let OAuth2Recipe = require("../../recipe/oauth2");
let OAuth2Recipe = require("../../recipe/oauth2provider");

describe(`OAuth2ClientTests: ${printPath("[test/oauth2/oauth2client.test.js]")}`, function () {
beforeEach(async function () {
Loading