Front-End Web & Mobile

Build a Mobile App with Passwordless Login on top of AWS Amplify

June 27, 2024: This blog post covers Amplify Gen 1. For new Amplify apps, we recommend using Amplify Gen 2. You can learn more about Gen 2 in our launch blog post.

Note: The Amazon Cognito hosted UI (Web) does not support the custom authentication flow.

According to the Gartner Group , 20% to 50% of all IT help desk tickets each year are password resets, meaning the average managed service provider (MSP) with 1,300 users wastes around $7.19 per user, adding up to an annual cost of $9,350. In a 2021 whitepaper, Auth0, an identity management platform for application builders and developers, stated that each password reset costs a company ~$70.

There is more to the story: Verizon’s 2022 Data Breach Investigations Report (DBIR) cites that over 80% of breaches are attributed to stolen credentials. And since 2017, there has been a steady increase in stolen credentials by roughly 30%. The Fast Identity Online (FIDO) Alliance, an open industry association with a stated mission to reduce an over-reliance on passwords, reports that users have over 90 online accounts of which up to 51% of user’s passwords are reused. What’s more, passwords alone don’t prove the user’s identity when they log in to a system or service. They act as a proxy for the user’s identity. That means that a compromised password is a compromised identity. With this information in mind, one begins to wonder if there is a better way to securely log in to systems and services.

Passwordless is a secure authentication method that allows users to log into a service or system without having to remember a password. To log in to a system or service using this method, a user needs to provide a form of authentication such as an email address, phone number or a biometric element (e.g. finger print or facial recognition). The authentication process completes when the user provides a registered device or token. Passwordless logins provide users with a frictionless login experience, while reducing administrative burden and overall security risks for the user and the website administrators. But implementing passwordless login flows can be tedious, especially when scaling to large user groups.

There are three primary types of passwordless authentication:

  1. Email – user receives a unique code or a unique URL via email, as part of the  log-in process
  2. SMS – user receives a unique code to their cell phone to get a unique onetime code to log-in
  3. Biometric – where fingerprint scanning, iris scanning, or face-scanning used to authenticate a user

Overview of Solution

In this blog post, we will work through a solution that demonstrates how to create a passwordless login. You will be using SwiftUI in this project. The diagram, below, illustrates the steps that will use to build out our passwordless log-in project.

Figure 1 – The steps described below inform us on each step that is taken to authenticate the user into our project:

  1. Amplify– Our Authentication Challenge is built and resides here. After a user enters their email address to sign-in, AWS Amplify triggers the AWS Lambda function

a. The user enters an email address in the custom sign-in page; this creates an identity that gets sent to AWS Amplify.

b.  Amazon Cognito invokes the Verify Auth Challenge trigger to verify a valid response from the Auth challenge.

  1. AWS Lambda – Once the AWS Lambda function is triggered, an Authentication Challenge is presented to the user
  2. Ability for AWS Amplify application/ iOS can now call other AWS Services

Custom Authentication Challenge with Lambda

In Step 1, the authentication challenge is built and resides in AWS Amplify. In step 2, the AWS Lambda function is triggered. When the AWS Lambda Function is triggered, it will prompt the user to enter an email address in the custom sign-in page. The result is this creates an identity that gets sent back to AWS Amplify. Then, Amazon Cognito will invoke the verify authorization challenge. This will verify a valid response from the user's authentication challenge answer that they answered. In step 3, the final step, if the authentication challenge is answered correctly, then the ability for the AWS Amplify application to call other AWS services is enabled. If the challenge is answered incorrectly, an error message is presented to the user.

Figure 1. overview of solution diagram

Figure 2 – To Create a Custom Authentication Flow: This Lambda triggers, issues, and verifies their own challenges as part of a user pool custom authentication flow. The three stages are:

  1. Define Auth challenge Lambda trigger – Cognito invokes this trigger to start the custom authentication flow.
  2. Create Auth challenge Lambda trigger – Cognito invokes this trigger after Define Auth Challenge to create a custom challenge.
  3. Verify Auth challenge response Lambda trigger – Cognito invokes this trigger to verify, if the response from the end user for a custom challenge is valid or not.

