loading...

Multiple Authentication Types in AWS Amplify

robertbroersma profile image Robert Updated on ・4 min read

The goal is to create a backend that allows us to:

  1. Signup and login
  2. Create and manage posts through a GraphQL API
  3. Enable everyone (also unauthenticated users) to READ all posts
  4. Prevent users from editing posts by others

We run the following commands to setup our Amplify project:

amplify init
? Initialize amplify with your prefered settings
amplify add auth
? Do you want to use the default authentication and security configuration
Default configuration

? How do you want users to be able to sign in?
Username

? Do you want to configure advanced settings?
No, I am done.

# I went with all the defaults. Configure however you'd like!
amplify add api

? Please select from one of the below mentioned services: 
GraphQL

? Provide API name: 
amplifyPosts

? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
# Using cognito user pool for signing up and logging in users

? Do you want to configure advanced settings for the GraphQL API 
Yes, I want to make some additional changes.

? Configure additional auth types? 
Yes

? Choose the additional authorization types you want to configure for the API 
API key
# A public API Key is needed to read ALL the posts

API key configuration
? Enter a description for the API key: 
amplifyPosts

? After how many days from now the API key should expire (1-365): 
7

? Configure conflict detection? 
No

? Do you have an annotated GraphQL schema? 
No

? Do you want a guided schema creation? 
Yes

? What best describes your project: 
Single object with fields (e.g., “Todo” with ID, name, description)

? Do you want to edit the schema now? 
Yes
# This is where you copy and paste the schema below

✔ GraphQL schema compiled successfully.
amplify add codegen

? Choose the code generation language target 
javascript

? Enter the file name pattern of graphql queries, mutations and subscriptions
src/graphql/**/*.js

? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions 
Yes

? Enter maximum statement depth [increase from default if your schema is deeply nested] 
2

✔ Generated GraphQL operations successfully and saved at src/graphql

The schema:

type Post
  @model
  @auth(
    rules: [
      # The owner is allowed to do everything
      { allow: owner }
      # The public is only allowed to read posts
      { allow: public, operations: [read] }
    ]
  ) {
  id: ID!
  title: String!
  content: String
  owner: String
}

At this point we can either run amplify push to deploy our backend, or amplify mock to mock our backend locally (this still required you to deploy the auth module first, but the mock command will guide you through that!)

