├── src ├── views │ ├── Profile.js │ ├── Logout.js │ ├── DescopeLogin.js │ └── Dashboard.js ├── setupTests.js ├── App.test.js ├── index.css ├── reportWebVitals.js ├── index.js ├── App.js ├── logo.svg └── App.css ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .gitignore ├── package.json ├── README.md └── oidc-flow.json /src/views/Profile.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/aws-cognito-oidc-sample/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/aws-cognito-oidc-sample/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/aws-cognito-oidc-sample/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/views/Logout.js: -------------------------------------------------------------------------------- 1 | import { Auth } from "aws-amplify"; 2 | 3 | async function handleSignOut(event) { 4 | event.preventDefault(); 5 | try { 6 | await Auth.signOut(); 7 | } catch (error) { 8 | console.log("error signing out: ", error); 9 | } 10 | setUser(null); 11 | setIsLoading(false); 12 | } 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/views/DescopeLogin.js: -------------------------------------------------------------------------------- 1 | import { Descope } from "@descope/react-sdk"; 2 | 3 | 4 | export const DescopeLogin = () => { 5 | return ( 6 |
7 |
8 | console.log("Logged in!")} 11 | onError={(e) => console.log("Could not log in!")} 12 | theme="light" 13 | /> 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default DescopeLogin; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | #amplify-do-not-edit-begin 5 | amplify/\#current-cloud-backend 6 | amplify/.config/local-* 7 | amplify/logs 8 | amplify/mock-data 9 | amplify/mock-api-resources 10 | amplify/backend/amplify-meta.json 11 | amplify/backend/.temp 12 | build/ 13 | dist/ 14 | node_modules/ 15 | aws-exports.js 16 | awsconfiguration.json 17 | amplifyconfiguration.json 18 | amplifyconfiguration.dart 19 | amplify-build-config.json 20 | amplify-gradle-config.json 21 | amplifytools.xcconfig 22 | .secret-* 23 | **.sample 24 | #amplify-do-not-edit-end 25 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-cognito-hosted-ui-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@descope/react-sdk": "^1.0.8", 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.1.1", 9 | "@testing-library/user-event": "^13.5.0", 10 | "aws-amplify": "^4.3.18", 11 | "axios": "^0.27.2", 12 | "react": "^18.1.0", 13 | "react-dom": "^18.1.0", 14 | "react-json-pretty": "^2.2.0", 15 | "react-router-dom": "^6.3.0", 16 | "react-scripts": "5.0.1", 17 | "web-vitals": "^2.1.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import { BrowserRouter, Route, Routes } from "react-router-dom"; 5 | import App from "./App"; 6 | import Dashboard from "./views/Dashboard"; 7 | import reportWebVitals from "./reportWebVitals"; 8 | import DescopeLogin from "./views/DescopeLogin"; 9 | import Profile from "./views/Profile"; 10 | import { AuthProvider } from "@descope/react-sdk"; 11 | 12 | const root = ReactDOM.createRoot(document.getElementById("root")); 13 | root.render( 14 | 15 | 16 | 17 | 18 | }> 19 | }> 20 | }> 21 | }> 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | // If you want to start measuring performance in your app, pass a function 29 | // to log results (for example: reportWebVitals(console.log)) 30 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 31 | reportWebVitals(); 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Cognito with Descope in Hosted UI Sample App 2 | 3 | ## Description 4 | 5 | This is a very simple react app to demonstrate how to implement Descope as an OIDC provider with Cognito by using AWS Amplify. 6 | 7 | > **Note**: If you need the flow, to work with OIDC and Passkeys, it is the `oidc-flow.json` file in the root of this directory. This can be downloaded and imported via the [Descope Console](https://app.descope.com/flows). 8 | 9 | ## Project Setup 10 | 11 | 1. Create `.env` for env variables; 12 | 13 | ``` 14 | REACT_APP_USERPOOL_ID=_ 15 | REACT_APP_USERPOOL_WEB_CLIENT_ID= 16 | REACT_APP_AWS_REGION= 17 | REACT_APP_COGNITO_HOSTED_UI= 18 | REACT_APP_COGNITO_DOMAIN= 19 | REACT_APP_DESCOPE_PROJECT_ID= 20 | ``` 21 | 22 | > **Note**: This app uses the AWS Hosted UI, so it can be easily re-configured. The app would operately similarly however if a custom login page was used instead. 23 | 24 | 2. Install all of the packages 25 | 26 | You can install all of the necessary packages by running `yarn install` 27 | 28 | ## Run 29 | 30 | Run the program using: 31 | 32 | `yarn start` 33 | 34 | ## Issue Reporting 35 | 36 | If you have found a bug or if you have a feature request, please report them at this repository issues section. 37 | 38 | ## Author 39 | 40 | [Descope](https://descope.com) 41 | 42 | ## License 43 | 44 | This project is licensed under the MIT license. 45 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import Amplify from "aws-amplify"; 3 | import React from "react"; 4 | 5 | 6 | Amplify.configure({ 7 | Auth: { 8 | // REQUIRED - Amazon Cognito Region 9 | region: process.env.REACT_APP_AWS_REGION, 10 | 11 | // OPTIONAL - Amazon Cognito User Pool ID 12 | userPoolId: process.env.REACT_APP_USERPOOL_ID, 13 | 14 | // OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string) 15 | userPoolWebClientId: process.env.REACT_APP_USERPOOL_WEB_CLIENT_ID, 16 | 17 | // OPTIONAL - Enforce user authentication prior to accessing AWS resources or not 18 | mandatorySignIn: false, 19 | 20 | // OPTIONAL - Manually set the authentication flow type. Default is 'USER_SRP_AUTH' 21 | authenticationFlowType: "USER_PASSWORD_AUTH", 22 | 23 | // OPTIONAL - Hosted UI configuration 24 | oauth: { 25 | domain: `${process.env.REACT_APP_COGNITO_DOMAIN}.auth.us-west-2.amazoncognito.com`, 26 | scope: ["openid", "email", "phone", "profile"], 27 | redirectSignIn: "https://main.d1rua11q42kscj.amplifyapp.com/dashboard", 28 | redirectSignOut: "https://main.d1rua11q42kscj.amplifyapp.com/", 29 | responseType: "code", // or 'token', note that REFRESH token will only be generated when the responseType is code 30 | }, 31 | }, 32 | }); 33 | 34 | function App(props) { 35 | return ( 36 | <> 37 |
38 |

Sample App

39 | 48 |
49 | 50 |
51 |

Cognito Sample App with Hosted UI and Descope as an OIDC Provider

52 |

Powered by Descope

53 | 62 |
63 | 64 | ); 65 | } 66 | 67 | export default App; 68 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import "../App.css"; 3 | import { Auth } from "aws-amplify"; 4 | import { Navigate } from "react-router-dom"; 5 | import axios from "axios"; 6 | 7 | 8 | import JSONPretty from "react-json-pretty"; 9 | import "react-json-pretty/themes/monikai.css"; 10 | 11 | 12 | function Dashboard(props) { 13 | const [user, setUser] = useState(); 14 | const [apiResponse, setApiResponse] = useState(""); 15 | const [isLoading, setIsLoading] = useState(true); 16 | 17 | async function getCurrentUser() { 18 | try { 19 | const authUser = await Auth.currentAuthenticatedUser(); 20 | console.log(JSON.stringify(authUser)); 21 | setUser(authUser); 22 | } catch (e) { 23 | console.log("error happened", e); 24 | setUser(null); 25 | setIsLoading(false); 26 | } 27 | } 28 | 29 | useEffect(() => { 30 | getCurrentUser(); 31 | }, []); 32 | 33 | async function handleCallProtectedMethod(event) { 34 | event.preventDefault(); 35 | const accessToken = user["signInUserSession"]["accessToken"]["jwtToken"]; 36 | const headers = { 37 | Authorization: "Bearer " + accessToken, 38 | }; 39 | const apiResp = await axios.get( 40 | `https://hkbcq1nsnh.execute-api.us-west-2.amazonaws.com/Stage/oidc-get`, 41 | { headers } 42 | ); 43 | setApiResponse(JSON.stringify(apiResp.data)); 44 | } 45 | 46 | async function handleSignOut(event) { 47 | event.preventDefault(); 48 | try { 49 | await Auth.signOut(); 50 | } catch (error) { 51 | console.log("error signing out: ", error); 52 | } 53 | setUser(null); 54 | setIsLoading(false); 55 | } 56 | 57 | if (!user && !isLoading) { 58 | return ; 59 | } 60 | 61 | return ( 62 | <> 63 |
64 |

Sample App

65 | 68 |
69 | 70 |
71 |

You can test the AWS API Gateway Here

72 |

73 | Press this button to make a GET HTTP request to the OIDC-Test endpoint 74 | configured with our AWS API Gateway 75 |

76 | 79 |
80 | 81 |
82 |
83 | 84 | ); 85 | } 86 | 87 | export default Dashboard; 88 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;700&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"); 3 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"); 4 | @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400&display=swap"); 5 | 6 | html, 7 | body { 8 | font-family: "Inter", sans-serif; 9 | background-color: aliceblue; 10 | } 11 | h1, 12 | h2, 13 | p { 14 | max-width: 90vw; 15 | } 16 | 17 | /* ====== HOME PAGE ====== */ 18 | .home { 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | text-align: center; 24 | } 25 | .blue { 26 | color: #167ae4; 27 | } 28 | .home { 29 | padding: 4vw 0; 30 | } 31 | .title { 32 | font-weight: 500; 33 | margin-top: 0; 34 | font-size: 4em; 35 | width: 70vw; 36 | } 37 | .powered { 38 | margin: 0; 39 | display: flex; 40 | flex-direction: row; 41 | align-items: center; 42 | justify-content: center; 43 | background-color: #e5f2ff; 44 | padding: 20px 40px; 45 | border-radius: 8px; 46 | } 47 | .blue-dot { 48 | width: 10px; 49 | height: 10px; 50 | border-radius: 100px; 51 | margin-right: 15px; 52 | box-shadow: 0px 0px 5px #71adec, 0px 0px 5px #78b1ef; 53 | background-color: #167ae4; 54 | } 55 | .dashboard-btn { 56 | margin-top: 5vh; 57 | background-color: #167ae4; 58 | border: none; 59 | padding: 15px 35px; 60 | padding-right: 30px; 61 | font-size: 1.15em; 62 | color: aliceblue; 63 | border-radius: 8px; 64 | } 65 | .arrow { 66 | font-family: "Space Grotesk", sans-serif; 67 | } 68 | .dashboard-btn:hover { 69 | background-color: #0d6bd0; 70 | } 71 | 72 | /* ====== Dashboard ====== */ 73 | .dash-title { 74 | font-size: 2.2em; 75 | font-weight: 500; 76 | } 77 | .logout-btn { 78 | color: rgb(255, 68, 26); 79 | } 80 | .api-button { 81 | border: 2px solid #c9e1fb; 82 | cursor: pointer; 83 | background-color: #e5f2ff; 84 | color: #167ae4; 85 | padding: 2vh 5vh; 86 | font-size: 1.1em; 87 | border-radius: 8px; 88 | margin-top: 3vh; 89 | } 90 | .api-button:hover { 91 | box-shadow: 0px 0px 5px #cce6ff; 92 | } 93 | .api-response-box { 94 | padding: 2vh; 95 | margin-top: 5vh; 96 | width: 90vw; 97 | text-align: left; 98 | } 99 | 100 | .main-container-login { 101 | display: flex; 102 | align-items: center; 103 | justify-content: center; 104 | width: 120vw; 105 | height: 100vh; 106 | } 107 | 108 | .login-container { 109 | width: 100%; 110 | max-width: 800px; 111 | padding: 20px; 112 | } 113 | 114 | /* ====== NAVBAR ====== */ 115 | .sample-logo { 116 | font-family: "Space Grotesk", sans-serif; 117 | } 118 | .navbar { 119 | display: flex; 120 | justify-content: space-between; 121 | padding: 4vh 7vh; 122 | background-color: none; 123 | } 124 | .navbar h2 { 125 | margin: 0; 126 | } 127 | .login-btn { 128 | border: none; 129 | cursor: pointer; 130 | font-size: 1em; 131 | font-family: "Golos Text", sans-serif; 132 | background-color: #e5f2ff; 133 | padding: 12px 4vh; 134 | border-radius: 8px; 135 | /* color: #3965a2; */ 136 | } 137 | .login-btn:hover { 138 | background-color: #d4e9ff; 139 | } 140 | 141 | /* ===== Mobile ===== */ 142 | @media only screen and (max-width: 768px) { 143 | .title { 144 | font-size: 2.5em; 145 | } 146 | .navbar { 147 | padding: 4vh 3vh; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /oidc-flow.json: -------------------------------------------------------------------------------- 1 | {"screens":[{"id":"SC2QqHKqIewjP0zmAW0pUCSdbXoaN","version":7,"flowId":"oidc-flow","inputs":[],"interactions":[{"id":"pnOqEGpL1b","type":"","label":"Choose another authentication method","icon":"","subType":""},{"id":"resend","type":"","label":"Send again","icon":"","subType":""},{"id":"submit","type":"","label":"Submit","icon":"","subType":""}],"htmlTemplate":{"L1T9iO4p5U":{"custom":{},"displayName":"One Time Code","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"yZNZ2ZVVu_","props":{"data-errormessage-pattern-mismatch":"Must be a 6 digits number","digits":6,"fullWidth":false,"id":"L1T9iO4p5U","label":"","name":"code","required":true,"size":"lg","type":"tel"},"type":{"resolvedName":"Code"}},"ROOT":{"custom":{},"displayName":"Container","hidden":false,"isCanvas":true,"linkedNodes":{},"nodes":["auED4dEJkJ","eBGiu9cYSb","yZNZ2ZVVu_"],"props":{"align":"center","background":"","data-editor-type":"container","direction":"col","id":"ROOT","justify":"center","paddingX":"4","paddingY":"4","spaceBetween":"5","width":"100"},"type":{"resolvedName":"Container"}},"auED4dEJkJ":{"custom":{},"displayName":"Text","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"ROOT","props":{"align":"center","children":"We've sent a message containing a 6-digit code to {{sentTo.maskedEmail}}","fullWidth":false,"id":"auED4dEJkJ","italic":false,"typography":"subtitle1","variant":"primary"},"type":{"resolvedName":"Text"}},"eBGiu9cYSb":{"custom":{},"displayName":"Text","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"ROOT","props":{"align":"center","children":"Enter it below and press the button to continue","fullWidth":false,"id":"eBGiu9cYSb","italic":false,"typography":"subtitle2","variant":"primary"},"type":{"resolvedName":"Text"}},"fyz-aN6hyx":{"custom":{},"displayName":"Error Message","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"thNbUTWOGY","props":{"align":"center","color":"error","data-type":"error-message","fullWidth":false,"id":"fyz-aN6hyx","italic":false,"typography":"body1"},"type":{"resolvedName":"ErrorMessage"}},"oOOUcyEOVr":{"custom":{},"displayName":"Container","hidden":false,"isCanvas":true,"linkedNodes":{},"nodes":["resend","pnOqEGpL1b"],"parent":"yZNZ2ZVVu_","props":{"align":"start","background":"#ffffff00","data-editor-type":"container","direction":"row","id":"oOOUcyEOVr","justify":"center","paddingX":"5","paddingY":"0","spaceBetween":"0","width":"100"},"type":{"resolvedName":"Container"}},"pnOqEGpL1b":{"custom":{"type":"button"},"displayName":"Button","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"oOOUcyEOVr","props":{"children":"Choose another authentication method","color":"primary","data-type":"button","formNoValidate":true,"fullWidth":true,"id":"pnOqEGpL1b","shape":"","size":"sm","variant":"link"},"type":{"resolvedName":"Button"}},"resend":{"custom":{"type":"button"},"displayName":"Button","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"oOOUcyEOVr","props":{"children":"Send again","color":"primary","data-type":"button","formNoValidate":true,"fullWidth":true,"id":"resend","shape":"","size":"sm","variant":"link"},"type":{"resolvedName":"Button"}},"submit":{"custom":{"type":"button"},"displayName":"Button","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"thNbUTWOGY","props":{"children":"Submit","color":"primary","data-type":"button","formNoValidate":false,"fullWidth":true,"id":"submit","shape":"","size":"md","variant":"contained"},"type":{"resolvedName":"Button"}},"thNbUTWOGY":{"custom":{},"displayName":"Container","hidden":false,"isCanvas":true,"linkedNodes":{},"nodes":["submit","fyz-aN6hyx"],"parent":"yZNZ2ZVVu_","props":{"align":"start","background":"#ffffff00","data-editor-type":"container","direction":"row","id":"thNbUTWOGY","justify":"center","paddingX":"4","paddingY":"3","spaceBetween":"1","width":"100"},"type":{"resolvedName":"Container"}},"yZNZ2ZVVu_":{"custom":{},"displayName":"Container","hidden":false,"isCanvas":true,"linkedNodes":{},"nodes":["L1T9iO4p5U","thNbUTWOGY","oOOUcyEOVr"],"parent":"ROOT","props":{"align":"center","background":"","data-editor-type":"container","direction":"col","id":"yZNZ2ZVVu_","justify":"start","paddingX":"4","paddingY":"4","spaceBetween":"1.5","width":"100"},"type":{"resolvedName":"Container"}}}},{"id":"SC2QqHKtZYR6cP00mjyjRU43JAMym","version":7,"flowId":"oidc-flow","inputs":[],"interactions":[{"id":"4EMLSPCn8j","type":"","label":"Login with Biometrics","icon":"fingerprint","subType":""}],"htmlTemplate":{"1sMmkeVxVT":{"custom":{},"displayName":"Error Message","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"ROOT","props":{"align":"center","color":"error","data-type":"error-message","fullWidth":false,"id":"1sMmkeVxVT","italic":false,"typography":"body1"},"type":{"resolvedName":"ErrorMessage"}},"4EMLSPCn8j":{"custom":{"type":"button"},"displayName":"Biometrics Button","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"ROOT","props":{"children":"Login with Biometrics","color":"primary","data-type":"biometrics","formNoValidate":false,"fullWidth":false,"id":"4EMLSPCn8j","shape":"","size":"md","startIcon":"fingerprint","startIconColorText":true,"variant":"outline"},"type":{"resolvedName":"BiometricsButton"}},"Alqr97dFtY":{"custom":{"type":"input"},"displayName":"Email Input","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"ROOT","props":{"data-errormessage-pattern-mismatch":"Must be a valid email","fullWidth":true,"id":"Alqr97dFtY","label":"Email","max":100,"name":"email","pattern":"^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$","placeholder":"Email","required":false,"size":"md","type":"email"},"type":{"resolvedName":"EmailInput"}},"LmrSIGOWZb":{"custom":{},"displayName":"Text","hidden":false,"isCanvas":false,"linkedNodes":{},"nodes":[],"parent":"ROOT","props":{"align":"center","children":"Welcome!","fullWidth":false,"id":"LmrSIGOWZb","italic":false,"typography":"h3","variant":"primary"},"type":{"resolvedName":"Text"}},"ROOT":{"custom":{},"displayName":"Container","hidden":false,"isCanvas":true,"linkedNodes":{},"nodes":["LmrSIGOWZb","Alqr97dFtY","4EMLSPCn8j","1sMmkeVxVT"],"props":{"align":"center","background":"","data-editor-type":"container","direction":"col","id":"ROOT","justify":"center","paddingX":"4","paddingY":"8","spaceBetween":"4","width":"100"},"type":{"resolvedName":"Container"}}}}],"flow":{"id":"oidc-flow","version":8,"name":"Passkey OIDC Flow","description":"You should use this flow to implement Passkey authentication, when using Descope as an OIDC Provider with another IdP.","dsl":{"startTask":"1","tasks":{"0":{"action":"logged-in","id":"0","name":"End","next":{"rules":[]},"view":{"x":4148.528315394407,"y":-458.1860446534223}},"1":{"id":"1","name":"Sign in with Passkeys","next":{"rules":[{"interactionId":"4EMLSPCn8j","taskId":"2"}]},"screenId":"SC2QqHKtZYR6cP00mjyjRU43JAMym","type":"screen","view":{"x":-264.07407808,"y":-44.73801280000001}},"2":{"action":"webauthn-sign-up-or-in-start","arguments":{"finishStepId":{"type":"inline","value":"2.end"}},"id":"2","name":"Sign Up or In / WebAuthn","next":{"rules":[{"interactionId":"success","taskId":"2.end"}]},"type":"automated","view":{"x":168.68955968000023,"y":-26.16021568000002}},"3":{"conditions":[{"atomicConditions":[{"operator":"is-true","predicate":{"ArgType":"","type":"inline","value":""},"target":{"ArgType":"","type":"context","value":"user.verifiedEmail"}}],"interactionId":"zs5pg3cwhaj","name":"verifiedEmail"},{"interactionId":"ELSE","name":"!verifiedEmail"}],"id":"3","name":"Is the user's email verified?","next":{"rules":[{"interactionId":"zs5pg3cwhaj","taskId":"4"},{"interactionId":"ELSE","taskId":"5"}]},"type":"condition","view":{"x":599.1062085499391,"y":-36.75193241900209}},"4":{"action":"custom-claims","arguments":{"customClaims":{"ArgType":"","type":"inline","value":[{"key":"email","type":"context","value":"user.email"},{"key":"verified_email","type":"boolean","value":true}]}},"id":"4","name":"Add Email and EmailVerified Attributes","next":{"rules":[{"interactionId":"success","taskId":"8"}]},"type":"automated","view":{"x":1551.0807123199993,"y":-122.90609727999995}},"5":{"action":"update-user-otp-email","arguments":{"addToLoginIDs":{"ArgType":"","type":"inline","value":true},"onMergeUseExisting":{"ArgType":"","type":"inline","value":"true"}},"id":"5","name":"Update User / OTP / Email","next":{"rules":[{"interactionId":"success","taskId":"6"}]},"type":"automated","view":{"x":1067.3513043199998,"y":264.0774291199999}},"6":{"id":"6","name":"Verify OTP","next":{"rules":[{"interactionId":"resend","taskId":"5"},{"interactionId":"submit","taskId":"7"}]},"screenId":"SC2QqHKqIewjP0zmAW0pUCSdbXoaN","type":"screen","view":{"x":1487.3513043199998,"y":264.0774291199999}},"7":{"action":"verify-code-otp-email","id":"7","name":"Verify Code / OTP / Email","next":{"rules":[{"interactionId":"success","taskId":"4"}]},"type":"automated","view":{"x":1907.3513043199998,"y":264.0774291199999}},"8":{"action":"logged-in","arguments":{},"id":"8","name":"End","next":{"rules":[]},"type":"automated","view":{"x":1987.950546572639,"y":-164.44173841437947}},"2.end":{"action":"webauthn-sign-up-or-in-finish","arguments":{},"id":"2.end","name":"Sign Up or In / WebAuthn","next":{"rules":[{"interactionId":"success","taskId":"3"}]},"type":"automated","view":{"x":168.68955968000023,"y":-26.16021568000002}},"start":{"id":"start","name":"start","next":{"rules":[{"taskId":"1"}]},"view":{"x":-456.6067199999999,"y":0}}}},"disabled":false}} --------------------------------------------------------------------------------