├── .env.example ├── .gitignore ├── README.md ├── backend-nodejs ├── Dockerfile ├── app.ts ├── bin │ └── startup.sh ├── controllers │ ├── authCognitoController.ts │ └── authCorbadoController.ts ├── package.json ├── tsconfig.app.json ├── tsconfig.json └── utils │ ├── constants.ts │ └── helper.ts ├── docker-compose.yml └── frontend-angular ├── .editorconfig ├── .gitignore ├── Dockerfile ├── README.md ├── angular.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth.service.spec.ts │ ├── auth.service.ts │ ├── auth │ │ ├── auth.component.html │ │ ├── auth.component.scss │ │ ├── auth.component.spec.ts │ │ └── auth.component.ts │ └── logged-in │ │ ├── logged-in.component.html │ │ ├── logged-in.component.scss │ │ ├── logged-in.component.spec.ts │ │ └── logged-in.component.ts ├── assets │ └── .gitkeep ├── favicon.ico ├── index.html ├── main.ts └── styles.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.env.example: -------------------------------------------------------------------------------- 1 | AWS_ACCESS_KEY_ID= 2 | AWS_SECRET_ACCESS_KEY= 3 | 4 | COGNITO_REGION= 5 | COGNITO_USER_POOL_ID= 6 | COGNITO_CLIENT_ID= 7 | COGNITO_CLIENT_SECRET= 8 | COGNITO_JWKS= 9 | 10 | CORBADO_PROJECT_ID= 11 | CORBADO_API_SECRET= 12 | CORBADO_CLI_SECRET= 13 | CORBADO_WEBHOOK_USERNAME= 14 | CORBADO_WEBHOOK_PASSWORD= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | node_modules 4 | dist 5 | package-lock.json 6 | .vscode 7 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 |

Passkeys integration into Amazon Cognito

7 | 8 |

9 | Sample project that shows how to integrate passkeys into your existing password-based Amazon Cognito project. 10 |
11 |
12 | View passkeys demo 13 | · 14 | Report Bug 15 | · 16 | Request Feature 17 |

18 |
19 | 20 | 21 | 22 | 23 | ## Overview 24 | 25 | 26 | 27 | Passkeys are the new standard for authentication on the web. Currently, they're being rolled out by Apple, Google and Microsoft. Though, not many code 28 | sample projects exist, and even less for integrating them into existing authentication and user management systems. We provide a guide that shows how to easily add 29 | passkeys into your existing Amazon Cognito project. 30 | 31 | For this sample, we leverage [Corbado](https://www.corbado.com)'s passkey-first web components that let you integrate it in <1 hour, while still keeping Amazon Cognito as core user management system. 32 | 33 | Find a detailed step-by-step tutorial [here](https://www.corbado.com/blog/passkeys-amazon-cognito). 34 | 35 |

(back to top)

36 | 37 | 38 | 39 | ### Built With 40 | 41 | The sample project uses Angular as web frontend framework and Node.js / Express in the backend (both in TypeScript). 42 | 43 | * [![Angular][Angular.io]][Angular-url] 44 | * [![Node.js][Nodejs.org]][Nodejs-url] 45 | * [![Express][Expressjs.com]][Express-url] 46 | 47 |

(back to top)

48 | 49 | 50 | 51 | ## Getting Started 52 | 53 | We provide a docker file for quickly getting started. Note that you need to enter Amazon Cognito and Corbado environment variables to get things working (see `docker-compose.yml`). 54 | 55 | To start project without locally without Docker, you need to run the [Corbado CLI](https://docs.corbado.com/helpful-guides/corbado-cli). Besides, you may also need to copy the AWS CLI credentials from `.aws/credentials`: 56 | 57 | ``` 58 | AWS_ACCESS_KEY_ID= 59 | AWS_SECRET_ACCESS_KEY= 60 | 61 | COGNITO_REGION= 62 | COGNITO_USER_POOL_ID= 63 | COGNITO_CLIENT_ID= 64 | COGNITO_CLIENT_SECRET= 65 | COGNITO_JWKS= 66 | 67 | CORBADO_PROJECT_ID= 68 | CORBADO_API_SECRET= 69 | CORBADO_CLI_SECRET= 70 | CORBADO_WEBHOOK_USERNAME= 71 | CORBADO_WEBHOOK_PASSWORD= 72 | ``` 73 | 74 | Start the docker containers with: 75 | 76 | ``` 77 | docker compose up 78 | ``` 79 | 80 | ## Integration 81 | 82 | Please see [this link](https://www.corbado.com/blog/passkeys-amazon-cognito) for a detailed step-by-step tutorial on how to integrate passkeys into Amazon Cognito. 83 | 84 | ### Sign-up flow 85 | The following steps provide a high-level overview what happens during the passkey sign-up flow. 86 | 87 | 1. Sign-up via Corbado web component on Angular frontend 88 | 2. Sign-up handled by Corbado backend (existing users are checked via webhooks) 89 | 4. Redirect to `/api/corbado/authTokenValidate` on Node.js / Express backend with `corbadoAuthToken` 90 | 5. API call to Corbado backend to verify `corbadoAuthToken` (API returns `email`) 91 | 6. API calls to create the user in Amazon Cognito 92 | 1. AdminCreateUser (`email`=`email` returned from previous step, `email_verified`=`true`, `custom:createdByCorbado`=`true`) 93 | 2. AdminSetUserPassword (`Username`=`email`, `Password`=`randomPassword`) 94 | 7. API calls to create session in Amazon Cognito 95 | 1. AdminInitiateAuthCommand (`AuthFlow`=`CUSTOM_AUTH`, `USERNAME`=`email` returned from previous step, no password) 96 | 2. AWS Lambda functions are executed 97 | 1. `Define auth challenge`: Cognito invokes this trigger to initiate the custom auth flow 98 | 2. `Create auth challenge`: Cognito invokes this trigger after `Define auth challenge` to create custom challenge 99 | 3. `Verify auth challenge response`: Cognito invokes this trigger to verify response from user for custom challenge 100 | 3. RespondToAuthChallenge 101 | 8. Amazon Cognito returns JWTs (`idToken`, `accessToken`, `refreshToken`) 102 | 9. `accessToken` "saved in Angular" 103 | 10. Logged-in page in Angular verifies `accessToken` in JavaScript 104 | 105 | ### Login flow 106 | The following steps provide a high-level overview what happens during the passkey login flow. 107 | 108 | 1. Login via Corbado web component on Angular frontend 109 | 2. Login handled by Corbado backend (existing users are checked via webhooks) 110 | 3. Redirect to `/api/corbado/authTokenValidate` on Node.js / Express backend with `corbadoAuthToken` 111 | 4. API call to Corbado backend to verify `corbadoAuthToken` (API returns `email`) 112 | 5. API calls to create session in Amazon Cognito 113 | 1. AdminInitiateAuthCommand (`AuthFlow`=`CUSTOM_AUTH`, `USERNAME`=`email` returned from previous step, no password) 114 | 2. AWS Lambda functions are executed 115 | 1. `Define auth challenge`: Cognito invokes this trigger to initiate the custom auth flow 116 | 2. `Create auth challenge`: Cognito invokes this trigger after `Define auth challenge` to create custom challenge 117 | 3. `Verify auth challenge response`: Cognito invokes this trigger to verify response from user for custom challenge 118 | 3. RespondToAuthChallenge 119 | 6. Amazon Cognito returns JWTs (`idToken`, `accessToken`, `refreshToken`) 120 | 7. `accessToken` "saved in Angular" 121 | 8. Logged-in page in Angular verifies `accessToken` in JavaScript 122 | 123 |

