Welcome to new things

[Technical] [Electronic work] [Gadget] [Game] memo writing

Memo on how to create a minimal authentication site with AWS Amplify + React

I wanted to create a React site with an authentication function easily, but I gave up because it was too much trouble to build a backend each time.

However, we tried AWS Amplify to see if we could easily build a backend.

Since I have tried all the functions I wanted, I would like to make a rough note of how to make it so that it can be reused.

Since it is not a public site, it will not be elaborate, but will include the following minimum necessary functions.

  • hosting

    • Basic Authentication
  • Certification

    • DynamoDB access by authenticator
    • Lambda Access by Authenticator

document

Also refer to the help for the command, as the documentation may be out of date.

amplify --help
amplify [command] --help

Amplify CLI Installation

Since we will be working with the Amplify CLI, we will first install and initially configure it.

  • Create a user for Amplify in AWS IAM

    • Users must be granted administrative privileges.
  • Add a user for Amplify to the AWS CLI profile

    • See addendum for AWS CLI profile settings
  • Amplify CLI Installation
npm install -g @aws-amplify/cli
  • Amplify CLI Initial Setup
amplify configure

Project Creation

  • React app creation
npx create-react-app my-app --template typescript
  • Amplify Project Creation

    • Execute commands in the React directory
    • The amplify directory is created, and all Amplify-related files are created under this directory.
amplify init
  • Change settings of created projects
amplify configure project
  • Project Deletion

    • Sometimes the removal fails due to dependencies. In such cases, remove them from the Amplify Console as a last resort.
amplify delete

supplementary explanation

  • Every project needs one app.
  • Multiple React sites cannot be created in one project (as there is only one hosting per project)
  • The root directory of the application and the root directory of the project must be the same

hosting

  • Additional Hosting

    • You can choose between Amplify dedicated hosting and hosting using CloudFront and S3, but choose Amplify dedicated hosting.
    • Amplify dedicated hosting details are hidden and cannot be tampered with by the user
amplify hosting add
  • Deploy the added backend (hosting) to the actual AWS service
amplify push
  • Upload site content under React's build directory to hosting

    • They also do amplify push behind the scenes.
amplify publish
  • The URL of the hosting site will be displayed, and the site can be accessed from that URL.

  • Basic authentication for hosting

    • Amplify Console

      • [App Settings]-[Access Control]-[Manage Access]

Amplify command description

backend

  • amplify [category] add

    • Various backend settings added locally
  • amplify status

    • Displays status of local backend configuration
  • amplify push

    • Deploy the backend to the actual AWS service based on the backend configuration added locally.
    • Local backend settings are pushed to S3 as data
    • Always match S3 data with the state of the backend that is actually currently deployed in AWS
  • amplify [category] remove

    • Delete various backend settings added locally
    • To delete a locally deleted backend configuration from the actual backend deployed on the AWS service, use amplify push.
  • amplify pull

    • Override local backend configuration with actual backend built on AWS

env

It is possible to build the backend separately into environments such as dev and prod.

  • amplify env add

    • Add a new environment
  • amplify env checkout [env_name]

    • environment switchover
  • amplify env list

    • Environment List
  • amplify env remove [env_name]

    • environment deletion

Certification

  • Authentication addition

    • With amplify auth import, you can also use an existing Cognito without creating a new Cognito. Even if you use an existing Cognito, you can change Cognito depending on the environment.
amplify auth add
  • React Code