*Note that the Amazon Cognito hosted UI (Web) does not support the custom authentication flow.

AWS Amplify

The Amplify Framework provides CLI, Libraries and UI Components to build full stack iOS, Android, Flutter, and React Native applications. AWS Amplify UI announced a new version of the Authenticator component for mobile apps, giving developers the easiest way to add login experiences to their app with a single line of code.

The newest version of the Amplify UI library ships with connected components such as the Authentication to quickly set up secure authentication flows with a fully managed user directory. Amplify Auth allows control over what users have access to in your mobile. This gives you a single library to build your entire app user interface, leveraging our connected and primitive components.

The Auth category can be configured to perform a custom authentication flow defined by you. The following guide shows how to setup a simple passwordless authentication flow.

Add authentication

  • Define the authentication challenge
  • Register a user
  • Sign in a user
  • Confirm sign in with custom challenge
  • Lambda Trigger Setup.

The CLI creates a custom authentication flow skeleton you can manually edit.

Implement the Solution

Prerequisites

Before you begin, make sure you have the following components installed:

* Amplify can be installed through the Swift Package Manager, which is integrated into Xcode.

Steps

Amazon SES Setup

Amazon Simple Email Service (SES) is an email platform that lets you send and receive emails using your email addresses and domains. We’ll use it in this project to send a challenge code to the user. Follow these steps to set up Amazon SES to use in our project:

  1. Log in to your Amazon account. In the console search for Amazon SES and select it. I then brought you to the Amazon SES Homepage (shown below). Click the orange button, “Create identity”.
After logging in to your Amazon account, perform a search in the console for Amazon SES. Select Amazon SES in the search result. After, Amazon SES is selected, it will bring you to the Amazon SES homepage. On the Amazon SES homepage, select the orange button "create identity". This button is located on the upper right portion of the Amazon SES home screen.

Figure 2. Amazon SES Console home page

  1. Next, click the radio button, ‘“Email address” and enter the email address you want to use to set up and verify sender identity. Once you do this, click the orange button, “Create identity” which is found at the bottom of the screen.
After you have selected the create identity page on the Amazon SES home page, you will be brought to the create identity page. On the upper left portion of the create identity page, select the radio button email address. Then, enter your email address in the text box entitled, "Email address" The email address text box is located below the email address selection. Finally, click the next button. The next button is located near the bottom of the screen. On the next screen, a notice will present itself to confirm that you created an Amazon SES identity but that the verification status is pending. This is your prompt to go to your email address and find an email from Amazon Web Services. In that email, click the URL provided in the email body to verify your identity.

Figure 3. Create identity screen shot

  1. On the next screen, you see that Amazon SES created the identity but the verification status will be pending. Go to your selected email to find the email from Amazon Web Services. Click on the URL provided in the email body to verify identity.
  2. After you verify your identity, you will see a congratulations message, as shown below. Now you are ready to continue with configuring our Auth category.
 After you completed the previous step to verify your email address with Amazon Web Services, you will be brought to a Congratulations page. This page will confirm that you have successfully verified an email address. This means that you can start sending email from the verified email address. Now, you are ready to continue with configuring the authentication category.

Figure 4: Congratulations page.

Configure Auth Category

In terminal, navigate to your project, run amplify add auth, and choose the following options:

? Do you want to use the default authentication and security configuration? Manual configuration?
    `Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or other content, Analytics, and more)`
? Please provide a friendly name for your resource that will be used to label this category in the project:
    `<hit enter to take default or enter a custom label>`
? Please enter a name for your identity pool.
    `<hit enter to take default or enter a custom name>`
? Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM)
    `No`
? Do you want to enable 3rd party authentication providers in your identity pool?
    `No`
