aws-samples / amazon-cognito-passwordless-auth

Passwordless authentication with Amazon Cognito: FIDO2 (WebAuthn, support for Passkeys), Magic Link, SMS OTP Step Up
Apache License 2.0
337 stars 61 forks source link

Usage of stepUpAuthenticationWithSmsOtp in plain web. #159

Open fahadsadiq opened 3 months ago

fahadsadiq commented 3 months ago

Hello, I want to implement step up auth with SMS on some of the pages when a user is logged in. The application that we are using it in is svelte. We tried to re-enact the same login as in react end to end example for step up auth but we are not able to do the same procedure. When calling the function, the following api's are called:

  1. AWSCognitoIdentityProviderService.RespondToAuthChallenge with username as the auth parameter
  2. AWSCognitoIdentityProviderService.RespondToAuthChallenge four times. The first three times it works while the last time it fails with 400: "invalid username or password".

Could you please shine some light on how we would implement the same in plain web without the await able state that you have used in react.

ottokruse commented 3 months ago

The trick is to pass the right thing into the smsMfaCode prop: you need to pass in a function that will return a promise that will resolve to the OTP that the user entered. Create a custom promise for that.

For example, let's presume you have this form:

<form id="myForm">
  <label for="otp">OTP:</label>
  <input type="text" id="otp" name="otp" required>
  <button type="submit">Submit</button>
</form>

Then this is how your "promisify" the input and wire it into the lib:

import { stepUpAuthenticationWithSmsOtp } from "amazon-cognito-passwordless-auth/sms-otp-stepup";

function createSmsOtpPromise() {
  // return custom promise:
  return new Promise((resolve, reject) => {
    const form = document.querySelector("#myForm");
    form.addEventListener('submit', function(event) {
      event.preventDefault();
      const otp = new FormData(form).get('otp');
      resolve(otp);
    }, { once: true }); // Listener is removed after execution
  });
}

// call the SMS OTP Function
const { signedIn, abort } = stepUpAuthenticationWithSmsOtp({
  username: "john",
  smsMfaCode: createSmsOtpPromise,
}

The signature of the smsMfaCode function is actually: (phoneNumber: string, attempt: number) => Promise<string> but in the above example I don't use phoneNumber and attempt. When the library calls createOtpPromise it will pass in the phone number that the OTP was sent to, as well as the nr of times the user has already tried, but you don't need to use these per se, but could be nice to show to the user. Then you need a bit more coding to show these values to the user.

Let me know if that helps!

fahadsadiq commented 3 months ago

Thank you for the help. Though the issue i am facing is a bit different i feel. I have created a demo application for this functionality.

<script lang="ts">
    import { stepUpAuthenticationWithSmsOtp } from 'amazon-cognito-passwordless-auth/sms-otp-stepup';
    import type { TokensFromStorage } from 'amazon-cognito-passwordless-auth/storage';
    import { getContext } from 'svelte';
    const tokens = getContext<TokensFromStorage>('session');
    $: otp = '';
    async function createSmsOtp() {
        return otp;
    }

    function handleStepUpAuthentication() {
        const { signedIn, abort } = stepUpAuthenticationWithSmsOtp({
            username: 'test',
            accessToken: tokens.accessToken,
            smsMfaCode: createSmsOtp
        });
    }
</script>

<div>
    <button type="submit" on:click={handleStepUpAuthentication}>Step up Auth</button>
    <input type="text" bind:value={otp} />
    <button type="button" on:click={createSmsOtp}>Submit</button>
</div>

This is a basic svelte file that i have created. In this component, when i click on step up auth button stepUpAuthenticationWithSmsOtp is called. Instead of just sending the sms OTP, it does 4 calls on of which is a submit call for otp. I have attached a vide os the same.

If there a way to use the functionality using functions instead of form on submit?

Step Up Auth Issue.webm

ottokruse commented 3 months ago

You need to pass a function that return a promise that only resolves once the user actually submits the OTP. I think your promise immediately resolves with an empty value, and does that 3 times in a row until the custom auth flow says "nope, no more attempts for you".

I lack the Vue knowledge to help you but it should be close enough to the createSmsOtpPromise function example above

ottokruse commented 3 months ago

Guessing, but something like this:

<script lang="ts">
    import { stepUpAuthenticationWithSmsOtp } from 'amazon-cognito-passwordless-auth/sms-otp-stepup';
    import type { TokensFromStorage } from 'amazon-cognito-passwordless-auth/storage';
    import { getContext } from 'svelte';
    const tokens = getContext<TokensFromStorage>('session');
    let submitOtp: (otp: string) => void;
    const smsMfaCode = () => new Promise<string>(resolve => submitOtp = resolve);
    $: otp = '';

    function handleStepUpAuthentication() {
        const { signedIn, abort } = stepUpAuthenticationWithSmsOtp({
            username: 'test',
            accessToken: tokens.accessToken,
            smsMfaCode,
        });
    }
</script>

<div>
    <button type="submit" on:click={handleStepUpAuthentication}>Step up Auth</button>
    <input type="text" bind:value={otp} />
    <button type="button" on:click={submitOtp}>Submit</button>
</div>
ottokruse commented 3 months ago

Did that help @fahadsadiq ? How's it going?