Serverless Authentication with AWS Amplify: A Practical Guide

Authentication is the process of telling the application who you are–a fairly standard feature of many apps. If you’re a frontend developer like me, chances are you’ve implemented some kind of authentication mechanism to countless apps in your time. 

While authentication is a pretty common requirement, it can become a rather complex endeavor. You need a signup and a login, you need to validate passwords and store them securely, and you need to send activation emails to users, plus check for spambots. Then when all of this is implemented in the backend, you also need to create an authentication UI.

Nowadays, we’re lucky that many companies offer user management as a service, so we don’t have to implement all of this by ourselves and can focus on valuable features instead. One of these solutions is AWS Amplify, allowing you to add serverless authentication to a frontend with just a few CLI commands and UI components.

In this article, I’ll create an app with a serverless backend service that will be protected by an Amplify-generated authentication.

What Is AWS Amplify?

Amplify is a serverless framework for frontend developers; it offers frontend libraries for JavaScript, iOS, Android, and React Native and a CLI that helps to create serverless backend services for different use cases. The CLI also comes with code generators for the glue code that links our frontend with the backend services.

Already use AWS Amplify? Read: 6 GraphQL Authorization Schemas for AWS Amplify

Setting Up Amplify

I tend to use Amplify via AWS Cloud9, an IDE that comes pre-configured with AWS tooling, runs in the cloud, and is accessible via a web browser.

On Cloud9, I’ll add a symlink for the AWS profile that is managed by Cloud9, so the Amplify CLI can find it when I initialize a new project later:

$ ln -s ~/.aws/credentials ~/.aws/config

To install the Amplify CLI on Cloud9, use the following command:

$ npm i -g @aws-amplify/cli

If you want to install the CLI locally, you can look at the “Getting Started” guide in the Amplify docs.

Create a Frontend Project

React is the frontend library I know best, so I’m going to set up a simple project with it. This project can then be “amplified” with some serverless backend services:

$ create-react-app snaplate

Add Authentication to the Project 

As Ofir Nachmani wrote in his article “Going Serverless? 8 Use Cases to Guide You,” a good reason to use serverless technology is “a faster time to market for new services.” Not having to implement your own authentication certainly helps with this.

To add a simple user authentication to this project, I have to init Amplify inside the project directory and add the auth category:

$ cd snaplate

$ amplify init

$ amplify add auth

$ amplify push

I will use all the default settings here and “dev” as my environment name.

The init command will create a deployment infrastructure in the cloud and add an amplify directory to the project that holds the Amplify-related configuration.

The add auth command will add an infrastructure configuration in the form of CloudFormation templates to the amplify directory, but it won’t deploy it yet.

Finally, the push command will deploy the new authentication service.

User authentication is handled by the Amazon Cognito service, but it requires some additional AWS resources, IAM policies, and roles to function properly. The Amplify CLI will take care of all of that.

To link this new service up with my frontend, I’ll use the Amplify SDK for JavaScript and the UI component library that Amplify offers for React. They can be installed with NPM:

$ npm i @aws-amplify/core \

@aws-amplify/auth \

@aws-amplify/ui-react

Now I need to import these packages into the frontend code. I can do this by adding the first two packages to the src/index.js file, like this:

import React from "react";

import ReactDOM from "react-dom";

import { Amplify } from "@aws-amplify/core";

import "@aws-amplify/auth";

import App from "./App";

import credentials from "./aws-exports";

Amplify.configure(credentials);

ReactDOM.render(

  <React.StrictMode>

<App />

  </React.StrictMode>,

  document.getElementById("root")

);

The push command created the aws-exports.js file and contains all the configuration the frontend needs to talk with Amazon Cognito.

Before I call the configure() method, I have to import the auth package so it gets correctly configured, too. I won’t use it directly, but indirectly through the ui-react package in the next step.

Now I’ll add signup and login screens to my application in the src/App.js file:

import React from "react";

import { withAuthenticator } from "@aws-amplify/ui-react";

function App() {

  return <h1>SnapLate</h1>;

}

export default withAuthenticator(App);

After that, I can start the application:

$ npm start

Three CLI commands and three NPM packages was all it took to add Amplify to a frontend project and configure user authentication to validate passwords, which, by the way, have to be at least eight characters long. It will also send activation emails with codes after signup. And all of these authentication-related features work out of the box.

IOD specializes in expert-based tech research and content creation. Contact us if you need expert-sourced how-tos for your tech blog.

Add AI Features

Now that the boring stuff is done, I’ll add some real functionality to my app: a backend service that extracts text from a photo and translates it.

Since I’ll be adding this feature with Amplify, it will be linked to the authentication I set up and protected from users that didn’t sign up.

To add the text extraction, I add the predictions category as follows:

$ amplify add predictions

I select “Identify” and “Identify text” from the CLI questions, but otherwise I use the defaults. In the last question, I’m asked if Amplify should prohibit unauthorized access to the service or make it publicly available. I choose accessible for “Auth users only.” 