? Please provide a name for your user pool:
    `<hit enter to take default or enter a custom name>`
? How do you want users to be able to sign in?
    `Username`
? Do you want to add User Pool Groups?
    `No`
? Do you want to add an admin queries API?
    `No`
? Multifactor authentication (MFA) user login options:
    `OFF`
? Email based user registration/forgot password:
    `Enabled (Requires per-user email entry at registration)`
? Please specify an email verification subject:
    `Your verification code`
? Please specify an email verification message:
    `Your verification code is {####}`
? Do you want to override the default password policy for this User Pool?
    `No`
? What attributes are required for signing up?
    `Email`
? Specify the apps refresh token expiration period (in days):
    `30`
? Do you want to specify the user attributes this app can read and write?
    `No`
? Do you want to enable any of the following capabilities?
    `NA`
? Do you want to use an OAuth flow?
    `No`
? Do you want to configure Lambda Triggers for Cognito?
    `Yes`
? Which triggers do you want to enable for Cognito?
    `Create Auth Challenge, Define Auth Challenge, Verify Auth Challenge Response`
? What functionality do you want to use for Create Auth Challenge?
    `Custom Auth Challenge Scaffolding (Creation)`
? What functionality do you want to use for Define Auth Challenge?
    `Custom Auth Challenge Scaffolding (Definition)`
? What functionality do you want to use for Verify Auth Challenge Response?
    `Custom Auth Challenge Scaffolding (Verification)`

? Do you want to edit your boilerplate-create-challenge function now?
    `Yes`
? Please edit the file in your editor: <local file path>/src/boilerplate-create-challenge.js
The boilerplate for Create Auth Challenge opens in your favorite code editor. Enter the following code to this file:
// https://nodejs.org/api/crypto.html#crypto_crypto_randomint_min_max_callback
const {
  randomInt
} = await import('node:crypto');

// Use SES or custom logic to send the secret code to the user.
function sendChallengeCode(emailAddress, secretCode) {
const params = {
    Destination: {
      ToAddresses: [emailAddress],
    },
    Message: {
      Body: {
        Text: { Data: secretCode },
      },
       Subject: { Data: "Email Verification Code" },
    },
    Source: <SES_Identity_Email>, // This is your SES Identity Email 
  };
 
 
  return ses.sendEmail(params).promise()

}

function createAuthChallenge(event) {
  if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
    // Generate a random code for the custom challenge
    const randomDigits = randomInt(6);
    const challengeCode = String(randomDigits).join('');

    // Send the custom challenge to the user
    sendChallengeCode(event.request.userAttributes.email, challengeCode);

    event.response.privateChallengeParameters = {};
    event.response.privateChallengeParameters.answer = challengeCode;
  }
}
 
exports.handler = async (event) => {
  createAuthChallenge(event);
};

Amazon Cognito invokes the Create Auth Challenge trigger after Define Auth Challenge to create a custom challenge. In this Lambda trigger, you define the challenge to present to the user. privateChallengeParameters contains all the information to validate the response from the user. Save and close the file.

Save and close the file, then switch back to the terminal and follow the instructions:

? Press enter to continue `Hit Enter` 
? Do you want to edit your boilerplate-define-challenge function now? `Yes` 
? Please edit the file in your editor: <local file path>/src/boilerplate-define-challenge.js

The boilerplate for Define Auth Challenge opens in your favorite code editor. Enter the following code to this file:

exports.handler = async function(event) {
    const singleSessionLengthAndSRP = event.request.session.length === 1 &&
    event.request.session[0].challengeName === 'SRP_A';
    const secondSessionCustomChallenge = event.request.session.length === 2 &&
    event.request.session[1].challengeName === 'CUSTOM_CHALLENGE' &&
    event.request.session[1].challengeResult === true

  if (singleSessionLengthAndSRP) {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'CUSTOM_CHALLENGE';
  } else if (secondSessionCustomChallenge) {
    event.response.issueTokens = true;
    event.response.failAuthentication = false;
    event.response.challengeName = 'CUSTOM_CHALLENGE';
  } else {
    event.response.issueTokens = false;
    event.response.failAuthentication = true;
  }
};

