Skip to content

Amplify Examples: AuthPostConfirmation + CDN (storage + cloudfront) #907

Open
@armenr

Description

@armenr

Is this related to a new or existing framework?

No response

Is this related to a new or existing API?

Authentication, Storage

Is this related to another service?

No response

Describe the feature you'd like to request

Two "features" come to mind that I think would be high-value for customers:

1. PostConfirmation Function Enhancement

It's extremely common for users of Amplify to want to insert a Cognito user into a table like User or Users after a user has successfully confirmed their signup.

Currently: When a user adds a PostConfirmation function, that function shows them how to catch cognito user being confirmed, and automatically add that user to a standard/default userpool group

Desirement: When an Amplify customer uses the CLI to add or update their Auth category, and they add a PostConfirmation function, it would be good to also boilerplate the code they would require in order to not only add that user to a cognito group, but also how to add that user to a table in Dynamo via AppSync

How I figured it out

I was able to figure this out in the following way:

  1. Added a postconfirmation function using the auth category cli flow
  2. Took a wild guess that maybe there's an example of how to interact with AppSync via a lambda by going through the Function category cli flow
  3. It turns out, I was right: -->
? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: doAppSyncStuffEasilyBecauseWeCan
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: AppSync - GraphQL API request (with IAM)

✅ Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
- Environment variables configuration
- Secret values configuration

? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? Yes
? Select the categories you want this function to have access to. api
? Api has 2 resources in this project. Select the one you would like your Lambda to access redactedprojectV2Gushak
? Select the operations you want to permit on redactedprojectV2Gushak Query, Mutation

You can access the following resource attributes as environment variables from your Lambda function
	API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIENDPOINTOUTPUT
	API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIIDOUTPUT
	API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIKEYOUTPUT
	ENV
	REGION
? Do you want to invoke this function on a recurring schedule? No
? Do you want to enable Lambda layers for this function? No
? Do you want to configure environment variables for this function? No
? Do you want to configure secret values this function can access? No
✔ Choose the package manager that you want to use: · PNPM
? Do you want to edit the local lambda function now? No
✅ Successfully added resource doAppSyncStuffEasilyBecauseWeCan locally.

Note: I wish this aspect of the CLI was documented clearly, or highlighted somewhere...it would save so many people SO much time.

We originally spent DAYS trying to figure out our own approach for this...it involved hacking on a custom lambda that wrote raw data directly to Dynamo, as well as figuring out how to try to automatically add and attach IAM roles to the lambda that would permit access to that DynamoDB table + environment variables to specify the table name itself (which is hard...because table names are always randomly generated, they are not straightforward, so each time you create a new backend, you have to first ship and deploy the API, then go get the table name, then set that as an env var).

^^ As you can see, this is a giant pain.

  1. Then I had to go and update my postConfirmation function to match the config/flow of the Lambda I just created in the Function category flow
❯ amplify update function
? Select the Lambda function you want to update redactedprojectV2DevelopAuthPostConfirmation
General information
- Name: redactedprojectV2DevelopAuthPostConfirmation
- Runtime: nodejs

Resource access permission
- redactedprojectV2Gushak (Query, Mutation)

Scheduled recurring invocation
- Not configured

Lambda layers
- Not configured

Environment variables:
- Not configured

Secrets configuration
- Not configured

? Which setting do you want to update? Resource access permissions
? Select the categories you want this function to have access to. api
? Api has 2 resources in this project. Select the one you would like your Lambda to access redactedprojectV2Gushak
? Select the operations you want to permit on redactedprojectV2Gushak Query, Mutation

You can access the following resource attributes as environment variables from your Lambda function
	API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIENDPOINTOUTPUT
	API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIIDOUTPUT
	API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIKEYOUTPUT
? Do you want to edit the local lambda function now? No
  1. Then I basically copy/pasted the code from the doAppSyncStuffEasilyBecauseWeCan lambda into my existing REDACTEDPROJECTV2DevelopAuthPostConfirmation function, and essentially modified the graphql mutation for inserting the user...(also, we customize our functions so that we can ship them in TypeScript...happy to share our implementation for that as well...it involves using rollup to emit a single .js file)
// file: amplify/backend/function/REDACTEDPROJECTV2DevelopAuthPostConfirmation/lib/add-to-users-via-api.ts
/* Amplify Params - DO NOT EDIT
  API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIENDPOINTOUTPUT
  API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIIDOUTPUT
  API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIKEYOUTPUT
  ENV
  REGION
Amplify Params - DO NOT EDIT */

import crypto from '@aws-crypto/sha256-js';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { HttpRequest } from '@aws-sdk/protocol-http';
import { PostConfirmationTriggerHandler } from 'aws-lambda';
import fetch from 'cross-fetch';

const appsyncUrl = process.env.API_REDACTEDPROJECTV2GUSHAK_GRAPHQLAPIENDPOINTOUTPUT;
const AWS_REGION = process.env.AWS_REGION || 'us-east-1';
const { Sha256 } = crypto;

