DEV Community

Christian Nwamba
Christian Nwamba

Posted on

An In-Depth Guide on Amplify GraphQL API Authorization

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:

  1. A React (can be any framework) project that you have set up with create-react-app
  2. 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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

image

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.
Enter fullscreen mode Exit fullscreen mode

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:

image

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

image

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

Run the push command to update remote schema:

amplify push
Enter fullscreen mode Exit fullscreen mode

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:

  1. *globalAuthRule* is not set to allow public access
  2. There is no public rule on a model to make it public to everyone including guests
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  • 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
  }
}
Enter fullscreen mode Exit fullscreen mode

Query the API again with the following to get the list of todos you created:

query MyQuery {
  listTodos {
    items {
      name
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
},
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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 }])
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Instead of choosing the Default configuration like you did earlier, choose Default configuration with Social Provider:

image

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:

image

Choose the default configuration as shown below and enter the redirect URL for your app:

image

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:

image

Choose a provider for your Social login from the list as shown below (use space bar to choose one):

image

You will be prompted to enter a client ID and secret as shown below:

image

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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. Customize Amplify UI
  2. 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (4)

Collapse
 
movingelectrons profile image
Jerome Stonebridge

How do I allow Admin group to list all Todos and not just the ones they have created?

Collapse
 
mansirewardle profile image
mansi-rewardle

Hey,

I want to call query in public site where I don't have any login. can we pass api key directly and call query?

Collapse
 
brianhhough profile image
Brian H. Hough • Edited

Hey @mansirewardle! The api key isn't really the best approach for public access to things. What you can do is enable AWS_IAM as an additional API method to do public querying. There's a few steps involved to set this up, but I show how to do this in a youtube video of a quote generator build where there is public access to call a lambda function via a custom query: https://www.youtube.com/live/XuCEi6SmIEo?feature=share&t=10274

Collapse
 
drewjhart profile image
drewjhart

Hey thanks for this, Christian. Really thorough breakdown. Your description of the working AppSynch query and the failing local query helped me resolve an issue I was stuck on. Cheers!