aws-amplify/aws-sdk-ios

AWSMobileClient Cognito custom passwordless email auth - escape hatch from PreSignUpTriggerHandler to sign in flow

occassionally opened this issue · 1 comments

Describe the bug
Note: I originally opened this as bug report, but since found some workarounds. This is now likely more of a general question or a feature request.

I'm attempting to implement a custom auth passwordless flow where the user signs in using a 6-digit code emailed to them.

It's largely based off this example from the docs. I'm using AWSMobileClient, not Amplify. This is within an iOS app.

The current problem is providing a single-flow sign in where if an account exists, the user is signed in, otherwise an account is created and then signed in. Unfortunately, this isn't the default behavior.

To handle this, I set up a PreSignUpTriggerHandler that uses listUsers to check if the account exists - if it does exist, do not allow the sign up. If it doesn't exist, allow the sign up.

How can I modify the pre-sign up handler so if the account does exist, it does not allow the sign up but jumps into the sign in flow? That way, I could always call AWSMobileClient.signUp() and handle it accordingly on the server.

The Cognito stack:

import {
    AccountRecovery,
    Mfa,
    UserPool,
    UserPoolClient
} from 'aws-cdk-lib/aws-cognito'
import {
    App,
    CfnOutput,
    Stack,
    StackProps
} from 'aws-cdk-lib'
import { join } from 'path'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
import { RetentionDays } from 'aws-cdk-lib/aws-logs'
import { Runtime } from 'aws-cdk-lib/aws-lambda'
import { PolicyStatement } from 'aws-cdk-lib/aws-iam'



const cognitoEmailAddress = 'me@example.com'
const cognitoEmailAddressArn = 'arn:xxxxxx'



export class CognitoStack extends Stack
{
    public userPool: UserPool
    public userPoolClient: UserPoolClient

    

    constructor(
        scope: App,
        id: string,
        props: StackProps
    )
    {
        super(scope, id, props)

        // Create Lambda functions.
        ...

        // Create a Cognito User Pool.
        this.userPool = new UserPool(this, 'CustomAuthUserPool',
        {
            accountRecovery: AccountRecovery.EMAIL_ONLY,
            autoVerify:
            {
                email: true,
                phone: false
            },
            lambdaTriggers:
            {
                preSignUp: preSignUpLambda,
                createAuthChallenge: createAuthLambda,
                defineAuthChallenge: defineAuthLambda,
                verifyAuthChallengeResponse: verifyAuthLambda
            },
            mfa: Mfa.REQUIRED,
            mfaSecondFactor:
            {
                otp: true,
                sms: false
            },
            selfSignUpEnabled: true,
            signInAliases:
            {
                email: true,
                phone: false,
                preferredUsername: false,
                username: false
            },
            signInCaseSensitive: false,
            standardAttributes:
            {
                email:
                {
                    required: true,
                    mutable: false
                }
            }
        })

        // Create a User Pool Client.
        this.userPoolClient = new UserPoolClient(this, 'UserPoolClient',
        {
            userPool: this.userPool,
            authFlows:
            {
                adminUserPassword: false,
                custom: true,
                userPassword: false,
                userSrp: false
            },
            preventUserExistenceErrors: true
        })

        new CfnOutput(this, 'UserPoolId', { value: this.userPool.userPoolId })
        new CfnOutput(this, 'UserPoolClientId', { value: this.userPoolClient.userPoolClientId })
    }
}

The preSignUp Lambda:

import {
    CognitoIdentityProviderClient,
    ListUsersCommand,
    ListUsersCommandInput,
    ListUsersCommandOutput
} from '@aws-sdk/client-cognito-identity-provider'
import {
    PreSignUpTriggerEvent,
    PreSignUpTriggerHandler
} from 'aws-lambda'



export const handler: PreSignUpTriggerHandler = async (
    event: PreSignUpTriggerEvent
) =>
{
    console.log('preSignUp event.request: ', JSON.stringify(event.request, null, 2))

    const email = event.request.userAttributes.email

    if (!email)
    {
        throw new Error("Email attribute is required.")
    }

    const input: ListUsersCommandInput =
    {
        UserPoolId: 'us-east-2_xxxxxx',
        Filter: `email = \"${email}\"`
    }

    try
    {
        const client = new CognitoIdentityProviderClient()
        const command = new ListUsersCommand(input)
        const response: ListUsersCommandOutput = await client.send(command)

        if (
            typeof response.Users !== 'undefined'
            && response.Users.length > 0
        )
        {
            // TODO: How to sign in the user instead?
            console.log("User with this email already exists.")
            throw new Error("Email already registered.")
        }
        else
        {
            console.log("User not found, allowing sign-up.")
            event.response.autoConfirmUser = true
            return event
        }
    }
    catch (error)
    {
        console.error("Error in PreSignUp Lambda: ", error)
        throw new Error("Error checking email existence.")
    }
}

To Reproduce
Steps to reproduce the behavior:
See the above explanation please.

Observed Behavior
The email attribute is not available in the event sent to the Lambda trigger.

Expected Behavior
The email attribute should be present.

Stack Trace
There is no stack trace specifically related to this. The Lambda will crash when attempting to send an email, but that is because email is undefined, so the stack trace is not directly relevant to the issue, but rather a side-effect.

Code Snippet
See the above explanation please.

Unique Configuration
If you are reporting an issue with a unique configuration or where configuration can make a difference in code execution (i.e. Cognito) please provide your configuration. Please make sure to obfuscate sensitive information from the configuration before posting.

Areas of the SDK you are using (AWSMobileClient, Cognito, Pinpoint, IoT, etc)?
AWSMobileClient, Cognito

Screenshots
N/A

Environment(please complete the following information):

  • SDK Version: AWSiOSSDKV2 2.37.1, "@aws-sdk/client-sesv2": "3.650.0", "aws-cdk-lib": "2.156.0"
  • Dependency Manager: Swift Package Manager

Device Information (please complete the following information):

  • Device: iOS Simulator
  • iOS Version: 17.5

Additional context
N/A

Relevant Console Output
N/A

Logs
N/A

This issue is now closed. Comments on closed issues are hard for our team to see.
If you need more assistance, please open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.