Configuring AWS Cognito as authenticator for React.js frontend -> Serverless Framework Lambda behind Api Gateway

Goal

My aim is simple (at least I thought so):

Additionally:

Solution

User Pool

Let’s start with User Pool in Cognito. Create one and create an App client for it.

If you want the user to be able to log in using the AWS Amplify you must not use App client secret so make sure you uncheck Generate client secret checkbox.

Also, check the ALLOW_USER_SRP_AUTH as it’s used by Amplify Javascript JDK.

Serverless Framework side

That’s how my function on the SLS side looks like:

getStatuses:
  handler: src/boundary/handler.getStatuses
  events:
    - http:
        method: get
        path: /statuses
        cors: true
        authorizer:
          arn: ${self:custom.cognitoAuthorizerUserArn.${self:provider.stage}}

The cors: true is important here. The authorizer is defined like follows in serverless.yml:

custom:
  cognitoAuthorizerUserArn:
    tests: "[YOUR_ARN_TO_TESTS_USER_POOL_GOES_HERE]"
    staging: "[YOUR_ARN_TO_STAGING_USER_POOL_GOES_HERE]"
    prod: "[YOUR_ARN_TO_PROD_USER_POOL_GOES_HERE]"

so nothing fancy - authorizer is using the User Pool you have defined in AWS and it’s guarding the function entrypoint.

If you’ll try to invoke the endpoint right nwo without the Authorization header this will end with 401 Unauthorized.

React.js side

On the frontend side I needed to add some AWS Amplify libraries but mind that I never invoked amplify init on the project, so the frontend is just using the AWS resources rather than having full control over them.

The dependencies I needed are as follows:

"@aws-amplify/auth": "^3.4.29",
"@aws-amplify/ui-components": "^1.1.1",
"@aws-amplify/ui-react": "^1.0.6",
"aws-amplify": "^3.3.26"

That’s how the index.js looks like:


Amplify.configure({
    Auth: {
        region: awsconfig.userPoolRegion,                   
        userPoolId: awsconfig.userPoolId,                   
        userPoolWebClientId: awsconfig.userPoolWebClientId, // App Client ID
    }
});

ReactDOM.render(
    <React.StrictMode>
        <AmplifyAuthenticator>
            <AmplifySignIn slot="sign-in" headerText="How you want to the sign in screen to configure" hideSignUp/>
            <App/>
            <AmplifySignOut/>
        </AmplifyAuthenticator>
    </React.StrictMode>,
    document.getElementById('root')
);

As you can see I used the ui-react elements like AmplifyAuthenticator or AmplifySignIn to configure how the screen looks like - check the documentation for other options.

The values used for Amplify.configure above are taken from the external file but in overall these are just regular Amplify configuration values.

The App.js component looks like this:

import React from 'react';
import YourApp from "./YourApp";
import {withAuthenticator} from "@aws-amplify/ui-react";


class App extends React.Component {

    render() {
        return <div id="wrapper">
            <YourApp/>
        </div>;
    }
}

export default withAuthenticator(App);

Now mind this withAuthenticator that will guard your whole application - this will ensure that nothing that lies within YourApp will be invoked if you’re not signed-in.

Mistakes I made

One of the mistakes I made was to use the AmplifyAuthenticator element and its children inside App.js component and ignoring withAuthenticator. This resulted in some weird state where the user was presented with a Sign In screen, but the underlying components were executed anyway!

At the same time when I added withAuthenticator to App.js it just ignored the configuration of the login screen I defined using AmplifyAuthenticator / AmplifySignIn.

That being said - the above code example worked like a charm for me.

Call API from the frontend

Now when the user is already signed in you can use his token to invoke the endpoint.

How to obtain the token from a signed-in user?

Auth.currentSession().then((result) => {
    const token = result.idToken.jwtToken;
});

Now, when you have the token you can use it to invoke the endpoint either by:

leveraging the Amplify API to make the call

(just remember to add @aws-amplify/api to your dependencies…)

API.get("statusesApi", "/statuses/", {
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': token
    }
});

(… and having some entry for API configuration like this:)

"endpoints": [
  {
    "name": "statusesApi",
    "endpoint": "https://your-api-endpoint"
  }
]

or by ignoring the Amplify API package and use whatever is your existing approach, e.g.

fetch(Config.apiPath('/statuses/'), {
    method: 'GET',
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': token
    }
});

Summary

This configuration for me was pretty hard.
When you see the whole solution - it’s quite obvious. However, when you try to work on it on your own – it’s not that easy, especially if you don’t want to use the default Amplify way.

It looks like AWS is heavily pushing you to use Amplify as a framework of choice, but I dislike tons of bloatware it creates, resources it manages while I believe the frontend should not deal with such things.

The whole AWS Amplify resembles me the CodeStar which is nice to get fast start but, in the end, you end up with a hell of a lot of dependencies, resources, heaviness, and things you don’t even know what they do.

I’m pretty sure might be even easier solutions for the original problem of this post, so if you know of any - how to simplify it, just let me know!

Hopefully, someone will save some time by reading this. Cheers!