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

How to add own additional Lambda functions to the fido2Api? #120

Closed nkhine closed 1 year ago

nkhine commented 1 year ago

Hello, I would like to add the amazon-cognito-passwordless-auth to an existing AWS-CDK stack that already has an API Gateway and AWS Cognito Pool.

When I try to synthesise my stack, I get the following error, as I have tried to add my functions to the fido2Api that was created by the PasswordlessStack.

Error: 'X11CiCdStack/Dev/ApiStack' depends on 'X11CiCdStack/Dev/PasswordlessStack' ({X11CiCdStack/Dev/ApiStack}.addDependency({X11CiCdStack/Dev/PasswordlessStack})). Adding this dependency (X11CiCdStack/Dev/PasswordlessStack -> X11CiCdStack/Dev/ApiStack/AccountFunction/Resource.Arn) would create a cyclic reference.

Here is my setup:

...
import { SesStack } from './stacks/ses/stack';
import { CognitoStack } from './stacks/cognito/stack';
import { PasswordlessStack } from './stacks/passwordless/stack';
import { ApiStack } from './stacks/api/stack';
import TaggingStack from './tagging';

interface EnvironmentStageProps extends StageProps {
  readonly config: BaseInfraConfig;
  readonly repo: RepositoryConfig;
  readonly codestarConnectionArn: string;
  readonly envName: string;
  readonly ses: SesAttributes;
}

export class EnvironmentStage extends Stage {
  constructor(scope: Construct, id: string, props: EnvironmentStageProps) {
    super(scope, id, props);

    const sesStack = new SesStack(this, 'SesStack', {
      // config: props.config,
      ses: props.ses,
    });

    const cognitoStack = new CognitoStack(this, 'CognitoStack', {
      userPoolName: props.config.cognito.userPoolName,
      env: props.config.env,
      sesDomainName: props.ses.domainAttr.zoneName,
    });

    const passwordlessStack = new PasswordlessStack(this, 'PasswordlessStack', {
      stage: id,
      userPool: cognitoStack.userPool, // Pass the userPoolId from CognitoStack to PasswordlessStack
    });

    const apiStack = new ApiStack(this, 'ApiStack', {
      name: props.envName,
      fido2Api: passwordlessStack.fido2Api,
    });

...

    cognitoStack.addDependency(sesStack);
    passwordlessStack.addDependency(cognitoStack);
    apiStack.addDependency(passwordlessStack);

And my passwordless.ts stack is just from the example with a slight change

import { Duration, RemovalPolicy, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { BillingMode } from 'aws-cdk-lib/aws-dynamodb';
import { Passwordless } from 'amazon-cognito-passwordless-auth/cdk';
import {
  RestApi,
} from 'aws-cdk-lib/aws-apigateway';
import TaggingStack from '../../tagging';

interface Props extends StackProps {
  readonly stage: string;
}

export class PasswordlessStack extends TaggingStack {
  passwordless: Passwordless;
  public readonly fido2Api: RestApi;

  constructor(scope: Construct, id: string, props: Props) {
    super(scope, id, props);

    this.passwordless = new Passwordless(this, "Passwordless", {
      // userPool: props.userPool,
      allowedOrigins: [
        // Modify these origins as per your requirements
        "http://localhost:5173",
        // ... other origins ...
      ],
      clientMetadataTokenKeys: ["consent_id"],
      magicLink: {
        // Adjust the sesFromAddress based on your setup
        sesFromAddress: "your-ses-from-address@example.com",
        secretsTableProps: {
          removalPolicy: RemovalPolicy.DESTROY,
          billingMode: BillingMode.PAY_PER_REQUEST,
        },
      },
      userPoolProps: {
        removalPolicy: RemovalPolicy.DESTROY,
      },
      fido2: {
        authenticatorsTableProps: {
          removalPolicy: RemovalPolicy.DESTROY,
          billingMode: BillingMode.PAY_PER_REQUEST,
        },
        relyingPartyName: "Passwordless Fido2 Example",
        allowedRelyingPartyIds: [
          "localhost",
          // ... other relying party ids ...
        ],
        attestation: "none",
        userVerification: "required",
      },
      smsOtpStepUp: {},
      userPoolClientProps: {
        idTokenValidity: Duration.minutes(5),
        accessTokenValidity: Duration.minutes(5),
        refreshTokenValidity: Duration.hours(1),
        preventUserExistenceErrors: false,
      },
      logLevel: "DEBUG",
    });
    if (!this.passwordless.fido2Api) {
      throw new Error('fido2Api not available on Passwordless construct.');
    }
    this.fido2Api = this.passwordless.fido2Api;
  }
}

Removing the fido2Api: passwordlessStack.fido2Api, everything builds correctly.

diagram

So it seems I will have two Api Gateway and two CognitoPools

How do I expose the RestApi so that I can add my functions?

Any advice is much appreciated

ottokruse commented 1 year ago

Hi @nkhine

The Passwordless solution needs to be in the same stack as the User Pool (have a read of #118 for more info on why this is). If I read your code and diagram right, they are in different stacks, and that won't work.

You could tweak your code e.g. like so:

interface Props {
  readonly stage: string;
  readonly userPool: cdk.aws_cognito.UserPool;
}

// Custom class that is not a stack but rather adds to an existing stack
export class PasswordlessAddition {
  passwordless: Passwordless;
  public readonly fido2Api: RestApi;

  constructor(scope: Construct, id: string, props: Props) {
    super(scope, id, props);

    const userPoolStack = cdk.Stack.of(props.userPool);
    // Use existing stack as the `scope` argument to the Passwordless component
    this.passwordless = new Passwordless(userPoolStack, "Passwordless", {
        userPool: props.userPool,
        // etc.
    });
}
nkhine commented 1 year ago

Hi @ottokruse Thanks for the reply, I got it working.