At this point, add some users and posts to the backend by writing some mutations in the Amplify Console (or in the GraphiQL client at http://localhost:20002 if you used amplify mock).

After you push or mock your backend, Amplify will generate some files for you in your specified src directory:

  1. aws-exports.js - A configuration file for our frontend to communicate with our backend
  2. src/graphql/{mutations,queries,subscriptions}.js - A starting point for GraphQL queries so we don't have to write them ourselves (you can write more optimized ones tho if you want!)

Now's the time to consume the backend in our frontend.

Install aws-amplify and whatever aws package that fits your framework (We use react and aws-amplify-react):

yarn add aws-amplify aws-amplify-react

Somewhere high up in our component tree, we configure Amplify:

import Amplify from 'aws-amplify'
import awsconfig from '../aws-exports'

Amplify.configure(awsconfig)

To query for the posts created by the authenticated user:

import React, { useState, useEffect } from 'react'
import { API, graphqlOperation } from 'aws-amplify'
import { withAuthenticator } from 'aws-amplify-react'
import { listPosts } from '../graphql/queries'

const MyPostsScreen = () => {
  const [myPosts, setMyPosts] = useState([])

  useEffect(() => {
    async function getPosts() {
      const { data } = await API.graphql(graphqlOperation(listPosts))
      setMyPosts(data.listPosts.items)
    }

    getPosts()
  }, [])

  return (
    <div>
      My Posts:
      <ul>
        {myPosts.map(post => (
          <Post key={post.id} post={post} />
        ))}
      </ul>
    </div>
  )
}

// withAuthenticator wraps our components in a signup/login, which makes sure we are logged in when accessing this component!
export const AuthenticatedMyPostsScreen = withAuthenticator(MyPostsScreen)

Now, to query for ALL posts, all we need to do is switch the authMode to API_KEY:

import React, { useState, useEffect } from 'react'
import { API } from 'aws-amplify'
import { listPosts } from '../graphql/queries'

export const AllPostsScreen = () => {
  const [allPosts, setAllPosts] = useState([])

  useEffect(() => {
    async function getPosts() {
      // Switch authMode to API_KEY
      const { data } = await API.graphql({ 
        query: listPosts, 
        authMode: 'API_KEY',
      })
      setPosts(data.listPosts.items)
    }

    getPosts()
  }, [])

  return (
    <div>
      All Posts:
      <ul>
        {allPosts.map(post => (
          <Post key={post.id} post={post} />
        ))}
      </ul>
    </div>
  )
}

That's it! Because of the way our GraphQL schema uses the @auth directive, it is still impossible to edit posts that you don't own. Trying to run a mutation using authMode: 'API_KEY' will result in an Unauthorized error.

It took me a while to figure out you need to switch the authMode. If there's a better or easier way to do this, I'd love to hear about it!

Cheers.

Posted on by:

robertbroersma profile

Robert

@robertbroersma

Robert is a developer from Amsterdam who likes to talk about himself in the third person.

Discussion

markdown guide
 

This article is well written, the Amplify docs still have a long way to go... This was not an easy thing to figure out just out of the docs.

The main things that tripped me up were that the sequence in which to add Auth first with Cognito user pools and THEN adding the API with the API key made a big difference.

The second thing that tripped me up was in the amplify CLI menus
[space to select] and [enter] to confirm choices... When you've gone through the flow 10 times you habitually scroll to an option and hit enter, and in the back of your mind that should have been selected...it was not.

Noob mistake, but maybe others can learn from it.
@robert , thanks for this article!

 

Nice! The multiple authorizer pattern for one AppSync GraphQL endpoint is an eventual necessity, IMHO. So now I'm wondering out loud for discussion, which authorizer is the best default one? My project starting point was different. I started with API_KEY. The API_KEY AppSync authorizer is perfect for calling the GraphQL endpoint with superuser powers. It's simple. @auth is perfect for entities that are specific to a web app user. I added a AMAZON_COGNITO_USER_POOLS authorizer, second. That's how I ended up with multiple authorizers, too. I started with one default AppSync authorizer: API_KEY. I added the second AppSync endpoint authorizer with amplify update api. Which is kinda same place though the default authorizer for an endpoint is simply different.

 

What you could also do is initialize 2 API clients, one with API_KEY as the default and one with AMAZON_COGNITO_USER_POOLS as the default, then use the public one in the public part of your app, and the private one in the private part of your app.

I'm not sure if this is possible using Amplify, but you could simply write your own wrapper!

 

Slightly less code, to not have to set 'authMode' for each API.graphql() invocation! This is very interesting. A single AppSync endpoint has a single default authorizer. A single Amplify GraphQL client instance may have a single default authorization mode. It's up to the developer to evolve this god for saken security model!

 

Thanks for putting this together! I've been trying to figure out how to make a graphql API have public models and enforce user pool permissions on others. I've gone through what you've done, but i think the Amplify lib might not be working right to configure the permissions and the api. It Looks like it only sets up what ever i first specify, in this case the cognito user pools permissions, and doesn't do anything with the api key when configuring more. I try and manually set it up in the console, but then i can't update the schema to have a public and owner permission... Not expecting you to help me debug, i'm just wondering what versions you are using and if you happen to run into anything weird like this.

 

Hey Russ. I'm using @aws-amplify/cli@4.13.4

One thing I ran into is that I was trying to configure the API key by running amplify add auth (I figured making stuff public has to do with auth?), while that should be done in amplify add/update api, but it doesn't sound like that's what's happening to you.

Does your generated aws-exports.js contain aws_appsync_apiKey? If you're using amplify mock it might remove some stuff from there when you shut it down.

 

It actually does, it also has the keys for the cognito user: aws_user_pools_id. In your amplify/backend/backend-config.json file, do you have an api.<API_NAME>.output.authConfig property that has values in the additionalAuthenticationProviders and defaultAuthentication?

Yes, I do. AMAZON_COGNITO_USER_POOLS is under defaultAuthentication and API_KEY is under additionalAuthenticationProviders