Skip to content

Commit 89d421c

Browse files
porcellusanku255
andauthored
feat!: add OAuth2 support (#834)
* feat: add boilerplate for OAuth2 recipe * feat!: add initial impl for OAuth2 (#833) * feat: add initial impl of OAuth2 recipe * build: add missing bundle conf * fix: wrong recipe id * feat: clean up todos * feat: make use of getLoginChallengeInfo * fix: self-review fixes * refactor: rename OAuth2 to OAuth2Provider * feat: show logo for oauth clients * fix: Rename oauth2 to oauth2provider * test: Add e2e test for OAuth2 (#843) * test: Add e2e test for OAuth2 * fix: PR changes * feat: add tryLinkingWithSessionUser, forceFreshAuth and small test fixes * test: add explanation comment to oauth2 tests --------- Co-authored-by: Mihaly Lengyel <[email protected]> * feat: add a route we can use to force refreshes * test: extend/stabilize tests * feat: Add functions and prebuiltUI for oauth2 logout (#850) * feat: Add functions and prebuiltUI for oauth2 logout * Update lib/ts/recipe/oauth2provider/components/themes/themeBase.tsx Co-authored-by: Mihály Lengyel <[email protected]> * fix: PR changes * fix: PR changes --------- Co-authored-by: Mihály Lengyel <[email protected]> * Add OAuth2 example apps (#854) * feat: Add st-oauth2-authorization-server example * feat: Add with-oauth2-without-supertokens * feat: Add with-oauth2-with-supertokens example * feat: keep the tenantId queryparam during redirections * feat: update to match node changes * test: stability fixes * test: update dep version and fix tests * fix: ignore appname in the oauth flow if it is empty * fix: fix typo * feat: handle not initialized OAuth2Provider recipe more gracefully * feat: ignore loginChallenge queryparam on auth page if we couldn't load it * feat: show an error if the getLoginChallengeInfo errors out * feat: update prebuiltui types and add test into with-typescript * test: add more debugging options for ci * fix: shouldTryLinkingWithSessionUser * chore: update versions * ci: do not forward browser logs into the console on CI * test: improve request logging in tests * test: update test expectations to match new node logic * chore: update web-js dep version in lock --------- Co-authored-by: Mihaly Lengyel <[email protected]> * refactor: self-review fixes * refactor: self-review fixes * docs: remove oauth2 examples until the restructuring is done * chore: expand changelog * chore: set web-js version to new version branch * chore: update size limits --------- Co-authored-by: Ankit Tiwari <[email protected]>
1 parent 7501c03 commit 89d421c

File tree

207 files changed

+5435
-843
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

207 files changed

+5435
-843
lines changed

CHANGELOG.md

+85
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [unreleased]
99

10+
## [0.49.0] - 2024-10-07
11+
12+
### Changes
13+
14+
- Added the `OAuth2Provider` recipe
15+
- Changed the input types and default implementation of `AuthPageHeader` to show the client information in OAuth2 flows
16+
17+
### Breaking changes
18+
19+
- Now only supporting FDI 4.0 (Node >= 24.0.0)
20+
- All `getRedirectionURL` functions now also get a new `tenantIdFromQueryParams` prop
21+
- This is used in OAuth2 + Multi-tenant flows.
22+
- This should be safe to ignore if:
23+
- You are not using those recipes
24+
- You have a custom `getTenantId` implementation
25+
- You are not customizing paths of the pages handled by SuperTokens.
26+
- This is used to keep the `tenantId` query param during internal redirections between pages handled by the SDK.
27+
- If you have custom paths, you should set the tenantId queryparam based on this. (See migrations below for more details)
28+
- Added a new `shouldTryLinkingToSessionUser` flag to sign in/up related function inputs:
29+
- No action is needed if you are not using MFA/session based account linking.
30+
- If you are implementing MFA:
31+
- Plase set this flag to `false` (or leave as undefined) during first factor sign-ins
32+
- Please set this flag to `true` for secondary factors.
33+
- Please forward this flag to the original implementation in any of your overrides.
34+
- Changed functions:
35+
- `EmailPassword`:
36+
- `signIn`, `signUp`: both override and callable functions
37+
- `ThirdParty`:
38+
- `getAuthorisationURLWithQueryParamsAndSetState`: both override and callable function
39+
- `redirectToThirdPartyLogin`: callable function takes this flag as an optional input (it defaults to false on the backend)
40+
- `Passwordless`:
41+
- Functions overrides: `consumeCode`, `resendCode`, `createCode`, `setLoginAttemptInfo`, `getLoginAttemptInfo`
42+
- Calling `createCode` and `setLoginAttemptInfo` take this flag as an optional input (it defaults to false on the backend)
43+
- Changed the default implementation of `getTenantId` to default to the `tenantId` query parameter (if present) then falling back to the public tenant instead of always defaulting to the public tenant
44+
- We now disable session based account linking in the magic link based flow in passwordless by default
45+
- This is to make it function more consistently instead of only working if the link was opened on the same device
46+
- You can override by overriding the `consumeCode` function in the Passwordless Recipe
47+
48+
### Migration
49+
50+
#### tenantIdFromQueryParams in getRedirectionURL
51+
52+
Before:
53+
54+
```ts
55+
EmailPassword.init({
56+
async getRedirectionURL(context) {
57+
if (context.action === "RESET_PASSWORD") {
58+
return `/reset-password`;
59+
}
60+
return "";
61+
},
62+
});
63+
```
64+
65+
After:
66+
67+
```ts
68+
EmailPassword.init({
69+
async getRedirectionURL(context) {
70+
return `/reset-password?tenantId=${context.tenantIdFromQueryParams}`;
71+
},
72+
});
73+
```
74+
75+
#### Session based account linking for magic link based flows
76+
77+
You can re-enable linking by overriding the `consumeCode` function in the passwordless recipe and setting `shouldTryLinkingToSessionUser` to `true`.
78+
79+
```ts
80+
Passwordless.init({
81+
override: {
82+
functions: (original) => {
83+
return {
84+
...original,
85+
consumeCode: async (input) => {
86+
// Please note that this is means that the session is required and will cause an error if it is not present
87+
return original.consumeCode({ ...input, shouldTryLinkingWithSessionUser: true });
88+
},
89+
};
90+
},
91+
},
92+
});
93+
```
94+
1095
## [0.47.1] - 2024-09-18
1196

1297
### Fixes

examples/for-tests/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
"private": true,
55
"dependencies": {
66
"axios": "^0.21.0",
7+
"oidc-client-ts": "^3.0.1",
78
"react": "^18.0.0",
89
"react-dom": "^18.0.0",
10+
"react-oidc-context": "^3.1.0",
911
"react-router-dom": "6.11.2",
1012
"react-scripts": "^5.0.1"
1113
},

examples/for-tests/src/App.js

+4-13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Multitenancy from "supertokens-auth-react/recipe/multitenancy";
1414
import UserRoles from "supertokens-auth-react/recipe/userroles";
1515
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
1616
import TOTP from "supertokens-auth-react/recipe/totp";
17+
import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider";
1718

1819
import axios from "axios";
1920
import { useSessionContext } from "supertokens-auth-react/recipe/session";
@@ -27,6 +28,7 @@ import { logWithPrefix } from "./logWithPrefix";
2728
import { ErrorBoundary } from "./ErrorBoundary";
2829
import { useNavigate } from "react-router-dom";
2930
import { getTestContext, getEnabledRecipes, getQueryParams } from "./testContext";
31+
import { getApiDomain, getWebsiteDomain } from "./config";
3032

3133
const loadv5RRD = window.localStorage.getItem("react-router-dom-is-v5") === "true";
3234
if (loadv5RRD) {
@@ -43,18 +45,6 @@ const withRouter = function (Child) {
4345

4446
Session.addAxiosInterceptors(axios);
4547

46-
export function getApiDomain() {
47-
const apiPort = process.env.REACT_APP_API_PORT || 8082;
48-
const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`;
49-
return apiUrl;
50-
}
51-
52-
export function getWebsiteDomain() {
53-
const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3031;
54-
const websiteUrl = process.env.REACT_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
55-
return getQueryParams("websiteDomain") ?? websiteUrl;
56-
}
57-
5848
/*
5949
* Use localStorage for tests configurations.
6050
*/
@@ -419,6 +409,7 @@ let recipeList = [
419409
console.log(`ST_LOGS SESSION ON_HANDLE_EVENT ${ctx.action}`);
420410
},
421411
}),
412+
OAuth2Provider.init(),
422413
];
423414

424415
let enabledRecipes = getEnabledRecipes();
@@ -452,6 +443,7 @@ if (testContext.enableMFA) {
452443
SuperTokens.init({
453444
usesDynamicLoginMethods: testContext.usesDynamicLoginMethods,
454445
clientType: testContext.clientType,
446+
enableDebugLogs: true,
455447
appInfo: {
456448
appName: "SuperTokens",
457449
websiteDomain: getWebsiteDomain(),
@@ -813,7 +805,6 @@ function getSignInFormFields(formType) {
813805
id: "test",
814806
},
815807
];
816-
return;
817808
}
818809
}
819810

examples/for-tests/src/AppWithReactDomRouter.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpass
77
import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
88
import { EmailVerificationPreBuiltUI } from "supertokens-auth-react/recipe/emailverification/prebuiltui";
99
import { ThirdPartyPreBuiltUI, SignInAndUpCallback } from "supertokens-auth-react/recipe/thirdparty/prebuiltui";
10+
import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui";
1011
import { AccessDeniedScreen } from "supertokens-auth-react/recipe/session/prebuiltui";
1112
import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui";
1213
import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui";
1314
import { BaseComponent, Home, Contact, Dashboard, DashboardNoAuthRequired } from "./App";
1415
import { getEnabledRecipes, getTestContext } from "./testContext";
16+
import OAuth2Page from "./OAuth2Page";
1517

1618
function AppWithReactDomRouter(props) {
1719
/**
@@ -30,7 +32,7 @@ function AppWithReactDomRouter(props) {
3032
const emailVerificationMode = window.localStorage.getItem("mode") || "OFF";
3133
const websiteBasePath = window.localStorage.getItem("websiteBasePath") || undefined;
3234

33-
let recipePreBuiltUIList = [TOTPPreBuiltUI];
35+
let recipePreBuiltUIList = [TOTPPreBuiltUI, OAuth2ProviderPreBuiltUI];
3436
if (enabledRecipes.some((r) => r.startsWith("thirdparty"))) {
3537
recipePreBuiltUIList.push(ThirdPartyPreBuiltUI);
3638
}
@@ -172,6 +174,9 @@ function AppWithReactDomRouter(props) {
172174
}
173175
/>
174176
)}
177+
178+
<Route path="/oauth/login" element={<OAuth2Page />} />
179+
<Route path="/oauth/callback" element={<OAuth2Page />} />
175180
</Routes>
176181
</BaseComponent>
177182
</Router>

examples/for-tests/src/OAuth2Page.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { AuthProvider, useAuth } from "react-oidc-context";
2+
import { getApiDomain, getWebsiteDomain } from "./config";
3+
4+
// NOTE: For convenience, the same page/component handles both login initiation and callback.
5+
// Separate pages for login and callback are not required.
6+
7+
const scopes = window.localStorage.getItem("oauth2-scopes") ?? "profile openid offline_access email";
8+
const extraConfig = JSON.parse(window.localStorage.getItem("oauth2-extra-config") ?? "{}");
9+
const extraSignInParams = JSON.parse(window.localStorage.getItem("oauth2-extra-sign-in-params") ?? "{}");
10+
const extraSignOutParams = JSON.parse(window.localStorage.getItem("oauth2-extra-sign-out-params") ?? "{}");
11+
12+
const oidcConfig = {
13+
client_id: window.localStorage.getItem("oauth2-client-id"),
14+
authority: `${getApiDomain()}/auth`,
15+
response_type: "code",
16+
redirect_uri: `${getWebsiteDomain()}/oauth/callback`,
17+
scope: scopes ? scopes : "profile openid offline_access email",
18+
...extraConfig,
19+
onSigninCallback: async (user) => {
20+
// Clears the response code and other params from the callback url
21+
window.history.replaceState({}, document.title, window.location.pathname);
22+
},
23+
};
24+
25+
function AuthPage() {
26+
const { signinRedirect, signinSilent, signoutSilent, signoutRedirect, user, error } = useAuth();
27+
28+
return (
29+
<div>
30+
<h1 style={{ textAlign: "center" }}>OAuth2 Login Test</h1>
31+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
32+
{error && <p id="oauth2-error-message">Error: {error.message}</p>}
33+
{user && (
34+
<>
35+
<pre id="oauth2-token-data">{JSON.stringify(user.profile, null, 2)}</pre>
36+
<button id="oauth2-logout-button" onClick={() => signoutSilent(extraSignOutParams)}>
37+
Logout
38+
</button>
39+
<button id="oauth2-logout-button-redirect" onClick={() => signoutRedirect(extraSignOutParams)}>
40+
Logout (Redirect)
41+
</button>
42+
</>
43+
)}
44+
<button id="oauth2-login-button" onClick={() => signinRedirect(extraSignInParams)}>
45+
Login With SuperTokens
46+
</button>
47+
<button id="oauth2-login-button-silent" onClick={() => signinSilent(extraSignInParams)}>
48+
Login With SuperTokens (silent)
49+
</button>
50+
<button
51+
id="oauth2-login-button-prompt-login"
52+
onClick={() =>
53+
signinRedirect({
54+
prompt: "login",
55+
...extraSignInParams,
56+
})
57+
}>
58+
Login With SuperTokens (prompt=login)
59+
</button>
60+
<button
61+
id="oauth2-login-button-max-age-3"
62+
onClick={() =>
63+
signinRedirect({
64+
max_age: 3,
65+
...extraSignInParams,
66+
})
67+
}>
68+
Login With SuperTokens (max_age=3)
69+
</button>
70+
<button
71+
id="oauth2-login-button-prompt-none"
72+
onClick={() =>
73+
signinRedirect({
74+
prompt: "none",
75+
...extraSignInParams,
76+
})
77+
}>
78+
Login With SuperTokens (prompt=none)
79+
</button>
80+
</div>
81+
</div>
82+
);
83+
}
84+
85+
export default function OAuth2Page() {
86+
return (
87+
<AuthProvider {...oidcConfig}>
88+
<AuthPage />
89+
</AuthProvider>
90+
);
91+
}

examples/for-tests/src/config.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { getQueryParams } from "./testContext";
2+
3+
export function getApiDomain() {
4+
const apiPort = process.env.REACT_APP_API_PORT || 8082;
5+
const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`;
6+
return apiUrl;
7+
}
8+
9+
export function getWebsiteDomain() {
10+
const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3031;
11+
const websiteUrl = process.env.REACT_APP_WEBSITE_URL || `http://localhost:${websitePort}`;
12+
return getQueryParams("websiteDomain") ?? websiteUrl;
13+
}

frontendDriverInterfaceSupported.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"_comment": "contains a list of frontend-backend interface versions that this package supports",
3-
"versions": ["2.0", "3.0"]
3+
"versions": ["4.0"]
44
}

hooks/pre-commit.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ else
3434
fi
3535

3636
npm run check-circular-dependencies
37-
circDep=$?
37+
circDep=$?
3838

3939
echo "$(tput setaf 3)* No circular dependencies?$(tput sgr 0)"
4040

lib/.eslintrc.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ module.exports = {
6060
],
6161
"@typescript-eslint/naming-convention": "off",
6262
"@typescript-eslint/no-explicit-any": "off",
63-
"@typescript-eslint/no-unused-vars": [2, { vars: "all", args: "all", varsIgnorePattern: "^React$|^jsx$" }],
63+
"@typescript-eslint/no-unused-vars": [
64+
2,
65+
{ vars: "all", args: "all", varsIgnorePattern: "^React$|^jsx$", argsIgnorePattern: "^_" },
66+
],
6467
"@typescript-eslint/prefer-namespace-keyword": "error",
6568
"@typescript-eslint/quotes": ["error", "double"],
6669
"@typescript-eslint/semi": ["error", "always"],

lib/build/components/assets/logoutIcon.d.ts

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/build/constants.d.ts

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/build/emailpassword-shared3.js

+5-15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/build/emailpassword.js

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)