Welcome to the Part 2 of our AWS Amplify Series! In this post we will be adding an authentication for our users with Amazon Cognito to like and rent books, comment on them and talk to other users. We will be also adding an administrator role for some users to be able to add new books and distinguish the accounts. You can check out the first post from the link below to follow the steps for deploying our application with AWS Amplify.
AWS Amplify is a set of tools and services that enables mobile and front-end web developers to build secure, scalable full stack applications, powered by AWS.
sufle.io/blog/aws-amplify-the-ultimate-bootstrapper-part-1Let's start by adding authentication for our app. We want our users to be able to login with their email addresses and passwords. Go ahead to console, navigate to project folder and type:
amplify add auth
Select Default configuration
from the options. As the second step, we want our users to be able to login via their email, so I’ve picked email, you can select any option you want.
After these choices, we will be asked for advanced configuration, which will ask for other login fields and other capabilities and challenges, like Google reCaptcha challenge for login. Let's skip these for now, we can add them later via amplify update auth
.
So far, we've completed our configuration part. Now let’s go ahead and provision our resources.
amplify push
From the CloudFormation logs, you can see that it created a new Cognito User Pool, an Identity Pool and a lot more. Let's check them from Console. Open your AWS Console and go ahead to Amazon Cognito, you will see that 1 identity pool is created alongside with 1 user pool. Now let's check out Lambda. You will see that there are 2 new Lambda functions that are created related to your applications user-related resource management.
Let’s open our application and integrate the authentication for our Amplify project.
To handle user authentication from our React Native application, we will start by adding Amplify's pre-built UI library for React and continue with adding React Router for some routing:
npm install @aws-amplify/ui-react react-router-dom
First things first, we need to tidy up our application. So, we will create a new component under src/components named List and move our listing logic under that component. We're doing this so that we can add additional routes, for details etc.
/* List.js */
import React, { useEffect, useState } from "react";
import { API, graphqlOperation } from "aws-amplify";
import { listBooks } from "../graphql/queries";
import { Link } from "react-router-dom";
function List() {
const [books, setBooks] = useState([]);
const [fetching, setFetching] = useState(false);
async function fetchBooks() {
setFetching(true);
try {
const bookData = await API.graphql(graphqlOperation(listBooks));
const books = bookData.data.listBooks.items;
setBooks(books);
setFetching(false);
} catch (err) {
console.error("error fetching books!", err);
}
setFetching(false);
}
useEffect(() => {
fetchBooks();
}, []);
return (
<div>
{fetching ? (
<p>Fetching books...</p>
) : (
<div>
<h2>Our books:</h2>
{books.length > 0 ? (
<ul>
{books.map((book, index) => (
<li key={index}>
<Link to={`/book/${book.id}`}>
{book.name} - {book.author}
</Link>
</li>
))}
</ul>
) : (
<p>
We don't have any books right now <span role="img">😢</span>
</p>
)}
</div>
)}
</div>
);
}
export default List;
/* Detail.js */
import React, { useState, useEffect } from 'react';
import { getBook } from "../graphql/queries";
import { API, graphqlOperation } from 'aws-amplify';
function Detail(props) {
const [book, setBook] = useState();
useEffect(() => {
let { id } = props.match.params;
console.log(id);
API.graphql(graphqlOperation(getBook, {id})).then(b => {
setBook(b.data.getBook);
})
}, []);
return <div>
{book ? <>
<h1>{book.name}</h1>
<p>{book.author}</p>
</> : null}
</div>
}
export default Detail;
Let's add a navigation bar, which will include our user info, link to login/logout etc.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
function Nav() {
return (
<header className="App-header">
<h1 className="flex-1">Book Store</h1>
<div className="float-right">
<Link to="/">Books</Link>
<Link to="/signup">Signup</Link>
<Link to="/login">Login</Link>
</div>
</header>
);
}
export default Nav;
aws-amplify
comes with a Hub
component, which is a local eventing system. You can dispatch events on AWS Amplify Hub -which we will cover, but you can also subscribe to authentication events, including login, logout, social logins, etc. We will use Hub for our session management. For logins, we will use the Auth
component in aws-amplify. It has a login
method, you pass a username, which is email in our case, and the user’s password. Our navigation component looks like this:
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Hub, Auth } from "aws-amplify";
function Nav() {
const [user, setUser] = useState(null);
const [userGroups, setUserGroups] = useState(null);
useEffect(() => {
Hub.listen("auth", ({ payload: { event, data } }) => {
switch (event) {
case "signIn":
getUser().then((userData) => setUser(userData));
break;
case "signOut":
setUser(null);
setUserGroups(null);
break;
case "signIn_failure":
console.log("Sign in failure", data);
break;
}
});
getUser().then((userData) => {
setUser(userData);
if (userData) {
setUserGroups(
userData.signInUserSession.accessToken.payload["cognito:groups"]
);
} else {
setUserGroups(null);
}
});
}, []);
function getUser() {
return Auth.currentAuthenticatedUser()
.then((userData) => userData)
.catch(() => console.log("Not signed in"));
}
return (
<header className="App-header">
<h1 className="flex-1">Book Store</h1>
<div className="float-right">
{user ? (
<>
<span style={{marginRight: "5px"}}>Welcome <b>{user ? user.attributes.name : null}</b></span>
{userGroups &&
userGroups.filter((f) => f.indexOf("Admins") > -1).length >
0 ? (
<Link to="/add-book">Add a book</Link>
) : null}
<Link to="/">Books</Link>
<button onClick={() => Auth.signOut()}>Sign Out</button>
</>
) : (
<>
<Link to="/">Books</Link>
<Link to="/signup">Signup</Link>
<Link to="/login">Login</Link>
</>
)}
</div>
</header>
);
}
export default Nav;
You can see that we are listening for signIn
events for signing in, and their signOut
in case of signing out. One more thing you can see from the code is that I've added a user group management state management, which we will manage some users to be admins. They will be able to create, update and delete books, while other users will only be able to rate books, as an example.
Now it's time to think about separating accounts that can manage books vs. other accounts. We will add a user pool group to distinguish these accounts.
amplify update auth
Select Create or update Cognito User Pool Groups
. I’ve added Admins, you can choose whatever you want. We will use this in our GraphQL model, as well as in the following storage part. Go ahead to schema.graphql
to add that.
type Book
@model
@auth(rules: [
{allow: public, operations:[read]},
{allow: groups, groups: ["Admins"], operations: [create, read, update, delete]}
]){
id: ID!
name: String!
author: String!,
description: String,
available: Boolean!,
score: Float!
}
@auth
directive allows us to define rules based on authentication methods or providers, even groups. You can see that everyone can get or list all books, but only the Admins
group can create, update or delete books. Looks great, now we have to
amplify update api
Remember when we initialized our API in our first post, we’ve added an API Key authentication for our API. Now, we need to add Cognito User Pools too, to be able to use user authentication checks in our code and graphql schema. After we run update, we will go to option Update auth configurations
, then check for API Key
first, enter a description and select days.
This API Key will be our public option, then select Yes for additional authentication, pick Cognito User Pools
. Go ahead and do an amplify push
to provision our resources.
Let’s begin with frontend integration. Start by adding a login page, and a signup page.
/* Login page */
import React, { useState } from "react";
import {} from "@aws-amplify/ui-react";
import { Auth } from "aws-amplify";
function Login() {
const [email, setEmail] = useState();
const [pass, setPass] = useState();
const [errorMessage, setErrorMessage] = useState();
async function login() {
try {
let result = await Auth.signIn(email, pass);
if (result) {
console.log(result);
window.location.href = "/";
}
} catch (err) {
setErrorMessage(err.message);
}
}
return (
<div>
{errorMessage ? <p style={{ color: "red" }}>{errorMessage}</p> : null}
<form>
Login
<div>
<div>
<input
type="text"
placeholder="Email"
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<input
type="password"
placeholder="Password"
onChange={(e) => setPass(e.target.value)}
/>
</div>
<div>
<button type="button" onClick={login}>
Login
</button>
</div>
</div>
</form>
</div>
);
}
export default Login;
/* Signup page */
import React, { useState } from "react";
import { Auth } from "aws-amplify";
export default function Signup() {
const [errorMessage, setErrorMessage] = useState();
const [email, setEmail] = useState();
const [name, setName] = useState();
const [pass, setPass] = useState();
const [screen, setScreen] = useState("signup");
const [code, setCode] = useState();
async function signup() {
try {
let result = await Auth.signUp({
username: email,
password: pass,
attributes: {
name,
email,
},
});
if (!result.userConfirmed) {
setScreen("code");
} else {
const user = await Auth.signIn(email, pass);
if (user) {
window.location.href = "/";
}
}
} catch (ex) {
setErrorMessage(ex.message);
}
}
async function verify() {
try {
let result = await Auth.confirmSignUp(email, code);
if (result) {
const user = await Auth.signIn(email, pass);
if (user) {
window.location.href = "/";
}
}
} catch (error) {}
}
return (
<div>
{errorMessage ? <p style={{ color: "red" }}>{errorMessage}</p> : null}
{screen === "signup" ? (
<div>
Signup
<form>
<div>
<input
type="text"
placeholder="Name"
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<input
type="text"
placeholder="Email"
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<input
type="password"
placeholder="Password"
onChange={(e) => setPass(e.target.value)}
/>
</div>
<div>
<button type="button" onClick={signup}>
Signup
</button>
</div>
</form>
</div>
) : screen === "code" ? (
<div>
Verification Code
<form>
<div>
<input
type="text"
placeholder="Code"
onChange={(e) => setCode(e.target.value)}
/>
</div>
<div>
<button type="button" onClick={verify}>
Verify
</button>
</div>
</form>
</div>
) : null}
</div>
);
}
You can see that there is an email code verification step, which I’ve added there too.
Now let's get back to one line at Nav.js component:
userData.signInUserSession.accessToken.payload["cognito:groups"]
This may seem a bit hacky, but it is the data that contains cognito groups within the user's token. With this one, we can check the user's group and render conditional components according to the data.
Let’s go ahead and move our create form into a new component.
/* Create.js */
import React, { useState } from "react";
import { API, graphqlOperation } from "aws-amplify";
import { createBook } from "../graphql/mutations";
function Create() {
const [bookForm, setBookForm] = useState({
name: "",
author: "",
description: "",
available: true,
score: 0,
});
const handleChange = (key) => {
return (e) => {
setBookForm({
...bookForm,
[key]: e.target.value,
});
};
};
const handleSubmit = (e) => {
e.preventDefault();
API.graphql(graphqlOperation(createBook, { input: bookForm }))
.then((e) => {
setBookForm({
name: "",
author: "",
description: "",
available: true,
score: 0,
});
window.location.href= "/";
})
.catch((err) => {
console.error(err);
});
};
return (
<form onSubmit={handleSubmit}>
<h2>Add new Book</h2>
<input
placeholder="Book Name"
type="text"
onChange={handleChange("name")}
/>
<input
placeholder="Author"
type="text"
onChange={handleChange("author")}
/>
<input
placeholder="Description"
type="text"
onChange={handleChange("description")}
/>
<input
placeholder="Score"
type="number"
onChange={handleChange("score")}
/>
<button type="submit">Add Book</button>
</form>
);
}
export default Create;
Voila!
So far we’ve added Authentication to our Amplify project, bounded our resources to user permissions, and created our custom authentication pages, all via Amplify CLI and SDK. It is doing a great job as a bootstrapper! In our next part of the AWS Amplify Series, we will talk more about Storage options and Analytics.
AWS Amplify provides a set of built-in components to add storage, file and image management into your frontend application in minutes, fully integrated with Amazon S3.
sufle.io/blog/aws-amplify-storage-part-3An experienced software engineer, Durul is indeed a technology lover and always excited to see and learn what technology offers. With his experience in startups from Entertainment to SaaS, he is keen on sharing his experience with the technology community.
Subscribe to Our Newsletter
Our Service
Specialties
Copyright © 2018-2024 Sufle
We use cookies to offer you a better experience with personalized content.
Cookies are small files that are sent to and stored in your computer by the websites you visit. Next time you visit the site, your browser will read the cookie and relay the information back to the website or element that originally set the cookie.
Cookies allow us to recognize you automatically whenever you visit our site so that we can personalize your experience and provide you with better service.