In this article, we will build a blog application using AWS Amplify which allows users to sign in and start blogging right after signup has been completed.
AWS Amplify enables front-end and mobile developers to easily build and deploy full-stack web and mobile applications powered by AWS. Authentication is one of the most vital features in an application as it determines the identity of a user. Recently, the Amplify team added a new feature to the Amplify authentication flow. The feature allows users to automatically sign in to the app immediately after signing up.
Pre-requisites
To follow along, you need to have:
- AWS account
- Knowledge of JavaScript and React
- A code editor (VS Code preferably)
- Nodejs >=v14 installed
- AWS Amplify CLI installed. Run this command if you don’t have it installed npm install -g @aws-amplify/cli
- Authorized AWS Amplify CLI amplify configure
The complete code for this post is on this Github repository.
Setting up the React project
We’ll be building a simple React application for this post. Run this command to scaffold a new React project:
npx create-react-app react-amplify-auth
When it’s done installing, change directory to the react project, and open the folder with your code editor.
cd react-amplify-auth
code .
Let’s go ahead to configure amplify for the project and create a user profile.
Setting up Amplify
We have setup a user profile for the amplify project, let’s now initialize amplify in the project. Run this command on your terminal.
amplify init
Select these options:
➜ react-amplify-auth git:(master) amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project reactamplifyauth
The following configuration will be applied:
Project information
| Name: reactamplifyauth
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: react
| Source Directory Path: src
| Distribution Directory Path: build
| Build Command: npm run-script build
| Start Command: npm run-script start
? Initialize the project with the above configuration? Yes
Using default provider awscloudformation
? Select the authentication method you want to use: AWS profile
For more information on AWS Profiles, see:
<https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html>
? Please choose the profile you want to use react-amplify-auth
You will see some installation logs and when it’s done, you will see a message like this:
CREATE_COMPLETE amplify-reactamplifyauth-dev-133741 AWS::CloudFormation::Stack Wed Aug 03 2022 13:38:19 GMT+0100 (West Africa Standard Time)
✔ Successfully created initial AWS cloud resources for deployments.
✔ Help improve Amplify CLI by sharing non sensitive configurations on failures (y/N) · yes
✔ Initialized provider successfully.
✅ Initialized your environment successfully.
Your project has been successfully initialized and connected to the cloud!
If you’ve gotten to this point, you’re Awesome 🙌🏾 An amplify folder has been created and the directory structure looks like this:
.
├── #current-cloud-backend
│ ├── amplify-meta.json
│ ├── awscloudformation
│ │ └── build
│ │ └── root-cloudformation-stack.json
│ └── tags.json
├── README.md
├── backend
│ ├── amplify-meta.json
│ ├── awscloudformation
│ │ └── build
│ │ └── root-cloudformation-stack.json
│ ├── backend-config.json
│ ├── tags.json
│ └── types
│ └── amplify-dependent-resources-ref.d.ts
├── cli.json
├── hooks
│ ├── README.md
│ ├── post-push.sh.sample
│ └── pre-push.js.sample
└── team-provider-info.json
Fantastic! Let’s now go ahead to add the authentication module to our React application.
Setting up Amplify Auth
We’ve successfully initialized amplify and connected our amplify project to the cloud. Let’s now create the authentication service. Run this command on your terminal:
amplify add auth
You will be prompted with these options:
You will be prompted with these options:
Using service: Cognito, provided by: awscloudformation
The current configured provider is Amazon Cognito.
Do you want to use the deUPDATE_COMPLETE amplify-reactamplifyauth-dev-133741 AWS::CloudFormation::Stack Wed Aug 03 2022 15:03:45 GMT+0100 (West Africa Standard Time)
✔ All resources are updated in the cloudfault authentication and security configuration? Default configuration
Warning: you will not be able to edit these selections.
How do you want users to be able to sign in? Username
Do you want to configure advanced settings? No, I am done.
✅ Successfully added auth resource reactamplifyauth963ad60d locally
This adds a auth/reactamplifyauth963ad60d
to the amplify backend directory. Now let’s go ahead and push the auth service to amplify cloud. Run this command on your terminal:
amplify push
You will see deploy logs and this message at the end
UPDATE_COMPLETE amplify-reactamplifyauth-dev-133741 AWS::CloudFormation::Stack Wed Aug 03 2022 15:03:45 GMT+0100 (West Africa Standard Time)
✔ All resources are updated in the cloud
Head over to your amplify studio with amplify console
and you will see something like this:
You can see Categories added: Authentication. Awesome. Let’s now integrate authentication to the frontend.
Setting up GraphQL API
We will be building a simple blog Posts API. Run this command to create an amplify api
amplify add api
Select these options:
➜ react-amplify-auth git:(master) ✗ amplify add api
? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
Update the schema.graphql
with these lines of code:
@auth(rules: [{allow: owner}])
type Posts @model {
id: ID!
title: String!
description: String
author: String
}
Next, run
amplify push --y
This will create
- a AppSync GraphQL API,
- a
graphql
folder in the appsrc
folder that consists of local GraphQL operations - a DynamoDB table for Posts
At this point, we have successfully created the backend for the project. Let’s go ahead to create the user interface with React.
Setting up Auth in React
Firstly, delete all the files except App.js
, index.js
, and aws-exports.js
. Install these dependencies:
npm i aws-amplify @aws-amplify/ui-react tailwindcss postcss autoprefixer
Update your index.js
to look like this:
//src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import Amplify from 'aws-amplify';
import config from './aws-exports';
Amplify.configure(config);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Create an index.css
and add these lines of code add tailwind styles to the project.
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Finally, update the content of tailwind.config.js
to this.
//tailwind.config.js
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Let’s go ahead and create the authentication flow. Create a Signup.js
and add these lines of code.
//src/Signup.js
import { Auth } from 'aws-amplify';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const initialFormState = { username: "", password: "", email: "" }
const Signup = () => {
const [formData, setFormData] = useState(initialFormState);
const navigate = useNavigate();
async function signUp() {
try {
const { user } = await Auth.signUp({
username: formData.username,
password: formData.password,
attributes: {
email: formData.email,
},
autoSignIn: { // optional - enables auto sign in after user is confirmed
enabled: true,
}
});
console.log(user);
localStorage.setItem('username', user.username);
navigate('/confirm', { replace: true })
} catch (error) {
console.log('error signing up:', error);
}
}
return ()
Here, we import Auth
from aws-amplify
. It has all the methods we will need to implement the auth flow. Methods such as signUp
, confirmSignUp
, signOut
, and a host of others. We went ahead to create an initialFormState
object with username
, password
, and email
as keys, they all have an empty string as initial values this will represent the initial state values for the form data we will be creating next.
Next, in the signUp
function, we destructure the data retrieved from the Auth.signup()
method into a user
object. The Auth.signUp()
method accepts a username, password, and attributes and any other optional parameters. Here, we included autoSignIn
and enabled it. The autoSignIn
method is a new feature that allows the user to sign in automatically without requiring Auth.signIn
to login.
Finally, we save the username
from the user
object to localStorage. Add the following lines of code:
<div className="bg-white">
<div className="grid max-w-screen-xl h-screen text-black m-auto place-content-center">
<div className="w-[30rem] space-y-6">
<div className="flex flex-col">
<label> Username </label>
<input
onChange={e => setFormData({ ...formData, 'username': e.target.value })}
placeholder="username"
value={formData.username}
type="text"
className="border border-sky-500 p-2 rounded w-full"
/>
</div>
<div className="flex flex-col">
<label>Password </label>
<input
onChange={e => setFormData({ ...formData, 'password': e.target.value })}
placeholder="pasword"
value={formData.password}
type="password"
className="border p-2 rounded border-sky-500"
/>
</div>
<div className="flex flex-col">
<label>Email </label>
<input
onChange={e => setFormData({ ...formData, 'email': e.target.value })}
placeholder="email"
value={formData.email}
type="email"
className="border p-2 rounded border-sky-500"
/>
</div>
<div>
<button className="border-none bg-sky-700 text-white p-2 mt-4 rounded m-auto" onClick={signUp}> Sign up </button>
</div>
</div>
</div>
</div>
Here, we have inputs
. On each field, we use an onChange
event and assign the value of the field to the formData
object.
Let’s go ahead and create the confirm sign up screen. Create a confirmSignup.js
and add these lines of code:
//src/confirmSignup.js
import { useState } from 'react';
import { Auth } from 'aws-amplify';
import { useNavigate } from 'react-router-dom';
const initialFormState = { code: "" }
const ConfirmSignup = () => {
const [formData, setFormData] = useState(initialFormState);
const navigate = useNavigate();
const username = localStorage.getItem('username')
const code = formData.code
async function confirmSignUp() {
try {
await Auth.confirmSignUp(username, code);
localStorage.clear();
navigate('/posts', { replace: true })
} catch (error) {
console.log('error confirming sign up', error);
}
}
return (
<div className="grid max-w-screen-xl h-screen text-black m-auto place-content-center">
<div className="w-[30rem] space-y-6">
<label htmlFor='Confirmation Code'> Enter the confirmation code sent to your email </label>
<input
onChange={e => setFormData({ ...formData, 'code': e.target.value })}
placeholder="code"
value={formData.code}
type="text"
className="border border-sky-500 p-2 rounded w-full shadow"
/>
</div>
<button className="border-2 bg-sky-700 border-none text-white p-2 mt-4 rounded m-auto" onClick={confirmSignUp}> Confirm</button>
</div>
)
}
export default ConfirmSignup
Here, the Auth.confirmSigUp()
accepts a username
and code
. We retrieve the username
from localStorage
and the code from the value of the input.
Go ahead and create a Home.js
and add these lines of code:
//src/Home.js
import { useNavigate } from "react-router-dom"
const Home = () => {
const navigate = useNavigate()
const redirectToSignup = () => {
navigate('/signup')
}
return (
<div className="bg-black">
<div className="grid max-w-screen-xl h-screen text-white m-auto place-content-center">
<h2 className="mb-8 text-4xl"> Amplify Posts app</h2>
<button className="border-2 p-2" onClick={redirectToSignup}> Create Account</button>
</div>
</div>
)
}
export default Home
In this page, we just have a button that’ll redirect us to the signup screen.
Update the App.js
with these lines of code:
//src/App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
import Signup from './Signup'
import Posts from './Post'
import Confirm from './confirmSignup'
import Home from "./Home"
const App = () => {
return (
<div>
<Router>
<Routes>
<Route path="/" element={<Home />}> Home</Route>
<Route path="/signup" element={<Signup />}> Signup </Route>
<Route path="/confirm" element={<Confirm />}></Route>
<Route path="/posts" element={<Posts />}> </Route>
</Routes>
</Router>
</div>
)
}
export default App
Here, we define the routes for our application. So far, we’ve created Home
, Signup
, and Confirm
routes, we should go ahead to create the Posts screen.
Create a src/Posts.js
and add these lines of code:
//src/Posts.js
import "@aws-amplify/ui-react/styles.css";
import { Auth, API } from "aws-amplify";
import { useState, useEffect } from 'react'
import { listPosts } from "./graphql/queries";
import { createPosts as createPostsMutation } from "./graphql/mutations";
import { useNavigate } from "react-router-dom";
const initialFormState = { title: "", author: "", description: "" }
function Posts() {
const [user, setUser] = useState()
const [posts, setPosts] = useState([]);
const [formData, setFormData] = useState(initialFormState);
const navigate = useNavigate()
async function fetchUser() {
Auth.currentAuthenticatedUser({
bypassCache: true // Optional, By default is false. If set to true, this call will send a request to Cognito to get the latest user data
})
.then(user => {
console.log(user)
setUser(user)
})
.catch(err => console.log(err));
}
async function fetchPosts() {
const apiData = await API.graphql({ query: listPosts });
setPosts(apiData.data.listPosts.items);
}
async function createPost() {
if (!formData.title || !formData.description || !formData.author) return;
await API.graphql({ query: createPostsMutation, variables: { input: formData } });
setPosts([...posts, formData]);
setFormData(initialFormState);
}
async function signOut() {
try {
await Auth.signOut({ global: true });
navigate('/signup', { replace: true })
} catch (error) {
console.log('error signing out: ', error);
}
}
useEffect(() => {
fetchUser()
fetchPosts()
}, [])
return ()
Let’s go through this code snippet. The posts
array will hold the posts we are fetching from the graphql API. While formData
will hold the values of the inputs. We also have these functions:
-
fetchPosts
- This function uses the API class to send a query to the GraphQL API and retrieve a list of posts. -
createPost
- This function also uses the API class to send a mutation to the GraphQL API. Here, we pass in the variables needed for a GraphQL mutation to create a new post with the form data. Let’s go ahead and build the UI. -
fetchUser()
- This uses theAuth.currentAuthenticatedUser()
method to check if the user has been authenticated. If yes, it returns the user object. -
signOut
- This function uses theAuth.signOut
method to sign out an authenticated user.
Let’s go ahead to implement the ui. Add these lines of jsx
<div className=" m-auto p-12">
<div className=" space-y-6 w-[30rem] m-auto">
<div className="flex flex-col">
<input
onChange={e => setFormData({ ...formData, 'title': e.target.value })}
placeholder="Post title"
value={formData.title}
type="text"
className="border border-sky-500 p-2 rounded w-full"
/>
</div>
<div className="flex flex-col">
<input
onChange={e => setFormData({ ...formData, 'author': e.target.value })}
placeholder="Post author"
value={formData.author}
type="text"
className="border border-sky-500 p-2 rounded w-full"
/>
</div>
<div className="flex flex-col">
<textarea
onChange={e => setFormData({ ...formData, 'description': e.target.value })}
placeholder="Post description"
value={formData.description}
className="border border-sky-500 p-2 rounded w-full"
/>
</div>
<button className="border-none bg-sky-700 text-white p-2 mt-4 rounded m-auto" onClick={createPost}>Create Post</button>
</div>
</div>
Finally, in this code snippet, we have a button that invokes the createPost()
function. Let’s move on and create the view for the posts.
<div className="mt-20">
<h1 className="text-3xl font-semibold "> Welcome {user && (<span>{user.username}</span>)} </h1>
<h2>Here are your posts:</h2>
{posts === 0 ?
(
<div>
<h3> They are no posts</h3>
</div>
) :
posts?.map(post => (
<div key={post.id || post.title} className="my-10">
<h2 className="text-xl font-semibold">Title: {post.title}</h2>
<p className="font-bold text-lg"> Author: {post.author}</p>
<p>Message: {post.description}</p>
</div>
))
}
</div>
<button className="border-2 bg-red-500 border-none text-white p-2 mt-8 rounded m-auto" onClick={signOut}> Signout</button>
</div>
Earlier, we created a posts
array that contains the posts fetched from the GraphQL API. Here, we map through the posts
and render the title, description, and author information. We also have a signOut button, we use the signOut
function from Authenticator here.
If you’ve noticed when you Create an account and pass the verification process, you’re automatically logged in to the app. This feature was added recently, and we can get to use it now! Behind the scenes, the confirm sign-up listener initiates auto sign-in once the account is confirmed. It happens if autoSignIn
is set to true in params for SignUp
function. They are three different ways you can choose to auto sign in which are code
, link
, or autoConfirm
. code
is the default.
Code implementation uses a hub listening event. Once the user confirms the account with code, it dispatches a listening event. The SignUp
function is listening for this event to initiate SignIn
process.
The Link implementation uses polling to sign users in since there is no explicit way to know if the user clicked the link. Polling is done within 3 minutes (time verification link is valid) every 5 seconds.
So that’s it, you should have something like this:
Conclusion
Amplify makes authentication easy while using Amazon Cognito as an Authentication provider. In this post, we learned how to configure and implement authentication with the Amplify framework.