Closed yashvesikar closed 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).
And thanks for your interest in this solution!
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.
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.
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!
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
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.
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?
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
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.
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!
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:
email
could be a prop to the Passwordless component, ok this is simple enoughPasswordless
component.Please let us know any feedback you have.
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.