import React, { useEffect, useState } from 'react';
import Amplify, { Auth } from 'aws-amplify';
import { AmplifyAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

function App() {
    const [username, setUsername] = useState<any>();

    useEffect(() => {
        Auth.currentAuthenticatedUser().then(user => {
            setUsername(user.username);
        }).catch(err => { });
    }, []);

    if (!username) {
        return (<AmplifyAuthenticator />);
    }

    return (
        <div>
            <div>{username}</div>
            <AmplifySignOut />
        </div>
    );
}

export default App;

GraphQL

Amplify can also use REST API, but since Lambda Function can also be called from GraphQL, access to the server is consolidated in GraphQL.

Access to backends such as "DynamoDB," "Lambda," and "S3" through GraphQL as a hub.

  • Add GraphQL

    • Authentication is done using Cognito. Then, only logged-in users can access GraphQL.
amplify api add
  • Schema Editing

    • Edit amplify/backend/api/[api_name]/schema.graphql
  • Schema Reflection
amplify api gql-compile

supplementary explanation

  • GraphQL here refers to AppSync.
  • AppSync is a gateway service that receives GraphQL queries and calls various AWS services such as DynamoDB, Lambda, and S3
  • amplify/backend/api/[api_name]/schema.graphql is a schema that serves as a template
  • Generate the actual GraphQL schema from the template with the amplify api gql-compile command
  • amplify/backend/api/[api_name]/build/schema.graphql is the actual GraphQL schema generated from the template

    • When you write a GraphQL query, you will be looking at this schema

DynamoDB

By simply writing a GraphQL schema, you can use GraphQL queries to read and write data in DynamoDB tables. (Convenient!)

  • Schema Editing

    • Edit amplify/backend/api/[api_name]/schema.graphql

How to write schema (basic)

simple

type Todo @model
{
  id: ID!
  name: String!
  description: String
}
  • Table is created in DynamoDB with @model
  • The id column is required as the primary key. If it is omitted, it will be added automatically.

    • No primary sort key
  • Creation date createdAt and update date updatedAt columns are automatically added.
  • Anyone can create, edit, and delete.

No Subscription

type Todo @model(subscriptions: null)
{
  id: ID!
  name: String!
  description: String
}
  • Do not generate schema for subscriptions

Schema Writing (Owner)

Only the owner can Read, Edit, and Delete.

type Todo @model
    @auth(rules: [{ allow: owner }])
{
  id: ID!
  name: String!
  description: String
}
  • If @auth is added, the owner information (owner) column is automatically added to allow access control.
  • Specify the subject with allow and the action that only the subject can perform with operations.
  • Actions not specified in operations can be done by others.
  • If operations is not specified, all actions are covered. In other words, only the person designated by allow can do it.

Have owner information, but allow everyone to Read, Edit, and Delete.

type Todo @model
    @auth(rules: [{ allow: owner, operations: [create] }])
{
  id: ID!
  name: String!
  description: String
}
  • Only creation (CREATE) is restricted to the owner (OWNER). In other words, other users can Read, Edit, and Delete.

Only the owner and the Admin group can Read, Edit, and Delete.

type Todo @model
    @auth(rules: [{ allow: owner }])
    @auth(rules: [{ allow: groups, groups: ["Admin"]}])
{
  id: ID!
  name: String!
  description: String
}
  • More than one @auth can be written.
  • Group permissions can be set with groups.

How to write schema (index)

DynamoDB index can be created with @key.

  • Enable to get values in specified columns
  • Allows sorting on specified columns
type Todo @model
    @auth(rules: [{allow: owner}])
    @key(name: "byOwner", fields: ["owner", "createdAt"], queryField: "listTodoByOwner")
{
  id: ID!
  name: String!
  description: String
  owner: String
  createdAt: AWSDateTime!
}
  • name is the index name
  • In the fields array, the first is the partition key and the second is the sort key. The sort key can be omitted.
  • queryField is the function name for query. If omitted, no query is created.

GraphQL (Client)

  • Called by API.graphql({query, variables})
  • You may write the query directly.

Type Definition

After pushing api, amplify codegen

  • TypeScript schema type definitions are generated in ./API
  • A sample query is generated for ./graphql/[queries|mutations|subscriptions]
  • Update queries with type definitions in amplify codegen when the GraphQL schema is edited

    • However, schema information is obtained from the actual AppSync, so this should be done after amplify push.

Usage examples

import { API } from 'aws-amplify';
import { getTodo } from './graphql/queries';

const result: any = await API.graphql({
    query: getTodo,
    variables: { id: '123' }
});
console.log(result.data.getTodo);

Usage example (directly writing a query)

const rawQuery = /* GraphQL */ `
  query ($id: ID!) {
    getTodo(id: $id) {
      id
      name
    }
  }
`;

const result: any = await API.graphql({
    query: rawQuery,
    variables: { id: '123' }
});
console.log(result.data.getTodo);

Example of use (ToDo application)

schema.graphql

type Todo @model
    @auth(rules: [{ allow: owner }])
    @key(name: "byOwner", fields: ["owner", "createdAt"], queryField: "listTodosByOwner")
{
  id: ID!
  name: String!
  owner: String!
  createdAt: AWSDateTime!
}

App.tsx

import React, { useEffect, useState } from 'react';
import { Box, Button, Input, Flex, Spacer } from "@chakra-ui/react"
import { atom, CallbackInterface, useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';

import Amplify, { API, Auth } from 'aws-amplify';
import { AmplifyAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";
import awsconfig from './aws-exports';

import { listTodosByOwner } from './graphql/queries';
import { createTodo, deleteTodo } from './graphql/mutations';
import { Todo, ListTodosByOwnerQuery } from './API';
import { GraphQLResult } from '@aws-amplify/api-graphql';

Amplify.configure(awsconfig);

const usernameAtom = atom<string>({
    key: `usernameAtom`,
    default: ''
});

const todoListAtom = atom<Array<Todo>>({
    key: `todoListAtom`,
    default: []
});

const loadTodoListAtom: (a: CallbackInterface) => () => void = ({ snapshot, set }) => async () => {
    const username = await snapshot.getPromise(usernameAtom);
    if (username) {
        const result = (await API.graphql({
            query: listTodosByOwner,
            variables: { owner: username, sortDirection: "DESC" }
        }) as GraphQLResult<ListTodosByOwnerQuery>);

        const items = result.data?.listTodosByOwner?.items;
        if (items) {
            set(todoListAtom, (items as Array<Todo>));
        }
    }
};

function App() {
    const [username, setUsername] = useRecoilState(usernameAtom);
    const todoList = useRecoilValue(todoListAtom);
    const loadTodoList = useRecoilCallback(loadTodoListAtom);
    const [inputText, setInputText] = useState<string>('');

    useEffect(() => {
        Auth.currentAuthenticatedUser().then((user) => {
            if (username !== user.username) {
                setUsername(user.username);
            }
            loadTodoList();
        }).catch(err => { });
    }, [username]);

    if (!username) {
        return (<AmplifyAuthenticator />);
    }

    const todoListDom = todoList.map((v) => {
        return (
            <Flex key={v.id} my="2" border="1px" borderColor="gray.300" p="2" borderRadius="md">
                <Box>
                    <Box fontWeight="bold">{v.name}</Box>
                    <Box>{v.createdAt}</Box>
                </Box>
                <Spacer />
                <Button onClick={async () => {
                    await API.graphql({
                        query: deleteTodo,
                        variables: { input: { id: v.id } }
                    });
                    await loadTodoList();
                }}>delete</Button>
            </Flex>
        );
    });

    return (
        <Box p="4">
            <Flex my="2">
                <Input mr="2" type="text" value={inputText} onChange={(e) => {
                    setInputText(e.target.value)
                }} />
                <Button onClick={async () => {
                    if (inputText.length === 0) {
                        return;
                    }
                    await API.graphql({
                        query: createTodo,
                        variables: { input: {
                            owner: username,
                            name: inputText
                        } }
                    });
                    setInputText('');
                    await loadTodoList();
                }}>add</Button>
            </Flex>
            <Box>
                {todoListDom}
            </Box>
            <AmplifySignOut />
        </Box>
    );
}

export default App;

Lambda Function

Add a Lambda Function so that it can be called from GraphQL.

amplify function add
  • GraphQL schema editing

    • Edit amplify/backend/api/[api_name]/schema.graphql
    • Call the Lambda specified by @function.
type Post{
    id: ID!
    msg: String!
    comment: [Comment!] @function(name: "[function_name]-${env}")
}

type Comment{
    postID: ID!
    msg: String!
}

type Query {
    getPost(id: ID!): Post @function(name: "[function_name]-${env}")
}
  • Local execution (mock)

    • Specify event data with --event
    • When adding environment variables, prefix them with [env_name]=[env_value] ....

      • Add AWS_REGION environment variable when an error occurs in a region during local execution
amplify mock function [function_name] --event=[event_json_file_path]
AWS_REGION=ap-northeast-1 \
amplify mock function [function_name] --event [event_json_file_path]

Lambda Function 実装

Excerpts from the following documents

event data

  • Query contents are stored in event
  • Determine from the event where in the query the Function was called.

    • Often create a resolver and use it for discriminative use, as in the sample in the document

(Example) When calling from getPost()

{
    typeName: "Query",
    fieldName: "getPost",
    arguments: {
        id: "123"
    },
    identity: {
        username: "...." /* cognito username */
    }
}

(Example) When calling from Post.comment

{
    typeName: "Post",
    fieldName: "comment",
    arguments: { },
    source: {
        id: "123",
        msg: "..."
    }
    identity: {
        username: "...." /* cognito username */
    }
}

Mounting example

const posts = [
    { id: 1, msg: "POST_01" },
    { id: 2, msg: "POST_02" },
    { id: 3, msg: "POST_03" }
];

const comments = [
    { postID: 1, msg: "COMMENT_01_A" },
    { postID: 1, msg: "COMMENT_01_B" },
    { postID: 3, msg: "COMMENT_03_C" }
];

const resolvers = {
    Query: {
        getPost: queryGetPost
    },

    Post: {
        comment: postComment
    }
}

function queryGetPost(event) {
    console.log(event);
    return posts.find(v => v.id === Number.parseInt(event.arguments.id));
}

function postComment(event) {
    console.log(event);
    return comments.filter(v => v.postID === event.source.id);
}

exports.handler = async (event) => {
    const typeHandler = resolvers[event.typeName];
    if (typeHandler) {
        const resolver = typeHandler[event.fieldName];
        if (resolver) {
            return await resolver(event);
        }
    }
    throw new Error("Resolver not found.");
};

Retrieve Cognito user information from within a Lambda Function

  • Granting access to Cognito to a Function

    • The function's environment variable AUTH_[cognito_name]_USERPOOLID stores the cognito userpoolid, which is used to access cognito.
amplify function update
  • mounting
import { CognitoIdentityServiceProvider } from 'aws-sdk';
const cognito = new CognitoIdentityServiceProvider();
const USER_POOL_ID = process.env.AUTH_[cognito_name]_USERPOOLID;

exports.handler = async (event) => {

    const cognitoUser = await cognito.adminGetUser({
        UserPoolId: USER_POOL_ID,
        Username: event.identity.username
    }).promise();

    return JSON.stringify(cognitoUser);
};

Call GraphQL from within a Lambda Function

Make Cognito the default authentication for GraphQL (AppSync), add authentication by API Key, and from Lambda Function, call GraphQL using API Key.

  • Add authentication method to GraphQL

    • Add API Key Authentication
amplify api update
  • Grant access to GraphQL to Lambda Function

    • The GraphQL URL and API Key environment variable are created and passed to the Lambda Function.

      • API_[api_name]_GRAPHQLAPIENDPOINTOUTPUT
      • API_[api_name]_GRAPHQLAPIKEYOUTPUT
    • Environment variables must be set to amplify push to match those set in AppSync before use.
amplify function update
  • GraphQL schema editing

    • Edit amplify/backend/api/[api_name]/schema.graphql
    • Default authentication is Cognito, so API Key authentication is not available.
    • Add @aws_api_key to the schema so that it can be accessed with API Key authentication.
type Todo @model
    @auth(rules: [{allow: owner}])
    @aws_api_key
{
  id: ID!
  name: String!
  owner: String!
}

type Mutation @aws_api_key{
    sample: String @function(name: "[function_name]-${env}")
}
  • mounting

    • Set the API Key in the header
    • Calling method is the same as normal GraphQL
import axios from 'axios';
const GRAPHQL_EP = process.env.API_AMPLIFYTEST_GRAPHQLAPIENDPOINTOUTPUT;
const GRAPHQL_KEY = process.env.API_AMPLIFYTEST_GRAPHQLAPIKEYOUTPUT;

const createTodo = /* GraphQL */ `
  mutation CreateTodo(
    $input: CreateTodoInput!
    $condition: ModelTodoConditionInput
  ) {
    createTodo(input: $input, condition: $condition) {
      id
      name
      owner
      createdAt
      updatedAt
    }
  }
`;

exports.handler = async (event) => {

    const variables = {
        input: {
            name: "TEST",
            owner: "owner_test"
        }
    };

    const graphqlData = await axios({
        url: GRAPHQL_EP,
        method: 'post',
        headers: {
            'x-api-key': GRAPHQL_KEY
        },
        data: {
            query: createTodo,
            variables
        }
    });

    return JSON.stringify(graphqlData.data);
};

Pass any environment variable to the Lambda Function

Impressions, etc.

I tried to use it without spending too much time, but it was quite difficult because of the various functions involved.

When an error occurs in Amplify or CloudFormation, you have to solve it by yourself, and since the error is caused by the actual infrastructure, you need to have some knowledge of the back-end.

However, the backend that Amplify creates is safe for production use. Considering that considerable skills are required to create a backend of the same level, we thought that the merit of using Amplify was sufficient.

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com

www.ekwbtblog.com