To add the translation, I add the predictions category again:

$ amplify add predictions

But this time, I select “Convert” and “Translate text into a different language.” I will choose “German” as my source language and “English” as the target language because I have some German documents laying around. I will also choose “Auth users only,” so only registered users can use this service.

To get this linked up with my frontend, I have to install the predictions package via NPM:

$ npm i @aws-amplify/predictions

And I have to update the src/index.js:

import React from "react";

import ReactDOM from "react-dom";

import Amplify from "@aws-amplify/core";

import "@aws-amplify/auth";

import { AmazonAIPredictionsProvider } from "@aws-amplify/predictions";

import credentials from "./aws-exports";

import App from "./App";

Amplify.configure(credentials);

Amplify.addPluggable(new AmazonAIPredictionsProvider());

ReactDOM.render(

  <React.StrictMode>

<App />

  </React.StrictMode>,

  document.getElementById("root")

);

The update links up the frontend with the new predictions service, so I can use it inside src/App.js, which I will update like this:

import React from "react";

import { Predictions } from "@aws-amplify/predictions";

import { withAuthenticator } from "@aws-amplify/ui-react";

function canvasToFile(canvas) {}

class App extends React.Component {

  state = { translation: "", processing: false };

  async componentDidMount() {

    const devices = navigator.mediaDevices;

    this.video.srcObject = await devices.getUserMedia({

      video: true,

    });

    this.video.onloadedmetadata = () => this.video.play();

  }

  takeSnapshot = () => {

    const context = this.canvas.getContext("2d");

    context.drawImage(this.video, 0, 0);

    const base64 = this.canvas

      .toDataURL("image/png")

      .split(",")[1];

    const charCodeArray = [...atob(base64)].map((c) =>

      c.charCodeAt(0)

    );

    const blobData = [new Uint8Array(charCodeArray)];

    return new Blob(blobData, { type: "image/png" });

  };

  processPicture = async () => {

    this.setState({ processing: true });

    try {

      const file = this.takeSnapshot();

      let response = await Predictions.identify({

        text: { source: { file } },

      });

      const text = response.text.fullText;

      response = await Predictions.convert({

        translateText: { source: { text } },

      });

      this.setState({

        translation: response.text,

        processing: false,

      });

    } catch (e) {

      this.setState({

        translation: e.message,

        processing: false,

      });

    }

  };

  render() {

    const { processing, translation } = this.state;

    return (

      <div style={{ maxWidth: 400, margin: "auto" }}>

        <h1>Snap/Late</h1>

        <video

          width={400}

          height={400}

          ref={(r) => (this.video = r)}

        />

  <canvas

          width={400}

          height={400}

          ref={(r) => (this.canvas = r)}

 />

        <br />

        <button

          style={{ width: "100%", height: 50 }}

          onClick={this.processPicture}

          disabled={processing}

        >

          {processing ? "Translating..." : "Translate!"}

        </button>

        <textArea style={{ width: 400, height: 400 }}>

          {translation}

        </textArea>

      </div>

    );

  }

}

export default withAuthenticator(App);

That’s a big chunk of code, so I’ll go through the important sections.

First, I created a canvasToFile() function, which will convert the content of a canvas to a Blob, the type Amplify’s Predictions library expects.

I then updated the UI of the App component with a video, canvas, and button element. The video will render a stream from the webcam and the button will save a photo of that stream to the canvas.

All the work of extracting the text and translating it happens inside the processImage method of the App component:

  1. Render the current video frame into the canvas.
  2. Convert the canvas to a Blob.
  3. Send the Blob to the identify API.
  4. Send the extracted text to the convert API.
  5. Update the translation state.
  6. Render the new translation state in a textArea element.

Check Protected Service

We can also send a request with cURL to our backend service without any authentication headers and see that it isn’t accessible:

$ curl -XPOST -H \

'Content-Type: application/x-amz-json-1.1' \

-H 'x-amz-target: RekognitionService.DetectText' \

-d '{"Image": {"Bytes": "..." }}' \

https://rekognition.eu-west-1.amazonaws.com

We’ll get the following response:

{

  "__type":"MissingAuthenticationTokenException",

  "message":"Missing Authentication Token"

}

Here, the authentication token also includes what AWS account this request is related to.

Adding Authentication Was Never Easier

Adding a secure serverless authentication service to an app only took me a few lines of code and three CLI commands, and I was only using the default configuration here.

Amplify’s CLI commands allow for much customization, like reCAPTCHA, MFA, and more. And if I need something really specific, I can dig into the generated CloudFormation templates and customize them even further.

If you want to learn even more about serverless technology, check out this article by Artem Arkhipov on how to build a serverless Telegram bot with AWS Lambda.

IOD publishes weekly how-tos, product comparisons, and other tech articles on cloud, devops, and more. We are currently seeking experts in GCP, cloud security, and NetApp. Learn more.

Related posts