{
- Passwordless.createCode({ phoneNumber: number })
+ Passwordless.createCode({ phoneNumber: number, shouldTryLinkingWithSessionUser: true })
.then(async (info) => {
if (info.status !== "OK") {
setError(info.reason);
diff --git a/examples/with-multiple-email-sign-in/api-server/epOverride.ts b/examples/with-multiple-email-sign-in/api-server/epOverride.ts
index b37bd0db3..13c151fa1 100644
--- a/examples/with-multiple-email-sign-in/api-server/epOverride.ts
+++ b/examples/with-multiple-email-sign-in/api-server/epOverride.ts
@@ -7,7 +7,7 @@ export function epOverride(oI: APIInterface): APIInterface {
signInPOST: async function (input) {
const emailField = input.formFields.find((f) => f.id === "email")!;
- let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value);
+ let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value as string);
if (primaryEmail !== undefined) {
emailField.value = primaryEmail;
}
@@ -16,7 +16,7 @@ export function epOverride(oI: APIInterface): APIInterface {
signUpPOST: async function (input) {
const emailField = input.formFields.find((f) => f.id === "email")!;
- let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value);
+ let primaryEmail = getPrimaryEmailFromInputEmail(emailField.value as string);
if (primaryEmail !== undefined) {
emailField.value = primaryEmail;
}
diff --git a/examples/with-phone-password-mfa/api-server/index.ts b/examples/with-phone-password-mfa/api-server/index.ts
index 213df7df6..61d3aa417 100644
--- a/examples/with-phone-password-mfa/api-server/index.ts
+++ b/examples/with-phone-password-mfa/api-server/index.ts
@@ -77,7 +77,7 @@ supertokens.init({
// We format the phone number here to get it to a standard format
const emailField = input.formFields.find((field) => field.id === "email");
if (emailField) {
- const phoneNumber = parsePhoneNumber(emailField.value);
+ const phoneNumber = parsePhoneNumber(emailField.value as string);
if (phoneNumber !== undefined && phoneNumber.isValid()) {
emailField.value = phoneNumber.number;
}
@@ -93,7 +93,7 @@ supertokens.init({
// We format the phone number here to get it to a standard format
const emailField = input.formFields.find((field) => field.id === "email");
if (emailField) {
- const phoneNumber = parsePhoneNumber(emailField.value);
+ const phoneNumber = parsePhoneNumber(emailField.value as string);
if (phoneNumber !== undefined && phoneNumber.isValid()) {
emailField.value = phoneNumber.number;
}
diff --git a/examples/with-phone-password/api-server/index.ts b/examples/with-phone-password/api-server/index.ts
index 6a697598a..e7b6c7e0b 100644
--- a/examples/with-phone-password/api-server/index.ts
+++ b/examples/with-phone-password/api-server/index.ts
@@ -75,7 +75,7 @@ supertokens.init({
// We format the phone number here to get it to a standard format
const emailField = input.formFields.find((field) => field.id === "email");
if (emailField) {
- const phoneNumber = parsePhoneNumber(emailField.value);
+ const phoneNumber = parsePhoneNumber(emailField.value as string);
if (phoneNumber !== undefined && phoneNumber.isValid()) {
emailField.value = phoneNumber.number;
}
@@ -91,7 +91,7 @@ supertokens.init({
// We format the phone number here to get it to a standard format
const emailField = input.formFields.find((field) => field.id === "email");
if (emailField) {
- const phoneNumber = parsePhoneNumber(emailField.value);
+ const phoneNumber = parsePhoneNumber(emailField.value as string);
if (phoneNumber !== undefined && phoneNumber.isValid()) {
emailField.value = phoneNumber.number;
}
diff --git a/examples/with-phone-password/src/PhoneVerification/index.tsx b/examples/with-phone-password/src/PhoneVerification/index.tsx
index 00d045561..e8e90b82c 100644
--- a/examples/with-phone-password/src/PhoneVerification/index.tsx
+++ b/examples/with-phone-password/src/PhoneVerification/index.tsx
@@ -37,6 +37,7 @@ const CustomSignInUpTheme = (props: AuthPageThemeProps) => {
const res = await Passwordless.createCode({
phoneNumber,
+ shouldTryLinkingWithSessionUser: true,
userContext: { additionalAttemptInfo },
});
diff --git a/examples/with-unified-login/.gitignore b/examples/with-unified-login/.gitignore
new file mode 100644
index 000000000..c6bba5913
--- /dev/null
+++ b/examples/with-unified-login/.gitignore
@@ -0,0 +1,130 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
diff --git a/examples/with-unified-login/auth-provider/backend/config.ts b/examples/with-unified-login/auth-provider/backend/config.ts
new file mode 100644
index 000000000..b910d0bcb
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/backend/config.ts
@@ -0,0 +1,184 @@
+import Multitenancy from "supertokens-node/recipe/multitenancy";
+import EmailPassword from "supertokens-node/recipe/emailpassword";
+import ThirdParty from "supertokens-node/recipe/thirdparty";
+import Passwordless from "supertokens-node/recipe/passwordless";
+import Session from "supertokens-node/recipe/session";
+import OAuth2Provider from "supertokens-node/recipe/oauth2provider";
+import { TypeInput } from "supertokens-node/types";
+import Dashboard from "supertokens-node/recipe/dashboard";
+import UserRoles from "supertokens-node/recipe/userroles";
+import { readFile, writeFile } from "fs/promises";
+
+export const websitePort = process.env.REACT_APP_WEBSITE_PORT || 3000;
+export const apiPort = process.env.REACT_APP_API_PORT || 3001;
+
+export function getApiDomain() {
+ const apiUrl = process.env.REACT_APP_API_URL || `http://localhost:${apiPort}`;
+ return apiUrl;
+}
+
+export function getWebsiteDomain() {
+ 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: "http://localhost:3567",
+ },
+ 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: [
+ Multitenancy.init(),
+ EmailPassword.init(),
+ ThirdParty.init({
+ signInAndUpFeature: {
+ providers: [
+ // We have provided you with development keys which you can use for testing.
+ // IMPORTANT: Please replace them with your own OAuth keys for production use.
+ {
+ config: {
+ thirdPartyId: "google",
+ clients: [
+ {
+ clientId:
+ "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com",
+ clientSecret: "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",
+ },
+ ],
+ },
+ },
+ {
+ config: {
+ thirdPartyId: "github",
+ clients: [
+ {
+ clientId: "467101b197249757c71f",
+ clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd",
+ },
+ ],
+ },
+ },
+ {
+ config: {
+ thirdPartyId: "apple",
+ clients: [
+ {
+ clientId: "4398792-io.supertokens.example.service",
+ additionalConfig: {
+ keyId: "7M48Y4RYDL",
+ privateKey:
+ "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----",
+ teamId: "YWQCXGJRJL",
+ },
+ },
+ ],
+ },
+ },
+ {
+ config: {
+ thirdPartyId: "twitter",
+ clients: [
+ {
+ clientId: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ",
+ clientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC",
+ },
+ ],
+ },
+ },
+ ],
+ },
+ }),
+ Passwordless.init({
+ contactMethod: "EMAIL_OR_PHONE",
+ flowType: "USER_INPUT_CODE_AND_MAGIC_LINK",
+ getCustomUserInputCode: async (userContext) => {
+ return "123456";
+ },
+ emailDelivery: {
+ override: (oI) => ({
+ ...oI,
+ sendEmail: async (input) => {
+ console.log(input);
+ },
+ }),
+ },
+ smsDelivery: {
+ override: (oI) => ({
+ ...oI,
+ sendSms: async (input) => {
+ console.log(input);
+ },
+ }),
+ },
+ }),
+ Session.init(),
+ Dashboard.init(),
+ UserRoles.init(),
+ OAuth2Provider.init(),
+ ],
+};
+
+let clients: Record = {};
+
+export async function setupTenants() {
+ try {
+ const data = await readFile("../clients.json", "utf8");
+ clients = JSON.parse(data);
+ } catch (e) {
+ console.error(e);
+ }
+
+ if (Object.keys(clients).length === 0) {
+ const client1 = await OAuth2Provider.createOAuth2Client({
+ redirectUris: ["http://localhost:3011/auth/callback"],
+ tokenEndpointAuthMethod: "client_secret_post",
+ });
+ if (client1.status !== "OK") {
+ throw new Error("Failed to create client1");
+ }
+ clients["tenant1"] = {
+ clientId: client1.client.clientId,
+ clientSecret: client1.client.clientSecret,
+ };
+
+ const client2 = await OAuth2Provider.createOAuth2Client({
+ redirectUris: ["http://localhost:3012/auth/callback"],
+ postLogoutRedirectUris: ["http://localhost:3012/loggedout"],
+ policyUri: "http://localhost:3012/policy",
+ clientName: "Tenant 2",
+ clientUri: "http://localhost:3012",
+ logoUri: "https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg",
+ tokenEndpointAuthMethod: "none",
+ });
+ if (client2.status !== "OK") {
+ throw new Error("Failed to create client2");
+ }
+ clients["tenant2"] = {
+ clientId: client2.client.clientId,
+ clientSecret: client2.client.clientSecret,
+ };
+
+ const client3 = await OAuth2Provider.createOAuth2Client({
+ redirectUris: ["http://localhost:3013/oauth2/redirect"],
+ logoUri: "https://www.passportjs.org/images/logo.svg",
+ scope: "openid profile email phoneNumber test",
+ tokenEndpointAuthMethod: "client_secret_post",
+ });
+ if (client3.status !== "OK") {
+ throw new Error("Failed to create client3");
+ }
+ clients["tenant3"] = {
+ clientId: client3.client.clientId,
+ clientSecret: client3.client.clientSecret,
+ };
+
+ await writeFile("../clients.json", JSON.stringify(clients, null, 2));
+ }
+}
diff --git a/examples/with-unified-login/auth-provider/backend/index.ts b/examples/with-unified-login/auth-provider/backend/index.ts
new file mode 100644
index 000000000..5ba7459f2
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/backend/index.ts
@@ -0,0 +1,63 @@
+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 { apiPort, getWebsiteDomain, setupTenants, SuperTokensConfig } from "./config";
+import OAuth2Provider from "supertokens-node/recipe/oauth2provider";
+
+supertokens.init(SuperTokensConfig);
+
+const app = express();
+
+app.use(
+ cors({
+ origin: [getWebsiteDomain(), "http://localhost:3011", "http://localhost:3012", "http://localhost:3013"],
+ 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(),
+ });
+});
+
+const testObj = {
+ name: "John Doe",
+ age: 30,
+ email: "john.doe@client.localhost",
+};
+
+app.get("/test", async (req, res) => {
+ let you: any = "not authorized";
+ if (req.headers.authorization) {
+ const token = await OAuth2Provider.validateOAuth2AccessToken(req.headers.authorization.split(" ")[1]);
+ you = token.payload;
+ }
+
+ res.send({
+ resp: testObj[req.query.prop as keyof typeof testObj] ?? "no query?",
+ you,
+ });
+});
+
+// In case of session related errors, this error handler
+// returns 401 to the client.
+app.use(errorHandler());
+
+app.listen(3001, async () => {
+ console.log("Setting up tenants");
+ await setupTenants();
+ console.log("Tenants setup complete");
+ console.log(`API Server listening on port ${apiPort}`);
+});
diff --git a/examples/with-unified-login/auth-provider/backend/package.json b/examples/with-unified-login/auth-provider/backend/package.json
new file mode 100644
index 000000000..3d8ff3319
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/backend/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "supertokens-node",
+ "version": "0.0.1",
+ "private": true,
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "NODE_OPTIONS=--use-openssl-ca npx ts-node-dev --project ./tsconfig.json ./index.ts"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.18.1",
+ "helmet": "^5.1.0",
+ "morgan": "^1.10.0",
+ "npm-run-all": "^4.1.5",
+ "supertokens-node": "latest",
+ "ts-node-dev": "^2.0.0",
+ "typescript": "^4.7.2"
+ },
+ "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"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC"
+}
diff --git a/examples/with-unified-login/auth-provider/backend/tsconfig.json b/examples/with-unified-login/auth-provider/backend/tsconfig.json
new file mode 100644
index 000000000..8a91acaae
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/clients.json b/examples/with-unified-login/auth-provider/clients.json
new file mode 100644
index 000000000..b686efb46
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/clients.json
@@ -0,0 +1,13 @@
+{
+ "tenant1": {
+ "clientId": "stcl_f1412d57-29ab-43c1-bb67-29b701a8d098",
+ "clientSecret": "pctjhrEzpi6q46dZ~q1OaTR3.r"
+ },
+ "tenant2": {
+ "clientId": "stcl_944d226e-cde4-4edf-b625-487c4b02e79e"
+ },
+ "tenant3": {
+ "clientId": "stcl_2ba3b96c-07a8-4c3e-8b26-0dd94ee1f30f",
+ "clientSecret": "DNZIl9sULJqsf_r9HDpuutf1Vq"
+ }
+}
diff --git a/examples/with-unified-login/auth-provider/frontend/.gitignore b/examples/with-unified-login/auth-provider/frontend/.gitignore
new file mode 100644
index 000000000..4d29575de
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/frontend/LICENSE.md b/examples/with-unified-login/auth-provider/frontend/LICENSE.md
new file mode 100644
index 000000000..588f27e68
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/frontend/package.json b/examples/with-unified-login/auth-provider/frontend/package.json
new file mode 100644
index 000000000..a1d078db2
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "supertokens-react",
+ "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",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.2.1",
+ "react-scripts": "5.0.1",
+ "supertokens-auth-react": "latest",
+ "supertokens-web-js": "latest",
+ "typescript": "^4.8.2",
+ "web-vitals": "^2.1.4"
+ },
+ "scripts": {
+ "start": "BROWSER=none PORT=3000 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-unified-login/auth-provider/frontend/public/favicon.ico b/examples/with-unified-login/auth-provider/frontend/public/favicon.ico
new file mode 100644
index 000000000..a11777cc4
Binary files /dev/null and b/examples/with-unified-login/auth-provider/frontend/public/favicon.ico differ
diff --git a/examples/with-unified-login/auth-provider/frontend/public/index.html b/examples/with-unified-login/auth-provider/frontend/public/index.html
new file mode 100644
index 000000000..6f1f7cb51
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/public/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
diff --git a/examples/with-unified-login/auth-provider/frontend/public/manifest.json b/examples/with-unified-login/auth-provider/frontend/public/manifest.json
new file mode 100644
index 000000000..f01493ff0
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/frontend/public/robots.txt b/examples/with-unified-login/auth-provider/frontend/public/robots.txt
new file mode 100644
index 000000000..e9e57dc4d
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/examples/with-unified-login/auth-provider/frontend/src/App.css b/examples/with-unified-login/auth-provider/frontend/src/App.css
new file mode 100644
index 000000000..8a98a2341
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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/with-unified-login/auth-provider/frontend/src/App.tsx b/examples/with-unified-login/auth-provider/frontend/src/App.tsx
new file mode 100644
index 000000000..9462ebcdf
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/App.tsx
@@ -0,0 +1,40 @@
+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 } from "./config";
+
+SuperTokens.init(SuperTokensConfig);
+
+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/with-unified-login/auth-provider/frontend/src/Home/CallAPIView.tsx b/examples/with-unified-login/auth-provider/frontend/src/Home/CallAPIView.tsx
new file mode 100644
index 000000000..6a9d510d4
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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 (
+
+ );
+}
diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/fonts/MenloRegular.ttf b/examples/with-unified-login/auth-provider/frontend/src/assets/fonts/MenloRegular.ttf
new file mode 100644
index 000000000..033dc6d21
Binary files /dev/null and b/examples/with-unified-login/auth-provider/frontend/src/assets/fonts/MenloRegular.ttf differ
diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/arrow-right-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/arrow-right-icon.svg
new file mode 100644
index 000000000..95aa1fec6
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/arrow-right-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/background.png b/examples/with-unified-login/auth-provider/frontend/src/assets/images/background.png
new file mode 100644
index 000000000..2147c15c2
Binary files /dev/null and b/examples/with-unified-login/auth-provider/frontend/src/assets/images/background.png differ
diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/blogs-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/blogs-icon.svg
new file mode 100644
index 000000000..a2fc9dd62
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/blogs-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/celebrate-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/celebrate-icon.svg
new file mode 100644
index 000000000..3b40b1efa
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/celebrate-icon.svg
@@ -0,0 +1,9 @@
+
diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/guide-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/guide-icon.svg
new file mode 100644
index 000000000..bd85af72b
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/guide-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/index.ts b/examples/with-unified-login/auth-provider/frontend/src/assets/images/index.ts
new file mode 100644
index 000000000..7adf036c4
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/frontend/src/assets/images/separator-line.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/separator-line.svg
new file mode 100644
index 000000000..7127a00dc
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/separator-line.svg
@@ -0,0 +1,10 @@
+
diff --git a/examples/with-unified-login/auth-provider/frontend/src/assets/images/sign-out-icon.svg b/examples/with-unified-login/auth-provider/frontend/src/assets/images/sign-out-icon.svg
new file mode 100644
index 000000000..6cc4f85fd
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/assets/images/sign-out-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/with-unified-login/auth-provider/frontend/src/config.tsx b/examples/with-unified-login/auth-provider/frontend/src/config.tsx
new file mode 100644
index 000000000..fe9ba012e
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/config.tsx
@@ -0,0 +1,64 @@
+import EmailPassword from "supertokens-auth-react/recipe/emailpassword";
+import ThirdParty from "supertokens-auth-react/recipe/thirdparty";
+import MultiTenancy from "supertokens-auth-react/recipe/multitenancy";
+import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider";
+import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui";
+import Passwordless from "supertokens-auth-react/recipe/passwordless";
+import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui";
+import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui";
+import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
+import Session from "supertokens-auth-react/recipe/session";
+import React from "react";
+
+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 = {
+ usesDynamicLoginMethods: true,
+ 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(),
+ ThirdParty.init({
+ signInAndUpFeature: {
+ providers: [
+ ThirdParty.Github.init(),
+ ThirdParty.Google.init(),
+ ThirdParty.Apple.init(),
+ ThirdParty.Twitter.init(),
+ ],
+ },
+ }),
+ Passwordless.init({
+ contactMethod: "EMAIL_OR_PHONE",
+ }),
+ Session.init(),
+ OAuth2Provider.init() as any,
+ MultiTenancy.init(),
+ ],
+};
+
+export const recipeDetails = {
+ docsLink: "https://supertokens.com/docs/thirdpartypasswordless/introduction",
+};
+
+export const PreBuiltUIList = [
+ EmailPasswordPreBuiltUI,
+ ThirdPartyPreBuiltUI,
+ PasswordlessPreBuiltUI,
+ OAuth2ProviderPreBuiltUI as any,
+];
diff --git a/examples/with-unified-login/auth-provider/frontend/src/index.css b/examples/with-unified-login/auth-provider/frontend/src/index.css
new file mode 100644
index 000000000..04146b5e7
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/frontend/src/index.tsx b/examples/with-unified-login/auth-provider/frontend/src/index.tsx
new file mode 100644
index 000000000..399c737cd
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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/with-unified-login/auth-provider/frontend/src/react-app-env.d.ts b/examples/with-unified-login/auth-provider/frontend/src/react-app-env.d.ts
new file mode 100644
index 000000000..6431bc5fc
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/frontend/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/with-unified-login/auth-provider/frontend/tsconfig.json b/examples/with-unified-login/auth-provider/frontend/tsconfig.json
new file mode 100644
index 000000000..c0555cbc6
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/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-unified-login/auth-provider/package.json b/examples/with-unified-login/auth-provider/package.json
new file mode 100644
index 000000000..36e11d945
--- /dev/null
+++ b/examples/with-unified-login/auth-provider/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "auth-provider",
+ "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": {
+ "npm-run-all": "^4.1.5"
+ }
+}
diff --git a/examples/with-unified-login/client1/.gitignore b/examples/with-unified-login/client1/.gitignore
new file mode 100644
index 000000000..4d29575de
--- /dev/null
+++ b/examples/with-unified-login/client1/.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-unified-login/client1/LICENSE.md b/examples/with-unified-login/client1/LICENSE.md
new file mode 100644
index 000000000..588f27e68
--- /dev/null
+++ b/examples/with-unified-login/client1/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-unified-login/client1/package.json b/examples/with-unified-login/client1/package.json
new file mode 100644
index 000000000..ff4a4116a
--- /dev/null
+++ b/examples/with-unified-login/client1/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "supertokens-react",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "start": "BROWSER=none PORT=3010 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"
+ ]
+ },
+ "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",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-oidc-context": "^3.1.1",
+ "react-router-dom": "^6.2.1",
+ "react-scripts": "5.0.1",
+ "supertokens-auth-react": "latest",
+ "typescript": "^4.8.2",
+ "web-vitals": "^2.1.4"
+ }
+}
diff --git a/examples/with-unified-login/client1/public/favicon.ico b/examples/with-unified-login/client1/public/favicon.ico
new file mode 100644
index 000000000..a11777cc4
Binary files /dev/null and b/examples/with-unified-login/client1/public/favicon.ico differ
diff --git a/examples/with-unified-login/client1/public/index.html b/examples/with-unified-login/client1/public/index.html
new file mode 100644
index 000000000..6f1f7cb51
--- /dev/null
+++ b/examples/with-unified-login/client1/public/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
diff --git a/examples/with-unified-login/client1/public/manifest.json b/examples/with-unified-login/client1/public/manifest.json
new file mode 100644
index 000000000..f01493ff0
--- /dev/null
+++ b/examples/with-unified-login/client1/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-unified-login/client1/public/robots.txt b/examples/with-unified-login/client1/public/robots.txt
new file mode 100644
index 000000000..e9e57dc4d
--- /dev/null
+++ b/examples/with-unified-login/client1/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/examples/with-unified-login/client1/src/App.css b/examples/with-unified-login/client1/src/App.css
new file mode 100644
index 000000000..b9ee98c15
--- /dev/null
+++ b/examples/with-unified-login/client1/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-unified-login/client1/src/App.tsx b/examples/with-unified-login/client1/src/App.tsx
new file mode 100644
index 000000000..b3fec4494
--- /dev/null
+++ b/examples/with-unified-login/client1/src/App.tsx
@@ -0,0 +1,144 @@
+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 clients from "./clients.json";
+import "./App.css";
+
+SuperTokens.init(SuperTokensConfig);
+
+const tenantId = "tenant1";
+const clientId = clients[tenantId].clientId;
+
+const redirectURI = `${getWebsiteDomain()}/auth/callback`;
+const oidcConfig: AuthProviderProps = {
+ client_id: clientId,
+ authority: `${getApiDomain()}/auth`,
+ response_type: "code",
+ redirect_uri: redirectURI,
+ scope: "openid offline_access",
+ skipSigninCallback: true,
+ extraQueryParams: {
+ tenant_id: tenantId,
+ },
+};
+
+function AuthPage() {
+ const navigate = useNavigate();
+ const authCallbackHandled = useRef(false);
+ const { signinRedirect, signoutSilent, isLoading, user, error } = useAuth();
+
+ useEffect(() => {
+ // Redirect to home if supertokens session exists
+ doesSessionExist().then((sessionExists) => {
+ if (sessionExists) {
+ navigate("/");
+ }
+ });
+ }, []);
+
+ useEffect(() => {
+ if (authCallbackHandled.current) return;
+
+ const params = new URLSearchParams(window.location.search);
+ if (params.get("code") === null || params.get("state") === null) return;
+
+ authCallbackHandled.current = true;
+
+ async function handleSign() {
+ const state = params.get("state");
+ const savedInfo = localStorage.getItem(`oidc.${state}`);
+ if (!savedInfo) return;
+ const { id, code_verifier } = JSON.parse(savedInfo);
+ if (id !== state) return;
+
+ const res = await fetch(`${getApiDomain()}/auth/oauth/client/signin`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ clientId,
+ redirectURIInfo: {
+ redirectURI: redirectURI,
+ redirectURIQueryParams: Object.fromEntries(params.entries()),
+ pkceCodeVerifier: code_verifier,
+ },
+ }),
+ });
+ if (res.ok) {
+ const data = await res.json();
+ if (data.status === "OK") {
+ navigate("/");
+ } else {
+ // setIsLoading(false);
+ window.alert("Login Failed ");
+ }
+ }
+ }
+
+ handleSign();
+ }, [user, navigate, signoutSilent]);
+
+ if (isLoading || user) {
+ return (
+
+
Loading...
+
+ );
+ }
+
+ return (
+
+
OAuth2 Example With Supertokens
+ {error &&
{error.message}
}
+
+
+
+
+
+
+ );
+}
+
+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-unified-login/client1/src/Home/CallAPIView.tsx b/examples/with-unified-login/client1/src/Home/CallAPIView.tsx
new file mode 100644
index 000000000..6a9d510d4
--- /dev/null
+++ b/examples/with-unified-login/client1/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 (
+
+ );
+}
diff --git a/examples/with-unified-login/client1/src/assets/fonts/MenloRegular.ttf b/examples/with-unified-login/client1/src/assets/fonts/MenloRegular.ttf
new file mode 100644
index 000000000..033dc6d21
Binary files /dev/null and b/examples/with-unified-login/client1/src/assets/fonts/MenloRegular.ttf differ
diff --git a/examples/with-unified-login/client1/src/assets/images/arrow-right-icon.svg b/examples/with-unified-login/client1/src/assets/images/arrow-right-icon.svg
new file mode 100644
index 000000000..95aa1fec6
--- /dev/null
+++ b/examples/with-unified-login/client1/src/assets/images/arrow-right-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/with-unified-login/client1/src/assets/images/background.png b/examples/with-unified-login/client1/src/assets/images/background.png
new file mode 100644
index 000000000..2147c15c2
Binary files /dev/null and b/examples/with-unified-login/client1/src/assets/images/background.png differ
diff --git a/examples/with-unified-login/client1/src/assets/images/blogs-icon.svg b/examples/with-unified-login/client1/src/assets/images/blogs-icon.svg
new file mode 100644
index 000000000..a2fc9dd62
--- /dev/null
+++ b/examples/with-unified-login/client1/src/assets/images/blogs-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/with-unified-login/client1/src/assets/images/celebrate-icon.svg b/examples/with-unified-login/client1/src/assets/images/celebrate-icon.svg
new file mode 100644
index 000000000..3b40b1efa
--- /dev/null
+++ b/examples/with-unified-login/client1/src/assets/images/celebrate-icon.svg
@@ -0,0 +1,9 @@
+
diff --git a/examples/with-unified-login/client1/src/assets/images/guide-icon.svg b/examples/with-unified-login/client1/src/assets/images/guide-icon.svg
new file mode 100644
index 000000000..bd85af72b
--- /dev/null
+++ b/examples/with-unified-login/client1/src/assets/images/guide-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/with-unified-login/client1/src/assets/images/index.ts b/examples/with-unified-login/client1/src/assets/images/index.ts
new file mode 100644
index 000000000..7adf036c4
--- /dev/null
+++ b/examples/with-unified-login/client1/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-unified-login/client1/src/assets/images/separator-line.svg b/examples/with-unified-login/client1/src/assets/images/separator-line.svg
new file mode 100644
index 000000000..7127a00dc
--- /dev/null
+++ b/examples/with-unified-login/client1/src/assets/images/separator-line.svg
@@ -0,0 +1,10 @@
+
diff --git a/examples/with-unified-login/client1/src/assets/images/sign-out-icon.svg b/examples/with-unified-login/client1/src/assets/images/sign-out-icon.svg
new file mode 100644
index 000000000..6cc4f85fd
--- /dev/null
+++ b/examples/with-unified-login/client1/src/assets/images/sign-out-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/with-unified-login/client1/src/clients.json b/examples/with-unified-login/client1/src/clients.json
new file mode 120000
index 000000000..c2ba927f0
--- /dev/null
+++ b/examples/with-unified-login/client1/src/clients.json
@@ -0,0 +1 @@
+../../auth-provider/clients.json
\ No newline at end of file
diff --git a/examples/with-unified-login/client1/src/config.tsx b/examples/with-unified-login/client1/src/config.tsx
new file mode 100644
index 000000000..25d8182f5
--- /dev/null
+++ b/examples/with-unified-login/client1/src/config.tsx
@@ -0,0 +1,38 @@
+import Session from "supertokens-auth-react/recipe/session";
+
+export function getApiDomain() {
+ const apiPort = 3001;
+ const apiUrl = `http://localhost:${apiPort}`;
+ return apiUrl;
+}
+
+export function getWebsiteDomain() {
+ const websitePort = 3011;
+ const websiteUrl = `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({
+ tokenTransferMethod: "header",
+ }),
+ ],
+};
+
+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-unified-login/client1/src/index.css b/examples/with-unified-login/client1/src/index.css
new file mode 100644
index 000000000..04146b5e7
--- /dev/null
+++ b/examples/with-unified-login/client1/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-unified-login/client1/src/index.tsx b/examples/with-unified-login/client1/src/index.tsx
new file mode 100644
index 000000000..126116f9b
--- /dev/null
+++ b/examples/with-unified-login/client1/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-unified-login/client1/src/react-app-env.d.ts b/examples/with-unified-login/client1/src/react-app-env.d.ts
new file mode 100644
index 000000000..6431bc5fc
--- /dev/null
+++ b/examples/with-unified-login/client1/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/with-unified-login/client1/tsconfig.json b/examples/with-unified-login/client1/tsconfig.json
new file mode 100644
index 000000000..c0555cbc6
--- /dev/null
+++ b/examples/with-unified-login/client1/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-unified-login/client2/LICENSE.md b/examples/with-unified-login/client2/LICENSE.md
new file mode 100644
index 000000000..588f27e68
--- /dev/null
+++ b/examples/with-unified-login/client2/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-unified-login/client2/README.md b/examples/with-unified-login/client2/README.md
new file mode 100644
index 000000000..1507029d7
--- /dev/null
+++ b/examples/with-unified-login/client2/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-unified-login/client2/package.json b/examples/with-unified-login/client2/package.json
new file mode 100644
index 000000000..960c16119
--- /dev/null
+++ b/examples/with-unified-login/client2/package.json
@@ -0,0 +1,49 @@
+{
+ "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-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": "PORT=3020 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-unified-login/client2/public/favicon.ico b/examples/with-unified-login/client2/public/favicon.ico
new file mode 100644
index 000000000..a11777cc4
Binary files /dev/null and b/examples/with-unified-login/client2/public/favicon.ico differ
diff --git a/examples/with-unified-login/client2/public/index.html b/examples/with-unified-login/client2/public/index.html
new file mode 100644
index 000000000..6f1f7cb51
--- /dev/null
+++ b/examples/with-unified-login/client2/public/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
diff --git a/examples/with-unified-login/client2/public/manifest.json b/examples/with-unified-login/client2/public/manifest.json
new file mode 100644
index 000000000..f01493ff0
--- /dev/null
+++ b/examples/with-unified-login/client2/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-unified-login/client2/public/robots.txt b/examples/with-unified-login/client2/public/robots.txt
new file mode 100644
index 000000000..e9e57dc4d
--- /dev/null
+++ b/examples/with-unified-login/client2/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/examples/with-unified-login/client2/src/App.css b/examples/with-unified-login/client2/src/App.css
new file mode 100644
index 000000000..9886f003d
--- /dev/null
+++ b/examples/with-unified-login/client2/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-unified-login/client2/src/App.tsx b/examples/with-unified-login/client2/src/App.tsx
new file mode 100644
index 000000000..3734f17b4
--- /dev/null
+++ b/examples/with-unified-login/client2/src/App.tsx
@@ -0,0 +1,49 @@
+import { AuthProvider, AuthProviderProps, useAuth } from "react-oidc-context";
+import "./App.css";
+import clients from "./clients.json";
+
+const tenantId = "tenant2";
+const clientId = clients[tenantId].clientId;
+
+const redirectURI = `http://localhost:3012/auth/callback`;
+const logoutURI = `http://localhost:3012/loggedout`;
+const oidcConfig: AuthProviderProps = {
+ client_id: clientId,
+ authority: `http://localhost:3001/auth`,
+ response_type: "code",
+ redirect_uri: redirectURI,
+ scope: "openid offline_access",
+ extraQueryParams: {
+ tenant_id: tenantId,
+ },
+};
+
+function AuthPage() {
+ const { signinRedirect, signoutRedirect, 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-unified-login/client2/src/assets/fonts/MenloRegular.ttf b/examples/with-unified-login/client2/src/assets/fonts/MenloRegular.ttf
new file mode 100644
index 000000000..033dc6d21
Binary files /dev/null and b/examples/with-unified-login/client2/src/assets/fonts/MenloRegular.ttf differ
diff --git a/examples/with-unified-login/client2/src/clients.json b/examples/with-unified-login/client2/src/clients.json
new file mode 120000
index 000000000..c2ba927f0
--- /dev/null
+++ b/examples/with-unified-login/client2/src/clients.json
@@ -0,0 +1 @@
+../../auth-provider/clients.json
\ No newline at end of file
diff --git a/examples/with-unified-login/client2/src/index.css b/examples/with-unified-login/client2/src/index.css
new file mode 100644
index 000000000..04146b5e7
--- /dev/null
+++ b/examples/with-unified-login/client2/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-unified-login/client2/src/index.tsx b/examples/with-unified-login/client2/src/index.tsx
new file mode 100644
index 000000000..399c737cd
--- /dev/null
+++ b/examples/with-unified-login/client2/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-unified-login/client2/src/react-app-env.d.ts b/examples/with-unified-login/client2/src/react-app-env.d.ts
new file mode 100644
index 000000000..6431bc5fc
--- /dev/null
+++ b/examples/with-unified-login/client2/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/with-unified-login/client2/tsconfig.json b/examples/with-unified-login/client2/tsconfig.json
new file mode 100644
index 000000000..c0555cbc6
--- /dev/null
+++ b/examples/with-unified-login/client2/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-unified-login/client3/.gitignore b/examples/with-unified-login/client3/.gitignore
new file mode 100644
index 000000000..fa85cd8da
--- /dev/null
+++ b/examples/with-unified-login/client3/.gitignore
@@ -0,0 +1,9 @@
+.env
+var
+
+# Node.js
+node_modules/
+npm-debug.log*
+
+# Mac OS X
+.DS_Store
diff --git a/examples/with-unified-login/client3/LICENSE b/examples/with-unified-login/client3/LICENSE
new file mode 100644
index 000000000..00d2e135a
--- /dev/null
+++ b/examples/with-unified-login/client3/LICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
\ No newline at end of file
diff --git a/examples/with-unified-login/client3/README.md b/examples/with-unified-login/client3/README.md
new file mode 100644
index 000000000..b099d3580
--- /dev/null
+++ b/examples/with-unified-login/client3/README.md
@@ -0,0 +1,63 @@
+# todos-express-openidconnect
+
+This app illustrates how to use [Passport](https://www.passportjs.org/) with
+[Express](https://expressjs.com/) to sign users in via OpenID Connect. Use this
+example as a starting point for your own web applications.
+
+## Quick Start
+
+To run this app, clone the repository and install dependencies:
+
+```bash
+$ git clone https://github.com/passport/todos-express-openidconnect.git
+$ cd todos-express-openidconnect
+$ npm install
+```
+
+This app must be configured with an OpenID Provider (OP)'s endpoints, as well as
+a client ID and secret that has been issued by the OP.
+
+The endpoints should be set as options to `OpenIDConnectStrategy` in
+[routes/auth.js](https://github.com/passport/todos-express-openidconnect/blob/master/routes/auth.js#L7-L10).
+
+Once the client ID and secret have been obtained, create a `.env` file and add
+the following environment variables:
+
+```
+CLIENT_ID=__INSERT_CLIENT_ID_HERE__
+CLIENT_SECRET=__INSERT_CLIENT_SECRET_HERE__
+```
+
+Start the server.
+
+```bash
+$ npm start
+```
+
+Navigate to [`http://localhost:3000`](http://localhost:3000).
+
+## Overview
+
+This example illustrates how to use Passport and the [`passport-openidconnect`](https://www.passportjs.org/packages/passport-openidconnect/)
+strategy within an Express application to sign users in via OpenID Connect.
+
+This app implements the features of a typical [TodoMVC](https://todomvc.com/)
+app, and adds sign in functionality. This app is a traditional web application,
+in which all application logic and data persistence is handled on the server.
+
+User interaction is performed via HTML pages and forms, which are rendered via
+[EJS](https://ejs.co/) templates and styled with vanilla CSS. Data is stored in
+and queried from a [SQLite](https://www.sqlite.org/) database.
+
+After users sign in, a login session is established and maintained between the
+server and the browser with a cookie. As authenticated users interact with the
+app, creating and editing todo items, the login state is restored by
+authenticating the session.
+
+## License
+
+[The Unlicense](https://opensource.org/licenses/unlicense)
+
+## Credit
+
+Created by [Jared Hanson](https://www.jaredhanson.me/)
diff --git a/examples/with-unified-login/client3/app.js b/examples/with-unified-login/client3/app.js
new file mode 100644
index 000000000..11dcd8061
--- /dev/null
+++ b/examples/with-unified-login/client3/app.js
@@ -0,0 +1,73 @@
+require("dotenv").config();
+
+var createError = require("http-errors");
+var express = require("express");
+var path = require("path");
+var cookieParser = require("cookie-parser");
+var session = require("express-session");
+var csrf = require("csurf");
+var passport = require("passport");
+var logger = require("morgan");
+
+// pass the session to the connect sqlite3 module
+// allowing it to inherit from session.Store
+var SQLiteStore = require("connect-sqlite3")(session);
+
+var indexRouter = require("./routes/index");
+var authRouter = require("./routes/auth");
+
+var app = express();
+
+// view engine setup
+app.set("views", path.join(__dirname, "views"));
+app.set("view engine", "ejs");
+
+app.locals.pluralize = require("pluralize");
+
+app.use(logger("dev"));
+app.use(express.json());
+app.use(express.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(express.static(path.join(__dirname, "public")));
+app.use(
+ session({
+ secret: "keyboard cat",
+ resave: false, // don't save session if unmodified
+ saveUninitialized: false, // don't create session until something stored
+ store: new SQLiteStore({ db: "sessions.db", dir: "var/db" }),
+ })
+);
+app.use(csrf());
+app.use(passport.authenticate("session"));
+app.use(function (req, res, next) {
+ var msgs = req.session.messages || [];
+ res.locals.messages = msgs;
+ res.locals.hasMessages = !!msgs.length;
+ req.session.messages = [];
+ next();
+});
+app.use(function (req, res, next) {
+ res.locals.csrfToken = req.csrfToken();
+ next();
+});
+
+app.use("/", indexRouter);
+app.use("/", authRouter);
+
+// catch 404 and forward to error handler
+app.use(function (req, res, next) {
+ next(createError(404));
+});
+
+// error handler
+app.use(function (err, req, res, next) {
+ // set locals, only providing error in development
+ res.locals.message = err.message;
+ res.locals.error = req.app.get("env") === "development" ? err : {};
+
+ // render the error page
+ res.status(err.status || 500);
+ res.render("error");
+});
+
+module.exports = app;
diff --git a/examples/with-unified-login/client3/bin/www b/examples/with-unified-login/client3/bin/www
new file mode 100755
index 000000000..4472f5b15
--- /dev/null
+++ b/examples/with-unified-login/client3/bin/www
@@ -0,0 +1,86 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var app = require("../app");
+var debug = require("debug")("todos:server");
+var http = require("http");
+
+/**
+ * Get port from environment and store in Express.
+ */
+
+var port = normalizePort(process.env.PORT || "3030");
+app.set("port", port);
+
+/**
+ * Create HTTP server.
+ */
+
+var server = http.createServer(app);
+
+/**
+ * Listen on provided port, on all network interfaces.
+ */
+
+server.listen(port);
+server.on("error", onError);
+server.on("listening", onListening);
+
+/**
+ * Normalize a port into a number, string, or false.
+ */
+
+function normalizePort(val) {
+ var port = parseInt(val, 10);
+
+ if (isNaN(port)) {
+ // named pipe
+ return val;
+ }
+
+ if (port >= 0) {
+ // port number
+ return port;
+ }
+
+ return false;
+}
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+
+function onError(error) {
+ if (error.syscall !== "listen") {
+ throw error;
+ }
+
+ var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
+
+ // handle specific listen errors with friendly messages
+ switch (error.code) {
+ case "EACCES":
+ console.error(bind + " requires elevated privileges");
+ process.exit(1);
+ break;
+ case "EADDRINUSE":
+ console.error(bind + " is already in use");
+ process.exit(1);
+ break;
+ default:
+ throw error;
+ }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+
+function onListening() {
+ var addr = server.address();
+ var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
+ debug("Listening on " + bind);
+}
diff --git a/examples/with-unified-login/client3/clients.json b/examples/with-unified-login/client3/clients.json
new file mode 120000
index 000000000..b024041e2
--- /dev/null
+++ b/examples/with-unified-login/client3/clients.json
@@ -0,0 +1 @@
+../auth-provider/clients.json
\ No newline at end of file
diff --git a/examples/with-unified-login/client3/db.js b/examples/with-unified-login/client3/db.js
new file mode 100644
index 000000000..b91e0589a
--- /dev/null
+++ b/examples/with-unified-login/client3/db.js
@@ -0,0 +1,18 @@
+var sqlite3 = require("sqlite3");
+var mkdirp = require("mkdirp");
+
+mkdirp.sync("var/db");
+
+var db = new sqlite3.Database("var/db/todos.db");
+
+db.serialize(function () {
+ db.run(
+ "CREATE TABLE IF NOT EXISTS todos ( \
+ owner_id INTEGER NOT NULL, \
+ title TEXT NOT NULL, \
+ completed INTEGER \
+ )"
+ );
+});
+
+module.exports = db;
diff --git a/examples/with-unified-login/client3/package.json b/examples/with-unified-login/client3/package.json
new file mode 100644
index 000000000..e056d1ac7
--- /dev/null
+++ b/examples/with-unified-login/client3/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "todos-express-openidconnect",
+ "version": "0.0.0",
+ "private": true,
+ "description": "Todo app using Express, Passport, and SQLite for sign in via OpenID Connect.",
+ "keywords": [
+ "example",
+ "express",
+ "passport",
+ "sqlite"
+ ],
+ "author": {
+ "name": "Jared Hanson",
+ "email": "jaredhanson@gmail.com",
+ "url": "https://www.jaredhanson.me/"
+ },
+ "homepage": "https://github.com/passport/todos-express-openidconnect",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/passport/todos-express-openidconnect.git"
+ },
+ "bugs": {
+ "url": "https://github.com/passport/todos-express-openidconnect/issues"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ },
+ "license": "Unlicense",
+ "scripts": {
+ "start": "NODE_OPTIONS=--use-openssl-ca node ./bin/www"
+ },
+ "dependencies": {
+ "connect-ensure-login": "^0.1.1",
+ "connect-sqlite3": "^0.9.13",
+ "cookie-parser": "~1.4.4",
+ "csurf": "^1.11.0",
+ "debug": "~2.6.9",
+ "dotenv": "^8.6.0",
+ "ejs": "~2.6.1",
+ "express": "~4.16.1",
+ "express-session": "^1.17.2",
+ "http-errors": "~1.6.3",
+ "mkdirp": "^1.0.4",
+ "morgan": "~1.9.1",
+ "passport": "^0.5.2",
+ "passport-openidconnect": "^0.1.1",
+ "pluralize": "^8.0.0",
+ "sqlite3": "^5.0.2"
+ }
+}
diff --git a/examples/with-unified-login/client3/public/css/app.css b/examples/with-unified-login/client3/public/css/app.css
new file mode 100644
index 000000000..ac19f01a6
--- /dev/null
+++ b/examples/with-unified-login/client3/public/css/app.css
@@ -0,0 +1,55 @@
+.nav {
+ position: absolute;
+ top: -130px;
+ right: 0;
+}
+
+.nav ul {
+ margin: 0;
+ list-style: none;
+ text-align: center;
+}
+
+.nav li {
+ display: inline-block;
+ height: 40px;
+ margin-left: 12px;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 40px;
+}
+
+.nav a {
+ display: block;
+ color: inherit;
+ text-decoration: none;
+}
+
+.nav a:hover {
+ border-bottom: 1px solid #db7676;
+}
+
+.nav button {
+ height: 40px;
+}
+
+.nav button:hover {
+ border-bottom: 1px solid #db7676;
+ cursor: pointer;
+}
+
+/* background image by Cole Bemis */
+.nav .user {
+ padding-left: 20px;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center left;
+}
+
+/* background image by Cole Bemis */
+.nav .logout {
+ padding-left: 20px;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-log-out'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'%3E%3C/path%3E%3Cpolyline points='16 17 21 12 16 7'%3E%3C/polyline%3E%3Cline x1='21' y1='12' x2='9' y2='12'%3E%3C/line%3E%3C/svg%3E%0A");
+ background-repeat: no-repeat;
+ background-position: center left;
+}
diff --git a/examples/with-unified-login/client3/public/css/base.css b/examples/with-unified-login/client3/public/css/base.css
new file mode 100644
index 000000000..d3938100c
--- /dev/null
+++ b/examples/with-unified-login/client3/public/css/base.css
@@ -0,0 +1,141 @@
+hr {
+ margin: 20px 0;
+ border: 0;
+ border-top: 1px dashed #c5c5c5;
+ border-bottom: 1px dashed #f7f7f7;
+}
+
+.learn a {
+ font-weight: normal;
+ text-decoration: none;
+ color: #b83f45;
+}
+
+.learn a:hover {
+ text-decoration: underline;
+ color: #787e7e;
+}
+
+.learn h3,
+.learn h4,
+.learn h5 {
+ margin: 10px 0;
+ font-weight: 500;
+ line-height: 1.2;
+ color: #000;
+}
+
+.learn h3 {
+ font-size: 24px;
+}
+
+.learn h4 {
+ font-size: 18px;
+}
+
+.learn h5 {
+ margin-bottom: 0;
+ font-size: 14px;
+}
+
+.learn ul {
+ padding: 0;
+ margin: 0 0 30px 25px;
+}
+
+.learn li {
+ line-height: 20px;
+}
+
+.learn p {
+ font-size: 15px;
+ font-weight: 300;
+ line-height: 1.3;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+#issue-count {
+ display: none;
+}
+
+.quote {
+ border: none;
+ margin: 20px 0 60px 0;
+}
+
+.quote p {
+ font-style: italic;
+}
+
+.quote p:before {
+ content: "“";
+ font-size: 50px;
+ opacity: 0.15;
+ position: absolute;
+ top: -20px;
+ left: 3px;
+}
+
+.quote p:after {
+ content: "”";
+ font-size: 50px;
+ opacity: 0.15;
+ position: absolute;
+ bottom: -42px;
+ right: 3px;
+}
+
+.quote footer {
+ position: absolute;
+ bottom: -40px;
+ right: 0;
+}
+
+.quote footer img {
+ border-radius: 3px;
+}
+
+.quote footer a {
+ margin-left: 5px;
+ vertical-align: middle;
+}
+
+.speech-bubble {
+ position: relative;
+ padding: 10px;
+ background: rgba(0, 0, 0, 0.04);
+ border-radius: 5px;
+}
+
+.speech-bubble:after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ right: 30px;
+ border: 13px solid transparent;
+ border-top-color: rgba(0, 0, 0, 0.04);
+}
+
+.learn-bar > .learn {
+ position: absolute;
+ width: 272px;
+ top: 8px;
+ left: -300px;
+ padding: 10px;
+ border-radius: 5px;
+ background-color: rgba(255, 255, 255, 0.6);
+ transition-property: left;
+ transition-duration: 500ms;
+}
+
+@media (min-width: 899px) {
+ .learn-bar {
+ width: auto;
+ padding-left: 300px;
+ }
+
+ .learn-bar > .learn {
+ left: 8px;
+ }
+}
diff --git a/examples/with-unified-login/client3/public/css/home.css b/examples/with-unified-login/client3/public/css/home.css
new file mode 100644
index 000000000..51c0b5973
--- /dev/null
+++ b/examples/with-unified-login/client3/public/css/home.css
@@ -0,0 +1,41 @@
+.todohome {
+ margin: 130px 0 40px 0;
+ position: relative;
+}
+
+.todohome h1 {
+ position: absolute;
+ top: -140px;
+ width: 100%;
+ font-size: 80px;
+ font-weight: 200;
+ text-align: center;
+ color: #b83f45;
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
+
+.todohome section {
+ padding-top: 1px;
+ text-align: center;
+}
+
+.todohome h2 {
+ padding-bottom: 48px;
+ font-size: 28px;
+ font-weight: 300;
+}
+
+.todohome .button {
+ padding: 13px 45px;
+ font-size: 16px;
+ font-weight: 500;
+ color: white;
+ border-radius: 5px;
+ background: #d83f45;
+}
+
+.todohome a.button {
+ text-decoration: none;
+}
diff --git a/examples/with-unified-login/client3/public/css/index.css b/examples/with-unified-login/client3/public/css/index.css
new file mode 100644
index 000000000..be48f991e
--- /dev/null
+++ b/examples/with-unified-login/client3/public/css/index.css
@@ -0,0 +1,387 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #f5f5f5;
+ color: #111111;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-weight: 300;
+}
+
+.hidden {
+ display: none;
+}
+
+.todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+.todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 400;
+ color: rgba(0, 0, 0, 0.4);
+}
+
+.todoapp input::-moz-placeholder {
+ font-style: italic;
+ font-weight: 400;
+ color: rgba(0, 0, 0, 0.4);
+}
+
+.todoapp input::input-placeholder {
+ font-style: italic;
+ font-weight: 400;
+ color: rgba(0, 0, 0, 0.4);
+}
+
+.todoapp h1 {
+ position: absolute;
+ top: -140px;
+ width: 100%;
+ font-size: 80px;
+ font-weight: 200;
+ text-align: center;
+ color: #b83f45;
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
+
+.new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ color: inherit;
+ padding: 6px;
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.new-todo {
+ padding: 16px 16px 16px 60px;
+ height: 65px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
+}
+
+.main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+}
+
+.toggle-all {
+ width: 1px;
+ height: 1px;
+ border: none; /* Mobile Safari */
+ opacity: 0;
+ position: absolute;
+ right: 100%;
+ bottom: 100%;
+}
+
+.toggle-all + label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 45px;
+ height: 65px;
+ font-size: 0;
+ position: absolute;
+ top: -65px;
+ left: -0;
+}
+
+.toggle-all + label:before {
+ content: "❯";
+ display: inline-block;
+ font-size: 22px;
+ color: #949494;
+ padding: 10px 27px 10px 27px;
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+
+.toggle-all:checked + label:before {
+ color: #484848;
+}
+
+.todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+ border-bottom: none;
+}
+
+.todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+.todo-list li.editing .edit {
+ display: block;
+ width: calc(100% - 43px);
+ padding: 12px 16px;
+ margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+ display: none;
+}
+
+.todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ /* auto, since non-WebKit browsers doesn't support input styling */
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.todo-list li .toggle {
+ opacity: 0;
+}
+
+.todo-list li .toggle + label {
+ /*
+ Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
+ IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
+ */
+ background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center left;
+}
+
+.todo-list li .toggle:checked + label {
+ background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E");
+}
+
+.todo-list li label {
+ word-break: break-all;
+ padding: 15px 15px 15px 60px;
+ display: block;
+ line-height: 1.2;
+ transition: color 0.4s;
+ font-weight: 400;
+ color: #484848;
+}
+
+.todo-list li.completed label {
+ color: #949494;
+ text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #949494;
+ transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover,
+.todo-list li .destroy:focus {
+ color: #c18585;
+}
+
+.todo-list li .destroy:after {
+ content: "×";
+ display: block;
+ height: 100%;
+ line-height: 1.1;
+}
+
+.todo-list li:hover .destroy {
+ display: block;
+}
+
+.todo-list li .edit {
+ display: none;
+}
+
+.todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
+
+.footer {
+ padding: 10px 15px;
+ height: 20px;
+ text-align: center;
+ font-size: 15px;
+ border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+ content: "";
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2),
+ 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+ float: left;
+ text-align: left;
+}
+
+.todo-count strong {
+ font-weight: 300;
+}
+
+.filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+.filters li {
+ display: inline;
+}
+
+.filters li a {
+ color: inherit;
+ margin: 3px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+}
+
+.filters li a:hover {
+ border-color: #db7676;
+}
+
+.filters li a.selected {
+ border-color: #ce4646;
+}
+
+.clear-completed,
+html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 19px;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.clear-completed:hover {
+ text-decoration: underline;
+}
+
+.info {
+ margin: 65px auto 0;
+ color: #4d4d4d;
+ font-size: 11px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+.info p {
+ line-height: 1;
+}
+
+.info a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 400;
+}
+
+.info a:hover {
+ text-decoration: underline;
+}
+
+/*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+ .toggle-all,
+ .todo-list li .toggle {
+ background: none;
+ }
+
+ .todo-list li .toggle {
+ height: 40px;
+ }
+}
+
+@media (max-width: 430px) {
+ .footer {
+ height: 50px;
+ }
+
+ .filters {
+ bottom: 10px;
+ }
+}
+
+:focus,
+.toggle:focus + label,
+.toggle-all:focus + label {
+ box-shadow: 0 0 2px 2px #cf7d7d;
+ outline: 0;
+}
diff --git a/examples/with-unified-login/client3/routes/auth.js b/examples/with-unified-login/client3/routes/auth.js
new file mode 100644
index 000000000..dc6de7272
--- /dev/null
+++ b/examples/with-unified-login/client3/routes/auth.js
@@ -0,0 +1,57 @@
+var express = require("express");
+var clients = require("../clients.json");
+var passport = require("passport");
+var OpenIDConnectStrategy = require("passport-openidconnect");
+
+const tenantId = "tenant3";
+const clientId = clients[tenantId].clientId;
+const clientSecret = clients[tenantId].clientSecret;
+
+passport.use(
+ new OpenIDConnectStrategy(
+ {
+ issuer: `http://localhost:3001/auth`,
+ authorizationURL: "http://localhost:3001/auth/oauth/auth",
+ tokenURL: "http://localhost:3001/auth/oauth/token",
+ userInfoURL: "http://localhost:3001/auth/oauth/userinfo",
+ clientID: clientId,
+ clientSecret: clientSecret,
+ callbackURL: "http://localhost:3013/oauth2/redirect",
+ scope: ["profile"],
+ },
+ function verify(issuer, profile, cb) {
+ return cb(null, profile);
+ }
+ )
+);
+
+passport.serializeUser(function (user, cb) {
+ process.nextTick(function () {
+ cb(null, { id: user.id, username: user.username, name: user.displayName });
+ });
+});
+
+passport.deserializeUser(function (user, cb) {
+ process.nextTick(function () {
+ return cb(null, user);
+ });
+});
+
+var router = express.Router();
+
+router.get("/login", passport.authenticate("openidconnect"));
+
+router.get(
+ "/oauth2/redirect",
+ passport.authenticate("openidconnect", {
+ successReturnToOrRedirect: "/",
+ failureRedirect: "/login",
+ })
+);
+
+router.post("/logout", function (req, res, next) {
+ req.logout();
+ res.redirect("/");
+});
+
+module.exports = router;
diff --git a/examples/with-unified-login/client3/routes/index.js b/examples/with-unified-login/client3/routes/index.js
new file mode 100644
index 000000000..1b4302c5e
--- /dev/null
+++ b/examples/with-unified-login/client3/routes/index.js
@@ -0,0 +1,154 @@
+var express = require("express");
+var ensureLogIn = require("connect-ensure-login").ensureLoggedIn;
+var db = require("../db");
+
+var ensureLoggedIn = ensureLogIn();
+
+function fetchTodos(req, res, next) {
+ db.all("SELECT rowid AS id, * FROM todos WHERE owner_id = ?", [req.user.id], function (err, rows) {
+ if (err) {
+ return next(err);
+ }
+
+ var todos = rows.map(function (row) {
+ return {
+ id: row.id,
+ title: row.title,
+ completed: row.completed == 1 ? true : false,
+ url: "/" + row.id,
+ };
+ });
+ res.locals.todos = todos;
+ res.locals.activeCount = todos.filter(function (todo) {
+ return !todo.completed;
+ }).length;
+ res.locals.completedCount = todos.length - res.locals.activeCount;
+ next();
+ });
+}
+
+var router = express.Router();
+
+/* GET home page. */
+router.get(
+ "/",
+ function (req, res, next) {
+ if (!req.user) {
+ return res.render("home");
+ }
+ next();
+ },
+ fetchTodos,
+ function (req, res, next) {
+ res.locals.filter = null;
+ res.render("index", { user: req.user });
+ }
+);
+
+router.get("/active", ensureLoggedIn, fetchTodos, function (req, res, next) {
+ res.locals.todos = res.locals.todos.filter(function (todo) {
+ return !todo.completed;
+ });
+ res.locals.filter = "active";
+ res.render("index", { user: req.user });
+});
+
+router.get("/completed", ensureLoggedIn, fetchTodos, function (req, res, next) {
+ res.locals.todos = res.locals.todos.filter(function (todo) {
+ return todo.completed;
+ });
+ res.locals.filter = "completed";
+ res.render("index", { user: req.user });
+});
+
+router.post(
+ "/",
+ ensureLoggedIn,
+ function (req, res, next) {
+ req.body.title = req.body.title.trim();
+ next();
+ },
+ function (req, res, next) {
+ if (req.body.title !== "") {
+ return next();
+ }
+ return res.redirect("/" + (req.body.filter || ""));
+ },
+ function (req, res, next) {
+ db.run(
+ "INSERT INTO todos (owner_id, title, completed) VALUES (?, ?, ?)",
+ [req.user.id, req.body.title, req.body.completed == true ? 1 : null],
+ function (err) {
+ if (err) {
+ return next(err);
+ }
+ return res.redirect("/" + (req.body.filter || ""));
+ }
+ );
+ }
+);
+
+router.post(
+ "/:id(\\d+)",
+ ensureLoggedIn,
+ function (req, res, next) {
+ req.body.title = req.body.title.trim();
+ next();
+ },
+ function (req, res, next) {
+ if (req.body.title !== "") {
+ return next();
+ }
+ db.run("DELETE FROM todos WHERE rowid = ? AND owner_id = ?", [req.params.id, req.user.id], function (err) {
+ if (err) {
+ return next(err);
+ }
+ return res.redirect("/" + (req.body.filter || ""));
+ });
+ },
+ function (req, res, next) {
+ db.run(
+ "UPDATE todos SET title = ?, completed = ? WHERE rowid = ? AND owner_id = ?",
+ [req.body.title, req.body.completed !== undefined ? 1 : null, req.params.id, req.user.id],
+ function (err) {
+ if (err) {
+ return next(err);
+ }
+ return res.redirect("/" + (req.body.filter || ""));
+ }
+ );
+ }
+);
+
+router.post("/:id(\\d+)/delete", ensureLoggedIn, function (req, res, next) {
+ db.run("DELETE FROM todos WHERE rowid = ? AND owner_id = ?", [req.params.id, req.user.id], function (err) {
+ if (err) {
+ return next(err);
+ }
+ return res.redirect("/" + (req.body.filter || ""));
+ });
+});
+
+router.post("/toggle-all", ensureLoggedIn, function (req, res, next) {
+ db.run(
+ "UPDATE todos SET completed = ? WHERE owner_id = ?",
+ [req.body.completed !== undefined ? 1 : null, req.user.id],
+ function (err) {
+ if (err) {
+ return next(err);
+ }
+ return res.redirect("/" + (req.body.filter || ""));
+ }
+ );
+});
+
+router.post("/clear-completed", ensureLoggedIn, function (req, res, next) {
+ db.run("DELETE FROM todos WHERE owner_id = ? AND completed = ?", [req.user.id, 1], function (err) {
+ if (err) {
+ return next(err);
+ }
+ return res.redirect("/" + (req.body.filter || ""));
+ });
+});
+
+module.exports = router;
diff --git a/examples/with-unified-login/client3/views/error.ejs b/examples/with-unified-login/client3/views/error.ejs
new file mode 100644
index 000000000..7cf94edf1
--- /dev/null
+++ b/examples/with-unified-login/client3/views/error.ejs
@@ -0,0 +1,3 @@
+