Amazon Cognito Passwordless Authentication

repository·main·Indexed 19 days ago

https://github.com/aws-samples/amazon-cognito-passwordless-auth

An AWS solution providing passwordless authentication for Amazon Cognito using custom authentication flows. It supports FIDO2 (WebAuthn/Passkeys), Magic Links, and SMS OTP Step-Up. The package includes a Passwordless CDK construct for backend infrastructure and client libraries for Web, React, and React Native, with built-in compatibility for aws-amplify.

Tokens
18.5K
Snippets
44
Records
70
Agent score
65%

What's inside amazon-cognito-passwordless-auth

  1. Components of the End-to-end Example

    main

    The end-to-end example demonstrates the integration of the following core components:

    • Passwordless CDK construct: A high-level construct used to provision the backend infrastructure.
    • React hooks and components: Prebuilt UI elements and logic for handling passwordless authentication in a web application.
    • Web Hosting: A sample web application hosted using Amazon CloudFront and Amazon S3.
  2. How Magic Links work in this solution

    main

    This solution implements a passwordless authentication flow using Amazon Cognito's Custom Authentication flow. The process relies on several AWS services to ensure security and usability:

    • Amazon Cognito: Manages the custom authentication flow (DefineAuth, CreateAuthChallenge, VerifyAnswer).
    • AWS Lambda: Implements the Cognito triggers to handle the logic of the custom flow.
    • Amazon SES: Sends the magic link via email to the user.
    • Amazon DynamoDB: Stores cryptographic hashes of the magic links to enforce security constraints:
      • Ensures a magic link can only be used once.
      • Limits users to a maximum of one outstanding unused magic link.
      • Enforces a minimum one-minute wait time between magic link requests.
    • AWS KMS: Uses an asymmetric key to sign magic links. This allows the system to store only non-sensitive metadata in DynamoDB, preventing an attacker with DynamoDB access from signing in as users.
    • Front End Libraries: Provided for Web, React, and React Native to interact with this custom flow.
  3. Sequence: Requesting a Magic Link

    main

    The following sequence describes how a user initiates the magic link request:

    1. User enters their username and clicks sign-in in the Browser JavaScript.
    2. Browser JavaScript initiates CUSTOM_AUTH with Cognito.
    3. Cognito invokes DefineAuth and CreateAuthChallenge.
    4. Cognito sends a PROVIDE_AUTH_PARAMETERS challenge to the Browser JavaScript.
    5. Browser JavaScript responds to the challenge by sending a send link request to Cognito.
    6. Cognito invokes VerifyAnswer (which returns null) and then DefineAuth.
    7. Cognito invokes CreateAuthChallenge, which:
      • Checks DynamoDB for existing magic link metadata.
      • Signs the message using AWS KMS.
      • Stores the new metadata in DynamoDB.
      • Sends the email via Amazon SES.
    8. Cognito issues a MAGIC_LINK challenge to the Browser JavaScript.
    9. Browser JavaScript stores the session in Browser Storage and notifies the user.
  4. How FIDO2 authentication works in this solution

    main

    This solution implements FIDO2 authentication (e.g., FaceID, TouchID, YubiKey) using a combination of AWS services and client-side libraries.

    Key components include:

    • Amazon DynamoDB: Stores FIDO2 credentials (public keys) along with metadata like friendly names (e.g., "My iPhone"), last used date, and usage count.
    • Amazon REST API: Provides endpoints to create, update, and delete FIDO2 credentials. This API is protected by a Cognito User Pools authorizer, requiring an existing session (e.g., via Magic Link) to register new credentials. The sign-in-challenge resource is public to support "usernameless" flows.
    • AWS Lambda: Implements the Amazon Cognito Custom Authentication flow by reading public keys from DynamoDB to verify FIDO2 signatures.
    • Front End Libraries: Provided for Web, React, and React Native to interact with the Custom Auth flow.
    • React Components: Pre-built components are available to manage (add/update/delete) authenticators.
  5. Implement Step-up Authentication with SMS OTP

    main

    Step-up authentication allows a user to perform a high-security action (like a banking transaction) by re-verifying their identity via an SMS One-Time-Password (OTP) after they have already signed in via a primary method (e.g., Magic Link).

    Core Components

    • AWS Lambda Functions: Implement the Amazon Cognito Custom Authentication flow. They use Amazon Simple Notification Service (SNS) to deliver the OTP via SMS.
    • JWT Verification: Uses the aws-jwt-verify library within the Lambda triggers to ensure the user possesses a valid existing JWT, proving the request is a legitimate step-up attempt.
    • Client Libraries: Front-end functions are available for Web, React, and React Native to interact with this custom flow.

    Customization

    To customize the authentication beyond the standard Passwordless construct (for example, to use your own SMS templates), refer to the CUSTOMIZE-AUTH.md guide.

  6. Sequence: Completing Sign-in (Same Browser, New Tab)

    main

    When a user opens the magic link in the same browser where they initiated the request (e.g., a new tab):

    1. User opens the magic link.
    2. Browser JavaScript loads the existing session from Browser Storage and then deletes it.
    3. Browser JavaScript responds to the MAGIC_LINK challenge by sending a secret hash to Cognito.
    4. Cognito invokes VerifyAnswer, which:
      • Deletes the magic-link metadata from DynamoDB.
      • Downloads the public key from AWS KMS.
      • Verifies the signature and checks the username, expiry, and issuedAt timestamp.
    5. Cognito invokes DefineAuth to succeed the authentication.
    6. Cognito returns the JWTs to Browser JavaScript.
    7. Browser JavaScript stores the JWTs in Browser Storage and completes the sign-in.
  7. Sequence: Completing Sign-in (Different Browser)

    main

    When a user opens the magic link in a different browser or device than the one used to request it:

    1. User opens the magic link.
    2. Browser JavaScript attempts to load a session from Browser Storage but finds nothing (null).
    3. Browser JavaScript must initiate a new CUSTOM_AUTH flow with Cognito.
    4. Cognito invokes DefineAuth and CreateAuthChallenge to provide a PROVIDE_AUTH_PARAMETERS challenge.
    5. Browser JavaScript responds to the challenge by sending the secret hash (extracted from the link) to Cognito.
    6. Cognito invokes VerifyAnswer, which:
      • Deletes the magic-link metadata from DynamoDB.
      • Downloads the public key from AWS KMS.
      • Verifies the signature and checks the username, expiry, and issuedAt timestamp.
    7. Cognito invokes DefineAuth to succeed the authentication.
    8. Cognito returns the JWTs to Browser JavaScript.
    9. Browser JavaScript stores the JWTs in Browser Storage and completes the sign-in.
  8. Sequence diagram for usernameless FIDO2 authentication

    main

    The following sequence describes the interaction between the Browser, the REST API (Lambda), Amazon Cognito, and DynamoDB during a usernameless passkey sign-in flow:

    1. User clicks "Sign in with passkey".
    2. Browser JS calls POST /sign-in-challenge on the REST API.
    3. API generates a challenge, stores it in DynamoDB, and returns it to the Browser JS.
    4. Browser JS calls navigator.credentials.get() via the Browser WebAuthn core.
    5. User performs a gesture (touch/face) to authorize.
    6. WebAuthn core returns the FIDO2 signature response to Browser JS.
    7. Browser JS calls InitiateAuth with Cognito.
    8. Cognito invokes DefineAuthChallenge and CreateAuthChallenge.
    9. CreateAuthChallenge queries DynamoDB for credential IDs and returns the FIDO2 challenge/options to the Browser JS.
    10. Browser JS calls RespondToAuthChallenge.
    11. Cognito invokes VerifyAnswer.
    12. VerifyAnswer checks the challenge in DynamoDB (performing an atomic read+delete for replay protection), retrieves the public key from DynamoDB, and verifies the signature.
    13. Cognito returns JWTs to the Browser JS, which stores them in Browser Storage.
    sequenceDiagram
        autonumber
        actor User
        participant BJS as Browser JavaScript
        participant BLS as Browser Storage
        participant BC as Browser WebAuthn core
        participant API as REST API+Lambda
        participant C as Cognito
        participant DA as DefineAuth
        participant CA as CreateAuthChallenge
        participant VA as VerifyAnswer
        participant DB as DynamoDB
        User->>BJS: Open web app
        Activate User
        Activate BJS
        BJS->>User: Show "Sign in with passkey" button
        Deactivate BJS
        BJS->>User: Click "Sign in with passkey"
        Activate BJS
        BJS->>API: POST /sign-in-challenge
        Activate API
        API->>API: Generate challenge
        API->>DB: Store challenge
        Activate DB
        DB->>API: Ok
        Deactivate DB
        API->>BJS: Challenge
        BJS->>BC: navigator.credentials.get()
        Activate BC
        BC->>User: Show sign-use-authenticator native dialog
        User->>BC: Execute gesture (e.g. touch, face)
        BC->>BJS: FIDO2 signature response
        Deactivate BC
        BJS->>C: InitiateAuth
        Activate C
        C->>DA: Invoke
        Activate DA
        DA->>C: Custom challenge
        Deactivate DA
        C->>CA: Invoke
        Activate CA
        CA->>CA: Generate challenge
        CA->>DB: Query credential IDs
        Activate DB
        DB->>CA: Credential IDs
        Deactivate DB
        CA->>C: FIDO2 challenge, credential IDs, options
        Deactivate CA
        C->>BJS: FIDO2 challenge, credential IDs, options
        Deactivate C
        BJS->>C: RespondToAuthChallenge
        Activate C
        C->>VA: Invoke
        Activate VA
        VA->>VA: Verify client data
        VA->>DB: Check challenge exists (and delete if so)
        Activate DB
        DB->>VA: OK
        Deactivate DB
        VA->>DB: Get credential public key
        Activate DB
        DB->>VA: Credential public key
        Deactivate DB
        VA->>VA: Verify signature
        VA->>C: Answer correct: true
        Deactivate VA
        C->>DA: Invoke
        Activate DA
        DA->>C: Succeed Auth
        Deactivate DA
        C->> BJS: JWTs
        Deactivate C
        BJS->>BLS: Store JWTs
        Activate BLS
        BJS->>BLS: OK
        Deactivate BLS
        BJS->>User: "You are signed in"
        Deactivate BJS
        Deactivate User
  9. Security and Ownership Considerations

    main

    Security Disclaimer

    This project is provided as sample code. While it was developed by Amazon Cognito experts and has undergone internal reviews and pentesting by Amazon's application security team, users are responsible for reviewing the solution and determining its suitability for their specific security posture.

    Ownership

    Because this is hosted in aws-samples, it is intended to be used as a starting point. If you integrate this into a production environment, you should be prepared to maintain and own your own fork of the repository.

  10. React Native Authentication Limitations

    main

    When using the React Native client, be aware of the following current limitations:

    • Unsupported Methods:
      • Magic Links
      • Username/Password authentication with Secure Remote Password (SRP)
    • Supported Password Method: Only username/password authentication with plaintext password is supported.
    • Passkey Support:
      • Natively supported on iOS (requires iOS 16.0+ and Associated Domains setup).
      • Android support is expected soon.
  11. Customize Lambda logic using the configure() method

    main

    You can extend the existing custom authentication challenge Lambda triggers (Magic Links, FIDO2, or SMS OTP) by using the configure() method provided by the library. This allows you to inject custom logic—such as custom email templates or different email providers—without rewriting the entire authentication flow.

    To use this approach:

    1. Import the specific auth module (e.g., magicLink) from amazon-cognito-passwordless-auth/custom-auth.
    2. Export the library's createAuthChallengeHandler as your Lambda handler.
    3. Call configure() to override specific properties like contentCreator or emailSender.
    4. The library will merge your custom logic with its internal createAuthChallenge function.
    import { magicLink } from "amazon-cognito-passwordless-auth/custom-auth";
    
    // Export the solution's handler to be the handler of YOUR Lambda function too:
    export { createAuthChallengeHandler as handler } from "amazon-cognito-passwordless-auth/custom-auth";
    
    // Calling configure() without arguments retrieves the current configuration:
    const defaultConfig = magicLink.configure();
    
    // Add your own logic:
    magicLink.configure({
      async contentCreator({ secretLoginLink }) {
        return {
          html: {
            data: `<html><body><p>Your secret sign-in link: <a href="${secretLoginLink}">sign in</a></p>This link is valid for ${Math.floor(
              defaultConfig.secondsUntilExpiry / 60
            )} minutes<p></p></body></html>`,
            charSet: "UTF-8",
          },
          text: {
            data: `Your secret sign-in link: ${secretLoginLink}`,
            charSet: "UTF-8",
          },
          subject: {
            data: "Your secret sign-in link",
            charSet: "UTF-8",
          },
        };
      },
    });
  12. Install the Amazon Cognito Passwordless Auth React Native client

    main

    To use this library in a React Native project, you must install the core package along with two mandatory peer dependencies: react-native-passkey (for FIDO2/Passkey support) and @react-native-async-storage/async-storage (to persist the authentication session).

    # Install the core library
    npm install amazon-cognito-passwordless-auth
    
    # Install mandatory peer dependency for Passkeys
    npm install react-native-passkey@^2.1.1
    
    # Install mandatory peer dependency for storage
    npm install @react-native-async-storage/async-storage