export const addUsersViaAPIHandler: PostConfirmationTriggerHandler = async (event) => {
  if (event.request.userAttributes.sub) {
    console.log(`EVENT: ${JSON.stringify(event.request.userAttributes.sub)}`)
  }

  const queryVars = {
    birthdate: event.request.userAttributes.birthdate,
    emailAddress: event.request.userAttributes.email,
    firstName: event.request.userAttributes.given_name,
    id: event.request.userAttributes.sub,
    lastName: event.request.userAttributes.family_name,
    owner: event.request.userAttributes.sub,
    phoneNumber: event.request.userAttributes.phone_number,
  };

  console.log(`queryVars: ${JSON.stringify(queryVars)}`)
  console.log(queryVars)

  // specify GraphQL request POST body or import from an extenal GraphQL document
  const createUserBody = {
    query: `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            birthdate
            createdAt
            emailAddress
            firstName
            id
            lastName
            owner
            phoneNumber
            updatedAt
          }
        }
      `,
    operationName: 'CreateUser',
    variables: {
      input: {
        birthdate: event.request.userAttributes.birthdate,
        emailAddress: event.request.userAttributes.email,
        firstName: event.request.userAttributes.given_name,
        id: event.request.userAttributes.sub,
        lastName: event.request.userAttributes.family_name,
        owner: event.request.userAttributes.sub,
        phoneNumber: event.request.userAttributes.phone_number,
      },
    },
  };


  // parse URL into its portions such as hostname, pathname, query string, etc.
  const url = new URL(appsyncUrl);

  // set up the HTTP request
  const request = new HttpRequest({
    hostname: url.hostname,
    path: url.pathname,
    body: JSON.stringify(createUserBody),
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      host: url.hostname,
    },
  });

  // create a signer object with the credentials, the service name and the region
  const signer = new SignatureV4({
    credentials: defaultProvider(),
    service: 'appsync',
    region: AWS_REGION,
    sha256: Sha256,
  });

  try {
    // sign the request and extract the signed headers, body and method
    const { headers, body, method } = await signer.sign(request);

    // send the signed request and extract the response as JSON
    const response = await fetch(appsyncUrl, { headers, body, method });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const result = await response.json();
    console.log(`RESULT: ${JSON.stringify(result)}`);

    // Cognito User Pool expects the event object to be returned from this function
    return event;
  } catch (error) {
    console.error('An error occurred:', error);
    throw error;
  }
};
  1. Then import all this into the main index file, and make sure it gets invoked sequentially (after the user is successfully added to the userPool group first)
/**
 * @fileoverview
 *
 * This CloudFormation Trigger creates a handler which awaits the other handlers
 * specified in the `MODULES` env var, located at `./${MODULE}`.
 */

import { PostConfirmationTriggerHandler } from 'aws-lambda';
import { addUsersViaAPIHandler } from './add-to-users-via-api';
import { addToGroupHandler } from './add-to-group';

/**
 * The names of modules to load are stored as a comma-delimited string in the
 * `MODULES` env var.
 */
// const moduleNames = (process.env.MODULES || '').split(',');

/**
 * This async handler iterates over the given modules and awaits them.
 *
 * @see https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html#nodejs-handler-async
 *
 */
export const handler: PostConfirmationTriggerHandler = async (event, context, callback) => {
  /**
   * Instead of naively iterating over all handlers, run them concurrently with
   * `await Promise.all(...)`. This would otherwise just be determined by the
   * order of names in the `MODULES` var.
   */

  try {
    await addToGroupHandler(event, context, callback);
    await addUsersViaAPIHandler(event, context, callback);
  } catch (error) {
    console.error(error)
    return error
  }

  return event
};

Final note on this one: Frankly, even if the code is not actually boiler-plated for you, if there was just a section in the documentation under the "Auth" category which walks you through doing this as a "Common Use-Case" or "Common Example" in an "Amplify-centric" way...even THAT would probably be high-value for customers.

2. An example of adding a custom CDN for Storage category

It's extremely common for users of Amplify to want to add some kind of CDN in front of their Storage. S3 + CloudFront is an extremely ubiquitous pattern...in fact, it's pretty much table stakes in any architecture.

It's been a time-consuming battle for us to figure out how to correctly add and then use a custom CDK resource which provides us with a fast CDN for serving files from S3 that have been uploaded into the app.

I'm still stuck trying to figure out how to protect files that were uploaded "privately" (only for the eyes of a user, or a userPool group), VS files available for all logged in users of the platform.

...I've seen countless posts across the web (StackOverflow, etc) where people basically attempt to implement this pattern, or ask for help to implement this pattern, or give up and abandon Amplify because they don't have a reference implementation or starting point for implementing this pattern.

I realize that in Gen2, this is likely much more intuitive or figure-out-able, but in gen1, this is pretty hard 😢 . It adds to the adoption curve being unfriendly for new users.

Describe the solution you'd like

  1. Straightforward scaffold, example code, or at least example documentation with some reference code for having a PostConfirmation function that talks to AppSync and inserts a confirmed user for you, into your DB
  2. Straightforward scaffold, example code, or at least example/reference code + documentation that allows an Amplify customer to correctly and straightforward-ly add a CDN (cloudfront) with/alongside their Storage category.

Describe alternatives you've considered

Ditching Amplify altogether, and going pure CDK (which would suck, because there's lots of goodness in the amplify cli + amplify categories based workflows).

Additional context

No response

Is this something that you'd be interested in working on?

  • 👋 I may be able to implement this feature request
  • ⚠️ This feature might incur a breaking change

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationfeature-requestNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions