diff --git a/examples/for-tests/src/App.js b/examples/for-tests/src/App.js index 7bdb1092c..2d668989c 100644 --- a/examples/for-tests/src/App.js +++ b/examples/for-tests/src/App.js @@ -443,6 +443,7 @@ if (testContext.enableMFA) { SuperTokens.init({ usesDynamicLoginMethods: testContext.usesDynamicLoginMethods, clientType: testContext.clientType, + enableDebugLogs: true, appInfo: { appName: "SuperTokens", websiteDomain: getWebsiteDomain(), diff --git a/examples/st-oauth2-authorization-server/README.md b/examples/st-oauth2-authorization-server/README.md new file mode 100644 index 000000000..85f92edfd --- /dev/null +++ b/examples/st-oauth2-authorization-server/README.md @@ -0,0 +1,43 @@ +![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) + +# SuperTokens OAuth2 Authorization Server + +This example app uses SuperTokens `OAuth2Provider` recipe to expose OAuth2 APIs. This app acts as an OAuth2 authorization server for other OAuth2 examples in this repo. + +## Project setup + +Clone the repo, enter the directory, and use `npm` to install the project dependencies: + +```bash +git clone https://github.com/supertokens/supertokens-auth-react +cd supertokens-auth-react/examples/st-oauth2-authorization-server +npm install +``` + +## Set Up Frontend and Backend URLs + +When running locally, we recommend using a different domain than `localhost` for the authorization server to prevent cookie sharing with other client apps running on `localhost`. + +By default, the frontend runs at `http://localhost.com:3005` and the backend at `http://localhost.com:3006`. You can customize these by setting the `REACT_APP_AUTH_SERVER_WEBSITE_URL` and `REACT_APP_AUTH_SERVER_API_URL` environment variables. + +After configuring the URLs, add the domain to `/etc/hosts`. For example, if your domain is `localhost.com`, add: + +```bash +127.0.0.1 localhost.com +``` + +## Run the demo app + +This compiles and serves the React app and starts the backend API server. + +```bash +npm run start +``` + +## Author + +Created with :heart: by the folks at supertokens.com. + +## License + +This project is licensed under the Apache 2.0 license. diff --git a/examples/st-oauth2-authorization-server/backend/config.ts b/examples/st-oauth2-authorization-server/backend/config.ts new file mode 100644 index 000000000..2e341ec42 --- /dev/null +++ b/examples/st-oauth2-authorization-server/backend/config.ts @@ -0,0 +1,27 @@ +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; +import Session from "supertokens-node/recipe/session"; +import { TypeInput } from "supertokens-node/types"; + +export function getWebsiteDomain() { + return process.env.REACT_APP_AUTH_SERVER_WEBSITE_URL || "http://localhost.com:3005"; +} + +export function getApiDomain() { + return process.env.REACT_APP_AUTH_SERVER_API_URL || "http://localhost.com:3006"; +} + +export const SuperTokensConfig: TypeInput = { + supertokens: { + // this is the location of the SuperTokens core. + connectionURI: "https://try.supertokens.com", + }, + appInfo: { + appName: "SuperTokens Demo App", + apiDomain: getApiDomain(), + websiteDomain: getWebsiteDomain(), + }, + // recipeList contains all the modules that you want to + // use from SuperTokens. See the full list here: https://supertokens.com/docs/guides + recipeList: [EmailPassword.init(), OAuth2Provider.init(), Session.init()], +}; diff --git a/examples/st-oauth2-authorization-server/backend/index.ts b/examples/st-oauth2-authorization-server/backend/index.ts new file mode 100644 index 000000000..a43b57bd4 --- /dev/null +++ b/examples/st-oauth2-authorization-server/backend/index.ts @@ -0,0 +1,49 @@ +import express from "express"; +import cors from "cors"; +import supertokens from "supertokens-node"; +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { middleware, errorHandler, SessionRequest } from "supertokens-node/framework/express"; +import { getApiDomain, getWebsiteDomain, SuperTokensConfig } from "./config"; +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +supertokens.init(SuperTokensConfig); + +const app = express(); + +app.use( + cors({ + origin: [getWebsiteDomain(), "http://localhost:3000"], + // origin: [getWebsiteDomain()], + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + methods: ["GET", "PUT", "POST", "DELETE"], + credentials: true, + }) +); + +// This exposes all the APIs from SuperTokens to the client. +app.use(middleware()); + +// An example API that requires session verification +app.get("/sessioninfo", verifySession(), async (req: SessionRequest, res) => { + let session = req.session; + res.send({ + sessionHandle: session!.getHandle(), + userId: session!.getUserId(), + accessTokenPayload: session!.getAccessTokenPayload(), + }); +}); + +// This API is used by the frontend to create the tenants drop down when the app loads. +// Depending on your UX, you can remove this API. +app.get("/tenants", async (req, res) => { + let tenants = await Multitenancy.listAllTenants(); + res.send(tenants); +}); + +// In case of session related errors, this error handler +// returns 401 to the client. +app.use(errorHandler()); + +const PORT = process.env.PORT || 3006; + +app.listen(PORT, () => console.log(`API Server listening on ${getApiDomain()}`)); diff --git a/examples/st-oauth2-authorization-server/backend/package.json b/examples/st-oauth2-authorization-server/backend/package.json new file mode 100644 index 000000000..67f613481 --- /dev/null +++ b/examples/st-oauth2-authorization-server/backend/package.json @@ -0,0 +1,15 @@ +{ + "name": "supertokens-node", + "version": "0.0.1", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "start": "PORT=3006 npx ts-node-dev --project ./tsconfig.json ./index.ts" + }, + "dependencies": {}, + "devDependencies": {}, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/examples/st-oauth2-authorization-server/backend/tsconfig.json b/examples/st-oauth2-authorization-server/backend/tsconfig.json new file mode 100644 index 000000000..8a91acaae --- /dev/null +++ b/examples/st-oauth2-authorization-server/backend/tsconfig.json @@ -0,0 +1,62 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/examples/st-oauth2-authorization-server/frontend/.env b/examples/st-oauth2-authorization-server/frontend/.env new file mode 100644 index 000000000..7d910f148 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true \ No newline at end of file diff --git a/examples/st-oauth2-authorization-server/frontend/.gitignore b/examples/st-oauth2-authorization-server/frontend/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/st-oauth2-authorization-server/frontend/LICENSE.md b/examples/st-oauth2-authorization-server/frontend/LICENSE.md new file mode 100644 index 000000000..588f27e68 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/LICENSE.md @@ -0,0 +1,192 @@ +Copyright (c) 2020, 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 software except in compliance with the License. A copy +of the License is available below the line. + +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. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/examples/st-oauth2-authorization-server/frontend/package.json b/examples/st-oauth2-authorization-server/frontend/package.json new file mode 100644 index 000000000..a86573ad4 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "supertokens-react", + "version": "0.1.0", + "private": true, + "dependencies": {}, + "scripts": { + "start": "PORT=3005 react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/st-oauth2-authorization-server/frontend/public/favicon.ico b/examples/st-oauth2-authorization-server/frontend/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/st-oauth2-authorization-server/frontend/public/favicon.ico differ diff --git a/examples/st-oauth2-authorization-server/frontend/public/index.html b/examples/st-oauth2-authorization-server/frontend/public/index.html new file mode 100644 index 000000000..6f1f7cb51 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + React App + + + +
+ + diff --git a/examples/st-oauth2-authorization-server/frontend/public/manifest.json b/examples/st-oauth2-authorization-server/frontend/public/manifest.json new file mode 100644 index 000000000..f01493ff0 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/st-oauth2-authorization-server/frontend/public/robots.txt b/examples/st-oauth2-authorization-server/frontend/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/st-oauth2-authorization-server/frontend/src/App.css b/examples/st-oauth2-authorization-server/frontend/src/App.css new file mode 100644 index 000000000..8a98a2341 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/App.css @@ -0,0 +1,27 @@ +.App { + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + font-family: Rubik; +} + +.fill { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; +} + +.sessionButton { + padding-left: 13px; + padding-right: 13px; + padding-top: 8px; + padding-bottom: 8px; + background-color: black; + border-radius: 10px; + cursor: pointer; + color: white; + font-weight: bold; + font-size: 17px; +} diff --git a/examples/st-oauth2-authorization-server/frontend/src/App.tsx b/examples/st-oauth2-authorization-server/frontend/src/App.tsx new file mode 100644 index 000000000..314f34be1 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/App.tsx @@ -0,0 +1,41 @@ +import "./App.css"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import { Routes, BrowserRouter as Router, Route } from "react-router-dom"; +import Home from "./Home"; +import { PreBuiltUIList, SuperTokensConfig, ComponentWrapper } from "./config"; + +SuperTokens.init(SuperTokensConfig as any); + +function App() { + return ( + + +
+ +
+ + {/* This shows the login UI on "/auth" route */} + {getSuperTokensRoutesForReactRouterDom(require("react-router-dom"), PreBuiltUIList)} + only if the user is logged in. + Else it redirects the user to "/auth" */ + + + + } + /> + +
+
+
+
+
+ ); +} + +export default App; diff --git a/examples/st-oauth2-authorization-server/frontend/src/Home/CallAPIView.tsx b/examples/st-oauth2-authorization-server/frontend/src/Home/CallAPIView.tsx new file mode 100644 index 000000000..6a9d510d4 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/Home/CallAPIView.tsx @@ -0,0 +1,15 @@ +import axios from "axios"; +import { getApiDomain } from "../config"; + +export default function CallAPIView() { + async function callAPIClicked() { + let response = await axios.get(getApiDomain() + "/sessioninfo"); + window.alert("Session Information:\n" + JSON.stringify(response.data, null, 2)); + } + + return ( +
+ Call API +
+ ); +} diff --git a/examples/st-oauth2-authorization-server/frontend/src/Home/Home.css b/examples/st-oauth2-authorization-server/frontend/src/Home/Home.css new file mode 100644 index 000000000..a056cb2eb --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/Home/Home.css @@ -0,0 +1,189 @@ +@font-face { + font-family: Menlo; + src: url("../assets/fonts/MenloRegular.ttf"); +} + +.app-container { + font-family: Rubik, sans-serif; +} + +.app-container * { + box-sizing: border-box; +} + +.bold-400 { + font-variation-settings: "wght" 400; +} + +.bold-500 { + font-variation-settings: "wght" 500; +} + +.bold-600 { + font-variation-settings: "wght" 600; +} + +#home-container { + align-items: center; + min-height: 100vh; + background: url("../assets/images/background.png"); + background-size: cover; +} + +.bold-700 { + font-variation-settings: "wght" 700; +} + +.app-container .main-container { + box-shadow: 0px 0px 60px 0px rgba(0, 0, 0, 0.16); + width: min(635px, calc(100% - 24px)); + border-radius: 16px; + margin-block-end: 159px; + background-color: #ffffff; +} + +.main-container .success-title { + line-height: 1; + padding-block: 26px; + background-color: #e7ffed; + text-align: center; + color: #3eb655; + display: flex; + justify-content: center; + align-items: flex-end; + font-size: 20px; +} + +.success-title img.success-icon { + margin-right: 8px; +} + +.main-container .inner-content { + padding-block: 48px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.inner-content #user-id { + position: relative; + padding: 14px 17px; + border-image-slice: 1; + width: min(430px, calc(100% - 30px)); + margin-inline: auto; + margin-block: 11px 23px; + border-radius: 9px; + line-height: 1; + font-family: Menlo, serif; + cursor: text; +} + +.inner-content #user-id:before { + content: ""; + position: absolute; + inset: 0; + border-radius: 9px; + padding: 2px; + background: linear-gradient(90.31deg, #ff9933 0.11%, #ff3f33 99.82%); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask-composite: exclude; + -webkit-mask-composite: xor; +} + +.main-container > .top-band, +.main-container > .bottom-band { + border-radius: inherit; +} + +.main-container .top-band { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.main-container .bottom-band { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.main-container .sessionButton { + box-sizing: border-box; + background: #ff9933; + border: 1px solid #ff8a15; + box-shadow: 0px 3px 6px rgba(255, 153, 51, 0.16); + border-radius: 6px; + font-size: 16px; +} + +.bottom-cta-container { + display: flex; + justify-content: flex-end; + padding-inline: 21px; + background-color: #212d4f; +} + +.bottom-cta-container .view-code { + padding-block: 11px; + color: #bac9f5; + cursor: pointer; + font-size: 14px; +} + +.bottom-links-container { + display: grid; + grid-template-columns: repeat(4, auto); + margin-bottom: 22px; +} + +.bottom-links-container .link { + display: flex; + align-items: center; + margin-inline-end: 68px; + cursor: pointer; +} + +.bottom-links-container .link:last-child { + margin-right: 0; +} + +.truncate { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.separator-line { + max-width: 100%; +} + +.link .link-icon { + width: 15px; + margin-right: 5px; +} + +@media screen and (max-width: 768px) { + .bottom-links-container { + grid-template-columns: repeat(2, 1fr); + column-gap: 64px; + row-gap: 34px; + } + + .bottom-links-container .link { + margin-inline-end: 0; + } + + .separator-line { + max-width: 200px; + } +} + +@media screen and (max-width: 480px) { + #home-container { + justify-content: start; + padding-block-start: 25px; + } + + .app-container .main-container { + margin-block-end: 90px; + } +} diff --git a/examples/st-oauth2-authorization-server/frontend/src/Home/SuccessView.tsx b/examples/st-oauth2-authorization-server/frontend/src/Home/SuccessView.tsx new file mode 100644 index 000000000..bea4566ef --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/Home/SuccessView.tsx @@ -0,0 +1,72 @@ +import { useNavigate } from "react-router-dom"; +import { signOut } from "supertokens-auth-react/recipe/session"; +import { recipeDetails } from "../config"; +import CallAPIView from "./CallAPIView"; +import { BlogsIcon, CelebrateIcon, GuideIcon, SeparatorLine, SignOutIcon } from "../assets/images"; + +interface ILink { + name: string; + onClick: () => void; + icon: string; +} + +export default function SuccessView(props: { userId: string }) { + let userId = props.userId; + + const navigate = useNavigate(); + + async function logoutClicked() { + await signOut(); + navigate("/auth"); + } + + function openLink(url: string) { + window.open(url, "_blank"); + } + + const links: ILink[] = [ + { + name: "Blogs", + onClick: () => openLink("https://supertokens.com/blog"), + icon: BlogsIcon, + }, + { + name: "Documentation", + onClick: () => openLink(recipeDetails.docsLink), + icon: GuideIcon, + }, + { + name: "Sign Out", + onClick: logoutClicked, + icon: SignOutIcon, + }, + ]; + + return ( + <> +
+
+ Login successful Login successful +
+
+
Your userID is:
+
+ {userId} +
+ +
+
+
+ {links.map((link) => ( +
+ {link.name} +
+ {link.name} +
+
+ ))} +
+ separator + + ); +} diff --git a/examples/st-oauth2-authorization-server/frontend/src/Home/index.tsx b/examples/st-oauth2-authorization-server/frontend/src/Home/index.tsx new file mode 100644 index 000000000..0c3f288e8 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/Home/index.tsx @@ -0,0 +1,17 @@ +import SuccessView from "./SuccessView"; +import { useSessionContext } from "supertokens-auth-react/recipe/session"; +import "./Home.css"; + +export default function Home() { + const sessionContext = useSessionContext(); + + if (sessionContext.loading === true) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/fonts/MenloRegular.ttf b/examples/st-oauth2-authorization-server/frontend/src/assets/fonts/MenloRegular.ttf new file mode 100644 index 000000000..033dc6d21 Binary files /dev/null and b/examples/st-oauth2-authorization-server/frontend/src/assets/fonts/MenloRegular.ttf differ diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/images/arrow-right-icon.svg b/examples/st-oauth2-authorization-server/frontend/src/assets/images/arrow-right-icon.svg new file mode 100644 index 000000000..95aa1fec6 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/assets/images/arrow-right-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/images/background.png b/examples/st-oauth2-authorization-server/frontend/src/assets/images/background.png new file mode 100644 index 000000000..2147c15c2 Binary files /dev/null and b/examples/st-oauth2-authorization-server/frontend/src/assets/images/background.png differ diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/images/blogs-icon.svg b/examples/st-oauth2-authorization-server/frontend/src/assets/images/blogs-icon.svg new file mode 100644 index 000000000..a2fc9dd62 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/assets/images/blogs-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/images/celebrate-icon.svg b/examples/st-oauth2-authorization-server/frontend/src/assets/images/celebrate-icon.svg new file mode 100644 index 000000000..3b40b1efa --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/assets/images/celebrate-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/images/guide-icon.svg b/examples/st-oauth2-authorization-server/frontend/src/assets/images/guide-icon.svg new file mode 100644 index 000000000..bd85af72b --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/assets/images/guide-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/images/index.ts b/examples/st-oauth2-authorization-server/frontend/src/assets/images/index.ts new file mode 100644 index 000000000..7adf036c4 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/assets/images/index.ts @@ -0,0 +1,8 @@ +import SeparatorLine from "./separator-line.svg"; +import ArrowRight from "./arrow-right-icon.svg"; +import SignOutIcon from "./sign-out-icon.svg"; +import GuideIcon from "./guide-icon.svg"; +import BlogsIcon from "./blogs-icon.svg"; +import CelebrateIcon from "./celebrate-icon.svg"; + +export { SeparatorLine, ArrowRight, SignOutIcon, GuideIcon, BlogsIcon, CelebrateIcon }; diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/images/separator-line.svg b/examples/st-oauth2-authorization-server/frontend/src/assets/images/separator-line.svg new file mode 100644 index 000000000..7127a00dc --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/assets/images/separator-line.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/examples/st-oauth2-authorization-server/frontend/src/assets/images/sign-out-icon.svg b/examples/st-oauth2-authorization-server/frontend/src/assets/images/sign-out-icon.svg new file mode 100644 index 000000000..6cc4f85fd --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/assets/images/sign-out-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/st-oauth2-authorization-server/frontend/src/config.tsx b/examples/st-oauth2-authorization-server/frontend/src/config.tsx new file mode 100644 index 000000000..fcb9378eb --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/config.tsx @@ -0,0 +1,33 @@ +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import Session from "supertokens-auth-react/recipe/session"; + +export function getWebsiteDomain() { + return process.env.REACT_APP_AUTH_SERVER_WEBSITE_URL || "http://localhost.com:3005"; +} + +export function getApiDomain() { + return process.env.REACT_APP_AUTH_SERVER_API_URL || "http://localhost.com:3006"; +} + +export const SuperTokensConfig = { + appInfo: { + appName: "SuperTokens Demo App", + apiDomain: getApiDomain(), + websiteDomain: getWebsiteDomain(), + }, + // recipeList contains all the modules that you want to + // use from SuperTokens. See the full list here: https://supertokens.com/docs/guides + recipeList: [EmailPassword.init(), OAuth2Provider.init(), Session.init()], +}; + +export const recipeDetails = { + docsLink: "https://supertokens.com/docs/thirdparty/introduction", +}; + +export const PreBuiltUIList = [EmailPasswordPreBuiltUI]; + +export const ComponentWrapper = (props: { children: JSX.Element }): JSX.Element => { + return props.children; +}; diff --git a/examples/st-oauth2-authorization-server/frontend/src/index.css b/examples/st-oauth2-authorization-server/frontend/src/index.css new file mode 100644 index 000000000..04146b5e7 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} diff --git a/examples/st-oauth2-authorization-server/frontend/src/index.tsx b/examples/st-oauth2-authorization-server/frontend/src/index.tsx new file mode 100644 index 000000000..399c737cd --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/index.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); +root.render( + + + +); diff --git a/examples/st-oauth2-authorization-server/frontend/src/react-app-env.d.ts b/examples/st-oauth2-authorization-server/frontend/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/st-oauth2-authorization-server/frontend/tsconfig.json b/examples/st-oauth2-authorization-server/frontend/tsconfig.json new file mode 100644 index 000000000..c0555cbc6 --- /dev/null +++ b/examples/st-oauth2-authorization-server/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/st-oauth2-authorization-server/package.json b/examples/st-oauth2-authorization-server/package.json new file mode 100644 index 000000000..7f4d72673 --- /dev/null +++ b/examples/st-oauth2-authorization-server/package.json @@ -0,0 +1,49 @@ +{ + "name": "st-oauth2-authorization-server", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "start:frontend": "cd frontend && npm run start", + "start:frontend-live-demo-app": "cd frontend && npx serve -s build", + "start:backend": "cd backend && npm run start", + "start:backend-live-demo-app": "cd backend && ./startLiveDemoApp.sh", + "start": "npm-run-all --parallel start:frontend start:backend", + "start-live-demo-app": "npx npm-run-all --parallel start:frontend-live-demo-app start:backend-live-demo-app" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.11.56", + "@types/react": "^18.0.18", + "@types/react-dom": "^18.0.6", + "axios": "^0.21.0", + "cors": "^2.8.5", + "express": "^4.18.1", + "helmet": "^5.1.0", + "morgan": "^1.10.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.2.1", + "react-scripts": "5.0.1", + "supertokens-auth-react": "github:supertokens/supertokens-auth-react#feat/oauth2/base", + "supertokens-node": "github:supertokens/supertokens-node#feat/oauth2/base", + "supertokens-web-js": "github:supertokens/supertokens-web-js#feat/oauth2/base", + "ts-node-dev": "^2.0.0", + "typescript": "^4.8.2", + "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@types/cors": "^2.8.12", + "@types/express": "^4.17.17", + "@types/morgan": "^1.9.3", + "@types/node": "^16.11.38", + "nodemon": "^2.0.16", + "npm-run-all": "^4.1.5" + } +} diff --git a/examples/with-oauth2-with-supertokens/README.md b/examples/with-oauth2-with-supertokens/README.md new file mode 100644 index 000000000..8734320ea --- /dev/null +++ b/examples/with-oauth2-with-supertokens/README.md @@ -0,0 +1,57 @@ +![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) + +# Example App with SuperTokens Auth and SuperTokens as OAuth2 Provider + +This example app shows how to use SuperTokens as an OAuth2 provider in a React app that already implements SuperTokens Auth, utilizing the `OAuth2Client` recipe on the backend. + +## Project setup + +Clone the repo, enter the directory, and use `npm` to install the project dependencies: + +```bash +git clone https://github.com/supertokens/supertokens-auth-react +cd supertokens-auth-react/examples/with-oauth2-with-supertokens +npm install +``` + +## 1. Create an OAuth2 Client + +```bash +curl -X POST http://localhost:3567/recipe/oauth/clients \ + -H "Content-Type: application/json" \ + -d '{ + "scope": "offline_access openid email", + "redirectUris": ["http://localhost:3000/auth/callback"], + "tokenEndpointAuthMethod": "none", + "grantTypes": ["authorization_code", "refresh_token"], + "responseTypes": ["code", "id_token"] + }' +``` + +Note down the `client_id` from the response. + +## 2. Run the st-oauth2-authorization-server + +1. Open a new terminal window and navigate to `supertokens-auth-react/examples/ +st-oauth2-authorization-server` +2. Read the README.md to setup `st-oauth2-authorization-server ` and run it using `npm start` + +## 3. Update config + +Update `clientId` and `AUTH_SERVER_URL` values as per step 1 and step 2 in both `frontend/src/App.tsx` and `backend/src/config.ts`. + +## 4. Run the demo app + +This compiles and serves the React app. + +```bash +npm run start +``` + +## Author + +Created with :heart: by the folks at supertokens.com. + +## License + +This project is licensed under the Apache 2.0 license. diff --git a/examples/with-oauth2-with-supertokens/backend/config.ts b/examples/with-oauth2-with-supertokens/backend/config.ts new file mode 100644 index 000000000..02b0370ec --- /dev/null +++ b/examples/with-oauth2-with-supertokens/backend/config.ts @@ -0,0 +1,40 @@ +import OAuth2Client from "supertokens-node/recipe/oauth2client"; +import Session from "supertokens-node/recipe/session"; +import { TypeInput } from "supertokens-node/types"; + +const AUTH_SERVER_URL = process.env.REACT_APP_AUTH_SERVER_API_URL || "http://localhost.com:3006"; + +export function getApiDomain() { + const apiPort = process.env.REACT_APP_API_PORT || 3001; + const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`; + return apiUrl; +} + +export function getWebsiteDomain() { + const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3000; + const websiteUrl = process.env.REACT_APP_WEBSITE_URL || `http://localhost:${websitePort}`; + return websiteUrl; +} + +export const SuperTokensConfig: TypeInput = { + supertokens: { + // this is the location of the SuperTokens core. + connectionURI: "https://try.supertokens.com", + }, + appInfo: { + appName: "SuperTokens Demo App", + apiDomain: getApiDomain(), + websiteDomain: getWebsiteDomain(), + }, + // recipeList contains all the modules that you want to + // use from SuperTokens. See the full list here: https://supertokens.com/docs/guides + recipeList: [ + OAuth2Client.init({ + providerConfig: { + clientId: "REPLACE_WITH_YOUR_CLIENT_ID", + oidcDiscoveryEndpoint: `${AUTH_SERVER_URL}/auth/.well-known/openid-configuration`, + }, + }), + Session.init(), + ], +}; diff --git a/examples/with-oauth2-with-supertokens/backend/index.ts b/examples/with-oauth2-with-supertokens/backend/index.ts new file mode 100644 index 000000000..67c228198 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/backend/index.ts @@ -0,0 +1,38 @@ +import express from "express"; +import cors from "cors"; +import supertokens from "supertokens-node"; +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { middleware, errorHandler, SessionRequest } from "supertokens-node/framework/express"; +import { getWebsiteDomain, SuperTokensConfig } from "./config"; + +supertokens.init(SuperTokensConfig); + +const app = express(); + +app.use( + cors({ + origin: getWebsiteDomain(), + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + methods: ["GET", "PUT", "POST", "DELETE"], + credentials: true, + }) +); + +// This exposes all the APIs from SuperTokens to the client. +app.use(middleware()); + +// An example API that requires session verification +app.get("/sessioninfo", verifySession(), async (req: SessionRequest, res) => { + let session = req.session; + res.send({ + sessionHandle: session!.getHandle(), + userId: session!.getUserId(), + accessTokenPayload: session!.getAccessTokenPayload(), + }); +}); + +// In case of session related errors, this error handler +// returns 401 to the client. +app.use(errorHandler()); + +app.listen(3001, () => console.log(`API Server listening on port 3001`)); diff --git a/examples/with-oauth2-with-supertokens/backend/package.json b/examples/with-oauth2-with-supertokens/backend/package.json new file mode 100644 index 000000000..5d69ff8af --- /dev/null +++ b/examples/with-oauth2-with-supertokens/backend/package.json @@ -0,0 +1,15 @@ +{ + "name": "supertokens-node", + "version": "0.0.1", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "start": "npx ts-node-dev --project ./tsconfig.json ./index.ts" + }, + "dependencies": {}, + "devDependencies": {}, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/examples/with-oauth2-with-supertokens/backend/tsconfig.json b/examples/with-oauth2-with-supertokens/backend/tsconfig.json new file mode 100644 index 000000000..8a91acaae --- /dev/null +++ b/examples/with-oauth2-with-supertokens/backend/tsconfig.json @@ -0,0 +1,62 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/examples/with-oauth2-with-supertokens/frontend/.env b/examples/with-oauth2-with-supertokens/frontend/.env new file mode 100644 index 000000000..7d910f148 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true \ No newline at end of file diff --git a/examples/with-oauth2-with-supertokens/frontend/.gitignore b/examples/with-oauth2-with-supertokens/frontend/.gitignore new file mode 100644 index 000000000..4d29575de --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/with-oauth2-with-supertokens/frontend/LICENSE.md b/examples/with-oauth2-with-supertokens/frontend/LICENSE.md new file mode 100644 index 000000000..588f27e68 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/LICENSE.md @@ -0,0 +1,192 @@ +Copyright (c) 2020, 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 software except in compliance with the License. A copy +of the License is available below the line. + +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. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/examples/with-oauth2-with-supertokens/frontend/package.json b/examples/with-oauth2-with-supertokens/frontend/package.json new file mode 100644 index 000000000..0c9b5703f --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "supertokens-react", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/with-oauth2-with-supertokens/frontend/public/favicon.ico b/examples/with-oauth2-with-supertokens/frontend/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/with-oauth2-with-supertokens/frontend/public/favicon.ico differ diff --git a/examples/with-oauth2-with-supertokens/frontend/public/index.html b/examples/with-oauth2-with-supertokens/frontend/public/index.html new file mode 100644 index 000000000..6f1f7cb51 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + React App + + + +
+ + diff --git a/examples/with-oauth2-with-supertokens/frontend/public/manifest.json b/examples/with-oauth2-with-supertokens/frontend/public/manifest.json new file mode 100644 index 000000000..f01493ff0 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/with-oauth2-with-supertokens/frontend/public/robots.txt b/examples/with-oauth2-with-supertokens/frontend/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/with-oauth2-with-supertokens/frontend/src/App.css b/examples/with-oauth2-with-supertokens/frontend/src/App.css new file mode 100644 index 000000000..b9ee98c15 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/App.css @@ -0,0 +1,34 @@ +.App { + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + font-family: Rubik; +} + +button, +.sessionButton { + padding-left: 13px; + padding-right: 13px; + padding-top: 8px; + padding-bottom: 8px; + background-color: black; + border-radius: 10px; + cursor: pointer; + color: white; + font-weight: bold; + font-size: 17px; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; +} + +.fill { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; +} diff --git a/examples/with-oauth2-with-supertokens/frontend/src/App.tsx b/examples/with-oauth2-with-supertokens/frontend/src/App.tsx new file mode 100644 index 000000000..26367cd49 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/App.tsx @@ -0,0 +1,127 @@ +import "./App.css"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { SessionAuth, doesSessionExist } from "supertokens-auth-react/recipe/session"; +import { Routes, BrowserRouter as Router, Route, useNavigate } from "react-router-dom"; +import Home from "./Home"; +import { SuperTokensConfig, ComponentWrapper, getApiDomain, getWebsiteDomain } from "./config"; +import { useEffect, useRef } from "react"; +import { AuthProvider, AuthProviderProps, useAuth } from "react-oidc-context"; +import "./App.css"; + +SuperTokens.init(SuperTokensConfig); + +const AUTH_SERVER_URL = process.env.REACT_APP_AUTH_SERVER_API_URL || "http://localhost.com:3006"; +const clientId = "REPLACE_WITH_YOUR_CLIENT_ID"; + +const oidcConfig: AuthProviderProps = { + client_id: clientId, + authority: `${AUTH_SERVER_URL}/auth`, + response_type: "code", + redirect_uri: `${getWebsiteDomain()}/auth/callback`, + scope: "openid offline_access email", +}; + +function AuthPage() { + const navigate = useNavigate(); + const authCallbackHandled = useRef(false); + const { signinRedirect, signoutSilent, isLoading, user } = useAuth(); + + useEffect(() => { + // Redirect to home if supertokens session exists + doesSessionExist().then((sessionExists) => { + if (sessionExists) { + navigate("/"); + } + }); + }, []); + + useEffect(() => { + if (authCallbackHandled.current) return; + if (!user?.access_token) return; + + authCallbackHandled.current = true; + + async function handleSign() { + const res = await fetch(`${getApiDomain()}/auth/oauth/client/signin`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + oAuthTokens: { + access_token: user?.access_token, + }, + }), + }); + if (res.ok) { + const data = await res.json(); + if (data.status === "OK") { + signoutSilent(); + navigate("/"); + } else { + // setIsLoading(false); + window.alert("Login Failed "); + } + } + } + + handleSign(); + }, [user, navigate, signoutSilent]); + + if (isLoading || user) { + return ( +
+

Loading...

+
+ ); + } + + return ( +
+

OAuth2 Example With Supertokens

+
+
+ +
+
+
+ ); +} + +function App() { + return ( + + +
+ +
+ + + + + } + /> + only if the user is logged in. + Else it redirects the user to "/auth" */ + + + + } + /> + +
+
+
+
+
+ ); +} + +export default App; diff --git a/examples/with-oauth2-with-supertokens/frontend/src/Home/CallAPIView.tsx b/examples/with-oauth2-with-supertokens/frontend/src/Home/CallAPIView.tsx new file mode 100644 index 000000000..6a9d510d4 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/Home/CallAPIView.tsx @@ -0,0 +1,15 @@ +import axios from "axios"; +import { getApiDomain } from "../config"; + +export default function CallAPIView() { + async function callAPIClicked() { + let response = await axios.get(getApiDomain() + "/sessioninfo"); + window.alert("Session Information:\n" + JSON.stringify(response.data, null, 2)); + } + + return ( +
+ Call API +
+ ); +} diff --git a/examples/with-oauth2-with-supertokens/frontend/src/Home/Home.css b/examples/with-oauth2-with-supertokens/frontend/src/Home/Home.css new file mode 100644 index 000000000..a056cb2eb --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/Home/Home.css @@ -0,0 +1,189 @@ +@font-face { + font-family: Menlo; + src: url("../assets/fonts/MenloRegular.ttf"); +} + +.app-container { + font-family: Rubik, sans-serif; +} + +.app-container * { + box-sizing: border-box; +} + +.bold-400 { + font-variation-settings: "wght" 400; +} + +.bold-500 { + font-variation-settings: "wght" 500; +} + +.bold-600 { + font-variation-settings: "wght" 600; +} + +#home-container { + align-items: center; + min-height: 100vh; + background: url("../assets/images/background.png"); + background-size: cover; +} + +.bold-700 { + font-variation-settings: "wght" 700; +} + +.app-container .main-container { + box-shadow: 0px 0px 60px 0px rgba(0, 0, 0, 0.16); + width: min(635px, calc(100% - 24px)); + border-radius: 16px; + margin-block-end: 159px; + background-color: #ffffff; +} + +.main-container .success-title { + line-height: 1; + padding-block: 26px; + background-color: #e7ffed; + text-align: center; + color: #3eb655; + display: flex; + justify-content: center; + align-items: flex-end; + font-size: 20px; +} + +.success-title img.success-icon { + margin-right: 8px; +} + +.main-container .inner-content { + padding-block: 48px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.inner-content #user-id { + position: relative; + padding: 14px 17px; + border-image-slice: 1; + width: min(430px, calc(100% - 30px)); + margin-inline: auto; + margin-block: 11px 23px; + border-radius: 9px; + line-height: 1; + font-family: Menlo, serif; + cursor: text; +} + +.inner-content #user-id:before { + content: ""; + position: absolute; + inset: 0; + border-radius: 9px; + padding: 2px; + background: linear-gradient(90.31deg, #ff9933 0.11%, #ff3f33 99.82%); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask-composite: exclude; + -webkit-mask-composite: xor; +} + +.main-container > .top-band, +.main-container > .bottom-band { + border-radius: inherit; +} + +.main-container .top-band { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.main-container .bottom-band { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.main-container .sessionButton { + box-sizing: border-box; + background: #ff9933; + border: 1px solid #ff8a15; + box-shadow: 0px 3px 6px rgba(255, 153, 51, 0.16); + border-radius: 6px; + font-size: 16px; +} + +.bottom-cta-container { + display: flex; + justify-content: flex-end; + padding-inline: 21px; + background-color: #212d4f; +} + +.bottom-cta-container .view-code { + padding-block: 11px; + color: #bac9f5; + cursor: pointer; + font-size: 14px; +} + +.bottom-links-container { + display: grid; + grid-template-columns: repeat(4, auto); + margin-bottom: 22px; +} + +.bottom-links-container .link { + display: flex; + align-items: center; + margin-inline-end: 68px; + cursor: pointer; +} + +.bottom-links-container .link:last-child { + margin-right: 0; +} + +.truncate { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.separator-line { + max-width: 100%; +} + +.link .link-icon { + width: 15px; + margin-right: 5px; +} + +@media screen and (max-width: 768px) { + .bottom-links-container { + grid-template-columns: repeat(2, 1fr); + column-gap: 64px; + row-gap: 34px; + } + + .bottom-links-container .link { + margin-inline-end: 0; + } + + .separator-line { + max-width: 200px; + } +} + +@media screen and (max-width: 480px) { + #home-container { + justify-content: start; + padding-block-start: 25px; + } + + .app-container .main-container { + margin-block-end: 90px; + } +} diff --git a/examples/with-oauth2-with-supertokens/frontend/src/Home/SuccessView.tsx b/examples/with-oauth2-with-supertokens/frontend/src/Home/SuccessView.tsx new file mode 100644 index 000000000..bea4566ef --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/Home/SuccessView.tsx @@ -0,0 +1,72 @@ +import { useNavigate } from "react-router-dom"; +import { signOut } from "supertokens-auth-react/recipe/session"; +import { recipeDetails } from "../config"; +import CallAPIView from "./CallAPIView"; +import { BlogsIcon, CelebrateIcon, GuideIcon, SeparatorLine, SignOutIcon } from "../assets/images"; + +interface ILink { + name: string; + onClick: () => void; + icon: string; +} + +export default function SuccessView(props: { userId: string }) { + let userId = props.userId; + + const navigate = useNavigate(); + + async function logoutClicked() { + await signOut(); + navigate("/auth"); + } + + function openLink(url: string) { + window.open(url, "_blank"); + } + + const links: ILink[] = [ + { + name: "Blogs", + onClick: () => openLink("https://supertokens.com/blog"), + icon: BlogsIcon, + }, + { + name: "Documentation", + onClick: () => openLink(recipeDetails.docsLink), + icon: GuideIcon, + }, + { + name: "Sign Out", + onClick: logoutClicked, + icon: SignOutIcon, + }, + ]; + + return ( + <> +
+
+ Login successful Login successful +
+
+
Your userID is:
+
+ {userId} +
+ +
+
+
+ {links.map((link) => ( +
+ {link.name} +
+ {link.name} +
+
+ ))} +
+ separator + + ); +} diff --git a/examples/with-oauth2-with-supertokens/frontend/src/Home/index.tsx b/examples/with-oauth2-with-supertokens/frontend/src/Home/index.tsx new file mode 100644 index 000000000..0c3f288e8 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/Home/index.tsx @@ -0,0 +1,17 @@ +import SuccessView from "./SuccessView"; +import { useSessionContext } from "supertokens-auth-react/recipe/session"; +import "./Home.css"; + +export default function Home() { + const sessionContext = useSessionContext(); + + if (sessionContext.loading === true) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/fonts/MenloRegular.ttf b/examples/with-oauth2-with-supertokens/frontend/src/assets/fonts/MenloRegular.ttf new file mode 100644 index 000000000..033dc6d21 Binary files /dev/null and b/examples/with-oauth2-with-supertokens/frontend/src/assets/fonts/MenloRegular.ttf differ diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/images/arrow-right-icon.svg b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/arrow-right-icon.svg new file mode 100644 index 000000000..95aa1fec6 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/arrow-right-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/images/background.png b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/background.png new file mode 100644 index 000000000..2147c15c2 Binary files /dev/null and b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/background.png differ diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/images/blogs-icon.svg b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/blogs-icon.svg new file mode 100644 index 000000000..a2fc9dd62 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/blogs-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/images/celebrate-icon.svg b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/celebrate-icon.svg new file mode 100644 index 000000000..3b40b1efa --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/celebrate-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/images/guide-icon.svg b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/guide-icon.svg new file mode 100644 index 000000000..bd85af72b --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/guide-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/images/index.ts b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/index.ts new file mode 100644 index 000000000..7adf036c4 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/index.ts @@ -0,0 +1,8 @@ +import SeparatorLine from "./separator-line.svg"; +import ArrowRight from "./arrow-right-icon.svg"; +import SignOutIcon from "./sign-out-icon.svg"; +import GuideIcon from "./guide-icon.svg"; +import BlogsIcon from "./blogs-icon.svg"; +import CelebrateIcon from "./celebrate-icon.svg"; + +export { SeparatorLine, ArrowRight, SignOutIcon, GuideIcon, BlogsIcon, CelebrateIcon }; diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/images/separator-line.svg b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/separator-line.svg new file mode 100644 index 000000000..7127a00dc --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/separator-line.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/examples/with-oauth2-with-supertokens/frontend/src/assets/images/sign-out-icon.svg b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/sign-out-icon.svg new file mode 100644 index 000000000..6cc4f85fd --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/assets/images/sign-out-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/with-oauth2-with-supertokens/frontend/src/config.tsx b/examples/with-oauth2-with-supertokens/frontend/src/config.tsx new file mode 100644 index 000000000..a12a11d12 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/config.tsx @@ -0,0 +1,34 @@ +import Session from "supertokens-auth-react/recipe/session"; + +export function getApiDomain() { + const apiPort = process.env.REACT_APP_API_PORT || 3001; + const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`; + return apiUrl; +} + +export function getWebsiteDomain() { + const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3000; + const websiteUrl = process.env.REACT_APP_WEBSITE_URL || `http://localhost:${websitePort}`; + return websiteUrl; +} + +export const SuperTokensConfig = { + appInfo: { + appName: "SuperTokens Demo App", + apiDomain: getApiDomain(), + websiteDomain: getWebsiteDomain(), + }, + // recipeList contains all the modules that you want to + // use from SuperTokens. See the full list here: https://supertokens.com/docs/guides + recipeList: [Session.init()], +}; + +export const recipeDetails = { + docsLink: "https://supertokens.com/docs/thirdparty/introduction", +}; + +export const PreBuiltUIList = []; + +export const ComponentWrapper = (props: { children: JSX.Element }): JSX.Element => { + return props.children; +}; diff --git a/examples/with-oauth2-with-supertokens/frontend/src/index.css b/examples/with-oauth2-with-supertokens/frontend/src/index.css new file mode 100644 index 000000000..04146b5e7 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} diff --git a/examples/with-oauth2-with-supertokens/frontend/src/index.tsx b/examples/with-oauth2-with-supertokens/frontend/src/index.tsx new file mode 100644 index 000000000..126116f9b --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); + +root.render( + + + +); diff --git a/examples/with-oauth2-with-supertokens/frontend/src/react-app-env.d.ts b/examples/with-oauth2-with-supertokens/frontend/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/with-oauth2-with-supertokens/frontend/tsconfig.json b/examples/with-oauth2-with-supertokens/frontend/tsconfig.json new file mode 100644 index 000000000..c0555cbc6 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/with-oauth2-with-supertokens/package.json b/examples/with-oauth2-with-supertokens/package.json new file mode 100644 index 000000000..ed281d3c3 --- /dev/null +++ b/examples/with-oauth2-with-supertokens/package.json @@ -0,0 +1,51 @@ +{ + "name": "with-oauth2-with-supertokens", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "start:frontend": "cd frontend && npm run start", + "start:frontend-live-demo-app": "cd frontend && npx serve -s build", + "start:backend": "cd backend && npm run start", + "start:backend-live-demo-app": "cd backend && ./startLiveDemoApp.sh", + "start": "npm-run-all --parallel start:frontend start:backend", + "start-live-demo-app": "npx npm-run-all --parallel start:frontend-live-demo-app start:backend-live-demo-app" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.11.56", + "@types/react": "^18.0.18", + "@types/react-dom": "^18.0.6", + "axios": "^0.21.0", + "cors": "^2.8.5", + "express": "^4.18.1", + "helmet": "^5.1.0", + "morgan": "^1.10.0", + "oidc-client-ts": "^3.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-oidc-context": "^3.1.0", + "react-router-dom": "^6.2.1", + "react-scripts": "5.0.1", + "supertokens-auth-react": "github:supertokens/supertokens-auth-react#feat/oauth2/base", + "supertokens-node": "github:supertokens/supertokens-node#feat/oauth2/base", + "supertokens-web-js": "github:supertokens/supertokens-web-js#feat/oauth2/base", + "ts-node-dev": "^2.0.0", + "typescript": "^4.8.2", + "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@types/cors": "^2.8.12", + "@types/express": "^4.17.17", + "@types/morgan": "^1.9.3", + "@types/node": "^16.11.38", + "nodemon": "^2.0.16", + "npm-run-all": "^4.1.5" + } +} diff --git a/examples/with-oauth2-without-supertokens/LICENSE.md b/examples/with-oauth2-without-supertokens/LICENSE.md new file mode 100644 index 000000000..588f27e68 --- /dev/null +++ b/examples/with-oauth2-without-supertokens/LICENSE.md @@ -0,0 +1,192 @@ +Copyright (c) 2020, 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 software except in compliance with the License. A copy +of the License is available below the line. + +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. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/examples/with-oauth2-without-supertokens/README.md b/examples/with-oauth2-without-supertokens/README.md new file mode 100644 index 000000000..1507029d7 --- /dev/null +++ b/examples/with-oauth2-without-supertokens/README.md @@ -0,0 +1,57 @@ +![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) + +# Example React App using a Generic OAuth2 Library and SuperTokens as OAuth2 Provider + +This examples app demonstrates how to use SuperTokens as OAuth2 Provider in a React App using a generic OAuth2 library such as [react-oidc-context](https://github.com/authts/react-oidc-context). + +## Project setup + +Clone the repo, enter the directory, and use `npm` to install the project dependencies: + +```bash +git clone https://github.com/supertokens/supertokens-auth-react +cd supertokens-auth-react/examples/with-oauth2-without-supertokens +npm install +``` + +## 1. Create an OAuth2 Client + +```bash +curl -X POST http://localhost:3567/recipe/oauth/clients \ + -H "Content-Type: application/json" \ + -d '{ + "scope": "offline_access openid email", + "redirectUris": ["http://localhost:3000"], + "grantTypes": ["authorization_code", "refresh_token"], + "responseTypes": ["code", "id_token"], + "tokenEndpointAuthMethod": "none" + }' +``` + +Note down the `client_id` from the response. + +## 2. Run the st-oauth2-authorization-server + +1. Open a new terminal window and navigate to `supertokens-auth-react/examples/ +st-oauth2-authorization-server` +2. Read the README.md to setup `st-oauth2-authorization-server ` and run it using `npm start` + +## 3. Update config + +Open the `App.tsx` file and update `clientId` and `authServerUrl` values as per step 1 and step 2. + +## 4. Run the demo app + +This compiles and serves the React app. + +```bash +npm run start +``` + +## Author + +Created with :heart: by the folks at supertokens.com. + +## License + +This project is licensed under the Apache 2.0 license. diff --git a/examples/with-oauth2-without-supertokens/package.json b/examples/with-oauth2-without-supertokens/package.json new file mode 100644 index 000000000..adb7336c5 --- /dev/null +++ b/examples/with-oauth2-without-supertokens/package.json @@ -0,0 +1,50 @@ +{ + "name": "with-oauth2-without-supertokens", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.11.56", + "@types/react": "^18.0.18", + "@types/react-dom": "^18.0.6", + "axios": "^0.21.0", + "helmet": "^5.1.0", + "oidc-client-ts": "^3.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-oauth2-code-pkce": "^1.20.1", + "react-oidc-context": "^3.1.0", + "react-router-dom": "^6.2.1", + "react-scripts": "5.0.1", + "ts-node-dev": "^2.0.0", + "typescript": "^4.8.2", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "BROWSER=none react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/with-oauth2-without-supertokens/public/favicon.ico b/examples/with-oauth2-without-supertokens/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/with-oauth2-without-supertokens/public/favicon.ico differ diff --git a/examples/with-oauth2-without-supertokens/public/index.html b/examples/with-oauth2-without-supertokens/public/index.html new file mode 100644 index 000000000..6f1f7cb51 --- /dev/null +++ b/examples/with-oauth2-without-supertokens/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + React App + + + +
+ + diff --git a/examples/with-oauth2-without-supertokens/public/manifest.json b/examples/with-oauth2-without-supertokens/public/manifest.json new file mode 100644 index 000000000..f01493ff0 --- /dev/null +++ b/examples/with-oauth2-without-supertokens/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/with-oauth2-without-supertokens/public/robots.txt b/examples/with-oauth2-without-supertokens/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/with-oauth2-without-supertokens/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/with-oauth2-without-supertokens/src/App.css b/examples/with-oauth2-without-supertokens/src/App.css new file mode 100644 index 000000000..9886f003d --- /dev/null +++ b/examples/with-oauth2-without-supertokens/src/App.css @@ -0,0 +1,26 @@ +.App { + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + font-family: Rubik; +} + +button { + padding-left: 13px; + padding-right: 13px; + padding-top: 8px; + padding-bottom: 8px; + background-color: black; + border-radius: 10px; + cursor: pointer; + color: white; + font-weight: bold; + font-size: 17px; +} + +.center { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/examples/with-oauth2-without-supertokens/src/App.tsx b/examples/with-oauth2-without-supertokens/src/App.tsx new file mode 100644 index 000000000..d390207b0 --- /dev/null +++ b/examples/with-oauth2-without-supertokens/src/App.tsx @@ -0,0 +1,48 @@ +import { AuthProvider, AuthProviderProps, useAuth } from "react-oidc-context"; +import "./App.css"; + +const authServerUrl = process.env.REACT_APP_AUTH_SERVER_API_URL || "http://localhost.com:3006"; +const clientId = "REPLACE_WITH_YOUR_CLIENT_ID"; + +const oidcConfig: AuthProviderProps = { + client_id: clientId, + authority: `${authServerUrl}/auth`, + response_type: "code", + redirect_uri: "http://localhost:3000", + scope: "openid offline_access email", + revokeTokensOnSignout: true, + onSigninCallback: async () => { + // Clears the response code and other params from the callback url + window.history.replaceState({}, document.title, window.location.pathname); + }, +}; + +function AuthPage() { + const { signinRedirect, signoutSilent, user } = useAuth(); + + return ( +
+

OAuth2 Example With Generic OAuth2 Lib

+
+ {user ? ( +
+
{JSON.stringify(user.profile, null, 2)}
+ +
+ ) : ( +
+ +
+ )} +
+
+ ); +} + +export default function App() { + return ( + + + + ); +} diff --git a/examples/with-oauth2-without-supertokens/src/assets/fonts/MenloRegular.ttf b/examples/with-oauth2-without-supertokens/src/assets/fonts/MenloRegular.ttf new file mode 100644 index 000000000..033dc6d21 Binary files /dev/null and b/examples/with-oauth2-without-supertokens/src/assets/fonts/MenloRegular.ttf differ diff --git a/examples/with-oauth2-without-supertokens/src/index.css b/examples/with-oauth2-without-supertokens/src/index.css new file mode 100644 index 000000000..04146b5e7 --- /dev/null +++ b/examples/with-oauth2-without-supertokens/src/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} diff --git a/examples/with-oauth2-without-supertokens/src/index.tsx b/examples/with-oauth2-without-supertokens/src/index.tsx new file mode 100644 index 000000000..399c737cd --- /dev/null +++ b/examples/with-oauth2-without-supertokens/src/index.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); +root.render( + + + +); diff --git a/examples/with-oauth2-without-supertokens/src/react-app-env.d.ts b/examples/with-oauth2-without-supertokens/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/examples/with-oauth2-without-supertokens/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/with-oauth2-without-supertokens/tsconfig.json b/examples/with-oauth2-without-supertokens/tsconfig.json new file mode 100644 index 000000000..c0555cbc6 --- /dev/null +++ b/examples/with-oauth2-without-supertokens/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/frontendDriverInterfaceSupported.json b/frontendDriverInterfaceSupported.json index fb1c46a67..06d758578 100644 --- a/frontendDriverInterfaceSupported.json +++ b/frontendDriverInterfaceSupported.json @@ -1,4 +1,4 @@ { "_comment": "contains a list of frontend-backend interface versions that this package supports", - "versions": ["2.0", "3.0"] + "versions": ["3.1", "4.0"] } diff --git a/lib/build/constants.d.ts b/lib/build/constants.d.ts index f4997a2e1..8ba89d3bd 100644 --- a/lib/build/constants.d.ts +++ b/lib/build/constants.d.ts @@ -1,4 +1,5 @@ export declare const RECIPE_ID_QUERY_PARAM = "rid"; +export declare const TENANT_ID_QUERY_PARAM = "tenantId"; export declare const DEFAULT_API_BASE_PATH = "/auth"; export declare const DEFAULT_WEBSITE_BASE_PATH = "/auth"; export declare const ST_ROOT_ID = "supertokens-root"; diff --git a/lib/build/emailpassword-shared3.js b/lib/build/emailpassword-shared3.js index 7382236e5..9abb90a67 100644 --- a/lib/build/emailpassword-shared3.js +++ b/lib/build/emailpassword-shared3.js @@ -2,7 +2,6 @@ var genericComponentOverrideContext = require("./genericComponentOverrideContext.js"); var EmailPasswordWebJS = require("supertokens-web-js/recipe/emailpassword"); -var NormalisedURLPath = require("supertokens-web-js/utils/normalisedURLPath"); var index = require("./authRecipe-shared2.js"); var types = require("./multifactorauth-shared.js"); var constants = require("./emailpassword-shared4.js"); @@ -13,7 +12,6 @@ function _interopDefault(e) { } var EmailPasswordWebJS__default = /*#__PURE__*/ _interopDefault(EmailPasswordWebJS); -var NormalisedURLPath__default = /*#__PURE__*/ _interopDefault(NormalisedURLPath); var getFunctionOverrides = function (onHandleEvent) { return function (originalImp) { @@ -600,22 +598,15 @@ var EmailPassword = /** @class */ (function (_super) { _this.firstFactorIds = [types.FactorIds.EMAILPASSWORD]; _this.getDefaultRedirectionURL = function (context) { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { - var resetPasswordPath; return genericComponentOverrideContext.__generator(this, function (_a) { if (context.action === "RESET_PASSWORD") { - resetPasswordPath = new NormalisedURLPath__default.default( - constants.DEFAULT_RESET_PASSWORD_PATH - ); return [ 2 /*return*/, - "" - .concat( - this.config.appInfo.websiteBasePath - .appendPath(resetPasswordPath) - .getAsStringDangerous(), - "?rid=" - ) - .concat(this.config.recipeId), + genericComponentOverrideContext.getDefaultRedirectionURLForPath( + this.config, + constants.DEFAULT_RESET_PASSWORD_PATH, + context + ), ]; } return [2 /*return*/, this.getAuthRecipeDefaultRedirectionURL(context)]; diff --git a/lib/build/emailpasswordprebuiltui.js b/lib/build/emailpasswordprebuiltui.js index 50ad9e111..9871f7907 100644 --- a/lib/build/emailpasswordprebuiltui.js +++ b/lib/build/emailpasswordprebuiltui.js @@ -730,7 +730,7 @@ var SignInForm = uiEntry.withOverride("EmailPasswordSignInForm", function EmailP 4 /*yield*/, props.recipeImplementation.signIn({ formFields: formFields, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -834,7 +834,15 @@ function useChildProps$1(recipe$2, onAuthSuccess, error, onError, clearError, us return React.useMemo( function () { var onForgotPasswordClick = function () { - return recipe$2.redirect({ action: "RESET_PASSWORD" }, navigate, undefined, userContext); + return recipe$2.redirect( + { + action: "RESET_PASSWORD", + tenantIdFromQueryParams: genericComponentOverrideContext.getTenantIdFromQueryParams(), + }, + navigate, + undefined, + userContext + ); }; var signInAndUpFeature = recipe$2.config.signInAndUpFeature; var signInFeature = signInAndUpFeature.signInForm; @@ -914,6 +922,8 @@ function useChildProps$1(recipe$2, onAuthSuccess, error, onError, clearError, us evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, navigate, undefined, @@ -990,7 +1000,7 @@ var getModifiedRecipeImplementation$1 = function (origImpl) { origImpl.signIn( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input), - { tryLinkingWithSessionUser: false } + { shouldTryLinkingWithSessionUser: false } ) ), ]; @@ -1012,7 +1022,7 @@ var getModifiedRecipeImplementation$1 = function (origImpl) { origImpl.signUp( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input), - { tryLinkingWithSessionUser: false } + { shouldTryLinkingWithSessionUser: false } ) ), ]; @@ -1064,7 +1074,7 @@ var SignUpForm = uiEntry.withOverride("EmailPasswordSignUpForm", function EmailP 4 /*yield*/, props.recipeImplementation.signUp({ formFields: formFields, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -1205,6 +1215,8 @@ function useChildProps(recipe, onAuthSuccess, error, onError, clearError, userCo evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, navigate, undefined, @@ -1287,7 +1299,7 @@ var getModifiedRecipeImplementation = function (origImpl) { origImpl.signIn( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input), - { tryLinkingWithSessionUser: false } + { shouldTryLinkingWithSessionUser: false } ) ), ]; @@ -1309,7 +1321,7 @@ var getModifiedRecipeImplementation = function (origImpl) { origImpl.signUp( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input), - { tryLinkingWithSessionUser: false } + { shouldTryLinkingWithSessionUser: false } ) ), ]; diff --git a/lib/build/emailverification-shared.js b/lib/build/emailverification-shared.js index e75d4a068..1c7d9189f 100644 --- a/lib/build/emailverification-shared.js +++ b/lib/build/emailverification-shared.js @@ -2,7 +2,6 @@ var genericComponentOverrideContext = require("./genericComponentOverrideContext.js"); var EmailVerificationWebJS = require("supertokens-web-js/recipe/emailverification"); -var NormalisedURLPath = require("supertokens-web-js/utils/normalisedURLPath"); var postSuperTokensInitCallbacks = require("supertokens-web-js/utils/postSuperTokensInitCallbacks"); var sessionClaimValidatorStore = require("supertokens-web-js/utils/sessionClaimValidatorStore"); var index = require("./recipeModule-shared.js"); @@ -12,7 +11,6 @@ function _interopDefault(e) { } var EmailVerificationWebJS__default = /*#__PURE__*/ _interopDefault(EmailVerificationWebJS); -var NormalisedURLPath__default = /*#__PURE__*/ _interopDefault(NormalisedURLPath); var _a = genericComponentOverrideContext.createGenericComponentsOverrideContext(), useContext = _a[0], @@ -55,7 +53,14 @@ var EmailVerificationClaimClass = /** @class */ (function (_super) { } var recipe = EmailVerification.getInstanceOrThrow(); if (recipe.config.mode === "REQUIRED") { - return recipe.getRedirectUrl({ action: "VERIFY_EMAIL" }, args.userContext); + return recipe.getRedirectUrl( + { + action: "VERIFY_EMAIL", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + }, + args.userContext + ); } return undefined; }, @@ -202,20 +207,15 @@ var EmailVerification = /** @class */ (function (_super) { _this.recipeID = EmailVerification.RECIPE_ID; _this.getDefaultRedirectionURL = function (context) { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { - var verifyEmailPath; return genericComponentOverrideContext.__generator(this, function (_a) { if (context.action === "VERIFY_EMAIL") { - verifyEmailPath = new NormalisedURLPath__default.default(DEFAULT_VERIFY_EMAIL_PATH); return [ 2 /*return*/, - "" - .concat( - this.config.appInfo.websiteBasePath - .appendPath(verifyEmailPath) - .getAsStringDangerous(), - "?rid=" - ) - .concat(this.config.recipeId), + genericComponentOverrideContext.getDefaultRedirectionURLForPath( + this.config, + DEFAULT_VERIFY_EMAIL_PATH, + context + ), ]; } else { return [2 /*return*/, "/"]; diff --git a/lib/build/genericComponentOverrideContext.js b/lib/build/genericComponentOverrideContext.js index 8cb5d736f..4d7d6e80c 100644 --- a/lib/build/genericComponentOverrideContext.js +++ b/lib/build/genericComponentOverrideContext.js @@ -245,6 +245,7 @@ typeof SuppressedError === "function" * Consts. */ var RECIPE_ID_QUERY_PARAM = "rid"; +var TENANT_ID_QUERY_PARAM = "tenantId"; var DEFAULT_API_BASE_PATH = "/auth"; var DEFAULT_WEBSITE_BASE_PATH = "/auth"; var ST_ROOT_ID = "supertokens-root"; @@ -265,7 +266,7 @@ var SSR_ERROR = * License for the specific language governing permissions and limitations * under the License. */ -var package_version = "0.42.2"; +var package_version = "0.43.0"; /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. * @@ -389,6 +390,32 @@ function getRedirectToPathFromURL() { } } } +function getTenantIdFromQueryParams() { + var _a; + return (_a = getQueryParams(TENANT_ID_QUERY_PARAM)) !== null && _a !== void 0 ? _a : undefined; +} +function getDefaultRedirectionURLForPath(config, defaultPath, context, extraQueryParams) { + var redirectPath = config.appInfo.websiteBasePath + .appendPath(new NormalisedURLPath__default.default(defaultPath)) + .getAsStringDangerous(); + var queryParams = new URLSearchParams(); + if (context.tenantIdFromQueryParams !== undefined) { + queryParams.set(TENANT_ID_QUERY_PARAM, context.tenantIdFromQueryParams); + } + if (extraQueryParams !== undefined) { + Object.entries(extraQueryParams).forEach(function (_a) { + var key = _a[0], + value = _a[1]; + if (value !== undefined) { + queryParams.set(key, value); + } + }); + } + if (queryParams.toString() !== "") { + return "".concat(redirectPath, "?").concat(queryParams.toString()); + } + return redirectPath; +} /* * isTest */ @@ -1220,6 +1247,7 @@ var SuperTokens = /** @class */ (function () { { action: "TO_AUTH", showSignIn: options.show === "signin", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, options.userContext ), @@ -1359,22 +1387,33 @@ var SuperTokens = /** @class */ (function () { SuperTokens.prototype.getRedirectUrl = function (context, userContext) { var _a; return __awaiter(this, void 0, void 0, function () { - var userRes, redirectUrl; - return __generator(this, function (_b) { - switch (_b.label) { + var userRes, redirectUrl, basePath; + var _b; + return __generator(this, function (_c) { + switch (_c.label) { case 0: if (!this.userGetRedirectionURL) return [3 /*break*/, 2]; return [4 /*yield*/, this.userGetRedirectionURL(context, userContext)]; case 1: - userRes = _b.sent(); + userRes = _c.sent(); if (userRes !== undefined) { return [2 /*return*/, userRes]; } - _b.label = 2; + _c.label = 2; case 2: if (context.action === "TO_AUTH") { redirectUrl = this.appInfo.websiteBasePath.getAsStringDangerous(); - return [2 /*return*/, appendTrailingSlashToURL(redirectUrl)]; + basePath = appendTrailingSlashToURL(redirectUrl); + if (context.tenantIdFromQueryParams) { + return [ + 2 /*return*/, + appendQueryParamsToURL( + basePath, + ((_b = {}), (_b[TENANT_ID_QUERY_PARAM] = context.tenantIdFromQueryParams), _b) + ), + ]; + } + return [2 /*return*/, basePath]; } else if (context.action === "SUCCESS") { return [2 /*return*/, (_a = context.redirectToPath) !== null && _a !== void 0 ? _a : "/"]; } @@ -1450,10 +1489,12 @@ exports.createGenericComponentsOverrideContext = createGenericComponentsOverride exports.getCurrentLanguageFromCookie = getCurrentLanguageFromCookie; exports.getCurrentNormalisedUrlPath = getCurrentNormalisedUrlPath; exports.getCurrentNormalisedUrlPathWithQueryParamsAndFragments = getCurrentNormalisedUrlPathWithQueryParamsAndFragments; +exports.getDefaultRedirectionURLForPath = getDefaultRedirectionURLForPath; exports.getLocalStorage = getLocalStorage; exports.getNormalisedUserContext = getNormalisedUserContext; exports.getQueryParams = getQueryParams; exports.getRedirectToPathFromURL = getRedirectToPathFromURL; +exports.getTenantIdFromQueryParams = getTenantIdFromQueryParams; exports.getURLHash = getURLHash; exports.isTest = isTest; exports.logDebugMessage = logDebugMessage; diff --git a/lib/build/index2.js b/lib/build/index2.js index afe235f69..64bb8d9df 100644 --- a/lib/build/index2.js +++ b/lib/build/index2.js @@ -677,9 +677,9 @@ var AuthPageHeader = withOverride("AuthPageHeader", function AuthPageHeader(_a) var t = translationContext.useTranslation(); return jsxRuntime.jsxs(React.Fragment, { children: [ - (oauth2ClientInfo === null || oauth2ClientInfo === void 0 ? void 0 : oauth2ClientInfo.clientLogoUri) && + (oauth2ClientInfo === null || oauth2ClientInfo === void 0 ? void 0 : oauth2ClientInfo.logoUri) && jsxRuntime.jsx("img", { - src: oauth2ClientInfo.clientLogoUri, + src: oauth2ClientInfo.logoUri, alt: oauth2ClientInfo.clientName, "data-supertokens": "authPageTitleOAuthClientLogo", }), @@ -705,6 +705,8 @@ var AuthPageHeader = withOverride("AuthPageHeader", function AuthPageHeader(_a) ) ), oauth2ClientInfo && + oauth2ClientInfo.clientName !== undefined && + oauth2ClientInfo.clientName.length > 0 && jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( @@ -1006,22 +1008,45 @@ var AuthPageInner = function (props) { }, [loadedDynamicLoginMethods, setLoadedDynamicLoginMethods] ); - React.useEffect( + genericComponentOverrideContext.useOnMountAPICall( function () { - if (oauth2ClientInfo) { - return; - } - var oauth2Recipe = recipe.OAuth2Provider.getInstance(); - if (oauth2Recipe !== undefined && loginChallenge !== null) { - void recipe.OAuth2Provider.getInstanceOrThrow() - .webJSRecipe.getLoginChallengeInfo({ loginChallenge: loginChallenge, userContext: userContext }) - .then(function (_a) { - var info = _a.info; - return setOAuth2ClientInfo(info); - }); - } + return genericComponentOverrideContext.__awaiter(void 0, void 0, void 0, function () { + var oauth2Recipe; + return genericComponentOverrideContext.__generator(this, function (_a) { + if (oauth2ClientInfo) { + return [2 /*return*/]; + } + oauth2Recipe = recipe.OAuth2Provider.getInstance(); + if (oauth2Recipe !== undefined && loginChallenge !== null) { + return [ + 2 /*return*/, + oauth2Recipe.webJSRecipe.getLoginChallengeInfo({ + loginChallenge: loginChallenge, + userContext: userContext, + }), + ]; + } + return [2 /*return*/, undefined]; + }); + }); + }, + function (info) { + return genericComponentOverrideContext.__awaiter(void 0, void 0, void 0, function () { + return genericComponentOverrideContext.__generator(this, function (_a) { + if (info !== undefined) { + if (info.status === "OK") { + setOAuth2ClientInfo(info.info); + } else { + setError("SOMETHING_WENT_WRONG_ERROR"); + } + } + return [2 /*return*/]; + }); + }); }, - [setOAuth2ClientInfo, loginChallenge, oauth2ClientInfo] + function () { + return genericComponentOverrideContext.clearQueryParams(["loginChallenge"]); + } ); React.useEffect( function () { @@ -1039,23 +1064,46 @@ var AuthPageInner = function (props) { types.Session.getInstanceOrThrow().config.onHandleEvent({ action: "SESSION_ALREADY_EXISTS", }); - if (loginChallenge !== null) { - void types.Session.getInstanceOrThrow() - .validateGlobalClaimsAndHandleSuccessRedirection( - { - action: "SUCCESS_OAUTH2", - createdNewUser: false, - isNewRecipeUser: false, - loginChallenge: loginChallenge, - newSessionCreated: false, - recipeId: types.Session.RECIPE_ID, - }, - types.Session.RECIPE_ID, - genericComponentOverrideContext.getRedirectToPathFromURL(), - userContext, - props.navigate - ) - .catch(rethrowInRender); + var oauth2Recipe_1 = recipe.OAuth2Provider.getInstance(); + if (loginChallenge !== null && oauth2Recipe_1 !== undefined) { + (function () { + return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () { + var frontendRedirectTo; + return genericComponentOverrideContext.__generator(this, function (_a) { + switch (_a.label) { + case 0: + return [ + 4 /*yield*/, + oauth2Recipe_1.webJSRecipe.getRedirectURLToContinueOAuthFlow({ + loginChallenge: loginChallenge, + userContext: userContext, + }), + ]; + case 1: + frontendRedirectTo = _a.sent().frontendRedirectTo; + return [ + 2 /*return*/, + types.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( + { + action: "SUCCESS_OAUTH2", + createdNewUser: false, + isNewRecipeUser: false, + frontendRedirectTo: frontendRedirectTo, + newSessionCreated: false, + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + recipeId: types.Session.RECIPE_ID, + }, + types.Session.RECIPE_ID, + genericComponentOverrideContext.getRedirectToPathFromURL(), + userContext, + props.navigate + ), + ]; + } + }); + }); + })().catch(rethrowInRender); } else { void types.Session.getInstanceOrThrow() .validateGlobalClaimsAndHandleSuccessRedirection( @@ -1131,26 +1179,69 @@ var AuthPageInner = function (props) { ); var onAuthSuccess = React.useCallback( function (ctx) { - return types.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - loginChallenge !== null - ? genericComponentOverrideContext.__assign(genericComponentOverrideContext.__assign({}, ctx), { - action: "SUCCESS_OAUTH2", - loginChallenge: loginChallenge, - }) - : genericComponentOverrideContext.__assign(genericComponentOverrideContext.__assign({}, ctx), { - action: "SUCCESS", - redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), - }), - ctx.recipeId, - genericComponentOverrideContext.getRedirectToPathFromURL(), - userContext, - props.navigate - ); + return genericComponentOverrideContext.__awaiter(void 0, void 0, void 0, function () { + var oauth2Recipe, frontendRedirectTo; + return genericComponentOverrideContext.__generator(this, function (_a) { + switch (_a.label) { + case 0: + oauth2Recipe = recipe.OAuth2Provider.getInstance(); + if (loginChallenge === null || oauth2Recipe === undefined) { + return [ + 2 /*return*/, + types.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( + genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, ctx), + { + action: "SUCCESS", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + redirectToPath: + genericComponentOverrideContext.getRedirectToPathFromURL(), + } + ), + ctx.recipeId, + genericComponentOverrideContext.getRedirectToPathFromURL(), + userContext, + props.navigate + ), + ]; + } + return [ + 4 /*yield*/, + oauth2Recipe.webJSRecipe.getRedirectURLToContinueOAuthFlow({ + loginChallenge: loginChallenge, + userContext: userContext, + }), + ]; + case 1: + frontendRedirectTo = _a.sent().frontendRedirectTo; + return [ + 2 /*return*/, + types.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( + genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, ctx), + { + action: "SUCCESS_OAUTH2", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + frontendRedirectTo: frontendRedirectTo, + } + ), + ctx.recipeId, + genericComponentOverrideContext.getRedirectToPathFromURL(), + userContext, + props.navigate + ), + ]; + } + }); + }); }, [loginChallenge] ); var childProps = - authComponentListInfo !== undefined && (loginChallenge === null || oauth2ClientInfo !== undefined) + authComponentListInfo !== undefined && + (loginChallenge === null || oauth2ClientInfo !== undefined || recipe.OAuth2Provider.getInstance() === undefined) ? genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, authComponentListInfo), { diff --git a/lib/build/multifactorauth-shared.js b/lib/build/multifactorauth-shared.js index 9da088995..7b88f6acf 100644 --- a/lib/build/multifactorauth-shared.js +++ b/lib/build/multifactorauth-shared.js @@ -351,6 +351,7 @@ var Session = /** @class */ (function (_super) { createdNewUser: false, isNewRecipeUser: false, newSessionCreated: false, + tenantIdFromQueryParams: genericComponentOverrideContext.getTenantIdFromQueryParams(), }; _a.label = 13; case 13: @@ -391,6 +392,7 @@ var Session = /** @class */ (function (_super) { _this.getDefaultRedirectionURL = function () { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { return genericComponentOverrideContext.__generator(this, function (_a) { + // We do not use the util here, since we are redirecting outside of the SDK routes return [2 /*return*/, "/"]; }); }); diff --git a/lib/build/multifactorauth-shared2.js b/lib/build/multifactorauth-shared2.js index 4eb51565b..3663607b4 100644 --- a/lib/build/multifactorauth-shared2.js +++ b/lib/build/multifactorauth-shared2.js @@ -3,7 +3,6 @@ var genericComponentOverrideContext = require("./genericComponentOverrideContext.js"); var MultiFactorAuthWebJS = require("supertokens-web-js/recipe/multifactorauth"); var utils = require("supertokens-web-js/utils"); -var NormalisedURLPath = require("supertokens-web-js/utils/normalisedURLPath"); var postSuperTokensInitCallbacks = require("supertokens-web-js/utils/postSuperTokensInitCallbacks"); var sessionClaimValidatorStore = require("supertokens-web-js/utils/sessionClaimValidatorStore"); var windowHandler = require("supertokens-web-js/utils/windowHandler"); @@ -15,7 +14,6 @@ function _interopDefault(e) { } var MultiFactorAuthWebJS__default = /*#__PURE__*/ _interopDefault(MultiFactorAuthWebJS); -var NormalisedURLPath__default = /*#__PURE__*/ _interopDefault(NormalisedURLPath); /* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. * @@ -349,6 +347,23 @@ function normaliseMultiFactorAuthFeature(config) { ); } function getAvailableFactors(factors, nextArrayQueryParam, recipe, userContext) { + genericComponentOverrideContext.logDebugMessage( + "getAvailableFactors: allowed to setup: ".concat(factors.allowedToSetup) + ); + genericComponentOverrideContext.logDebugMessage( + "getAvailableFactors: already setup: ".concat(factors.alreadySetup) + ); + genericComponentOverrideContext.logDebugMessage("getAvailableFactors: next from factorInfo: ".concat(factors.next)); + genericComponentOverrideContext.logDebugMessage( + "getAvailableFactors: nextArrayQueryParam: ".concat(nextArrayQueryParam) + ); + genericComponentOverrideContext.logDebugMessage( + "getAvailableFactors: secondary factors: ".concat( + recipe.getSecondaryFactors(userContext).map(function (f) { + return f.id; + }) + ) + ); // There are 3 cases here: // 1. The app provided an array of factors to show (nextArrayQueryParam) -> we show whatever is in the array // 2. no app provided list and validator passed -> we show all factors available to set up or complete @@ -389,27 +404,36 @@ var MultiFactorAuth = /** @class */ (function (_super) { _this.secondaryFactors = []; _this.getDefaultRedirectionURL = function (context, userContext) { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { - var chooserPath, url, redirectInfo, url; + var nParam, redirectInfo; return genericComponentOverrideContext.__generator(this, function (_b) { if (context.action === "FACTOR_CHOOSER") { - chooserPath = new NormalisedURLPath__default.default(DEFAULT_FACTOR_CHOOSER_PATH); - url = this.config.appInfo.websiteBasePath.appendPath(chooserPath).getAsStringDangerous(); - if (context.nextFactorOptions && context.nextFactorOptions.length > 0) { - url += "?n=".concat(context.nextFactorOptions.join(",")); - } - return [2 /*return*/, url]; + nParam = + context.nextFactorOptions && context.nextFactorOptions.length > 0 + ? context.nextFactorOptions.join(",") + : undefined; + return [ + 2 /*return*/, + genericComponentOverrideContext.getDefaultRedirectionURLForPath( + this.config, + DEFAULT_FACTOR_CHOOSER_PATH, + context, + { n: nParam } + ), + ]; } else if (context.action === "GO_TO_FACTOR") { redirectInfo = this.getSecondaryFactors(userContext).find(function (f) { return f.id === context.factorId; }); if (redirectInfo !== undefined) { - url = this.config.appInfo.websiteBasePath - .appendPath(new NormalisedURLPath__default.default(redirectInfo.path)) - .getAsStringDangerous(); - if (context.forceSetup) { - url += "?setup=true"; - } - return [2 /*return*/, url]; + return [ + 2 /*return*/, + genericComponentOverrideContext.getDefaultRedirectionURLForPath( + this.config, + redirectInfo.path, + context, + context.forceSetup ? { setup: "true" } : {} + ), + ]; } throw new Error("Requested redirect to unknown factor id: " + context.factorId); } else { @@ -512,7 +536,13 @@ var MultiFactorAuth = /** @class */ (function (_super) { return [ 4 /*yield*/, this.getRedirectUrl( - { action: "GO_TO_FACTOR", forceSetup: forceSetup, factorId: factorId }, + { + action: "GO_TO_FACTOR", + forceSetup: forceSetup, + factorId: factorId, + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + }, utils.getNormalisedUserContext(userContext) ), ]; @@ -577,7 +607,12 @@ var MultiFactorAuth = /** @class */ (function (_super) { return [ 4 /*yield*/, this.getRedirectUrl( - { action: "FACTOR_CHOOSER", nextFactorOptions: nextFactorOptions }, + { + action: "FACTOR_CHOOSER", + nextFactorOptions: nextFactorOptions, + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + }, utils.getNormalisedUserContext(userContext) ), ]; @@ -633,7 +668,19 @@ var MultiFactorAuth = /** @class */ (function (_super) { return genericComponentOverrideContext.__generator(_a, function (_b) { switch (_b.label) { case 0: - return [4 /*yield*/, this.getInstanceOrThrow().getRedirectUrl(context, userContext)]; + return [ + 4 /*yield*/, + this.getInstanceOrThrow().getRedirectUrl( + genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, context), + { + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + } + ), + userContext + ), + ]; case 1: return [2 /*return*/, _b.sent() || undefined]; } diff --git a/lib/build/oauth2provider-shared.js b/lib/build/oauth2provider-shared.js index 1c266487a..44efceeaa 100644 --- a/lib/build/oauth2provider-shared.js +++ b/lib/build/oauth2provider-shared.js @@ -160,16 +160,13 @@ var OAuth2Provider = /** @class */ (function (_super) { }; OAuth2Provider.prototype.getDefaultRedirectionURL = function (ctx) { return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () { - var domain, basePath; return genericComponentOverrideContext.__generator(this, function (_a) { - if (ctx.action === "SUCCESS_OAUTH2" || ctx.action === "CONTINUE_OAUTH2_AFTER_REFRESH") { - domain = this.config.appInfo.apiDomain.getAsStringDangerous(); - basePath = this.config.appInfo.apiBasePath.getAsStringDangerous(); - return [ - 2 /*return*/, - "".concat(domain).concat(basePath, "/oauth/login?loginChallenge=").concat(ctx.loginChallenge), - ]; - } else if (ctx.action === "POST_OAUTH2_LOGOUT_REDIRECT") { + // We do not use the util here, because we are likely redirecting across domains here. + if ( + ctx.action === "SUCCESS_OAUTH2" || + ctx.action === "CONTINUE_OAUTH2_AFTER_REFRESH" || + ctx.action === "POST_OAUTH2_LOGOUT_REDIRECT" + ) { return [2 /*return*/, ctx.frontendRedirectTo]; } else { throw new Error( diff --git a/lib/build/oauth2provider.js b/lib/build/oauth2provider.js index 8127cd159..aee148ac4 100644 --- a/lib/build/oauth2provider.js +++ b/lib/build/oauth2provider.js @@ -53,6 +53,22 @@ var Wrapper = /** @class */ (function () { Wrapper.getLoginChallengeInfo = function (input) { return recipe.OAuth2Provider.getInstanceOrThrow().webJSRecipe.getLoginChallengeInfo(input); }; + /** + * Accepts the OAuth2 Login request and returns the redirect URL to continue the OAuth flow. + * + * @param loginChallenge The login challenge from the url + * + * @param userContext (OPTIONAL) Refer to {@link https://supertokens.com/docs/emailpassword/advanced-customizations/user-context the documentation} + * + * @param options (OPTIONAL) Use this to configure additional properties (for example pre api hooks) + * + * @returns `{status: "OK", frontendRedirectTo: string}` + * + * @throws STGeneralError if the API exposed by the backend SDKs returns `status: "GENERAL_ERROR"` + */ + Wrapper.getRedirectURLToContinueOAuthFlow = function (input) { + return recipe.OAuth2Provider.getInstanceOrThrow().webJSRecipe.getRedirectURLToContinueOAuthFlow(input); + }; /** * Accepts the OAuth2 Logout request, clears the SuperTokens session and returns post logout redirect URL. * diff --git a/lib/build/oauth2providerprebuiltui.js b/lib/build/oauth2providerprebuiltui.js index 545ede489..d0c5207ec 100644 --- a/lib/build/oauth2providerprebuiltui.js +++ b/lib/build/oauth2providerprebuiltui.js @@ -239,6 +239,8 @@ var OAuth2LogoutScreen = function (props) { { recipeId: "oauth2provider", action: "POST_OAUTH2_LOGOUT_REDIRECT", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), frontendRedirectTo: frontendRedirectTo, }, navigate, @@ -335,21 +337,46 @@ var TryRefreshPage$1 = function (props) { if (props.userContext !== undefined) { userContext = props.userContext; } + session.default.attemptRefreshingSession; React__namespace.useEffect( function () { if (sessionContext.loading === false) { - void props.recipe - .redirect( - { - action: "CONTINUE_OAUTH2_AFTER_REFRESH", - loginChallenge: loginChallenge !== null && loginChallenge !== void 0 ? loginChallenge : "", - recipeId: "oauth2provider", - }, - props.navigate, - {}, - userContext - ) - .catch(rethrowInRender); + if (loginChallenge) { + (function () { + return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () { + var frontendRedirectTo; + return genericComponentOverrideContext.__generator(this, function (_a) { + switch (_a.label) { + case 0: + return [ + 4 /*yield*/, + props.recipe.webJSRecipe.getRedirectURLToContinueOAuthFlow({ + loginChallenge: loginChallenge, + userContext: userContext, + }), + ]; + case 1: + frontendRedirectTo = _a.sent().frontendRedirectTo; + return [ + 2 /*return*/, + props.recipe.redirect( + { + action: "CONTINUE_OAUTH2_AFTER_REFRESH", + frontendRedirectTo: frontendRedirectTo, + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + recipeId: "oauth2provider", + }, + props.navigate, + {}, + userContext + ), + ]; + } + }); + }); + })().catch(rethrowInRender); + } } }, [loginChallenge, props.recipe, props.navigate, userContext, sessionContext] diff --git a/lib/build/passwordlessprebuiltui.js b/lib/build/passwordlessprebuiltui.js index 615fa049d..4ca8a7b77 100644 --- a/lib/build/passwordlessprebuiltui.js +++ b/lib/build/passwordlessprebuiltui.js @@ -499,6 +499,8 @@ var LinkClickedScreen = function (props) { (payloadBeforeCall === undefined || payloadBeforeCall.sessionHandle !== payloadAfterCall.sessionHandle), recipeId: props.recipe.recipeID, + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, props.recipe.recipeID, loginAttemptInfo === null || loginAttemptInfo === void 0 @@ -1198,6 +1200,8 @@ function useChildProps$4( session$1.accessTokenPayload.sessionHandle !== payloadAfterCall.sessionHandle), recipeId: recipe$1.recipeID, + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, recipe$1.recipeID, genericComponentOverrideContext.getRedirectToPathFromURL(), @@ -1246,6 +1250,8 @@ function useChildProps$4( 4 /*yield*/, evInstance.redirect( { + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), action: "VERIFY_EMAIL", }, navigate, @@ -1349,8 +1355,8 @@ function getModifiedRecipeImplementation$4(originalImpl, setError, rebuildAuthPa attemptInfo: genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, loginAttemptInfo), { - tryLinkingWithSessionUser: - (_a = loginAttemptInfo.tryLinkingWithSessionUser) !== null && + shouldTryLinkingWithSessionUser: + (_a = loginAttemptInfo.shouldTryLinkingWithSessionUser) !== null && _a !== void 0 ? _a : false, @@ -1556,7 +1562,7 @@ var EmailForm = uiEntry.withOverride("PasswordlessEmailForm", function Passwordl 4 /*yield*/, props.recipeImplementation.createCode({ email: email, - // tryLinkingWithSessionUser is set by the fn override + // shouldTryLinkingWithSessionUser is set by the fn override userContext: userContext, }), ]; @@ -3717,7 +3723,7 @@ var PhoneForm = uiEntry.withOverride("PasswordlessPhoneForm", function Passwordl 4 /*yield*/, props.recipeImplementation.createCode({ phoneNumber: phoneNumber, - // tryLinkingWithSessionUser is set by the fn override + // shouldTryLinkingWithSessionUser is set by the fn override userContext: userContext, }), ]; @@ -4617,6 +4623,8 @@ function useChildProps$3(recipe$1, recipeImplementation, state, contactMethod, d evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, navigate, undefined, @@ -4834,7 +4842,7 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { recipeImplementation.createCode( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, createCodeInfo), - { tryLinkingWithSessionUser: true, userContext: userContext } + { shouldTryLinkingWithSessionUser: true, userContext: userContext } ) ), ]; @@ -4871,6 +4879,8 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { 4 /*yield*/, evInstance.redirect( { + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), action: "VERIFY_EMAIL", }, props.navigate, @@ -4974,7 +4984,7 @@ function getModifiedRecipeImplementation$3(originalImpl, config, dispatch) { genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input), { - tryLinkingWithSessionUser: true, + shouldTryLinkingWithSessionUser: true, userContext: genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input.userContext), { additionalAttemptInfo: additionalAttemptInfo } @@ -5030,8 +5040,8 @@ function getModifiedRecipeImplementation$3(originalImpl, config, dispatch) { attemptInfo: genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, loginAttemptInfo), { - tryLinkingWithSessionUser: - (_a = loginAttemptInfo.tryLinkingWithSessionUser) !== null && + shouldTryLinkingWithSessionUser: + (_a = loginAttemptInfo.shouldTryLinkingWithSessionUser) !== null && _a !== void 0 ? _a : true, @@ -5070,7 +5080,15 @@ function getModifiedRecipeImplementation$3(originalImpl, config, dispatch) { return genericComponentOverrideContext.__generator(this, function (_a) { switch (_a.label) { case 0: - return [4 /*yield*/, originalImpl.consumeCode(input)]; + return [ + 4 /*yield*/, + originalImpl.consumeCode( + genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, input), + { shouldTryLinkingWithSessionUser: true } + ) + ), + ]; case 1: res = _a.sent(); if (!(res.status === "RESTART_FLOW_ERROR")) return [3 /*break*/, 3]; @@ -5295,7 +5313,7 @@ var EmailOrPhoneForm = uiEntry.withOverride( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, contactInfo), { - // tryLinkingWithSessionUser is set by the fn override + // shouldTryLinkingWithSessionUser is set by the fn override userContext: userContext, } ) @@ -5502,6 +5520,8 @@ function useChildProps$2( evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, navigate, undefined, @@ -5610,7 +5630,7 @@ function getModifiedRecipeImplementation$2(originalImpl, config, rebuildAuthPage genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input), { - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input.userContext), { additionalAttemptInfo: additionalAttemptInfo } @@ -5747,6 +5767,8 @@ var EPComboEmailForm = uiEntry.withOverride( onClick: function () { return recipe$1.EmailPassword.getInstanceOrThrow().redirect({ action: "RESET_PASSWORD", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }); }, "data-supertokens": "link linkButton formLabelLinkBtn forgotPasswordLink", @@ -5928,6 +5950,8 @@ var EPComboEmailOrPhoneForm = uiEntry.withOverride( onClick: function () { return recipe$1.EmailPassword.getInstanceOrThrow().redirect({ action: "RESET_PASSWORD", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }); }, "data-supertokens": "link linkButton formLabelLinkBtn forgotPasswordLink", @@ -6126,7 +6150,7 @@ function useChildProps$1( 4 /*yield*/, recipeImplementation.createCode({ phoneNumber: contactInfo, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -6173,7 +6197,7 @@ function useChildProps$1( 4 /*yield*/, recipeImplementation.createCode({ email: email, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -6224,7 +6248,7 @@ function useChildProps$1( 4 /*yield*/, recipe$1.EmailPassword.getInstanceOrThrow().webJSRecipe.signIn({ formFields: formFields, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: userContext, }), ]; @@ -6261,7 +6285,7 @@ function useChildProps$1( recipeImplementation.createCode( genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, createInfo), - { tryLinkingWithSessionUser: false, userContext: userContext } + { shouldTryLinkingWithSessionUser: false, userContext: userContext } ) ), ]; @@ -6364,6 +6388,8 @@ function useChildProps$1( evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, navigate, undefined, @@ -6485,7 +6511,7 @@ function getModifiedRecipeImplementation$1(originalImpl, config, rebuildAuthPage genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input), { - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, input.userContext), { additionalAttemptInfo: additionalAttemptInfo } @@ -6608,6 +6634,8 @@ function useChildProps( evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, navigate, undefined, @@ -6711,8 +6739,8 @@ function getModifiedRecipeImplementation(originalImpl, setError, rebuildAuthPage attemptInfo: genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, loginAttemptInfo), { - tryLinkingWithSessionUser: - (_a = loginAttemptInfo.tryLinkingWithSessionUser) !== null && + shouldTryLinkingWithSessionUser: + (_a = loginAttemptInfo.shouldTryLinkingWithSessionUser) !== null && _a !== void 0 ? _a : false, diff --git a/lib/build/recipe/authRecipe/components/theme/authPage/authPageHeader.d.ts b/lib/build/recipe/authRecipe/components/theme/authPage/authPageHeader.d.ts index bd8b9fd2a..127a6536f 100644 --- a/lib/build/recipe/authRecipe/components/theme/authPage/authPageHeader.d.ts +++ b/lib/build/recipe/authRecipe/components/theme/authPage/authPageHeader.d.ts @@ -8,7 +8,7 @@ export declare const AuthPageHeader: import("react").ComponentType<{ showBackButton: boolean; oauth2ClientInfo?: | { - clientLogoUri?: string | undefined; + logoUri?: string | undefined; clientUri?: string | undefined; clientName: string; } diff --git a/lib/build/recipe/emailpassword/index.d.ts b/lib/build/recipe/emailpassword/index.d.ts index 089a44d92..9bff21ab9 100644 --- a/lib/build/recipe/emailpassword/index.d.ts +++ b/lib/build/recipe/emailpassword/index.d.ts @@ -66,7 +66,7 @@ export default class Wrapper { id: string; value: string; }[]; - tryLinkingWithSessionUser?: boolean; + shouldTryLinkingWithSessionUser?: boolean; options?: RecipeFunctionOptions; userContext?: UserContext; }): Promise< @@ -94,7 +94,7 @@ export default class Wrapper { id: string; value: string; }[]; - tryLinkingWithSessionUser?: boolean; + shouldTryLinkingWithSessionUser?: boolean; options?: RecipeFunctionOptions; userContext?: UserContext; }): Promise< diff --git a/lib/build/recipe/emailpassword/recipe.d.ts b/lib/build/recipe/emailpassword/recipe.d.ts index 73be9e8f5..3f928c475 100644 --- a/lib/build/recipe/emailpassword/recipe.d.ts +++ b/lib/build/recipe/emailpassword/recipe.d.ts @@ -7,7 +7,12 @@ import type { NormalisedConfig, UserInput, } from "./types"; -import type { RecipeInitResult, NormalisedConfigWithAppInfoAndRecipeID, WebJSRecipeInterface } from "../../types"; +import type { + RecipeInitResult, + NormalisedConfigWithAppInfoAndRecipeID, + WebJSRecipeInterface, + NormalisedGetRedirectionURLContext, +} from "../../types"; export default class EmailPassword extends AuthRecipe< GetRedirectionURLContext, never, @@ -24,7 +29,9 @@ export default class EmailPassword extends AuthRecipe< config: NormalisedConfigWithAppInfoAndRecipeID, webJSRecipe?: WebJSRecipeInterface ); - getDefaultRedirectionURL: (context: GetRedirectionURLContext) => Promise; + getDefaultRedirectionURL: ( + context: NormalisedGetRedirectionURLContext + ) => Promise; static init( config?: UserInput ): RecipeInitResult; diff --git a/lib/build/recipe/emailverification/recipe.d.ts b/lib/build/recipe/emailverification/recipe.d.ts index e655d2dd2..c4899a2f3 100644 --- a/lib/build/recipe/emailverification/recipe.d.ts +++ b/lib/build/recipe/emailverification/recipe.d.ts @@ -10,6 +10,7 @@ import type { } from "./types"; import type { NormalisedConfigWithAppInfoAndRecipeID, + NormalisedGetRedirectionURLContext, RecipeInitResult, UserContext, WebJSRecipeInterface, @@ -38,6 +39,8 @@ export default class EmailVerification extends RecipeModule< isVerified: boolean; fetchResponse: Response; }>; - getDefaultRedirectionURL: (context: GetRedirectionURLContext) => Promise; + getDefaultRedirectionURL: ( + context: NormalisedGetRedirectionURLContext + ) => Promise; static reset(): void; } diff --git a/lib/build/recipe/multifactorauth/recipe.d.ts b/lib/build/recipe/multifactorauth/recipe.d.ts index 606f2552d..644b33919 100644 --- a/lib/build/recipe/multifactorauth/recipe.d.ts +++ b/lib/build/recipe/multifactorauth/recipe.d.ts @@ -12,6 +12,7 @@ import type { import type { Navigate, NormalisedConfigWithAppInfoAndRecipeID, + NormalisedGetRedirectionURLContext, RecipeInitResult, UserContext, WebJSRecipeInterface, @@ -37,7 +38,10 @@ export default class MultiFactorAuth extends RecipeModule< ): RecipeInitResult; static getInstance(): MultiFactorAuth | undefined; static getInstanceOrThrow(): MultiFactorAuth; - getDefaultRedirectionURL: (context: GetRedirectionURLContext, userContext: UserContext) => Promise; + getDefaultRedirectionURL: ( + context: NormalisedGetRedirectionURLContext, + userContext: UserContext + ) => Promise; addMFAFactors(secondaryFactors: SecondaryFactorRedirectionInfo[]): void; isFirstFactorEnabledOnClient(factorId: string): boolean; getSecondaryFactors(userContext: UserContext): SecondaryFactorRedirectionInfo[]; diff --git a/lib/build/recipe/oauth2provider/index.d.ts b/lib/build/recipe/oauth2provider/index.d.ts index c776eac18..1a574d79b 100644 --- a/lib/build/recipe/oauth2provider/index.d.ts +++ b/lib/build/recipe/oauth2provider/index.d.ts @@ -27,6 +27,28 @@ export default class Wrapper { info: LoginInfo; fetchResponse: Response; }>; + /** + * Accepts the OAuth2 Login request and returns the redirect URL to continue the OAuth flow. + * + * @param loginChallenge The login challenge from the url + * + * @param userContext (OPTIONAL) Refer to {@link https://supertokens.com/docs/emailpassword/advanced-customizations/user-context the documentation} + * + * @param options (OPTIONAL) Use this to configure additional properties (for example pre api hooks) + * + * @returns `{status: "OK", frontendRedirectTo: string}` + * + * @throws STGeneralError if the API exposed by the backend SDKs returns `status: "GENERAL_ERROR"` + */ + static getRedirectURLToContinueOAuthFlow(input: { + loginChallenge: string; + options?: RecipeFunctionOptions; + userContext?: any; + }): Promise<{ + status: "OK"; + frontendRedirectTo: string; + fetchResponse: Response; + }>; /** * Accepts the OAuth2 Logout request, clears the SuperTokens session and returns post logout redirect URL. * diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts index b7021b276..7c3bfa87a 100644 --- a/lib/build/recipe/oauth2provider/types.d.ts +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -7,7 +7,10 @@ import type { } from "../recipeModule/types"; import type OverrideableBuilder from "supertokens-js-override"; import type { LoginInfo, RecipeInterface } from "supertokens-web-js/recipe/oauth2provider/types"; -export declare type PreAndPostAPIHookAction = "GET_LOGIN_CHALLENGE_INFO" | "LOG_OUT"; +export declare type PreAndPostAPIHookAction = + | "GET_LOGIN_CHALLENGE_INFO" + | "GET_REDIRECT_URL_TO_CONTINUE_OAUTH_FLOW" + | "LOG_OUT"; export declare type PreAPIHookContext = { action: PreAndPostAPIHookAction; requestInit: RequestInit; @@ -44,7 +47,7 @@ export declare type OAuth2LogoutScreenConfig = NormalisedBaseConfig & { export declare type ContinueOAuth2AfterRefreshRedirectContext = { recipeId: "oauth2provider"; action: "CONTINUE_OAUTH2_AFTER_REFRESH"; - loginChallenge: string; + frontendRedirectTo: string; }; export declare type PostOAuth2LogoutRedirectContext = { recipeId: "oauth2provider"; diff --git a/lib/build/recipe/passwordless/index.d.ts b/lib/build/recipe/passwordless/index.d.ts index 5f851efbe..ca5a32715 100644 --- a/lib/build/recipe/passwordless/index.d.ts +++ b/lib/build/recipe/passwordless/index.d.ts @@ -20,13 +20,13 @@ export default class Wrapper { input: | { email: string; - tryLinkingWithSessionUser?: boolean; + shouldTryLinkingWithSessionUser?: boolean; userContext?: UserContext; options?: RecipeFunctionOptions; } | { phoneNumber: string; - tryLinkingWithSessionUser?: boolean; + shouldTryLinkingWithSessionUser?: boolean; userContext?: UserContext; options?: RecipeFunctionOptions; } diff --git a/lib/build/recipe/recipeModule/index.d.ts b/lib/build/recipe/recipeModule/index.d.ts index ff126debf..c83e68e16 100644 --- a/lib/build/recipe/recipeModule/index.d.ts +++ b/lib/build/recipe/recipeModule/index.d.ts @@ -1,6 +1,6 @@ import { BaseRecipeModule } from "./baseRecipeModule"; import type { NormalisedConfig } from "./types"; -import type { Navigate, UserContext } from "../../types"; +import type { Navigate, NormalisedGetRedirectionURLContext, UserContext } from "../../types"; export default abstract class RecipeModule< GetRedirectionURLContextType, Action, @@ -8,11 +8,17 @@ export default abstract class RecipeModule< N extends NormalisedConfig > extends BaseRecipeModule { redirect: ( - context: GetRedirectionURLContextType, + context: NormalisedGetRedirectionURLContext, navigate?: Navigate, queryParams?: Record, userContext?: UserContext ) => Promise; - getRedirectUrl: (context: GetRedirectionURLContextType, userContext: UserContext) => Promise; - getDefaultRedirectionURL(_: GetRedirectionURLContextType, _userContext: UserContext): Promise; + getRedirectUrl: ( + context: NormalisedGetRedirectionURLContext, + userContext: UserContext + ) => Promise; + getDefaultRedirectionURL( + _: NormalisedGetRedirectionURLContext, + _userContext: UserContext + ): Promise; } diff --git a/lib/build/recipe/recipeModule/types.d.ts b/lib/build/recipe/recipeModule/types.d.ts index 968d0008d..3df75b39f 100644 --- a/lib/build/recipe/recipeModule/types.d.ts +++ b/lib/build/recipe/recipeModule/types.d.ts @@ -1,4 +1,4 @@ -import type { UserContext } from "../../types"; +import type { NormalisedGetRedirectionURLContext, UserContext } from "../../types"; export declare type RecipePreAPIHookContext = { requestInit: RequestInit; url: string; @@ -20,7 +20,7 @@ export declare type RecipePostAPIHookFunction = (context: RecipePostAPIH export declare type RecipeOnHandleEventFunction = (context: EventType) => void; export declare type UserInput = { getRedirectionURL?: ( - context: GetRedirectionURLContextType, + context: NormalisedGetRedirectionURLContext, userContext: UserContext ) => Promise; preAPIHook?: RecipePreAPIHookFunction; @@ -35,7 +35,7 @@ export declare type Config; export declare type NormalisedConfig = { getRedirectionURL: ( - context: GetRedirectionURLContextType, + context: NormalisedGetRedirectionURLContext, userContext: UserContext ) => Promise; onHandleEvent: RecipeOnHandleEventFunction; diff --git a/lib/build/recipe/session/recipe.d.ts b/lib/build/recipe/session/recipe.d.ts index 4925a3630..674e168a9 100644 --- a/lib/build/recipe/session/recipe.d.ts +++ b/lib/build/recipe/session/recipe.d.ts @@ -55,6 +55,7 @@ export default class Session extends RecipeModule | Omit) & { recipeId: string; + tenantIdFromQueryParams: string | undefined; }) | undefined, fallbackRecipeId: string, diff --git a/lib/build/recipe/thirdparty/index.d.ts b/lib/build/recipe/thirdparty/index.d.ts index 9978aeb1f..7c8e6e943 100644 --- a/lib/build/recipe/thirdparty/index.d.ts +++ b/lib/build/recipe/thirdparty/index.d.ts @@ -41,7 +41,7 @@ export default class Wrapper { thirdPartyId: string; frontendRedirectURI: string; redirectURIOnProviderDashboard?: string; - tryLinkingWithSessionUser?: boolean; + shouldTryLinkingWithSessionUser?: boolean; userContext?: UserContext; options?: RecipeFunctionOptions; }): Promise; diff --git a/lib/build/superTokens.d.ts b/lib/build/superTokens.d.ts index 6fa48b469..3d5293d6d 100644 --- a/lib/build/superTokens.d.ts +++ b/lib/build/superTokens.d.ts @@ -3,7 +3,14 @@ import type RecipeModule from "./recipe/recipeModule"; import type { BaseRecipeModule } from "./recipe/recipeModule/baseRecipeModule"; import type { NormalisedConfig as NormalisedRecipeModuleConfig } from "./recipe/recipeModule/types"; import type { TranslationFunc, TranslationStore } from "./translation/translationHelpers"; -import type { Navigate, GetRedirectionURLContext, NormalisedAppInfo, SuperTokensConfig, UserContext } from "./types"; +import type { + Navigate, + GetRedirectionURLContext, + NormalisedAppInfo, + SuperTokensConfig, + UserContext, + NormalisedGetRedirectionURLContext, +} from "./types"; export default class SuperTokens { private static instance?; static usesDynamicLoginMethods: boolean; @@ -31,7 +38,10 @@ export default class SuperTokens { ): RecipeModule; changeLanguage: (lang: string) => Promise; loadTranslation(store: TranslationStore): void; - getRedirectUrl(context: GetRedirectionURLContext, userContext: UserContext): Promise; + getRedirectUrl( + context: NormalisedGetRedirectionURLContext, + userContext: UserContext + ): Promise; redirectToAuth: (options: { show?: "signin" | "signup" | undefined; navigate?: Navigate | undefined; @@ -41,7 +51,7 @@ export default class SuperTokens { }) => Promise; redirectToUrl: (redirectUrl: string, navigate?: Navigate) => Promise; redirect: ( - context: GetRedirectionURLContext, + context: NormalisedGetRedirectionURLContext, navigate?: Navigate, queryParams?: Record, userContext?: UserContext diff --git a/lib/build/thirdparty-shared.js b/lib/build/thirdparty-shared.js index c34e7f232..ed10b4874 100644 --- a/lib/build/thirdparty-shared.js +++ b/lib/build/thirdparty-shared.js @@ -1333,7 +1333,7 @@ function redirectToThirdPartyLogin(input) { thirdPartyId: input.thirdPartyId, frontendRedirectURI: provider.getRedirectURL(), redirectURIOnProviderDashboard: provider.getRedirectURIOnProviderDashboard(), - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: input.userContext, }), ]; diff --git a/lib/build/thirdpartyprebuiltui.js b/lib/build/thirdpartyprebuiltui.js index de2b8a93e..bdfe1d6ae 100644 --- a/lib/build/thirdpartyprebuiltui.js +++ b/lib/build/thirdpartyprebuiltui.js @@ -12,7 +12,8 @@ var authCompWrapper = require("./authCompWrapper.js"); var types = require("./multifactorauth-shared.js"); var STGeneralError = require("supertokens-web-js/utils/error"); var emailverification = require("./emailverification.js"); -var recipe$1 = require("./emailverification-shared.js"); +var recipe$2 = require("./emailverification-shared.js"); +var recipe$1 = require("./oauth2provider-shared.js"); require("supertokens-web-js"); require("supertokens-web-js/utils/cookieHandler"); require("supertokens-web-js/utils/postSuperTokensInitCallbacks"); @@ -27,8 +28,6 @@ require("./multifactorauth-shared2.js"); require("supertokens-web-js/recipe/multifactorauth"); require("supertokens-web-js/utils/sessionClaimValidatorStore"); require("./recipeModule-shared.js"); -require("./oauth2provider-shared.js"); -require("supertokens-web-js/recipe/oauth2provider"); require("./authRecipe-shared.js"); require("supertokens-web-js/lib/build/normalisedURLPath"); require("./multifactorauth-shared3.js"); @@ -37,6 +36,7 @@ require("./session-shared.js"); require("supertokens-web-js/recipe/thirdparty"); require("./authRecipe-shared2.js"); require("supertokens-web-js/recipe/emailverification"); +require("supertokens-web-js/recipe/oauth2provider"); function _interopDefault(e) { return e && e.__esModule ? e : { default: e }; @@ -437,7 +437,13 @@ var SignInAndUpCallback$1 = function (props) { var response = _a.response, payloadBeforeCall = _a.payloadBeforeCall; return genericComponentOverrideContext.__awaiter(void 0, void 0, void 0, function () { - var payloadAfterCall, stateResponse, redirectToPath, loginChallenge; + var payloadAfterCall, + stateResponse, + redirectToPath, + loginChallenge, + oauth2Recipe, + frontendRedirectTo, + e_1; return genericComponentOverrideContext.__generator(this, function (_c) { switch (_c.label) { case 0: @@ -468,7 +474,7 @@ var SignInAndUpCallback$1 = function (props) { }), ]; } - if (!(response.status === "OK")) return [3 /*break*/, 5]; + if (!(response.status === "OK")) return [3 /*break*/, 10]; payloadAfterCall = void 0; _c.label = 1; case 1: @@ -495,38 +501,68 @@ var SignInAndUpCallback$1 = function (props) { stateResponse === null || stateResponse === void 0 ? void 0 : stateResponse.oauth2LoginChallenge; + oauth2Recipe = recipe$1.OAuth2Provider.getInstance(); + if (!(loginChallenge !== undefined && oauth2Recipe !== undefined)) return [3 /*break*/, 9]; + _c.label = 5; + case 5: + _c.trys.push([5, 7, , 8]); + return [ + 4 /*yield*/, + oauth2Recipe.webJSRecipe.getRedirectURLToContinueOAuthFlow({ + loginChallenge: loginChallenge, + userContext: userContext, + }), + ]; + case 6: + frontendRedirectTo = _c.sent().frontendRedirectTo; + return [ + 2 /*return*/, + types.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( + { + action: "SUCCESS_OAUTH2", + frontendRedirectTo: frontendRedirectTo, + createdNewUser: + response.createdNewRecipeUser && response.user.loginMethods.length === 1, + isNewRecipeUser: response.createdNewRecipeUser, + newSessionCreated: + payloadAfterCall !== undefined && + (payloadBeforeCall === undefined || + payloadBeforeCall.sessionHandle !== payloadAfterCall.sessionHandle), + recipeId: props.recipe.recipeID, + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + }, + props.recipe.recipeID, + redirectToPath, + userContext, + props.navigate + ), + ]; + case 7: + e_1 = _c.sent(); + rethrowInRender(e_1); + return [3 /*break*/, 8]; + case 8: + return [3 /*break*/, 10]; + case 9: return [ 2 /*return*/, types.Session.getInstanceOrThrow() .validateGlobalClaimsAndHandleSuccessRedirection( - loginChallenge !== undefined - ? { - action: "SUCCESS_OAUTH2", - loginChallenge: loginChallenge, - createdNewUser: - response.createdNewRecipeUser && - response.user.loginMethods.length === 1, - isNewRecipeUser: response.createdNewRecipeUser, - newSessionCreated: - payloadAfterCall !== undefined && - (payloadBeforeCall === undefined || - payloadBeforeCall.sessionHandle !== - payloadAfterCall.sessionHandle), - recipeId: props.recipe.recipeID, - } - : { - action: "SUCCESS", - createdNewUser: - response.createdNewRecipeUser && - response.user.loginMethods.length === 1, - isNewRecipeUser: response.createdNewRecipeUser, - newSessionCreated: - payloadAfterCall !== undefined && - (payloadBeforeCall === undefined || - payloadBeforeCall.sessionHandle !== - payloadAfterCall.sessionHandle), - recipeId: props.recipe.recipeID, - }, + { + action: "SUCCESS", + createdNewUser: + response.createdNewRecipeUser && + response.user.loginMethods.length === 1, + isNewRecipeUser: response.createdNewRecipeUser, + newSessionCreated: + payloadAfterCall !== undefined && + (payloadBeforeCall === undefined || + payloadBeforeCall.sessionHandle !== payloadAfterCall.sessionHandle), + recipeId: props.recipe.recipeID, + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), + }, props.recipe.recipeID, redirectToPath, userContext, @@ -534,7 +570,7 @@ var SignInAndUpCallback$1 = function (props) { ) .catch(rethrowInRender), ]; - case 5: + case 10: return [2 /*return*/]; } }); @@ -571,12 +607,14 @@ var SignInAndUpCallback$1 = function (props) { _b.label = 2; case 2: _b.trys.push([2, 4, , 5]); - evInstance = recipe$1.EmailVerification.getInstanceOrThrow(); + evInstance = recipe$2.EmailVerification.getInstanceOrThrow(); return [ 4 /*yield*/, evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: + genericComponentOverrideContext.getTenantIdFromQueryParams(), }, props.navigate, undefined, diff --git a/lib/build/types.d.ts b/lib/build/types.d.ts index d4a235220..e53bc0308 100644 --- a/lib/build/types.d.ts +++ b/lib/build/types.d.ts @@ -29,7 +29,7 @@ export declare type SuccessRedirectContextInApp = SuccessRedirectContextCommon & }; export declare type SuccessRedirectContextOAuth2 = SuccessRedirectContextCommon & { action: "SUCCESS_OAUTH2"; - loginChallenge: string; + frontendRedirectTo: string; }; export declare type SuccessRedirectContext = SuccessRedirectContextInApp | SuccessRedirectContextOAuth2; export declare type GetRedirectionURLContext = @@ -74,7 +74,7 @@ export declare type SuperTokensConfig = { }; enableDebugLogs?: boolean; getRedirectionURL?: ( - context: GetRedirectionURLContext, + context: NormalisedGetRedirectionURLContext, userContext: UserContext ) => Promise; style?: string; @@ -226,4 +226,7 @@ export declare type PartialAuthComponent = { component: React.FC; }; export declare type AuthComponent = PartialAuthComponent | FullPageAuthComponent; +export declare type NormalisedGetRedirectionURLContext = RecipeContext & { + tenantIdFromQueryParams: string | undefined; +}; export {}; diff --git a/lib/build/ui/index.d.ts b/lib/build/ui/index.d.ts index c345f2b0c..9137df7c3 100644 --- a/lib/build/ui/index.d.ts +++ b/lib/build/ui/index.d.ts @@ -43,7 +43,7 @@ declare class UI { showBackButton: boolean; oauth2ClientInfo?: | { - clientLogoUri?: string | undefined; + logoUri?: string | undefined; clientUri?: string | undefined; clientName: string; } diff --git a/lib/build/ui/types.d.ts b/lib/build/ui/types.d.ts index 40a2b32c7..3ac887e16 100644 --- a/lib/build/ui/types.d.ts +++ b/lib/build/ui/types.d.ts @@ -1,6 +1,7 @@ import type { EmailPasswordPreBuiltUI } from "../recipe/emailpassword/prebuiltui"; import type { EmailVerificationPreBuiltUI } from "../recipe/emailverification/prebuiltui"; import type { MultiFactorAuthPreBuiltUI } from "../recipe/multifactorauth/prebuiltui"; +import type { OAuth2ProviderPreBuiltUI } from "../recipe/oauth2provider/prebuiltui"; import type { PasswordlessPreBuiltUI } from "../recipe/passwordless/prebuiltui"; import type { ThirdPartyPreBuiltUI } from "../recipe/thirdparty/prebuiltui"; import type { TOTPPreBuiltUI } from "../recipe/totp/prebuiltui"; @@ -18,4 +19,5 @@ export declare type PreBuiltRecipes = ( | typeof EmailVerificationPreBuiltUI | typeof MultiFactorAuthPreBuiltUI | typeof TOTPPreBuiltUI + | typeof OAuth2ProviderPreBuiltUI )[]; diff --git a/lib/build/utils.d.ts b/lib/build/utils.d.ts index 7e87db5b9..9c63de8a9 100644 --- a/lib/build/utils.d.ts +++ b/lib/build/utils.d.ts @@ -8,6 +8,7 @@ import type { Navigate, NormalisedAppInfo, NormalisedFormField, + NormalisedGetRedirectionURLContext, UserContext, } from "./types"; export declare function getRecipeIdFromSearch(search: string): string | null; @@ -17,6 +18,15 @@ export declare function clearErrorQueryParam(): void; export declare function getQueryParams(param: string): string | null; export declare function getURLHash(): string; export declare function getRedirectToPathFromURL(): string | undefined; +export declare function getTenantIdFromQueryParams(): string | undefined; +export declare function getDefaultRedirectionURLForPath( + config: { + appInfo: NormalisedAppInfo; + }, + defaultPath: string, + context: NormalisedGetRedirectionURLContext, + extraQueryParams?: Record +): string; export declare function isTest(): boolean; export declare function normaliseInputAppInfoOrThrowError(appInfo: AppInfoUserInput): NormalisedAppInfo; export declare function validateForm( diff --git a/lib/build/version.d.ts b/lib/build/version.d.ts index c763119b6..b3a7d195c 100644 --- a/lib/build/version.d.ts +++ b/lib/build/version.d.ts @@ -1 +1 @@ -export declare const package_version = "0.42.2"; +export declare const package_version = "0.43.0"; diff --git a/lib/ts/constants.ts b/lib/ts/constants.ts index 3727b23b1..ab313489b 100644 --- a/lib/ts/constants.ts +++ b/lib/ts/constants.ts @@ -17,6 +17,7 @@ * Consts. */ export const RECIPE_ID_QUERY_PARAM = "rid"; +export const TENANT_ID_QUERY_PARAM = "tenantId"; export const DEFAULT_API_BASE_PATH = "/auth"; diff --git a/lib/ts/recipe/authRecipe/components/feature/authPage/authPage.tsx b/lib/ts/recipe/authRecipe/components/feature/authPage/authPage.tsx index 66fa995f1..adc967efc 100644 --- a/lib/ts/recipe/authRecipe/components/feature/authPage/authPage.tsx +++ b/lib/ts/recipe/authRecipe/components/feature/authPage/authPage.tsx @@ -25,7 +25,15 @@ import SuperTokens from "../../../../../superTokens"; import { TranslationContextProvider } from "../../../../../translation/translationContext"; import { defaultTranslationsCommon } from "../../../../../translation/translations"; import { UserContextProvider, useUserContext } from "../../../../../usercontext"; -import { getRedirectToPathFromURL, mergeObjects, updateQueryParam, useRethrowInRender } from "../../../../../utils"; +import { + clearQueryParams, + getRedirectToPathFromURL, + getTenantIdFromQueryParams, + mergeObjects, + updateQueryParam, + useOnMountAPICall, + useRethrowInRender, +} from "../../../../../utils"; import MultiFactorAuth from "../../../../multifactorauth/recipe"; import { FactorIds } from "../../../../multifactorauth/types"; import DynamicLoginMethodsSpinner from "../../../../multitenancy/components/features/dynamicLoginMethodsSpinner"; @@ -168,17 +176,30 @@ const AuthPageInner: React.FC = (props) => { ); }, [loadedDynamicLoginMethods, setLoadedDynamicLoginMethods]); - useEffect(() => { - if (oauth2ClientInfo) { - return; - } - const oauth2Recipe = OAuth2Provider.getInstance(); - if (oauth2Recipe !== undefined && loginChallenge !== null) { - void OAuth2Provider.getInstanceOrThrow() - .webJSRecipe.getLoginChallengeInfo({ loginChallenge, userContext }) - .then(({ info }) => setOAuth2ClientInfo(info)); + useOnMountAPICall( + async () => { + if (oauth2ClientInfo) { + return; + } + const oauth2Recipe = OAuth2Provider.getInstance(); + if (oauth2Recipe !== undefined && loginChallenge !== null) { + return oauth2Recipe.webJSRecipe.getLoginChallengeInfo({ loginChallenge, userContext }); + } + return undefined; + }, + async (info) => { + if (info !== undefined) { + if (info.status === "OK") { + setOAuth2ClientInfo(info.info); + } else { + setError("SOMETHING_WENT_WRONG_ERROR"); + } + } + }, + () => { + return clearQueryParams(["loginChallenge"]); } - }, [setOAuth2ClientInfo, loginChallenge, oauth2ClientInfo]); + ); useEffect(() => { if (sessionLoadedAndNotRedirecting) { @@ -196,23 +217,30 @@ const AuthPageInner: React.FC = (props) => { Session.getInstanceOrThrow().config.onHandleEvent({ action: "SESSION_ALREADY_EXISTS", }); - if (loginChallenge !== null) { - void Session.getInstanceOrThrow() - .validateGlobalClaimsAndHandleSuccessRedirection( + const oauth2Recipe = OAuth2Provider.getInstance(); + if (loginChallenge !== null && oauth2Recipe !== undefined) { + (async function () { + const { frontendRedirectTo } = + await oauth2Recipe.webJSRecipe.getRedirectURLToContinueOAuthFlow({ + loginChallenge, + userContext, + }); + return Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( { action: "SUCCESS_OAUTH2", createdNewUser: false, isNewRecipeUser: false, - loginChallenge, + frontendRedirectTo, newSessionCreated: false, + tenantIdFromQueryParams: getTenantIdFromQueryParams(), recipeId: Session.RECIPE_ID, }, Session.RECIPE_ID, getRedirectToPathFromURL(), userContext, props.navigate - ) - .catch(rethrowInRender); + ); + })().catch(rethrowInRender); } else { void Session.getInstanceOrThrow() .validateGlobalClaimsAndHandleSuccessRedirection( @@ -284,19 +312,33 @@ const AuthPageInner: React.FC = (props) => { ]); const onAuthSuccess = useCallback( - (ctx: AuthSuccessContext) => { + async (ctx: AuthSuccessContext) => { + const oauth2Recipe = OAuth2Provider.getInstance(); + if (loginChallenge === null || oauth2Recipe === undefined) { + return Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( + { + ...ctx, + action: "SUCCESS", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), + redirectToPath: getRedirectToPathFromURL(), + }, + ctx.recipeId, + getRedirectToPathFromURL(), + userContext, + props.navigate + ); + } + const { frontendRedirectTo } = await oauth2Recipe.webJSRecipe.getRedirectURLToContinueOAuthFlow({ + loginChallenge, + userContext, + }); return Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - loginChallenge !== null - ? { - ...ctx, - action: "SUCCESS_OAUTH2", - loginChallenge, - } - : { - ...ctx, - action: "SUCCESS", - redirectToPath: getRedirectToPathFromURL(), - }, + { + ...ctx, + action: "SUCCESS_OAUTH2", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), + frontendRedirectTo, + }, ctx.recipeId, getRedirectToPathFromURL(), userContext, @@ -307,7 +349,8 @@ const AuthPageInner: React.FC = (props) => { ); const childProps: AuthPageThemeProps | undefined = - authComponentListInfo !== undefined && (loginChallenge === null || oauth2ClientInfo !== undefined) + authComponentListInfo !== undefined && + (loginChallenge === null || oauth2ClientInfo !== undefined || OAuth2Provider.getInstance() === undefined) ? { ...authComponentListInfo, oauth2ClientInfo, diff --git a/lib/ts/recipe/authRecipe/components/theme/authPage/authPageHeader.tsx b/lib/ts/recipe/authRecipe/components/theme/authPage/authPageHeader.tsx index 993300172..29a681466 100644 --- a/lib/ts/recipe/authRecipe/components/theme/authPage/authPageHeader.tsx +++ b/lib/ts/recipe/authRecipe/components/theme/authPage/authPageHeader.tsx @@ -35,7 +35,7 @@ export const AuthPageHeader = withOverride( resetFactorList: () => void; showBackButton: boolean; oauth2ClientInfo?: { - clientLogoUri?: string; + logoUri?: string; clientUri?: string; clientName: string; }; @@ -44,9 +44,9 @@ export const AuthPageHeader = withOverride( return ( - {oauth2ClientInfo?.clientLogoUri && ( + {oauth2ClientInfo?.logoUri && ( {oauth2ClientInfo.clientName} @@ -68,18 +68,24 @@ export const AuthPageHeader = withOverride( {/* empty span for spacing the back button */} - {oauth2ClientInfo && ( -
- {t("AUTH_PAGE_HEADER_TITLE_SIGN_IN_UP_TO_APP")} - {oauth2ClientInfo.clientUri !== undefined ? ( - - {oauth2ClientInfo.clientName} - - ) : ( - {oauth2ClientInfo.clientName} - )} -
- )} + {oauth2ClientInfo && + oauth2ClientInfo.clientName !== undefined && + oauth2ClientInfo.clientName.length > 0 && ( +
+ {t("AUTH_PAGE_HEADER_TITLE_SIGN_IN_UP_TO_APP")} + {oauth2ClientInfo.clientUri !== undefined ? ( + + {oauth2ClientInfo.clientName} + + ) : ( + + {oauth2ClientInfo.clientName} + + )} +
+ )} {hasSeparateSignUpView && (!isSignUp ? (
diff --git a/lib/ts/recipe/emailpassword/components/features/signin/index.tsx b/lib/ts/recipe/emailpassword/components/features/signin/index.tsx index dd580735f..4a98e12b0 100644 --- a/lib/ts/recipe/emailpassword/components/features/signin/index.tsx +++ b/lib/ts/recipe/emailpassword/components/features/signin/index.tsx @@ -23,7 +23,7 @@ import { useCallback } from "react"; import AuthComponentWrapper from "../../../../../components/authCompWrapper"; import { useTranslation } from "../../../../../translation/translationContext"; -import { useRethrowInRender } from "../../../../../utils"; +import { getTenantIdFromQueryParams, useRethrowInRender } from "../../../../../utils"; import { EmailVerificationClaim } from "../../../../emailverification"; import EmailVerification from "../../../../emailverification/recipe"; import { getInvalidClaimsFromResponse } from "../../../../session"; @@ -78,7 +78,12 @@ export function useChildProps( return useMemo(() => { const onForgotPasswordClick = () => - recipe.redirect({ action: "RESET_PASSWORD" }, navigate, undefined, userContext); + recipe.redirect( + { action: "RESET_PASSWORD", tenantIdFromQueryParams: getTenantIdFromQueryParams() }, + navigate, + undefined, + userContext + ); const signInAndUpFeature = recipe.config.signInAndUpFeature; const signInFeature = signInAndUpFeature.signInForm; @@ -118,6 +123,7 @@ export function useChildProps( await evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, navigate, undefined, @@ -183,11 +189,11 @@ const getModifiedRecipeImplementation = (origImpl: RecipeInterface): RecipeInter return { ...origImpl, signIn: async function (input) { - const response = await origImpl.signIn({ ...input, tryLinkingWithSessionUser: false }); + const response = await origImpl.signIn({ ...input, shouldTryLinkingWithSessionUser: false }); return response; }, signUp: async function (input) { - const response = await origImpl.signUp({ ...input, tryLinkingWithSessionUser: false }); + const response = await origImpl.signUp({ ...input, shouldTryLinkingWithSessionUser: false }); return response; }, }; diff --git a/lib/ts/recipe/emailpassword/components/features/signup/index.tsx b/lib/ts/recipe/emailpassword/components/features/signup/index.tsx index 46728ee42..0b3507973 100644 --- a/lib/ts/recipe/emailpassword/components/features/signup/index.tsx +++ b/lib/ts/recipe/emailpassword/components/features/signup/index.tsx @@ -24,7 +24,7 @@ import STGeneralError from "supertokens-web-js/utils/error"; import AuthComponentWrapper from "../../../../../components/authCompWrapper"; import { useUserContext } from "../../../../../usercontext"; -import { useRethrowInRender } from "../../../../../utils"; +import { getTenantIdFromQueryParams, useRethrowInRender } from "../../../../../utils"; import { EmailVerificationClaim } from "../../../../emailverification"; import EmailVerification from "../../../../emailverification/recipe"; import { getInvalidClaimsFromResponse } from "../../../../session"; @@ -96,6 +96,7 @@ export function useChildProps( await evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, navigate, undefined, @@ -167,11 +168,11 @@ const getModifiedRecipeImplementation = (origImpl: RecipeInterface): RecipeInter return { ...origImpl, signIn: async function (input) { - const response = await origImpl.signIn({ ...input, tryLinkingWithSessionUser: false }); + const response = await origImpl.signIn({ ...input, shouldTryLinkingWithSessionUser: false }); return response; }, signUp: async function (input) { - const response = await origImpl.signUp({ ...input, tryLinkingWithSessionUser: false }); + const response = await origImpl.signUp({ ...input, shouldTryLinkingWithSessionUser: false }); return response; }, }; diff --git a/lib/ts/recipe/emailpassword/components/themes/signIn/index.tsx b/lib/ts/recipe/emailpassword/components/themes/signIn/index.tsx index 7e57d8aef..a55e516cf 100644 --- a/lib/ts/recipe/emailpassword/components/themes/signIn/index.tsx +++ b/lib/ts/recipe/emailpassword/components/themes/signIn/index.tsx @@ -58,7 +58,7 @@ export const SignInForm = withOverride( const response = await props.recipeImplementation.signIn({ formFields, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext, }); if (response.status === "WRONG_CREDENTIALS_ERROR") { diff --git a/lib/ts/recipe/emailpassword/components/themes/signUp/index.tsx b/lib/ts/recipe/emailpassword/components/themes/signUp/index.tsx index b0c76e350..9a98d7752 100644 --- a/lib/ts/recipe/emailpassword/components/themes/signUp/index.tsx +++ b/lib/ts/recipe/emailpassword/components/themes/signUp/index.tsx @@ -58,7 +58,7 @@ export const SignUpForm = withOverride( const res = await props.recipeImplementation.signUp({ formFields, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext, }); diff --git a/lib/ts/recipe/emailpassword/index.ts b/lib/ts/recipe/emailpassword/index.ts index 98d170cf5..52957243b 100644 --- a/lib/ts/recipe/emailpassword/index.ts +++ b/lib/ts/recipe/emailpassword/index.ts @@ -100,7 +100,7 @@ export default class Wrapper { id: string; value: string; }[]; - tryLinkingWithSessionUser?: boolean; + shouldTryLinkingWithSessionUser?: boolean; options?: RecipeFunctionOptions; userContext?: UserContext; }): Promise< @@ -134,7 +134,7 @@ export default class Wrapper { id: string; value: string; }[]; - tryLinkingWithSessionUser?: boolean; + shouldTryLinkingWithSessionUser?: boolean; options?: RecipeFunctionOptions; userContext?: UserContext; }): Promise< diff --git a/lib/ts/recipe/emailpassword/recipe.tsx b/lib/ts/recipe/emailpassword/recipe.tsx index da00eb696..1374a6edb 100644 --- a/lib/ts/recipe/emailpassword/recipe.tsx +++ b/lib/ts/recipe/emailpassword/recipe.tsx @@ -18,10 +18,9 @@ */ import EmailPasswordWebJS from "supertokens-web-js/recipe/emailpassword"; -import NormalisedURLPath from "supertokens-web-js/utils/normalisedURLPath"; import { SSR_ERROR } from "../../constants"; -import { isTest } from "../../utils"; +import { getDefaultRedirectionURLForPath, isTest } from "../../utils"; import AuthRecipe from "../authRecipe"; import { FactorIds } from "../multifactorauth/types"; @@ -36,7 +35,12 @@ import type { NormalisedConfig, UserInput, } from "./types"; -import type { RecipeInitResult, NormalisedConfigWithAppInfoAndRecipeID, WebJSRecipeInterface } from "../../types"; +import type { + RecipeInitResult, + NormalisedConfigWithAppInfoAndRecipeID, + WebJSRecipeInterface, + NormalisedGetRedirectionURLContext, +} from "../../types"; import type { NormalisedAppInfo } from "../../types"; import type RecipeModule from "../recipeModule"; @@ -65,12 +69,11 @@ export default class EmailPassword extends AuthRecipe< super(config); } - getDefaultRedirectionURL = async (context: GetRedirectionURLContext): Promise => { + getDefaultRedirectionURL = async ( + context: NormalisedGetRedirectionURLContext + ): Promise => { if (context.action === "RESET_PASSWORD") { - const resetPasswordPath = new NormalisedURLPath(DEFAULT_RESET_PASSWORD_PATH); - return `${this.config.appInfo.websiteBasePath.appendPath(resetPasswordPath).getAsStringDangerous()}?rid=${ - this.config.recipeId - }`; + return getDefaultRedirectionURLForPath(this.config, DEFAULT_RESET_PASSWORD_PATH, context); } return this.getAuthRecipeDefaultRedirectionURL(context); diff --git a/lib/ts/recipe/emailverification/emailVerificationClaim.ts b/lib/ts/recipe/emailverification/emailVerificationClaim.ts index dd68daa42..550cf0dff 100644 --- a/lib/ts/recipe/emailverification/emailVerificationClaim.ts +++ b/lib/ts/recipe/emailverification/emailVerificationClaim.ts @@ -1,5 +1,7 @@ import { EmailVerificationClaimClass as EmailVerificationClaimClassWebJS } from "supertokens-web-js/recipe/emailverification"; +import { getTenantIdFromQueryParams } from "../../utils"; + import EmailVerification from "./recipe"; import type { UserContext, ValidationFailureCallback } from "../../types"; @@ -24,7 +26,10 @@ export class EmailVerificationClaimClass extends EmailVerificationClaimClassWebJ } const recipe = EmailVerification.getInstanceOrThrow(); if (recipe.config.mode === "REQUIRED") { - return recipe.getRedirectUrl({ action: "VERIFY_EMAIL" }, args.userContext); + return recipe.getRedirectUrl( + { action: "VERIFY_EMAIL", tenantIdFromQueryParams: getTenantIdFromQueryParams() }, + args.userContext + ); } return undefined; }, diff --git a/lib/ts/recipe/emailverification/recipe.tsx b/lib/ts/recipe/emailverification/recipe.tsx index bb2cbcb42..c4a7e0dfa 100644 --- a/lib/ts/recipe/emailverification/recipe.tsx +++ b/lib/ts/recipe/emailverification/recipe.tsx @@ -18,12 +18,11 @@ */ import EmailVerificationWebJS from "supertokens-web-js/recipe/emailverification"; -import NormalisedURLPath from "supertokens-web-js/utils/normalisedURLPath"; import { PostSuperTokensInitCallbacks } from "supertokens-web-js/utils/postSuperTokensInitCallbacks"; import { SessionClaimValidatorStore } from "supertokens-web-js/utils/sessionClaimValidatorStore"; import { SSR_ERROR } from "../../constants"; -import { isTest } from "../../utils"; +import { getDefaultRedirectionURLForPath, isTest } from "../../utils"; import RecipeModule from "../recipeModule"; import { DEFAULT_VERIFY_EMAIL_PATH } from "./constants"; @@ -40,6 +39,7 @@ import type { } from "./types"; import type { NormalisedConfigWithAppInfoAndRecipeID, + NormalisedGetRedirectionURLContext, RecipeInitResult, UserContext, WebJSRecipeInterface, @@ -133,12 +133,11 @@ export default class EmailVerification extends RecipeModule< }); } - getDefaultRedirectionURL = async (context: GetRedirectionURLContext): Promise => { + getDefaultRedirectionURL = async ( + context: NormalisedGetRedirectionURLContext + ): Promise => { if (context.action === "VERIFY_EMAIL") { - const verifyEmailPath = new NormalisedURLPath(DEFAULT_VERIFY_EMAIL_PATH); - return `${this.config.appInfo.websiteBasePath.appendPath(verifyEmailPath).getAsStringDangerous()}?rid=${ - this.config.recipeId - }`; + return getDefaultRedirectionURLForPath(this.config, DEFAULT_VERIFY_EMAIL_PATH, context); } else { return "/"; } diff --git a/lib/ts/recipe/multifactorauth/recipe.tsx b/lib/ts/recipe/multifactorauth/recipe.tsx index d05e7fa19..ff9f30f24 100644 --- a/lib/ts/recipe/multifactorauth/recipe.tsx +++ b/lib/ts/recipe/multifactorauth/recipe.tsx @@ -19,7 +19,6 @@ import MultiFactorAuthWebJS from "supertokens-web-js/recipe/multifactorauth"; import { getNormalisedUserContext } from "supertokens-web-js/utils"; -import NormalisedURLPath from "supertokens-web-js/utils/normalisedURLPath"; import { PostSuperTokensInitCallbacks } from "supertokens-web-js/utils/postSuperTokensInitCallbacks"; import { SessionClaimValidatorStore } from "supertokens-web-js/utils/sessionClaimValidatorStore"; import { WindowHandlerReference } from "supertokens-web-js/utils/windowHandler"; @@ -29,7 +28,9 @@ import SuperTokens from "../../superTokens"; import { appendQueryParamsToURL, getCurrentNormalisedUrlPathWithQueryParamsAndFragments, + getDefaultRedirectionURLForPath, getRedirectToPathFromURL, + getTenantIdFromQueryParams, isTest, } from "../../utils"; import RecipeModule from "../recipeModule"; @@ -51,6 +52,7 @@ import type { import type { Navigate, NormalisedConfigWithAppInfoAndRecipeID, + NormalisedGetRedirectionURLContext, RecipeInitResult, UserContext, WebJSRecipeInterface, @@ -69,7 +71,10 @@ export default class MultiFactorAuth extends RecipeModule< static MultiFactorAuthClaim = new MultiFactorAuthClaimClass( () => MultiFactorAuth.getInstanceOrThrow(), async (context, userContext) => - (await this.getInstanceOrThrow().getRedirectUrl(context, userContext)) || undefined + (await this.getInstanceOrThrow().getRedirectUrl( + { ...context, tenantIdFromQueryParams: getTenantIdFromQueryParams() }, + userContext + )) || undefined ); public recipeID = MultiFactorAuth.RECIPE_ID; @@ -147,24 +152,25 @@ export default class MultiFactorAuth extends RecipeModule< return MultiFactorAuth.instance; } - getDefaultRedirectionURL = async (context: GetRedirectionURLContext, userContext: UserContext): Promise => { + getDefaultRedirectionURL = async ( + context: NormalisedGetRedirectionURLContext, + userContext: UserContext + ): Promise => { if (context.action === "FACTOR_CHOOSER") { - const chooserPath = new NormalisedURLPath(DEFAULT_FACTOR_CHOOSER_PATH); - let url = this.config.appInfo.websiteBasePath.appendPath(chooserPath).getAsStringDangerous(); - if (context.nextFactorOptions && context.nextFactorOptions.length > 0) { - url += `?n=${context.nextFactorOptions.join(",")}`; - } - return url; + const nParam = + context.nextFactorOptions && context.nextFactorOptions.length > 0 + ? context.nextFactorOptions.join(",") + : undefined; + return getDefaultRedirectionURLForPath(this.config, DEFAULT_FACTOR_CHOOSER_PATH, context, { n: nParam }); } else if (context.action === "GO_TO_FACTOR") { const redirectInfo = this.getSecondaryFactors(userContext).find((f) => f.id === context.factorId); if (redirectInfo !== undefined) { - let url = this.config.appInfo.websiteBasePath - .appendPath(new NormalisedURLPath(redirectInfo.path)) - .getAsStringDangerous(); - if (context.forceSetup) { - url += "?setup=true"; - } - return url; + return getDefaultRedirectionURLForPath( + this.config, + redirectInfo.path, + context, + context.forceSetup ? { setup: "true" } : {} + ); } throw new Error("Requested redirect to unknown factor id: " + context.factorId); } else { @@ -197,7 +203,7 @@ export default class MultiFactorAuth extends RecipeModule< userContext?: UserContext ) { let url = await this.getRedirectUrl( - { action: "GO_TO_FACTOR", forceSetup, factorId }, + { action: "GO_TO_FACTOR", forceSetup, factorId, tenantIdFromQueryParams: getTenantIdFromQueryParams() }, getNormalisedUserContext(userContext) ); if (url === null) { @@ -233,7 +239,7 @@ export default class MultiFactorAuth extends RecipeModule< userContext?: UserContext ) { let url = await this.getRedirectUrl( - { action: "FACTOR_CHOOSER", nextFactorOptions }, + { action: "FACTOR_CHOOSER", nextFactorOptions, tenantIdFromQueryParams: getTenantIdFromQueryParams() }, getNormalisedUserContext(userContext) ); diff --git a/lib/ts/recipe/multifactorauth/utils.ts b/lib/ts/recipe/multifactorauth/utils.ts index 2e276e42e..fb1934d29 100644 --- a/lib/ts/recipe/multifactorauth/utils.ts +++ b/lib/ts/recipe/multifactorauth/utils.ts @@ -13,6 +13,7 @@ * under the License. */ +import { logDebugMessage } from "../../logger"; import { normaliseRecipeModuleConfig } from "../recipeModule/utils"; import type MultiFactorAuth from "./recipe"; @@ -48,6 +49,14 @@ export function getAvailableFactors( recipe: MultiFactorAuth, userContext: UserContext ) { + logDebugMessage(`getAvailableFactors: allowed to setup: ${factors.allowedToSetup}`); + logDebugMessage(`getAvailableFactors: already setup: ${factors.alreadySetup}`); + logDebugMessage(`getAvailableFactors: next from factorInfo: ${factors.next}`); + logDebugMessage(`getAvailableFactors: nextArrayQueryParam: ${nextArrayQueryParam}`); + logDebugMessage( + `getAvailableFactors: secondary factors: ${recipe.getSecondaryFactors(userContext).map((f) => f.id)}` + ); + // There are 3 cases here: // 1. The app provided an array of factors to show (nextArrayQueryParam) -> we show whatever is in the array // 2. no app provided list and validator passed -> we show all factors available to set up or complete diff --git a/lib/ts/recipe/oauth2provider/components/features/oauth2LogoutScreen/index.tsx b/lib/ts/recipe/oauth2provider/components/features/oauth2LogoutScreen/index.tsx index 4a18245e2..d9dd8672a 100644 --- a/lib/ts/recipe/oauth2provider/components/features/oauth2LogoutScreen/index.tsx +++ b/lib/ts/recipe/oauth2provider/components/features/oauth2LogoutScreen/index.tsx @@ -24,7 +24,7 @@ import FeatureWrapper from "../../../../../components/featureWrapper"; import SuperTokens from "../../../../../superTokens"; import UI from "../../../../../ui"; import { useUserContext } from "../../../../../usercontext"; -import { getQueryParams, useRethrowInRender } from "../../../../../utils"; +import { getQueryParams, getTenantIdFromQueryParams, useRethrowInRender } from "../../../../../utils"; import { SessionContext } from "../../../../session"; import OAuth2Provider from "../../../recipe"; import { OAuth2LogoutScreenTheme } from "../../themes/oauth2LogoutScreen"; @@ -67,6 +67,7 @@ export const OAuth2LogoutScreen: React.FC = (props) => { { recipeId: "oauth2provider", action: "POST_OAUTH2_LOGOUT_REDIRECT", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), frontendRedirectTo, }, navigate, diff --git a/lib/ts/recipe/oauth2provider/components/features/tryRefreshPage/index.tsx b/lib/ts/recipe/oauth2provider/components/features/tryRefreshPage/index.tsx index 9392157f1..63eb811db 100644 --- a/lib/ts/recipe/oauth2provider/components/features/tryRefreshPage/index.tsx +++ b/lib/ts/recipe/oauth2provider/components/features/tryRefreshPage/index.tsx @@ -22,9 +22,9 @@ import { useContext, Fragment } from "react"; import FeatureWrapper from "../../../../../components/featureWrapper"; import SuperTokens from "../../../../../superTokens"; import { useUserContext } from "../../../../../usercontext"; -import { getQueryParams, useRethrowInRender } from "../../../../../utils"; +import { getQueryParams, getTenantIdFromQueryParams, useRethrowInRender } from "../../../../../utils"; import DynamicLoginMethodsSpinner from "../../../../multitenancy/components/features/dynamicLoginMethodsSpinner"; -import { SessionContext } from "../../../../session"; +import SessionAPIWrapper, { SessionContext } from "../../../../session"; import { defaultTranslationsOAuth2Provider } from "../../themes/translations"; import type { FeatureBaseProps, UserContext } from "../../../../../types"; @@ -46,20 +46,28 @@ export const TryRefreshPage: React.FC = (props) => { userContext = props.userContext; } + SessionAPIWrapper.attemptRefreshingSession; React.useEffect(() => { if (sessionContext.loading === false) { - void props.recipe - .redirect( - { - action: "CONTINUE_OAUTH2_AFTER_REFRESH", - loginChallenge: loginChallenge ?? "", - recipeId: "oauth2provider", - }, - props.navigate, - {}, - userContext - ) - .catch(rethrowInRender); + if (loginChallenge) { + (async function () { + const { frontendRedirectTo } = await props.recipe.webJSRecipe.getRedirectURLToContinueOAuthFlow({ + loginChallenge, + userContext, + }); + return props.recipe.redirect( + { + action: "CONTINUE_OAUTH2_AFTER_REFRESH", + frontendRedirectTo, + tenantIdFromQueryParams: getTenantIdFromQueryParams(), + recipeId: "oauth2provider", + }, + props.navigate, + {}, + userContext + ); + })().catch(rethrowInRender); + } } }, [loginChallenge, props.recipe, props.navigate, userContext, sessionContext]); diff --git a/lib/ts/recipe/oauth2provider/index.ts b/lib/ts/recipe/oauth2provider/index.ts index c9ceb085f..b935f352f 100644 --- a/lib/ts/recipe/oauth2provider/index.ts +++ b/lib/ts/recipe/oauth2provider/index.ts @@ -49,6 +49,31 @@ export default class Wrapper { return OAuth2Provider.getInstanceOrThrow().webJSRecipe.getLoginChallengeInfo(input); } + /** + * Accepts the OAuth2 Login request and returns the redirect URL to continue the OAuth flow. + * + * @param loginChallenge The login challenge from the url + * + * @param userContext (OPTIONAL) Refer to {@link https://supertokens.com/docs/emailpassword/advanced-customizations/user-context the documentation} + * + * @param options (OPTIONAL) Use this to configure additional properties (for example pre api hooks) + * + * @returns `{status: "OK", frontendRedirectTo: string}` + * + * @throws STGeneralError if the API exposed by the backend SDKs returns `status: "GENERAL_ERROR"` + */ + static getRedirectURLToContinueOAuthFlow(input: { + loginChallenge: string; + options?: RecipeFunctionOptions; + userContext?: any; + }): Promise<{ + status: "OK"; + frontendRedirectTo: string; + fetchResponse: Response; + }> { + return OAuth2Provider.getInstanceOrThrow().webJSRecipe.getRedirectURLToContinueOAuthFlow(input); + } + /** * Accepts the OAuth2 Logout request, clears the SuperTokens session and returns post logout redirect URL. * diff --git a/lib/ts/recipe/oauth2provider/recipe.ts b/lib/ts/recipe/oauth2provider/recipe.ts index b1831450c..1509a632d 100644 --- a/lib/ts/recipe/oauth2provider/recipe.ts +++ b/lib/ts/recipe/oauth2provider/recipe.ts @@ -111,12 +111,12 @@ export default class OAuth2Provider extends RecipeModule< } async getDefaultRedirectionURL(ctx: GetRedirectionURLContext): Promise { - if (ctx.action === "SUCCESS_OAUTH2" || ctx.action === "CONTINUE_OAUTH2_AFTER_REFRESH") { - const domain = this.config.appInfo.apiDomain.getAsStringDangerous(); - const basePath = this.config.appInfo.apiBasePath.getAsStringDangerous(); - - return `${domain}${basePath}/oauth/login?loginChallenge=${ctx.loginChallenge}`; - } else if (ctx.action === "POST_OAUTH2_LOGOUT_REDIRECT") { + // We do not use the util here, because we are likely redirecting across domains here. + if ( + ctx.action === "SUCCESS_OAUTH2" || + ctx.action === "CONTINUE_OAUTH2_AFTER_REFRESH" || + ctx.action === "POST_OAUTH2_LOGOUT_REDIRECT" + ) { return ctx.frontendRedirectTo; } else { throw new Error("Should never come here: unknown action in OAuth2Provider.getDefaultRedirectionURL"); diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts index 6eca2e65b..cda9fc60b 100644 --- a/lib/ts/recipe/oauth2provider/types.ts +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -8,7 +8,10 @@ import type { import type OverrideableBuilder from "supertokens-js-override"; import type { LoginInfo, RecipeInterface } from "supertokens-web-js/recipe/oauth2provider/types"; -export type PreAndPostAPIHookAction = "GET_LOGIN_CHALLENGE_INFO" | "LOG_OUT"; +export type PreAndPostAPIHookAction = + | "GET_LOGIN_CHALLENGE_INFO" + | "GET_REDIRECT_URL_TO_CONTINUE_OAUTH_FLOW" + | "LOG_OUT"; export type PreAPIHookContext = { action: PreAndPostAPIHookAction; @@ -50,7 +53,7 @@ export type OAuth2LogoutScreenConfig = NormalisedBaseConfig & { export type ContinueOAuth2AfterRefreshRedirectContext = { recipeId: "oauth2provider"; action: "CONTINUE_OAUTH2_AFTER_REFRESH"; - loginChallenge: string; + frontendRedirectTo: string; }; export type PostOAuth2LogoutRedirectContext = { diff --git a/lib/ts/recipe/passwordless/components/features/linkClickedScreen/index.tsx b/lib/ts/recipe/passwordless/components/features/linkClickedScreen/index.tsx index 50ece20a0..89deed42d 100644 --- a/lib/ts/recipe/passwordless/components/features/linkClickedScreen/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/linkClickedScreen/index.tsx @@ -23,7 +23,13 @@ import { ComponentOverrideContext } from "../../../../../components/componentOve import FeatureWrapper from "../../../../../components/featureWrapper"; import SuperTokens from "../../../../../superTokens"; import { useUserContext } from "../../../../../usercontext"; -import { getQueryParams, getURLHash, useOnMountAPICall, useRethrowInRender } from "../../../../../utils"; +import { + getQueryParams, + getTenantIdFromQueryParams, + getURLHash, + useOnMountAPICall, + useRethrowInRender, +} from "../../../../../utils"; import Session from "../../../../session/recipe"; import { LinkClickedScreen as LinkClickedScreenTheme } from "../../themes/linkClickedScreen"; import { defaultTranslationsPasswordless } from "../../themes/translations"; @@ -148,6 +154,7 @@ const LinkClickedScreen: React.FC = (props) => { (payloadBeforeCall === undefined || payloadBeforeCall.sessionHandle !== payloadAfterCall.sessionHandle), recipeId: props.recipe.recipeID, + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, props.recipe.recipeID, loginAttemptInfo?.redirectToPath, diff --git a/lib/ts/recipe/passwordless/components/features/linkSent/index.tsx b/lib/ts/recipe/passwordless/components/features/linkSent/index.tsx index d31beed4a..d8b457233 100644 --- a/lib/ts/recipe/passwordless/components/features/linkSent/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/linkSent/index.tsx @@ -20,7 +20,12 @@ import { Fragment } from "react"; import { useMemo } from "react"; import AuthComponentWrapper from "../../../../../components/authCompWrapper"; -import { clearErrorQueryParam, getRedirectToPathFromURL, useRethrowInRender } from "../../../../../utils"; +import { + clearErrorQueryParam, + getRedirectToPathFromURL, + getTenantIdFromQueryParams, + useRethrowInRender, +} from "../../../../../utils"; import { EmailVerificationClaim } from "../../../../emailverification"; import EmailVerification from "../../../../emailverification/recipe"; import { getInvalidClaimsFromResponse } from "../../../../session"; @@ -81,6 +86,7 @@ export function useChildProps( (payloadAfterCall !== undefined && session.accessTokenPayload.sessionHandle !== payloadAfterCall.sessionHandle), recipeId: recipe.recipeID, + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, recipe.recipeID, getRedirectToPathFromURL(), @@ -98,6 +104,7 @@ export function useChildProps( const evInstance = EmailVerification.getInstanceOrThrow(); await evInstance.redirect( { + tenantIdFromQueryParams: getTenantIdFromQueryParams(), action: "VERIFY_EMAIL", }, navigate, @@ -204,7 +211,7 @@ function getModifiedRecipeImplementation( userContext: input.userContext, attemptInfo: { ...loginAttemptInfo, - tryLinkingWithSessionUser: loginAttemptInfo.tryLinkingWithSessionUser ?? false, + shouldTryLinkingWithSessionUser: loginAttemptInfo.shouldTryLinkingWithSessionUser ?? false, lastResend: timestamp, }, }); diff --git a/lib/ts/recipe/passwordless/components/features/mfa/index.tsx b/lib/ts/recipe/passwordless/components/features/mfa/index.tsx index c80e9d4c5..86f64f1d4 100644 --- a/lib/ts/recipe/passwordless/components/features/mfa/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/mfa/index.tsx @@ -29,6 +29,7 @@ import { clearErrorQueryParam, getQueryParams, getRedirectToPathFromURL, + getTenantIdFromQueryParams, useOnMountAPICall, useRethrowInRender, } from "../../../../../utils"; @@ -181,6 +182,7 @@ export function useChildProps( await evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, navigate, undefined, @@ -357,7 +359,7 @@ function useOnLoad( // createCode also dispatches the event that marks this page fully loaded createResp = await recipeImplementation!.createCode({ ...createCodeInfo, - tryLinkingWithSessionUser: true, + shouldTryLinkingWithSessionUser: true, userContext, }); } catch (err: any) { @@ -372,6 +374,7 @@ function useOnLoad( const evInstance = EmailVerificationRecipe.getInstanceOrThrow(); await evInstance.redirect( { + tenantIdFromQueryParams: getTenantIdFromQueryParams(), action: "VERIFY_EMAIL", }, props.navigate, @@ -464,7 +467,7 @@ function getModifiedRecipeImplementation( const res = await originalImpl.createCode({ ...input, - tryLinkingWithSessionUser: true, + shouldTryLinkingWithSessionUser: true, userContext: { ...input.userContext, additionalAttemptInfo }, }); @@ -495,7 +498,7 @@ function getModifiedRecipeImplementation( userContext: input.userContext, attemptInfo: { ...loginAttemptInfo, - tryLinkingWithSessionUser: loginAttemptInfo.tryLinkingWithSessionUser ?? true, + shouldTryLinkingWithSessionUser: loginAttemptInfo.shouldTryLinkingWithSessionUser ?? true, lastResend: timestamp, }, }); @@ -512,7 +515,10 @@ function getModifiedRecipeImplementation( }, consumeCode: async (input) => { - const res = await originalImpl.consumeCode(input); + const res = await originalImpl.consumeCode({ + ...input, + shouldTryLinkingWithSessionUser: true, + }); if (res.status === "RESTART_FLOW_ERROR") { await originalImpl.clearLoginAttemptInfo({ diff --git a/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx b/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx index 34c440ff6..4e29f5fc8 100644 --- a/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx @@ -21,7 +21,7 @@ import { useMemo } from "react"; import AuthComponentWrapper from "../../../../../components/authCompWrapper"; import { useUserContext } from "../../../../../usercontext"; -import { getRedirectToPathFromURL, useRethrowInRender } from "../../../../../utils"; +import { getRedirectToPathFromURL, getTenantIdFromQueryParams, useRethrowInRender } from "../../../../../utils"; import { EmailVerificationClaim } from "../../../../emailverification"; import EmailVerification from "../../../../emailverification/recipe"; import { getInvalidClaimsFromResponse } from "../../../../session"; @@ -93,6 +93,7 @@ export function useChildProps( await evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, navigate, undefined, @@ -208,7 +209,7 @@ function getModifiedRecipeImplementation( const res = await originalImpl.createCode({ ...input, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: { ...input.userContext, additionalAttemptInfo }, }); if (res.status === "OK") { diff --git a/lib/ts/recipe/passwordless/components/features/signInAndUpEPCombo/index.tsx b/lib/ts/recipe/passwordless/components/features/signInAndUpEPCombo/index.tsx index adc9e243c..179701925 100644 --- a/lib/ts/recipe/passwordless/components/features/signInAndUpEPCombo/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/signInAndUpEPCombo/index.tsx @@ -22,7 +22,12 @@ import STGeneralError from "supertokens-web-js/lib/build/error"; import AuthComponentWrapper from "../../../../../components/authCompWrapper"; import { useUserContext } from "../../../../../usercontext"; -import { getRedirectToPathFromURL, useRethrowInRender, validateForm } from "../../../../../utils"; +import { + getRedirectToPathFromURL, + getTenantIdFromQueryParams, + useRethrowInRender, + validateForm, +} from "../../../../../utils"; import EmailPassword from "../../../../emailpassword/recipe"; import { EmailVerificationClaim } from "../../../../emailverification"; import EmailVerification from "../../../../emailverification/recipe"; @@ -78,7 +83,7 @@ export function useChildProps( if (isPhoneNumber) { const createRes = await recipeImplementation.createCode({ phoneNumber: contactInfo, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext, }); @@ -111,7 +116,7 @@ export function useChildProps( // only pwless exists const createRes = await recipeImplementation.createCode({ email, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext, }); @@ -142,7 +147,7 @@ export function useChildProps( const response = await EmailPassword.getInstanceOrThrow().webJSRecipe.signIn({ formFields, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext, }); if (response.status === "WRONG_CREDENTIALS_ERROR") { @@ -158,7 +163,7 @@ export function useChildProps( const createInfo = isPhoneNumber ? { phoneNumber: contactInfo } : { email: contactInfo }; const createRes = await recipeImplementation.createCode({ ...createInfo, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext, }); if (createRes.status !== "OK") { @@ -203,6 +208,7 @@ export function useChildProps( await evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, navigate, undefined, @@ -326,7 +332,7 @@ function getModifiedRecipeImplementation( const res = await originalImpl.createCode({ ...input, - tryLinkingWithSessionUser: false, + shouldTryLinkingWithSessionUser: false, userContext: { ...input.userContext, additionalAttemptInfo }, }); if (res.status === "OK") { diff --git a/lib/ts/recipe/passwordless/components/features/userInputCode/index.tsx b/lib/ts/recipe/passwordless/components/features/userInputCode/index.tsx index bcdeff3b2..b49d83827 100644 --- a/lib/ts/recipe/passwordless/components/features/userInputCode/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/userInputCode/index.tsx @@ -20,7 +20,7 @@ import { Fragment } from "react"; import { useMemo } from "react"; import AuthComponentWrapper from "../../../../../components/authCompWrapper"; -import { clearErrorQueryParam, useRethrowInRender } from "../../../../../utils"; +import { clearErrorQueryParam, getTenantIdFromQueryParams, useRethrowInRender } from "../../../../../utils"; import { EmailVerificationClaim } from "../../../../emailverification"; import EmailVerification from "../../../../emailverification/recipe"; import { getInvalidClaimsFromResponse } from "../../../../session"; @@ -91,6 +91,7 @@ export function useChildProps( await evInstance.redirect( { action: "VERIFY_EMAIL", + tenantIdFromQueryParams: getTenantIdFromQueryParams(), }, navigate, undefined, @@ -199,7 +200,7 @@ function getModifiedRecipeImplementation( userContext: input.userContext, attemptInfo: { ...loginAttemptInfo, - tryLinkingWithSessionUser: loginAttemptInfo.tryLinkingWithSessionUser ?? false, + shouldTryLinkingWithSessionUser: loginAttemptInfo.shouldTryLinkingWithSessionUser ?? false, lastResend: timestamp, }, }); diff --git a/lib/ts/recipe/passwordless/components/themes/signInUp/emailForm.tsx b/lib/ts/recipe/passwordless/components/themes/signInUp/emailForm.tsx index 8eb3b1911..aadd55613 100644 --- a/lib/ts/recipe/passwordless/components/themes/signInUp/emailForm.tsx +++ b/lib/ts/recipe/passwordless/components/themes/signInUp/emailForm.tsx @@ -62,7 +62,7 @@ export const EmailForm = withOverride( const response = await props.recipeImplementation.createCode({ email, - // tryLinkingWithSessionUser is set by the fn override + // shouldTryLinkingWithSessionUser is set by the fn override userContext, }); diff --git a/lib/ts/recipe/passwordless/components/themes/signInUp/emailOrPhoneForm.tsx b/lib/ts/recipe/passwordless/components/themes/signInUp/emailOrPhoneForm.tsx index 2b4b609fb..afbe7000e 100644 --- a/lib/ts/recipe/passwordless/components/themes/signInUp/emailOrPhoneForm.tsx +++ b/lib/ts/recipe/passwordless/components/themes/signInUp/emailOrPhoneForm.tsx @@ -137,7 +137,7 @@ export const EmailOrPhoneForm = withOverride( const response = await props.recipeImplementation.createCode({ ...contactInfo, - // tryLinkingWithSessionUser is set by the fn override + // shouldTryLinkingWithSessionUser is set by the fn override userContext, }); diff --git a/lib/ts/recipe/passwordless/components/themes/signInUp/phoneForm.tsx b/lib/ts/recipe/passwordless/components/themes/signInUp/phoneForm.tsx index 58d84f969..cf733537c 100644 --- a/lib/ts/recipe/passwordless/components/themes/signInUp/phoneForm.tsx +++ b/lib/ts/recipe/passwordless/components/themes/signInUp/phoneForm.tsx @@ -79,7 +79,7 @@ export const PhoneForm = withOverride( const response = await props.recipeImplementation.createCode({ phoneNumber, - // tryLinkingWithSessionUser is set by the fn override + // shouldTryLinkingWithSessionUser is set by the fn override userContext, }); diff --git a/lib/ts/recipe/passwordless/components/themes/signInUpEPCombo/emailForm.tsx b/lib/ts/recipe/passwordless/components/themes/signInUpEPCombo/emailForm.tsx index 0fa0c7dc4..06560aa56 100644 --- a/lib/ts/recipe/passwordless/components/themes/signInUpEPCombo/emailForm.tsx +++ b/lib/ts/recipe/passwordless/components/themes/signInUpEPCombo/emailForm.tsx @@ -17,6 +17,7 @@ import STGeneralError from "supertokens-web-js/utils/error"; import { withOverride } from "../../../../../components/componentOverride/withOverride"; import { useTranslation } from "../../../../../translation/translationContext"; +import { getTenantIdFromQueryParams } from "../../../../../utils"; import { Label } from "../../../../emailpassword/components/library"; import FormBase from "../../../../emailpassword/components/library/formBase"; import EmailPassword from "../../../../emailpassword/recipe"; @@ -57,7 +58,12 @@ export const EPComboEmailForm = withOverride(