(back to top)

124 | 125 | 126 | 127 | ## Contact 128 | 129 | Vincent Delitz - [@vdelitz](https://twitter.com/vdelitz) - vincent@corbado.com 130 | 131 | Project link: [https://github.com/corbado/passkeys-amazon-cognito](https://github.com/corbado/passkeys-amazon-cognito) 132 | 133 | Tutorial link: [https://www.corbado.com/blog/passkeys-amazon-cognito](https://www.corbado.com/blog/passkeys-amazon-cognito) 134 | 135 |

(back to top)

136 | 137 | 138 | 139 | 140 | [product-screenshot]: images/screenshot.png 141 | [Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white 142 | [Angular-url]: https://angular.io/ 143 | 144 | [Nodejs-url]: https://nodejs.org/ 145 | [Nodejs.org]: https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=node.js&logoColor=white 146 | 147 | [Express-url]: https://expressjs.com/ 148 | [Expressjs.com]: https://img.shields.io/badge/Express-AEAEAE?style=for-the-badge&logo=express&logoColor=white 149 | -------------------------------------------------------------------------------- /backend-nodejs/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as a parent image 2 | FROM node:16-alpine 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | ENV CORBADO_PROJECT_ID \ 7 | CORBADO_API_SECRET \ 8 | CORBADO_CLI_SECRET 9 | 10 | 11 | RUN apk add --no-cache curl 12 | 13 | RUN apk add git 14 | 15 | RUN curl -o /tmp/corbado_cli.tar.gz -sSL https://github.com/corbado/cli/releases/download/v1.0.2/corbado_cli_v1.0.2_${TARGETOS}_${TARGETARCH}.tar.gz && tar xfz /tmp/corbado_cli.tar.gz && mv corbado /usr/local/bin/corbado && chmod +x /usr/local/bin/corbado 16 | 17 | # Set the working directory to /app 18 | WORKDIR /app 19 | 20 | # Copy the package.json and package-lock.json files to the container 21 | COPY package*.json ./ 22 | 23 | # Expose the port used by the backend 24 | EXPOSE 3000 25 | 26 | 27 | ENTRYPOINT /bin/sh bin/startup.sh 28 | -------------------------------------------------------------------------------- /backend-nodejs/app.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const express = require( 'express'); 3 | // @ts-ignore 4 | import cors from 'cors'; 5 | // @ts-ignore 6 | import bodyParser from 'body-parser'; 7 | // @ts-ignore 8 | import dotenv from 'dotenv'; 9 | import {/*signUp, login, */logout} from './controllers/authCognitoController'; 10 | import {handleWebhook, authTokenValidate} from './controllers/authCorbadoController'; 11 | import {json, Request, Response} from "express"; 12 | const Corbado = require('@corbado/node-sdk'); 13 | 14 | const projectID = process.env.CORBADO_PROJECT_ID; 15 | const apiSecret = process.env.CORBADO_API_SECRET; 16 | const corbadoWebhookUsername = process.env.CORBADO_WEBHOOK_USERNAME; 17 | const corbadoWebhookPassword = process.env.CORBADO_WEBHOOK_PASSWORD; 18 | 19 | const config = new Corbado.Configuration(projectID, apiSecret); 20 | config.webhookUsername = corbadoWebhookUsername; 21 | config.webhookPassword = corbadoWebhookPassword; 22 | const corbado = new Corbado.SDK(config); 23 | 24 | dotenv.config(); 25 | const app = express(); 26 | 27 | app.use(bodyParser.json()); 28 | app.use(cors()); 29 | 30 | // Old authentication process for Amazon Cognito 31 | //app.post('/api/auth/signup', signUp); 32 | //app.post('/api/auth/login', login); 33 | app.post('/api/auth/logout', logout); 34 | 35 | 36 | 37 | app.post('/api/corbado/webhook', corbado.webhooks.middleware, json(), handleWebhook); 38 | app.get('/api/corbado/authTokenValidate', json(), authTokenValidate); 39 | 40 | app.get('/ping', (req: Request, res: Response) => { 41 | res.send('pong'); 42 | }); 43 | 44 | 45 | const port = process.env.PORT || 3000; 46 | 47 | app.listen(port, () => { 48 | console.log(`Server started on port ${port}`); 49 | }); 50 | -------------------------------------------------------------------------------- /backend-nodejs/bin/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Function to check if npm run dev is successfully spun up 4 | function check_dev { 5 | response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/ping) 6 | if [[ $response == "200" ]]; then 7 | return 0 8 | else 9 | return 1 10 | fi 11 | } 12 | 13 | corbado login --projectID $CORBADO_PROJECT_ID --cliSecret $CORBADO_CLI_SECRET 14 | 15 | # Install dependencies and start npm run dev 16 | npm install && npm run dev & 17 | 18 | # Wait for npm run dev to spin up successfully 19 | while ! check_dev; do 20 | sleep 1 21 | echo "Check if backend is reachable" 22 | done 23 | 24 | # Wait for an additional 10 seconds to ensure the application is fully running 25 | sleep 10 26 | 27 | echo "Running corbado subscribe" 28 | 29 | # Subscribe to Corbado 30 | corbado subscribe http://localhost:3000 -------------------------------------------------------------------------------- /backend-nodejs/controllers/authCognitoController.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from "express"; 2 | import { 3 | AdminCreateUserCommand, 4 | AdminCreateUserCommandInput, 5 | AdminGetUserCommand, 6 | AdminGetUserCommandInput, 7 | AdminInitiateAuthCommand, 8 | AdminInitiateAuthCommandInput, 9 | AdminSetUserPasswordCommand, 10 | AdminSetUserPasswordCommandInput, AttributeType, 11 | AuthFlowType, 12 | ChallengeNameType, 13 | CognitoIdentityProviderClient, 14 | CognitoIdentityProviderClientConfig, 15 | GlobalSignOutCommand, 16 | InitiateAuthCommand, 17 | InitiateAuthCommandInput, 18 | InitiateAuthCommandOutput, 19 | MessageActionType, 20 | RespondToAuthChallengeCommand, 21 | RespondToAuthChallengeCommandInput, 22 | RespondToAuthChallengeCommandOutput, 23 | } from "@aws-sdk/client-cognito-identity-provider"; 24 | // @ts-ignore 25 | import crypto from "crypto"; 26 | // @ts-ignore 27 | import {hashSecret, validateJWT, generatePassword} from '../utils/helper'; 28 | import {EXISTS, NOT_EXISTS} from "../utils/constants"; 29 | 30 | require("dotenv").config({path: '../.env' }); 31 | 32 | 33 | const userPoolId = process.env.COGNITO_USER_POOL_ID; 34 | const clientId = process.env.COGNITO_CLIENT_ID || ""; 35 | const clientSecret = process.env.COGNITO_CLIENT_SECRET || ""; 36 | const region = process.env.COGNITO_REGION || ""; 37 | const poolData = {UserPoolId: userPoolId, ClientId: clientId}; 38 | 39 | const clientConfig: CognitoIdentityProviderClientConfig = { 40 | region: region, 41 | }; 42 | const client = new CognitoIdentityProviderClient(clientConfig); 43 | 44 | // old Amazon Cognito code commented out 45 | /*export const signUp = async (req: Request, res: Response) => { 46 | const {email, password} = req.body; 47 | 48 | // Workaround, as don't want to send out confirm emails 49 | const paramsAdminCreateUser: AdminCreateUserCommandInput = { 50 | ...poolData, 51 | Username: email, 52 | DesiredDeliveryMediums: ["EMAIL"], 53 | UserAttributes: [ 54 | {Name: 'email', Value: email}, 55 | {Name: 'email_verified', Value: 'true'}, 56 | ], 57 | MessageAction: MessageActionType.SUPPRESS 58 | }; 59 | 60 | try { 61 | const createUserCommand = new AdminCreateUserCommand(paramsAdminCreateUser); 62 | await client.send(createUserCommand); 63 | 64 | console.log("USER SUCCESSFULLY CREATED"); 65 | 66 | const paramsSetUserPassword: AdminSetUserPasswordCommandInput = { 67 | ...poolData, 68 | Username: email, 69 | Permanent: true, 70 | Password: password 71 | }; 72 | const confirmUserCommand = new AdminSetUserPasswordCommand(paramsSetUserPassword); 73 | await client.send(confirmUserCommand); 74 | 75 | console.log("USER SUCCESSFULLY CONFIRMED"); 76 | res.status(200).json({message: 'User created successfully'}); 77 | } catch (err) { 78 | console.log(err); 79 | res.status(500).json({message: 'An error occurred'}); 80 | } 81 | }; 82 | 83 | export const login = async (req: Request, res: Response) => { 84 | const {email, password} = req.body; 85 | 86 | const authParams = { 87 | ...poolData, 88 | AuthFlow: 'USER_PASSWORD_AUTH', 89 | AuthParameters: { 90 | USERNAME: email, 91 | PASSWORD: password, 92 | SECRET_HASH: "" 93 | }, 94 | }; 95 | const hash = hashSecret(clientSecret, email, clientId); 96 | if (hash && authParams.AuthParameters) { 97 | authParams.AuthParameters.SECRET_HASH = hash; 98 | } 99 | try { 100 | const initiateAuthCommand = new InitiateAuthCommand(authParams); 101 | const authResult = await client.send(initiateAuthCommand); 102 | 103 | if (!authResult.AuthenticationResult) { 104 | res.status(401).json({message: "Invalid credentials"}); 105 | return; 106 | } 107 | res.status(200).json({token: authResult.AuthenticationResult.AccessToken}); 108 | } catch (err) { 109 | console.log(err); 110 | res.status(401).json({message: 'Invalid credentials'}); 111 | } 112 | };*/ 113 | 114 | export const logout = async (req: Request, res: Response) => { 115 | const {token} = req.body; 116 | const params = { 117 | ...poolData, 118 | AccessToken: token, 119 | }; 120 | try { 121 | const globalSignOutCommand = new GlobalSignOutCommand(params); 122 | await client.send(globalSignOutCommand); 123 | res.status(200).json({message: 'User logged out successfully'}); 124 | } catch (err) { 125 | console.log(err); 126 | res.status(500).json({message: 'An error occurred'}); 127 | } 128 | }; 129 | 130 | 131 | export const verifyPassword = async (email: string, password: string): Promise => { 132 | 133 | if (!(email?.trim() && password?.trim())) { 134 | console.log("Error with email or password"); 135 | return false; 136 | } 137 | const params: InitiateAuthCommandInput = { 138 | ClientId: clientId, 139 | AuthFlow: AuthFlowType.USER_PASSWORD_AUTH, 140 | AuthParameters: { 141 | USERNAME: email, 142 | PASSWORD: password, 143 | SECRET_HASH: hashSecret(clientSecret, email, clientId) || "" 144 | }, 145 | }; 146 | try { 147 | const command = new InitiateAuthCommand(params); 148 | const output = (await client.send(command)) as InitiateAuthCommandOutput; 149 | return Boolean(output.AuthenticationResult); 150 | } catch (error) { 151 | console.log("Error authenticating user:", error); 152 | return false; 153 | } 154 | } 155 | 156 | interface UserAttributes { 157 | Name: string, 158 | Value: string 159 | } 160 | 161 | export const getUserStatus = async (email: string) => { 162 | 163 | const params: AdminGetUserCommandInput = { 164 | ...poolData, 165 | Username: email 166 | }; 167 | 168 | try { 169 | const command = new AdminGetUserCommand(params); 170 | const response = await client.send(command); 171 | 172 | // We add a workaround for users, as users who have never been showed a password 173 | // exist in Cognito (with a random password). Therefore, we introduce the 174 | // custom variable "createdByCorbado" in the user pool which is stored on 175 | // every create user event caused by the Corbado web component. 176 | 177 | if (!response || !response.UserAttributes || !Array.isArray(response.UserAttributes)) { 178 | throw new Error('Invalid response object'); 179 | } 180 | 181 | let createdByCorbado: boolean = false; 182 | 183 | 184 | response.UserAttributes.forEach((attr: AttributeType | undefined) => { 185 | if (attr && attr.Name === 'custom:createdByCorbado') { 186 | createdByCorbado = attr.Value === 'true'; 187 | } 188 | }); 189 | 190 | // if Corbado has created the user in AWS Cognito, we send back that the user 191 | // is not an existing user in the sense, he existed prior to Corbado 192 | return { 193 | userStatus: EXISTS, 194 | createdByCorbado: createdByCorbado 195 | }; 196 | } catch (error: any) { 197 | if (error.name === 'UserNotFoundException') { 198 | return { 199 | userStatus: NOT_EXISTS, 200 | createdByCorbado: false 201 | }; 202 | } else { 203 | throw error; 204 | } 205 | } 206 | } 207 | 208 | interface SessionInfo { 209 | email: string; 210 | cognitoUUID: string; 211 | name: string; 212 | expires: number; 213 | idToken: string; 214 | refreshToken: string; 215 | } 216 | 217 | 218 | export const createSession = async (username: string): Promise => { 219 | 220 | console.log("START CREATE SESSION") 221 | const params: AdminInitiateAuthCommandInput = { 222 | ...poolData, 223 | AuthFlow: AuthFlowType.CUSTOM_AUTH, 224 | ClientId: clientId, 225 | AuthParameters: { 226 | USERNAME: username, 227 | SECRET_HASH : hashSecret(clientSecret, username, clientId) || "" 228 | }, 229 | }; 230 | 231 | try { 232 | const adminInitiateAuthCommand = new AdminInitiateAuthCommand(params); 233 | const response = await client.send(adminInitiateAuthCommand); 234 | 235 | let answer = "FAILURE"; 236 | if (response.ChallengeParameters) { 237 | answer = response.ChallengeParameters.challenge; 238 | } 239 | 240 | console.log("ANSWER:", answer) 241 | const respondToAuthChallengeCommand: RespondToAuthChallengeCommandInput = { 242 | ClientId: clientId, 243 | ChallengeName: ChallengeNameType.CUSTOM_CHALLENGE, 244 | ChallengeResponses: { 245 | ANSWER: answer, 246 | USERNAME: username, 247 | SECRET_HASH : hashSecret(clientSecret, username, clientId) || "" 248 | 249 | }, 250 | Session: response.Session 251 | }; 252 | 253 | const AuthChallengeCommand = new RespondToAuthChallengeCommand(respondToAuthChallengeCommand); 254 | const authResult = await client.send(AuthChallengeCommand) as RespondToAuthChallengeCommandOutput; 255 | 256 | if (authResult?.AuthenticationResult) { 257 | const token = await validateJWT( 258 | authResult.AuthenticationResult.IdToken as string 259 | ) 260 | const user: any = token as any; 261 | 262 | return { 263 | email: user.email as string, 264 | cognitoUUID: user.sub, 265 | name: user.name, 266 | expires: user.exp, 267 | idToken: authResult.AuthenticationResult.IdToken as string, 268 | refreshToken: authResult.AuthenticationResult.RefreshToken as string, 269 | }; 270 | } else throw Error("Failed to create session"); 271 | } catch (error) { 272 | throw error; 273 | } 274 | } 275 | 276 | 277 | export const createUser = async (email: string) => { 278 | const params: AdminCreateUserCommandInput = { 279 | ...poolData, 280 | Username: email, 281 | DesiredDeliveryMediums: ["EMAIL"], 282 | UserAttributes: [ 283 | {Name: 'email', Value: email}, 284 | {Name: 'email_verified', Value: 'true'}, 285 | {Name: 'custom:createdByCorbado', Value: 'true'} 286 | ], 287 | MessageAction: MessageActionType.SUPPRESS 288 | }; 289 | try { 290 | const createUserCommand = new AdminCreateUserCommand(params); 291 | const responseCreateUserCommand = await client.send(createUserCommand); 292 | 293 | console.log("USER SUCCESSFULLY CREATED"); 294 | 295 | const setPasswordParams: AdminSetUserPasswordCommandInput = { 296 | ...poolData, 297 | Username: email, 298 | Permanent: true, 299 | Password: generatePassword(15) 300 | }; 301 | const confirmUserCommand = new AdminSetUserPasswordCommand(setPasswordParams); 302 | await client.send(confirmUserCommand); 303 | console.log("USER SUCCESSFULLY CONFIRMED"); 304 | 305 | return responseCreateUserCommand; 306 | } catch (error: any) { 307 | console.log("Error during user creation: ", error); 308 | return false; 309 | } 310 | } 311 | 312 | -------------------------------------------------------------------------------- /backend-nodejs/controllers/authCorbadoController.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from "express"; 2 | // @ts-ignore 3 | import crypto from "crypto"; 4 | // @ts-ignore 5 | import jwt from "jsonwebtoken"; 6 | import {verifyPassword, getUserStatus, createUser, createSession} from "./authCognitoController"; 7 | import {NOT_EXISTS} from "../utils/constants"; 8 | const Corbado = require('@corbado/node-sdk'); 9 | 10 | 11 | require("dotenv").config({path: '../.env' }); 12 | 13 | 14 | // Corbado Node.js SDK 15 | const CORBADO_PROJECT_ID = process.env.CORBADO_PROJECT_ID; 16 | const CORBADO_API_SECRET = process.env.CORBADO_API_SECRET; 17 | const config = new Corbado.Configuration(CORBADO_PROJECT_ID, CORBADO_API_SECRET); 18 | const corbado = new Corbado.SDK(config); 19 | 20 | 21 | export const handleWebhook = async (req: Request, res: Response) => { 22 | try { 23 | // Get the webhook action and act accordingly. Every Corbado 24 | // webhook has an action. 25 | 26 | let request: any; 27 | let response: any; 28 | console.log("BEFORE ACTION"); 29 | switch (corbado.webhooks.getAction(req)) { 30 | 31 | // Handle the "authMethods" action which basically checks 32 | // if a user exists on your side/in your database. 33 | case corbado.webhooks.WEBHOOK_ACTION.AUTH_METHODS: { 34 | console.log("WEBHOOK AUTH METHODS"); 35 | request = corbado.webhooks.getAuthMethodsRequest(req); 36 | 37 | // Now check if the given user/username exists in your 38 | // database and send status. Implement getUserStatus() 39 | // function below.# 40 | console.log("BEFORE USER STATUS"); 41 | 42 | const status = await getUserStatus(request.data.username); 43 | let correctUserStatus = status.userStatus; 44 | if(status.createdByCorbado) { 45 | correctUserStatus = "not_exists" 46 | } 47 | response = corbado.webhooks.getAuthMethodsResponse(correctUserStatus); 48 | res.json(response); 49 | break; 50 | } 51 | 52 | // Handle the "passwordVerify" action which basically checks 53 | // if the given username and password are valid. 54 | case corbado.webhooks.WEBHOOK_ACTION.PASSWORD_VERIFY: { 55 | console.log("WEBHOOK PASSWORD VERIFY"); 56 | request = corbado.webhooks.getPasswordVerifyRequest(req); 57 | 58 | // Now check if the given username and password is 59 | // valid. Implement verifyPassword() function below. 60 | const isValid = await verifyPassword(request.data.username, request.data.password) 61 | response = corbado.webhooks.getPasswordVerifyResponse(isValid); 62 | res.json(response); 63 | break; 64 | } 65 | default: { 66 | res.status(400).send('Bad Request'); 67 | return; 68 | } 69 | } 70 | } catch (error: any) { 71 | 72 | // We expose the full error message here. Usually you would 73 | // not do this (security!) but in this case Corbado is the 74 | // only consumer of your webhook. The error message gets 75 | // logged at Corbado and helps you and us debugging your 76 | // webhook. 77 | console.log(error); 78 | 79 | // If something went wrong just return HTTP status 80 | // code 500. For successful requests Corbado always 81 | // expects HTTP status code 200. Everything else 82 | // will be treated as error. 83 | 84 | res.status(500).send(error.message); 85 | return; 86 | } 87 | } 88 | 89 | export const authTokenValidate = async (req: Request, res: Response) => { 90 | console.log("AUTH TOKEN VALIDATE STARTED"); 91 | 92 | try { 93 | let corbadoAuthToken = req.query["corbadoAuthToken"] as string; 94 | let clientInfo = corbado.utils.getClientInfo(req); 95 | let corbadoUser = await corbado.authTokens.validate(corbadoAuthToken, clientInfo); 96 | let username = JSON.parse(corbadoUser.data.userData).username; 97 | const status = await getUserStatus(username); 98 | console.log("USER EXISTS: ", status.userStatus); 99 | 100 | // if the user does not yet exist in AWS Cognito, add him in AWS Cognito 101 | if (status.userStatus === NOT_EXISTS) { 102 | console.log("CREATING USER..."); 103 | await createUser(username); 104 | } 105 | 106 | // create an AWS Session 107 | console.log("GET AWS COGNITO SESSION TOKEN") 108 | let data = await createSession(username); 109 | 110 | res.json(data); 111 | } catch (error: any) { 112 | console.log(error); 113 | res.status(500).send(error.message) 114 | } 115 | }; -------------------------------------------------------------------------------- /backend-nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend-nodejs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "ts-node app.ts", 8 | "dev": "nodemon app.ts" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@aws-sdk/client-cognito-identity-provider": "^3.321.1", 14 | "@corbado/node-sdk": "^1.1.12", 15 | "amazon-cognito-identity-js": "^6.2.0", 16 | "aws-sdk": "^2.1368.0", 17 | "body-parser": "^1.20.2", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.0.3", 20 | "express": "^4.18.2", 21 | "express-validator": "^7.0.1", 22 | "jsonwebtoken": "^9.0.0", 23 | "jwk-to-pem": "^2.0.5" 24 | }, 25 | "devDependencies": { 26 | "@types/body-parser": "^1.19.2", 27 | "@types/cors": "^2.8.13", 28 | "@types/express": "^4.17.17", 29 | "@types/jsonwebtoken": "^9.0.2", 30 | "@types/jwk-to-pem": "^2.0.1", 31 | "@types/node": "^18.16.3", 32 | "nodemon": "^2.0.22", 33 | "ts-node": "^10.9.1", 34 | "typescript": "^5.0.4" 35 | }, 36 | "description": "" 37 | } 38 | -------------------------------------------------------------------------------- /backend-nodejs/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "target": "es5", 8 | "typeRoots": [ 9 | "node_modules/@types" 10 | ], 11 | "types": [ 12 | "node" 13 | ] 14 | }, 15 | "exclude": [ 16 | "test.ts", 17 | "**/*.spec.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /backend-nodejs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "CommonJS", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /backend-nodejs/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const EXISTS = 'exists'; 2 | export const NOT_EXISTS = 'not_exists'; 3 | export const BLOCKED = 'blocked'; 4 | -------------------------------------------------------------------------------- /backend-nodejs/utils/helper.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import crypto from "crypto"; 3 | // @ts-ignore 4 | import jwt from "jsonwebtoken"; 5 | import {int} from "aws-sdk/clients/datapipeline"; 6 | // @ts-ignore 7 | import jwkToPem, {JWK} from "jwk-to-pem"; 8 | 9 | require("dotenv").config({path: '../.env' }); 10 | 11 | const clientId = process.env.COGNITO_CLIENT_ID; 12 | const region = process.env.COGNITO_REGION; 13 | const envJWKS = process.env.COGNITO_JWKS; 14 | const userPoolId = process.env.COGNITO_USER_POOL_ID; 15 | const jwks: JWK[] | any[] = JSON.parse(envJWKS as string); 16 | 17 | export function hashSecret(clientSecret: string, username: string, clientId: string) { 18 | if (!clientSecret) { 19 | return null; 20 | } 21 | return crypto 22 | .createHmac("SHA256", clientSecret) 23 | .update(username + clientId) 24 | .digest("base64"); 25 | } 26 | 27 | export function validateJWT(jwtToken: string, skipExpiredCheck?: boolean) { 28 | let res; 29 | try { 30 | let pem = jwkToPem(jwks[0]); 31 | res = jwt.verify(jwtToken, pem, {algorithms: jwks[0].alg}); 32 | } catch (error) { 33 | let pem = jwkToPem(jwks[1]); 34 | res = jwt.verify(jwtToken, pem, {algorithms: jwks[1].alg}); 35 | } 36 | 37 | if (!jwtToken.trim()) { 38 | console.log("Error with JWT"); 39 | return; 40 | } 41 | let decoded = jwt.decode(jwtToken); 42 | const now = +new Date() / 1000; 43 | // @ts-ignore 44 | if (!skipExpiredCheck && decoded.exp < now) { 45 | console.log("Token expired"); 46 | return; 47 | } 48 | // @ts-ignore 49 | if (decoded.aud !== clientId) { 50 | console.log("Invalid audience in token"); 51 | return; 52 | } 53 | // @ts-ignore 54 | if (decoded.iss !== `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`) { 55 | console.log("Invalid iss in token"); 56 | return; 57 | } 58 | return res; 59 | } 60 | 61 | export function generatePassword(length: int) { 62 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+~`|}{[]:;?><,./-="; 63 | let password = ""; 64 | for (let i = 0; i < length; i++) { 65 | const randomIndex = Math.floor(Math.random() * charset.length); 66 | password += charset[randomIndex]; 67 | } 68 | return password; 69 | }; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | frontend: 4 | build: 5 | context: ./frontend-angular 6 | dockerfile: Dockerfile 7 | volumes: 8 | - './frontend-angular:/app' 9 | ports: 10 | - '4200:4200' 11 | backend: 12 | build: 13 | context: ./backend-nodejs 14 | dockerfile: Dockerfile 15 | volumes: 16 | - './backend-nodejs:/app' 17 | ports: 18 | - '3000:3000' 19 | environment: 20 | - COGNITO_REGION=${COGNITO_REGION} 21 | - COGNITO_USER_POOL_ID=${COGNITO_USER_POOL_ID} 22 | - COGNITO_CLIENT_ID=${COGNITO_CLIENT_ID} 23 | - COGNITO_CLIENT_SECRET=${COGNITO_CLIENT_SECRET} 24 | - COGNITO_JWKS=${COGNITO_JWKS} 25 | - CORBADO_PROJECT_ID=${CORBADO_PROJECT_ID} 26 | - CORBADO_API_SECRET=${CORBADO_API_SECRET} 27 | - CORBADO_CLI_SECRET=${CORBADO_CLI_SECRET} 28 | - CORBADO_WEBHOOK_USERNAME=${CORBADO_WEBHOOK_USERNAME} 29 | - CORBADO_WEBHOOK_PASSWORD=${CORBADO_WEBHOOK_PASSWORD} 30 | - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} 31 | - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} -------------------------------------------------------------------------------- /frontend-angular/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /frontend-angular/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /frontend-angular/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as a parent image 2 | FROM node:16-alpine 3 | 4 | # Set the working directory to /app 5 | WORKDIR /app 6 | 7 | # Copy the package.json and package-lock.json files to the container 8 | COPY package*.json ./ 9 | 10 | # Expose the port used by the frontend 11 | EXPOSE 4200 12 | 13 | # Start the frontend using the "ng serve" command 14 | CMD ["/bin/sh", "-c", "npm install && npm install -g @angular/cli && ng serve --host 0.0.0.0"] 15 | -------------------------------------------------------------------------------- /frontend-angular/README.md: -------------------------------------------------------------------------------- 1 | # FrontendAngular 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.2.7. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /frontend-angular/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "frontend-angular": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/frontend-angular", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": [ 24 | "zone.js" 25 | ], 26 | "tsConfig": "tsconfig.app.json", 27 | "inlineStyleLanguage": "scss", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "src/styles.scss" 34 | ], 35 | "scripts": [] 36 | }, 37 | "configurations": { 38 | "production": { 39 | "budgets": [ 40 | { 41 | "type": "initial", 42 | "maximumWarning": "500kb", 43 | "maximumError": "1mb" 44 | }, 45 | { 46 | "type": "anyComponentStyle", 47 | "maximumWarning": "2kb", 48 | "maximumError": "4kb" 49 | } 50 | ], 51 | "outputHashing": "all" 52 | }, 53 | "development": { 54 | "buildOptimizer": false, 55 | "optimization": false, 56 | "vendorChunk": true, 57 | "extractLicenses": false, 58 | "sourceMap": true, 59 | "namedChunks": true 60 | } 61 | }, 62 | "defaultConfiguration": "production" 63 | }, 64 | "serve": { 65 | "builder": "@angular-devkit/build-angular:dev-server", 66 | "configurations": { 67 | "production": { 68 | "browserTarget": "frontend-angular:build:production" 69 | }, 70 | "development": { 71 | "browserTarget": "frontend-angular:build:development" 72 | } 73 | }, 74 | "defaultConfiguration": "development" 75 | }, 76 | "extract-i18n": { 77 | "builder": "@angular-devkit/build-angular:extract-i18n", 78 | "options": { 79 | "browserTarget": "frontend-angular:build" 80 | } 81 | }, 82 | "test": { 83 | "builder": "@angular-devkit/build-angular:karma", 84 | "options": { 85 | "polyfills": [ 86 | "zone.js", 87 | "zone.js/testing" 88 | ], 89 | "tsConfig": "tsconfig.spec.json", 90 | "inlineStyleLanguage": "scss", 91 | "assets": [ 92 | "src/favicon.ico", 93 | "src/assets" 94 | ], 95 | "styles": [ 96 | "src/styles.scss" 97 | ], 98 | "scripts": [] 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /frontend-angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-angular", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^15.2.0", 14 | "@angular/common": "^15.2.0", 15 | "@angular/compiler": "^15.2.0", 16 | "@angular/core": "^15.2.0", 17 | "@angular/forms": "^15.2.0", 18 | "@angular/platform-browser": "^15.2.0", 19 | "@angular/platform-browser-dynamic": "^15.2.0", 20 | "@angular/router": "^15.2.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.12.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^15.2.7", 27 | "@angular/cli": "~15.2.7", 28 | "@angular/compiler-cli": "^15.2.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.5.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.1.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~4.9.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { AuthComponent } from './auth/auth.component'; 5 | import { LoggedInComponent } from './logged-in/logged-in.component'; 6 | 7 | const routes: Routes = [ 8 | { path: '', redirectTo: '/auth', pathMatch: 'full' }, 9 | { path: 'auth', component: AuthComponent }, 10 | { path: 'logged-in', component: LoggedInComponent } 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forRoot(routes)], 15 | exports: [RouterModule] 16 | }) 17 | export class AppRoutingModule { } 18 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corbado/passkeys-amazon-cognito/c0bb18986424f7d4e3b44bdfe59a7ad26b47441f/frontend-angular/src/app/app.component.scss -------------------------------------------------------------------------------- /frontend-angular/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'frontend-angular'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('frontend-angular'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('frontend-angular app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'frontend-angular'; 10 | } 11 | -------------------------------------------------------------------------------- /frontend-angular/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { FormsModule } from '@angular/forms'; 4 | import {HttpClientModule} from "@angular/common/http"; 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { AuthComponent } from './auth/auth.component'; 8 | import { LoggedInComponent } from './logged-in/logged-in.component'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | AppComponent, 13 | AuthComponent, 14 | LoggedInComponent 15 | ], 16 | imports: [ 17 | BrowserModule, 18 | FormsModule, 19 | AppRoutingModule, 20 | HttpClientModule 21 | ], 22 | providers: [], 23 | bootstrap: [AppComponent], 24 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 25 | }) 26 | export class AppModule { } 27 | -------------------------------------------------------------------------------- /frontend-angular/src/app/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /frontend-angular/src/app/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {Router} from '@angular/router'; 4 | import {map, Observable, Subject} from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthService { 10 | 11 | private isAuthenticated = false; 12 | private authStatusListener = new Subject(); 13 | private token: string = ''; 14 | private userId: string = ''; 15 | 16 | constructor(private http: HttpClient, private router: Router) { 17 | } 18 | 19 | getToken() { 20 | return this.token; 21 | } 22 | 23 | getUserId() { 24 | return this.userId; 25 | } 26 | 27 | getAuthStatusListener() { 28 | return this.authStatusListener.asObservable(); 29 | } 30 | 31 | signUp(email: string, password: string): Observable { 32 | const signUpData = {email: email, password: password}; 33 | return this.http.post<{ message: string }>('http://localhost:3000/api/auth/signup', signUpData); 34 | } 35 | 36 | login(email: string, password: string): Observable { 37 | const loginData = {email: email, password: password}; 38 | return this.http.post<{ token: string }>('http://localhost:3000/api/auth/login', loginData) 39 | .pipe( 40 | map(responseData => { 41 | const token = responseData.token; 42 | this.token = token; 43 | console.log(responseData) 44 | if (token) { 45 | this.isAuthenticated = true; 46 | this.authStatusListener.next(true); 47 | this.router.navigate(['/logged-in']); 48 | } 49 | }) 50 | ); 51 | } 52 | 53 | logout() { 54 | this.token = ''; 55 | this.isAuthenticated = false; 56 | this.authStatusListener.next(false); 57 | this.router.navigate(['/login']); 58 | } 59 | 60 | getIsAuth() { 61 | return this.isAuthenticated; 62 | } 63 | 64 | async corbadoAuthTokenValidate(corbadoAuthToken: string) { 65 | try { 66 | this.http.get<{ idToken: string }>(`http://localhost:3000/api/corbado/authTokenValidate?corbadoAuthToken=${corbadoAuthToken}`) 67 | .subscribe(responseData => { 68 | const token = responseData.idToken; 69 | this.token = token; 70 | if (token) { 71 | this.isAuthenticated = true; 72 | this.authStatusListener.next(true); 73 | this.router.navigate(['/logged-in']); 74 | } 75 | }); 76 | } catch (error) { 77 | console.error(error); 78 | throw error; 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /frontend-angular/src/app/auth/auth.component.html: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /frontend-angular/src/app/auth/auth.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corbado/passkeys-amazon-cognito/c0bb18986424f7d4e3b44bdfe59a7ad26b47441f/frontend-angular/src/app/auth/auth.component.scss -------------------------------------------------------------------------------- /frontend-angular/src/app/auth/auth.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthComponent } from './auth.component'; 4 | 5 | describe('AuthComponent', () => { 6 | let component: AuthComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AuthComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(AuthComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend-angular/src/app/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ActivatedRoute, Router} from '@angular/router'; 3 | import {AuthService} from '../auth.service'; 4 | import {Subscription} from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'app-auth', 8 | templateUrl: './auth.component.html', 9 | styleUrls: ['./auth.component.scss'] 10 | }) 11 | export class AuthComponent implements OnInit { 12 | 13 | email = ''; 14 | password = ''; 15 | errorMessage = ''; 16 | queryParamsSubscription!: Subscription; 17 | 18 | constructor(private authService: AuthService, private router: Router, private route: ActivatedRoute) { 19 | this.queryParamsSubscription = this.route.queryParams.subscribe((queryParams) => { 20 | if (queryParams['corbadoAuthToken'] != undefined) { 21 | let corbadoAuthToken = queryParams['corbadoAuthToken']; 22 | this.authService.corbadoAuthTokenValidate(corbadoAuthToken) 23 | .then(res => { 24 | router.navigate(['/logged-in']) 25 | }) 26 | .catch(error => console.log(error)); 27 | } 28 | }) 29 | } 30 | 31 | ngOnInit(): void { 32 | } 33 | 34 | signUp(): void { 35 | this.authService.signUp(this.email, this.password).subscribe( 36 | (response) => { 37 | console.log(response); 38 | this.router.navigate(['/logged-in']); 39 | }, 40 | (error) => { 41 | console.log(error); 42 | this.errorMessage = error.error.message; 43 | } 44 | ); 45 | } 46 | 47 | login(): void { 48 | this.authService.login(this.email, this.password).subscribe( 49 | (response) => { 50 | console.log(response); 51 | this.router.navigate(['/logged-in']); 52 | }, 53 | (error) => { 54 | console.log(error); 55 | this.errorMessage = error.error.message; 56 | } 57 | ); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /frontend-angular/src/app/logged-in/logged-in.component.html: -------------------------------------------------------------------------------- 1 |

Welcome!

2 | 3 |

You are now logged in.

4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend-angular/src/app/logged-in/logged-in.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corbado/passkeys-amazon-cognito/c0bb18986424f7d4e3b44bdfe59a7ad26b47441f/frontend-angular/src/app/logged-in/logged-in.component.scss -------------------------------------------------------------------------------- /frontend-angular/src/app/logged-in/logged-in.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoggedInComponent } from './logged-in.component'; 4 | 5 | describe('LoggedInComponent', () => { 6 | let component: LoggedInComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoggedInComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(LoggedInComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend-angular/src/app/logged-in/logged-in.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | import {AuthService} from '../auth.service'; 4 | 5 | @Component({ 6 | selector: 'app-logged-in', 7 | templateUrl: './logged-in.component.html', 8 | styleUrls: ['./logged-in.component.scss'] 9 | }) 10 | export class LoggedInComponent implements OnInit { 11 | 12 | constructor(private authService: AuthService, private router: Router) { 13 | } 14 | 15 | ngOnInit(): void { 16 | } 17 | 18 | logout(): void { 19 | this.authService.logout(); 20 | this.router.navigate(['/auth']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend-angular/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corbado/passkeys-amazon-cognito/c0bb18986424f7d4e3b44bdfe59a7ad26b47441f/frontend-angular/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend-angular/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corbado/passkeys-amazon-cognito/c0bb18986424f7d4e3b44bdfe59a7ad26b47441f/frontend-angular/src/favicon.ico -------------------------------------------------------------------------------- /frontend-angular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FrontendAngular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend-angular/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /frontend-angular/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /frontend-angular/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /frontend-angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend-angular/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------