AWS Amplify Auth gives a simple plug and play authentication and authorization experience that can be extended into advanced flows.
The terms authentication and authorization can get confusing. The line that divides them is blurry. For simplicity, when you see the word Auth, I am referring to both authentication and authorization. When I need to refer to them individually, I will spell out the full name.
Getting Started
Before you can set up auth, you need to have an Amplify project. An Amplify project can take different shapes but let’s assume throughout the rest of this article that a project is:
- A React (can be any framework) project that you have set up with
create-react-app
- And that you have run
amplify init
on the project
Regardless of how you set up your project, you should be able to enjoy this article as long as you’ve initialized an Amplify project with amplify init
. I will be using React but the same principles apply to all other supported frameworks or platforms.
Before we st up Auth, first add an Amplify API to your project. Ultimately, you need Auth to protect your API:
amplify add api
Choose the default options for all questions and wait for the CLI to create the API. You will get a warning that your API is public:
That’s because Amplify CLI adds the following line to your amplify/backend/api/<app name>/schema.graphql
:
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
This line makes your API public for testing. Comment it out to activate deny-by-default principle which we will talk about later in this article.
Now you can add Auth to your project. Run the add auth command at the root of your project:
amplify add auth
Amplify will ask you a few questions which it would use to configure Auth. Before you answer any of those questions, let’s walk through those options.
What Do Your Options Mean?
Here is what you get when you run amplify add auth
As a tip, if you are ever stuck making a decision when using the Amplify CLI, you can always select the I want to learn more option. For example here is a detailed explanation you get on what the listed options mean:
This utility allows you to set up Amazon Cognito User Pools and Identity Pools for your a
pplication.
Amazon Cognito User Pool makes it easy for developers to add sign-up and sign-in function
ality to web and mobile applications. It serves as your own identity provider to maintain
a user directory. It supports user registration and sign-in, as well as provisioning ide
ntity tokens for signed-in users.
Amazon Cognito identity pools provide temporary AWS credentials for users who are guests
(unauthenticated) and for users who have been authenticated and received a token. An iden
tity pool is a store of user identity data specific to your account.
If you choose to use the default configuration, this utility will set up both a Userpool
and an Identity Pool.
If you choose the 'Default configuration with Social Provider (Federation)', the provider
s will be federated with Cognito User Pools.
In either case, User Pools will be federated with Identity Pools allowing any users loggi
ng in to get both identity tokens as well as AWS Credentials.
The key takeaways is that you got two primary options:
Setup a Password-based Authentication (Default Configuration)
With this option, you users can sign up using username/email and password. You can also customize the form to add more fields like first and last name.
Setup a Social Login (Default Configuration with Social Provider)
The word federation might sound intimidating but Amplify is referring to what you might know as Social Login. You can configure Facebook, Google, and all other supported third-party auth providers
You’ll learn how to set up both, starting with the default configuration.
Set up the Login Screen
Identity is the basic of all kinds of Auth. Being able to confirm that users are who they claim they are is the first stop to protecting their data. Let’s walk through a flow on setting up a password-based authentication using Amplify Auth.
In your Amplify project, run amplify add auth
and the choose Default configuration
. Select Email and select No to advanced settings:
While you were setting up, Amplify CLI created an amplify
folder at the root of your project and has been updating it as you ran the add
commands. The amplify
folder contains a backend/auth/<id>/cli-inputs.json
. This is the config file for your auth and if you are ever in doubt about your config settings, look there.
You have configured auth locally but Amplify has not set up anything in the cloud to store user data. Run the following command to ask Amplify to provision all services needed to store and manage user data:
amplify push
Choose the default answers to all the questions and wait for the push to complete.
Amplify offers SDKs that you can take advantage when interacting with your Amplify API. You can install the SDK via npm:
npm install aws-amplify @aws-amplify/ui-react
Open the src/index.js
file and configure Amplify before rendering the React app:
import Amplify from 'aws-amplify';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
Amplify added the ./aws-exports
file in your src
folder during set up.
Do not edit the
./aws-exports
file. It is automatically generated based on the commands you run using the Amplify CLI
Wrap the React component you want to protect with Authenticator
and use render props to handle a successful authentication. You can do this to the src/App.js
component:
import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
export default function App() {
return (
<div>
<Authenticator>
{({ signOut, user }) => (
<main>
<h1>Hello {user.username}</h1>
<button onClick={signOut}>Sign out</button>
</main>
)}
</Authenticator>
</div>
);
}
Start the React app with npm start
and you should get an authentication form:
Identity
Before you can ask a customer for their ID, you need to issue them that ID. You can picture allowing users to create an account as issuing customers IDs.
Click the Create Account tab and fill the new account form:
Click the CREATE ACCOUNT button and you should get an email with a confirmation code. Enter the code in the next page and then click CONFIRM.
Now you have access to the app and you can sign out and test the log in page
Amplify stores your login session in your browser’s local storage. To confirm, open your local storage and look at the data stored for the port your app is running on
You can sign out by clearing this data.
Private vs Public vs Restricted Data
When you run the add api
command on an Amplify set up, the schema it generates contains the following line of code:
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
You can find the schema file in ~/amplify/backend/api/<project name>/schema.graphql
.
Testing in Public
The globalAuthRule
rule allows you to make all your API open to the public when you set the allow
property to public. It is great for testing but you definitely want to remove it in production. The fact that we've setup authentication does not protect our data yet.
The fact that we've setup authentication does not protect our data yet
Amplify encourages a deny-all-first approach so even if you have to make everything available to the public, the best practice is to explicitly declare each of the model as public.
Lets confirm that globalAuthRule
makes your data public by default. Run the amplify push
command if you haven’t then run the console command and choose GraphQL:
amplify console api
# Choose GraphQL after running the command
This will open the GraphQL queries page so you can test the API. Run the following command and you should get an empty array:
query MyQuery {
listTodos {
items {
name
}
}
}
If your API was private, you should have gotten a 401 instead.
Everything is Restricted First
Remove the following line from your schema.graphql
:
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!
Run the push command to update remote schema:
amplify push
Run amplify console api
again to open the GraphQL queries page. Run the queries you ran previously and you should get an unauthorized error:
Even if the user is logged in, they still won’t be able to access the data. Something is restricted if the following 3 are true:
-
*globalAuthRule*
is not set to allow public access - There is no public rule on a model to make it public to everyone including guests
- There is no private rule on a model to make it accessible to only signed in users
Making a Model Public
To make the Todo model public, you need to annotate its schema with the @auth
directive:
type Todo @model @auth(rules: [{ allow: public }]) {
id: ID!
name: String!
description: String
}
Setting the allow
rule to public
makes all data represented by this model to be public. That means, id
, name
, and description
data will be public.
Push and test if you can query the todos now:
You now have a deny-by-default (restricted) API with a public todo API. Your models are now private by default.
In the rest of this post, we will focus on private models so remove the public rule so as to keep the Todo model restricted
type Todo @model {
id: ID!
name: String!
description: String
}
How to Restrict Access to Owner/Creator
In a Todo app, you want each todo stored in your database to be read, updated, and deleted by ONLY the creator or owner of the todo. You don’t want user B to access user A’s todos.
A private access level user is just a signed in user. We want to go one step deeper than just signed in users. We want signed in users that created the todo. Here is how to write a schema that matches this rule:
type Todo @model
@auth(rules: [
{ allow: owner }
]) {
id: ID!
name: String!
description: String
}
- The model allows only signed in users with the
allow:owner
rule - This rule also enforces that that only the user who owns a record can create, read, update or delete it
Run the push command to push this change and open the console with amplify console api
.
You’d still get the Unauthorized error if you try to query for the todos. To see your todos, you have to login. When you add owner
rules, Amplify will detect it and give you an option to login from the console.
Click the Authorization Provider select box and select the ID of your Cognito User Pool
When you select the ID, a new Login with User Pools button will appear beside the run query button
Choose the Client ID that matches the aws_user_pools_web_client_id
property in your ~/src/aws-exports.js
config file.
The username and password should match the email and password you created when you signed up through the React app UI. Remember we are using emails as username.
Run the query again and you should get an empty array of todos cause you, the user/owner/creator has not created any todos yet
While you are here, run the following mutations one after the other to add some todos to your app for testing:
mutation createTodo {
createTodo(input: {
name: "Finish Demo"
description: "Complete your demo for Amplify Auth"
}) {
id
name
description
}
}
mutation createTodo {
createTodo(input: {
name: "Outline Article"
description: "Create outline and send for team review"
}) {
id
name
description
}
}
mutation createTodo {
createTodo(input: {
name: "Write first draft"
description: "Write and send draft to your editor for review"
}) {
id
name
description
}
}
mutation createTodo {
createTodo(input: {
name: "Finish and publish"
description: "Complete the article and share with your community"
}) {
id
name
description
}
}
Query the API again with the following to get the list of todos you created:
query MyQuery {
listTodos {
items {
name
}
}
}
Test Authorization in Client App
Update your ~/src/App.js
to fetch the signed in user's todo items and render the items on the page:
import React from 'react';
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';
import { API, graphqlOperation } from 'aws-amplify'
import { listTodos } from './graphql/queries'
function App () {
const [todos, setTodos] = React.useState([])
React.useEffect(() => {
async function fetchTodos() {
try {
const todoData = await API.graphql(graphqlOperation(listTodos))
const todos = todoData.data.listTodos.items
setTodos(todos)
} catch (err) { console.log('error fetching todos', err) }
}
fetchTodos();
})
return (
<div>
<AmplifySignOut />
{todos.map(todo => <p>{todo.name}</p>)}
</div>
);
}
export default withAuthenticator(App);
When you test in the browser, you will NOT get the list of todos. Instead, you will get an error in the console that you are unauthorized regardless of the fact that you logged in.
The reason is that if you inspect the headers for that request, you will not find an authorization header.
Request headers from localhost:
When you run the query from Amplify console like we did earlier, you see that the authorization header is added
Request headers from API console:
The reason for this disparity is that when I showed you how to set up Amplify API with amplify add api
, we chose only the default options. One of those options is the authorization type and it is set to API Key by default.
Owner authorization needs an authorization type that handles identity and authentication. Afterall an owner must be a logged in user first. The authorization type that fits this need is Amazon Congnito User Pool. To switch form API Key to Cognito User Pool, run the API update command:
amplify update api
Choose to update Authorization modes and then choose Amazon Cognito User Pool as the authorization type:
The rest of the answers should be default. The amplify/backend/backend-config.json
will update as well with the config:
"defaultAuthentication": {
"authenticationType": "AMAZON_COGNITO_USER_POOLS",
},
Run the push command to update your servers. Once the push is complete, refresh your app to get your todo list:
Allow access to only a group of people
Assuming you want a group of Admin users to have access to a model. It could also be that they are the only group of people that can perform a particular operation like deleting items. Amplify offers two ways to approach this.
Static Group Authorization
Using our Todo example, you can allow owners to create, read, update and delete but also allow Admin groups to delete.
type Todo @model
@auth(rules: [
{ allow: owner },
{ allow: groups, groups: ["Admin"], operations: [delete] }
]) {
id: ID!
name: String!
description: String
}
Dynamic Group Authorization
With dynamic group, you can have a group field in your model that specifies what group can access each record. So you can have shopping related todo items with a group field that has the value of family. And then have another set of todo items related to fitness with a group field that has the value of friends.
Each record can also have multiple groups which extends the power you have when it comes to assigning roles in your app. Here is an example:
# Dynamic group authorization with a single group
type Todo @model @auth(rules: [{ allow: groups, groupsField: "group" }]) {
id: ID!
title: String
group: String
}
# Dynamic group authorization with multiple groups
type Todo @model @auth(rules: [{ allow: groups, groupsField: "groups" }]) {
id: ID!
title: String
groups: [String]
}
groupField
tells Amplify which field in the model represents the group. To add users to a group, refer to this Cognito guide.
Field Level Authorization
Sometimes you want the auth rules for a field to be different from the rule that is applied to its parent model.
For instance, if you have a user profile schema that looks like this:
type Profile @model @auth(rules: [
{allow: private}
]) {
id: ID!
username: String!
email: String!
}
The model makes sense for a social media app. You want every signed in user (allow:private
) to view members profile.
It is common though, for users to dislike having their emails accessible to random people. So what if you want to make the email field only accessible to just the owner and not all signed in users?
type Profile @model @auth(rules: [
{allow: private}
]) {
id: ID!
username: String!
email: String! @auth(rules: [{ allow: owner }])
}
With this modification, you have a field email
with a different authorization rule from its parent model, Profile
. Only the owner of a profile can do anything to their email. Other signed in users can’t.
Field level authorization gives you a lot more power than a model level restriction. You can learn more about them in the rules docs.
Open ID/Social Authentication
You can take advantage of OAuth and Open ID providers for signing in. Instead of the username/email and password option we’ve seen previously, you can just have a big Sign in with X button on your authentication page.
To set up your Amplify app to authentication with Social, create a new Amplify project and add your API. Next run the Amplify add auth command to start the flow for configuring authentication:
amplify add auth
Instead of choosing the Default configuration like you did earlier, choose Default configuration with Social Provider:
You might also want to give the user an option to sign in with either email or social sign in. The next options allows you to pick username/email/etc alongside social:
Choose the default configuration as shown below and enter the redirect URL for your app:
You’ll also get the option to add multiple redirect URL. This is useful for having both a dev and production URLs as your redirect URL.
The sign out URL is where you want to redirect users after they have signed out of your site:
Choose a provider for your Social login from the list as shown below (use space bar to choose one):
You will be prompted to enter a client ID and secret as shown below:
You can get this credentials from your Social Provider’s site and the Social Sign In docs page describes in details how to get the credentials. You can also watch Nader's video on setting Social login using Google Auth.
Lastly, add a button in your React app to initiate the Social sign in flow:
<button onClick={() => Auth.federatedSignIn({provider: 'Google'})}>Open Google</button>
Is it possible to enable multi factor auth?
Yes! Since Amplify uses Amazon Cognito, it’s easy to allow a user choose how they want to set up Multi-Factor Authentication.
To learn how to set up MFA, refer to the Amplify MFA page. If you want to dive deeper into the options a user have for MFA, you can take a look at the Amazon Cognito MFA docs.
You can also use the Remember Device feature to make the user experience smoother by not always making them go through the MFA process.
What if a user lost their password?
Remember the sign in page from the Amplify UI library?
It has a Reset password link which takes you to a new page to reset your password as a user.
If you are using the UI library, then all you need to do is customize the UI to taste. But if you intend to build your UI from scratch, then you can rely on one of the authentication methods that Amplify exposes for resetting passwords
Build Auth UI from Scratch
If you want to build a custom authentication experience from scratch for your users, you’ve got two options:
- Customize Amplify UI
- Use the Authentication methods
You can choose to move the layout around or change the styles by customizing Amplify UI. You can get a completely unique user interface with just customizing Amplify UI.
If you need more power than just the looks, you can call the authentication methods manually. For example, if you have a React form with a username
and password
controlled inputs, here is the code snippet to use the inputs as a sign up form:
import { Auth } from "aws-amplify";
import React from "react";
async function signUp() {
const [{ username, password }, setCredentials] = React.useState();
try {
const { user } = await Auth.signUp({
username,
password,
});
console.log(user); // User info
} catch (error) {
console.log("error signing up:", error);
}
}
Auth.signUp
is the important line here. It takes an object of user credentials and returns a promise that can resolve to the user data or reject to an error if the authentication failed.
Auth.signUp
is just one of many methods. There is also signIn
, confirmSignUp
, signOut
, setupOTP
, forgotPassword
, currentAuthenticatedUser
, etc. All of these methods are documented in the Authentication library docs.
Conclusion
Feel free to use this article as a reference guide when you are not sure where to find what you are looking for. I left a lot of links in the article for this reason. You have all the fundamentals from what you’ve just learned, but you can go deeper when you need to by referencing the links in the article.