Amazon Cognito invokes the Define Auth Challenge trigger to initiate the custom authentication flow.

The Amplify Auth library always starts with an SRP_A flow, so in the code above, you bypass SRP_A and return CUSTOM_CHALLENGE in the first step.

In the second step, if CUSTOM_CHALLENGE returns with challengeResult == true you recognize the custom auth challenge is successful, and tell Cognito to issue tokens.

In the last else block, you tell Cognito to fail the authentication flow.

Save and close the file, then switch back to the terminal and follow the instructions:

? Press enter to continue
    `Hit Enter`

? Do you want to edit your boilerplate-verify function now?
    `Yes`
? Please edit the file in your editor: <local file path>/src/boilerplate-verify.js

The boilerplate for Verify Auth Challenge opens in your favorite code editor. Enter the following code to this file:

function verifyAuthChallengeResponse(event) {
  if (
    event.request.privateChallengeParameters.answer ===
    event.request.challengeAnswer
  ) {
    event.response.answerCorrect = true;
  } else {
    event.response.answerCorrect = false;
  }
}

exports.handler = async (event) => {
  verifyAuthChallengeResponse(event);
};

Amazon Cognito invokes the Verify Auth Challenge trigger to verify if the response from the end user for a custom challenge is valid or not. The response from the user will be available in event.request.challengeAnswer. The code above compares that with privateChallengeParameters value set in the Create Auth Challenge trigger. Save and close the file, then switch back to the terminal and follow the instructions:

? Press enter to continue 

`Hit Enter`

Once finished, run amplify push to publish your changes.

3) Register a user (iOS 13+)

The CLI flow, as mentioned above, requires a username and a valid email id as parameters to register a user. Invoke the following API to initiate a sign up flow.

func signUp(username: String, password: String, email: String) -> AnyCancellable {
    let userAttributes = [AuthUserAttribute(.email, value: email)]
    let options = AuthSignUpRequest.Options(userAttributes: userAttributes)
    let sink = Amplify.Auth.signUp(username: username, password: password, options: options)
        .resultPublisher
        .sink {
            if case let .failure(authError) = $0 {
                print("An error occurred while registering a user \(authError)")
            }
        }
        receiveValue: { signUpResult in
            if case let .confirmUser(deliveryDetails, _) = signUpResult.nextStep {
                print("Delivery details \(String(describing: deliveryDetails))")
            } else {
                print("Signup Complete")
            }

        }
    return sink
}

The next step in the sign up flow is to confirm the user. A confirmation code will be sent to the email ID provided during the sign-up process. Enter the confirmation code received via email in the confirmSignUp call.

func confirmSignUp(for username: String, with confirmationCode: String) -> AnyCancellable {
    Amplify.Auth.confirmSignUp(for: username, confirmationCode: confirmationCode)
        .resultPublisher
        .sink {
            if case let .failure(authError) = $0 {
                print("An error occurred while confirming sign up \(authError)")
            }
        }
        receiveValue: { _ in
            print("Confirm signUp succeeded")
        }
}

You will know the sign up flow is complete if you see the following in your console window:

Confirm signUp succeeded

4)  Confirm sign in with custom challenge (Combine iOS 13+)

Get the custom challenge (1234 in this case) from the user and pass it to the confirmSignin() API.

func customChallenge(response: String) -> AnyCancellable {
    Amplify.Auth.confirmSignIn(challengeResponse: response)
        .resultPublisher
        .sink {
            if case let .failure(authError) = $0 {
                print("Confirm sign in failed \(authError)")
            }
        }
        receiveValue: { _ in
            print("Confirm sign in succeeded")
        }
}

5) Lambda Trigger Setup

