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
380 stars 69 forks source link

Passwordless user sign up #92

Closed yashvesikar closed 1 year ago

yashvesikar commented 1 year ago

Hello and thank you for this excellent solution!

I am trying to implement a passwordless sign in/sign up for a NextJS 13 application and would like to understand if it is possible to automatically "Sign Up" a user if they request a magic link and they do not have an existing Cognito identity? If so is there a recommended method to implement that?

I was not able to find much in the other issues, or the documentation about this and want to apologize in advance I missed something.

I have implemented the solution and it works if my email is already in the user pool, but the pre-signup lambda trigger is never called for new users. I think maybe that could be the issue that I am facing but I haven't been able to get to the bottom of that that lambda is never triggered.

ottokruse commented 1 year ago

If you have enabled user existence errors for your app client, you can catch the "user does not exist" error client side, and offer the user to create the account and proceed (automatically call signUp() from the client and then request magic link again).

If you have not enabled user existence errors for your app client, it's harder now because it will appear that the magic link has been sent, while is has not (this is by intent, to prevent the ability to do user enumeration). However, if you allow users to sign up themselves, then as far as I'm aware it makes no sense to NOT enable user existence errors. Because signUp will always tell whether the user exists or not (which users have complained about but I believe this is by design).

ottokruse commented 1 year ago

And thanks for your interest in this solution!

yashvesikar commented 1 year ago

I see, would it make sense to handle this in the backend by removing this early return when the user does not exist and instead signing up the user here instead?

Would this pose any security risks? I think that would prevent the user enumeration issues you shared above.

ottokruse commented 1 year ago

At that point you are already in the auth flow for a non-existing user which will never yield JWTs and is just there to "spend time" and pretend we're signing in - so user enumeration is prevented.

Issue is, for such a dummy auth flow, you don't to see the event.request.userAttributes.email that was used, so you don't know which e-mail address they attempted.

yashvesikar commented 1 year ago

I see. For anyone in the future I ended up building a simple login component using @ottokruse's suggestions

'use client';
import { Passwordless as PasswordlessComponent, usePasswordless } from 'amazon-cognito-passwordless-auth/react';
import { signUp } from 'amazon-cognito-passwordless-auth/cognito-api';
import { useEffect } from 'react';
import { redirect } from 'next/navigation';

function Login() {
  const { lastError, requestSignInLink, signInStatus } = usePasswordless();
  useEffect(() => {
    async function signUpAndRequestSignInLink() {
      const email = document.getElementsByClassName('passwordless-email-input')[0].getAttribute('value');
      if (!email) {
        // TODO log this as a p0 error
        throw new Error('Unable to sign in');
      }
      const password = ((length = 32, characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$') =>
        Array.from(crypto.getRandomValues(new Uint32Array(length)))
          .map((x) => characters[x % characters.length])
          .join(''))();
      await signUp({
        username: email,
        password,
        userAttributes: [
          {
            name: 'email',
            value: email,
          },
        ],
      });
      // redirect to dashboard
      const { signInLinkRequested } = requestSignInLink({
        username: email,
        redirectUri: `${window.location.origin}/dashboard`,
      });
      if (await signInLinkRequested) {
        console.log(`Sign in link requested: ${signInLinkRequested}`);
      }
    }

    if (lastError) {
      if (lastError.name == 'UserNotFoundException') {
        signUpAndRequestSignInLink();
      }
    }
  }, [lastError]);

  // if signed in redirect to dashboard
  if (signInStatus === 'SIGNED_IN') {
    redirect('/dashboard');
  }

  return (
    <PasswordlessComponent
      brand={{
        backgroundImageUrl:
          'https://upload.wikimedia.org/wikipedia/commons/thumb/8/85/Manhattan_in_the_distance_%28Unsplash%29.jpg/2880px-Manhattan_in_the_distance_%28Unsplash%29.jpg',
        customerName: 'Amazon Web Services',
        customerLogoUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/1280px-Amazon_Web_Services_Logo.svg.png',
      }}
    />
  );
}

export { Login };

@ottokruse it would be useful to be able to pass a forwardRef or get the output of the Passwordless React component too. Thank you for the suggestions!

ottokruse commented 1 year ago

Thanks for sharing that code snippet! Awesome

it would be useful to be able to pass a forwardRef

Good idea. Which DOM element in the component do you want to ref, I take it the <input> for the email address? What exactly do you want to do, e.g. focus it?

or get the output of the Passwordless React component too

Not sure what you mean here? Have a link or so to more info on this pattern?

Cheers

yashvesikar commented 1 year ago

Which DOM element in the component do you want to ref, I take it the for the email address?

Yes exactly! I want to be able to either pre-fill or pull the value out of the <input /> tag without having to reach for document.getElementsByClassName. The issue I am running into at the moment is that I cannot very easily pre-fill the email address from a query parameter using the off the shelf component because I have to wait until the class name for the <Passwordless /> <input /> tag is rendered which there's no good signal for. A forwardRef might solve both issues.

or get the output of the Passwordless React component too Not sure what you mean here? Have a link or so to more info on this pattern?

I meant some way for the consumer to see the email value before the user clicks submit, a forwardRef might help with this as well.

Edit: After doing some testing it seems that when the user hits submit the <input /> element is removed from the react tree so the ref will go back to null. I am happy to look a little more into this as a contribution.

ottokruse commented 1 year ago

Ok got it. Worded differently, you need a getter and setter for the e-mail field, and you want to be able to call these from a parent component.

Setting I understand (prepopulate from query param), what's your case for wanting to read it too?

A normal forwardRef wouldn't allow you to set the email value (as far as I'm aware you can only interact with DOM directly using a ref, bypassing React state). So, it may be best for us to expose an imperative handle for this instead, with methods setEmail and getEmail. Sounds good?

yashvesikar commented 1 year ago

Ok got it. Worded differently, you need a getter and setter for the e-mail field, and you want to be able to call these from a parent component.

Yep!

what's your case for wanting to read it too?

Main use case it outlined in my sample code above for being able to sign up a user in the event that they don't exist. For that I need to be able to getEmail

You are correct, a normal forwardRef also would not allow for the the parent to be able to get the email value on submit (in the current implementation) because of the loading spinner that appears on submit.

Another approach could be to supply some onChange and onSubmit props to the Passwordless component, though I am not sure if that is a pattern you want to allow. You can see a reference implementation of that here

ottokruse commented 1 year ago

Main use case it outlined in my sample code above for being able to sign up a user in the event that they don't exist. For that I need to be able to getEmail

Ahh yes of course.

Okay thanks for thinking along and the suggestions!

@EricBorland let's decide on how to add this.

yashvesikar commented 1 year ago

Great! would love an update if/when you decide to take this on.

In the meantime is there a good way to setEmail from a url param? any suggestions would be appreciated!

ottokruse commented 1 year ago

Right now I don't think there is a good option for it.

In all honesty we expect customers to want to create their own components in 99% of the cases: to have full control over look and feel, and to add specific pieces to their use cases, such as what you want now: add auto sign up. Our components are "only" meant to get you started with something that works, so you can play around with the Passwordless features.

Our usePasswordless hook should allow you to create your components using this solution without much work.

Long story short: at this point I think it'd be best to create your own component (think you may already have, you forked this repo right?).

Other side of this is that we haven't figured out the best way to add the changes you need:

Please let us know any feedback you have.