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
382 stars 70 forks source link

Add option to register user if not already registered #131

Open brno32 opened 10 months ago

brno32 commented 10 months ago

The following code silently exits if the user does not exist in the user pool

https://github.com/aws-samples/amazon-cognito-passwordless-auth/blob/46bb71353588c3293df1b6b0ac888652e3f2c0f7/cdk/custom-auth/magic-link.ts#L290

This is good default behavior, as it prevents user enumeration.

However, it would be nice if there was an option to automatically register a user instead of silently erroring.

An option taken from the Passwordless cdk construct could be added here https://github.com/aws-samples/amazon-cognito-passwordless-auth/blob/46bb71353588c3293df1b6b0ac888652e3f2c0f7/cdk/custom-auth/magic-link.ts#L131

What I would like to do is register a user and have their first magic link email also verify their email when they sign in for the first time. I do not want a separate register page.

ottokruse commented 10 months ago

Thanks for the suggestion.

Had a similar discussion in #92 and #96

A problem for backend solutions as you suggest, is that in case the user does not exist and a "dummy" auth flow is started to prevent user enumeration, that the Cognito trigger payload does not show the alias the user used to sign in with, so you don't know their email address.

I'm not aware of any way around this currently.

Only option is to orchestrate this from the frontend as suggested in the discussion in #92, or build a custom API in front of Cognito.

Open to any other suggestions though!

brno32 commented 10 months ago

What if this feature required the email to also be part of the payload, or it could only be enabled if the app stores the email as the username? (this is what I'm doing for my app)

ottokruse commented 10 months ago

Yeah we could add the email address in the payload, good idea. Can add it in the client metadata of the respondToAuth challenge call. Then it's visible in the createAuthChallenge trigger.

Yeah, think I like that! Many users want this, as evidenced by the other issues logged for this.

brno32 commented 10 months ago

would this be possible to do from within requestSignInLink? I see it's already an authParameter, but these attributes don't seem to make their way into the event payload server-side

ottokruse commented 10 months ago

It is a known "issue" that the clientMetadata that you send in initiateAuth is not visible in the Lambda triggers (such as the CreateAuthChallenge trigger). However, the clientMetadata that you send in responseToAuthChallenge is visible in the Lambda triggers, so therein lies the solution.

For magic link, we always send a "PROVIDE_AUTH_PARAMETERS" challenge first, and the the client must respond with the auth parameters using responseToAuthChallenge, so there we could then plug the logic in to register the new user.

What's your idea about the automatic account creation. Should we do that, or ask the user for consent first, e.g. the e-mail could say something like "you requested a sign-in link but you don't have an account with us yet, click [here] to proceed and create an account with us". I think that may be nicer, but then we need more plumbing to handle that click, and only then create the account.

brno32 commented 10 months ago

What's your idea about the automatic account creation. Should we do that, or ask the user for consent first, e.g. the e-mail could say something like "you requested a sign-in link but you don't have an account with us yet, click [here] to proceed and create an account with us". I think that may be nicer, but then we need more plumbing to handle that click, and only then create the account.

I'd say supporting both would be worth it. Personally, we would like to automatically just register the user and send the magic link, but I think it would be good to also support a flow that requires user consent.

For magic link, we always send a "PROVIDE_AUTH_PARAMETERS" challenge first, and the the client must respond with the auth parameters using responseToAuthChallenge, so there we could then plug the logic in to register the new user.

Is the code that runs after respondToAuthChallenge where this could go here? https://github.com/aws-samples/amazon-cognito-passwordless-auth/blob/main/cdk/custom-auth/verify-auth-challenge-response.ts

ottokruse commented 10 months ago

Is the code that runs after respondToAuthChallenge where this could go here? https://github.com/aws-samples/amazon-cognito-passwordless-auth/blob/main/cdk/custom-auth/verify-auth-challenge-response.ts

Yes but Define Auth Challenge and Create Auth Challenge will also run again, in fact a whole chain runs after respondToAuthChallenge.

It works like this. Each custom auth flow triggers a chain of Define Auth Challenge, Create Auth Challenge and Verify Auth Challenge calls. This chain stops when you, in Define Auth Challenge return issueTokens: true or failAuthentication: true. But it goes on for as long as you don't return that, so that you can create multiple challenges, one after the other (e.g. first password, and then SMS OTP).

In our magic link implementation the chain looks like this:

graph TD;
    IA(client:InitiateAuth)-->DA(cognito:DefineAuthChallenge - CUSTOM_CHALLENGE);
    DA-->CA(cognito:CreateAuthChallenge- Provide auth parameters);
    CA-->RAC(client:RespondToAuthChallenge - I want a magic link);
    RAC-->VA(cognito:VerifyAuthChallengeResponse - dummy);
    VA-->DA2(cognito:DefineAuthChallenge  - CUSTOM_CHALLENGE);
    DA2-->CA2(cognito:CreateAuthChallenge - send magic link);
    CA2-->RAC2(client:RespondToAuthChallenge - provide magic link hash);
    RAC2-->VA2(cognito:VerifyAuthChallengeResponse - verify magic link hash);
    VA2-->DA3(cognito:DefineAuthChallenge - issueTokens true);
    DA3-->DONE(Done, JWTs issued to client);

(Refer to the Sequence diagrams for more detail)

We could make user consent another challenge, but for now since you don't need it we can skip it also.

Have to pick the best spot to do user auto registration in, might be best in the 2nd CreateAuthChallenge step above.