The Amplify CLI can be used to generate triggers required by a custom authentication flow. See the CLI Documentation for details. The CLI will create a custom auth flow skeleton that you can manually edit.

More information on available triggers can be found in the Cognito documentation.

AWSCognitoAuthPlugin assumes the custom auth flow starts with username and password (SRP_A). If you want a passwordless authentication flow, change your Define Auth Challenge Lambda trigger to bypass the initial username/password verification and proceed to the custom challenge:

exports.handler = (event, context) => {
  if (event.request.session.length === 1 && 
    event.request.session[0].challengeName === 'SRP_A') {
    event.response.issueTokens = false;
    event.response.failAuthentication = false;
    event.response.challengeName = 'CUSTOM_CHALLENGE';
  } else if (
    event.request.session.length === 2 &&
    event.request.session[1].challengeName === 'CUSTOM_CHALLENGE' &&
    event.request.session[1].challengeResult === true
  ) {
    event.response.issueTokens = true;
    event.response.failAuthentication = false;
  } else {
    event.response.issueTokens = false;
    event.response.failAuthentication = true;
  }
  context.done(null, event);
};

You also need to pass a dummy password during the signup process as shown above.

You will know the sign in flow is complete if you see the following in your console window:

Sign in succeeded

Cleanup

To avoid incurring future charges to your AWS accounts, delete the resources created using your AWS account for this project. To do this, you will want to navigate to:

Deleting Amplify resources

To delete AWS Amplify resources, Step 1, Open the AWS Amplify console, Step 2, Select the associated passwordless application, then in step 3, click the button actions. This button is located in the upper right portion of the page. From the drop down menu that appears, select the option delete application.

Figure 5. Clean up screen

Deleting Lambdas

  • Open AWS Lambda console
  • Select the Functions in the left-hand menu, search for “passwordless” and check the boxes next to each of those Lambdas
  • Click the Actions > Delete function in the top menu
  • Click Delete on the confirmation screen

Deleting an Amazon S3 Bucket

  • Open Amazon S3 console
  • Select Buckets from the left menu and click on the bucket that starts with amplify-passwordless-
  • Select all the files and click on the menu item Actions > Delete
  • Click Delete on the confirmation screen

Deleting an IAM Role/Policy

  • Open IAM Roles Console
  • Select Roles from the menu on the left and search for “passwordless” and check the checkboxes next to each of them
  • Click on the Delete role at the top of the screen
  • Click Yes, delete on the confirmation screen

Deleting a CloudWatch log

  • Open CloudWatch Console
  • Select Logs > Log groups on the left
  • Search for passwordless in the log group and click all the log groups starting with /aws/lambda/amplify-passwordless
  • Click the Actions > Delete log group button at the top of the screen
  • Click Delete on the confirmation screen

Conclusion

In this blog, you leveraged both open source and third-party tools in combination with AWS Amplify Authenticator to build a passwordless login. You extended on top of your skeleton iOS app an AWS Amplify custom authorization extension. Together, you went through the steps to create a custom challenge to send an email message and configured Lambda to verify the custom challenge. Finally, you then registered and signed in a new user.

For more information about the proof of concept code base for this use case check out our Github (https://github.com/aws-samples/amazon-amplify-passwordless-login-authenticator).

 About the authors:

Charlie Gibson

Charlie Gibson is a Delivery Practice Manager in the US Security Practice at Amazon Web Services. He helps customers build the trust, capability and confidence to operate their most important business processes securely and at scale. Charlie has architected and built multiple capabilities for customers in AWS Cloud, including: CI/CD Pipelines, AI/ML and IoT.

Michael Tran

Michael Tran is a Solutions Architect with Prototyping Acceleration team at Amazon Web Services. He provides technical guidance and helps customers accelerate their ability to innovate through showing the art of the possible on AWS. He specializes in building prototypes in the AI/ML and IoT for our customers. You can contact me @Mike_Trann on Twitter.