Amplify authentication flow without any front end frameworks ("Vanilla" JavaScript)
Tags: amplify, aws, javascript, authentication
Published: 2021-02-18 08:16:18

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5tx3c4swmptctv3s8nhp.png)

## Background

**Disclaimer:** I am junior dev and I am bound to make mistakes.  Please feel free to comment or provide _constructive_ feedback.  I would love to give back to the community, but do not want to contribute to bad practices.

### Why this guide?

I was playing around with Amplify last week and noticed the authentication guides are mostly written for frameworks, like React, Vue or Angular.  While there are individual JavaScript snippets, I couldn't find a clear example showing the entire authentication flow in plain JavaScript.

I hope to provide a template for basic Authentication Flow (Sign-up, Sign-in, Sign-out, authenticate pages, etc.), using pure Javascript, thus no front end frameworks at all (like React, Vue, Angular, etc.).

Visually, I will use Bootstrap as I find it easy to read and easily replaceable when required in future.

### Purposeful design decisions

I made some design decisions for this tutorial, as the point is to show the authentication flow clearly.  There are many components one would see in production that I have left out on purpose, e.g.

- No dynamic navbar
- No switching components based on state
- No hiding components based on authentication state
- No dynamic importing of modules
- There is heavy use of console.log and alerts to provide feedback to the user in terms of the timing of events and feedback from AWS services.

&nbsp;

## Index

- [Install and configure Amplify CLI](#install-and-configure-amplify-cli)
- [Set up a project](#setup-a-project)
- [Initialising Amplify](#initialising-amplify)
- [Adding Auth](#adding-auth)
- [Create the auth flow html pages](#create-the-auth-flow-html-pages)
- [Create the auth flow JavaScript files](#create-the-auth-flow-javascript-files)
- [Test it all](#test-it-all)
- [Final thoughts](#final-thoughts)

&nbsp;

## Install and configure Amplify CLI

### Prerequisites

- An AWS Account
- Make sure Node.js, npm and git is fairly up to date. You can see my setup below.

### My setup at the time of writing

- MacOS v11.2.1
- Node.js v14.15.4
- npm v7.5.4
- git v2.14

### Steps

Install the Amplify CLI globally.

```bash
# To install Amplify CLI
npm install -g @aws-amplify/cli 
```

### Setup Amplify

```bash
amplify configure
```

This will trigger an AWS sign-in tab in your browser. Create a user (any username) with an access type of `Programmatic Access`, and with `AdministratorAccess` to your account.  This will allow the user to provision AWS resources like AppSync, Cognito, etc.

At the final step, you will be presented with an Access Key and a Secret Key.  Copy the keys to someplace safe.  You will not have to opportunity to see these keys again, so make copies now.

Copy and paste the keys in the terminal to complete the setup.  Leave the Profile Name as `default`.

&nbsp;

## Set up a project

Create a new ‘plain’ JavaScript app with Webpack, using the following commands:

```bash
mkdir -p amplify-vanilla-auth-flow/src
cd amplify-vanilla-auth-flow
npm init -y
npm install aws-amplify --save-prod
npm install webpack webpack-dev-server webpack-cli copy-webpack-plugin --save-dev
touch index.html webpack.config.js src/index.js
```

Then proceed to open in your code editor of choice (VS Code in my case):

```bash
code .
```

The directory structure should be:

```bash
amplify-vanilla-auth-flowsrc
├── src
│   └── index.js
├── index.html
├── package.json
└── webpack.config.js
```

Add the following to the package.json file:

```diff
{
  "name": "amplify-vanilla-auth-flow",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
"scripts": {
-   "test": "echo \"Error: no test specified\" && exit 1",
+   "start": "webpack serve --mode development",
+   "build": "webpack"
   },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "aws-amplify": "^3.3.19"
  },
  "devDependencies": {
    "copy-webpack-plugin": "^7.0.0",
    "webpack": "^5.22.0",
    "webpack-cli": "^4.5.0",
    "webpack-dev-server": "^3.11.2"
  }
}

```

**Side note:**

One can see the versions of Amplify and Webpack used at the time of writing above.  One could also copy-paste the package.json file above into yours before continuing the tutorial to ensure there are no differences in major versions (just remember to remove the `+` and `-` symbols).

Install the local development dependencies (if package.json was manually edited):

```bash
npm install
```

Add the following to the `webpack.config.js` file.

```js
const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const path = require('path');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
        library: 'MyAuthLibrary',
        libraryTarget: 'umd'
    },
    devtool: "source-map",
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/
            }
        ]
    },
    devServer: {
        contentBase: './dist',
        overlay: true,
        hot: true,
        port: 8090,
        open: true
    },
    plugins: [
        new CopyWebpackPlugin({
            patterns: ['*.html']
        }),
        new webpack.HotModuleReplacementPlugin()
    ]
};
```

**An interim note**:

At the time of writing there were some breaking changes in Webpack 5, to **temporarily** get around the issues, you can update `webpack.config.js`:

```diff
module: {
        rules: [
-            {
-                test: /\.js$/,
-                exclude: /node_modules/
-            }
+            {
+                test: /\.m?jsx?$/,
+                resolve: {
+                    fullySpecified: false,
+                    fallback: {
+                        "crypto": false
+                        }
+                }
+            }
        ]
    },
```

Add the following to the `index.html` file (based on the Bootstrap 5 Starter Template):

```html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">

    <title>Amplify Auth Flow</title>
</head>

<body>
    <!-- Navbar -->
    <ul class="nav justify-content-end bg-light">
        <li class="nav-item">
            <a class="nav-link" href="./index.html">Home</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="./signup.html">Sign up</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="./login.html">Login</a>
        </li>
        <li class="nav-item">
            <a id="nav-logout" class="nav-link" href="./index.html">Logout</a>
        </li>
    </ul>

    <!-- Main Content -->
    <section id="landing-page">
        <div class="d-flex justify-content-center min-vh-100">
            <div class="align-self-center">
                <h1>My Landing Page</h1>
            </div>
        </div>        
    </section>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous">
    </script>
    <script src="main.bundle.js"></script>
</body>

</html>
```

Before we continue, let's confirm that our environment is working.

```bash
npm start
```

This should automatically open a browser tab and you should see your site, formatted with Bootstrap CSS, navbar and all.  Do not proceed until this loads properly.  <kbd>Ctrl</kbd>+<kbd>C</kbd> when done.

&nbsp;

## Initialising Amplify

```bash
amplify init
```

This will initialise the Amplify project. As part of this process, the `./amplify` folder will be created, which will define your backend and any other Amplify/AWS services you use.

Most defaults will be fine.  The options below are important to note though (in the context of this tutorial):

- ? Choose the type of app that you're building `javascript`
- ? What javascript framework are you using `none`
- ? Source Directory Path:  `src`

&nbsp;

## Adding Auth

Now to add authentication to our Amplify app.  From the root folder of your project, run the following command:

```bash
amplify add auth
```

The options below are important:

- ? Do you want to use the default authentication and security configuration? `Default configuration`
- ? How do you want users to be able to sign in? `Email`

&nbsp;

Once done, you'll have to push these changes to the Amplify service:

```bash
amplify push
```

&nbsp;

### Review your Cognito settings (optional)

```bash
amplify console
```

The goal is to get to the Amplify UI.  At the time of writing, I had to select the older `Amplify console` option and then activate the newer UI.

Once the Amplify UI is loaded, navigate to **User Management** and **Create user**.  We are not going to create a user, but note what fields are available to you.  If you followed the instructions above you should see two fields - **Email address** and **password**.  These are the two fields that we are going to use to set up our forms in the following section.

I am merely showing this in case you choose different auth settings earlier in the tutorial.  In those cases, you will have to customise your forms and scripts accordingly.

You can close the Amplify UI once you are done looking around.

&nbsp;

## Create the auth flow html pages

We are going to create separate html pages for the basic auth flow as well as a "secret.html" page which should load once a user has signed in.

We will use index.html as the template and you will only update the `<!-- Main Content -->` sections as shown below.

Whilst copying and pasting, note how the main content starts with a `<section>` tag with a unique id that starts with `auth-x`.  Where forms are required, the id of the form will typically have an id of `form-auth-x`.  These id's will be used later on for Event Listeners.

From the root folder of your project:

```bash
cp index.html signup.html
cp index.html signup_confirm.html
cp index.html login.html
cp index.html forgot.html
cp index.html forgot_confirm.html
cp index.html secret.html
```

&nbsp;

`signup.html`

```html
<!-- Main Content -->
<section id="auth-signup">   
    <div class="d-flex justify-content-center min-vh-100">
        <div class="align-self-center">
            <h2>Sign up</h2>
            <form id="form-auth-signup">
                <div class="mb-3">
                    <label for="formSignUpEmail" class="form-label">Email address</label>
                    <input type="email" class="form-control" id="formSignUpEmail" aria-describedby="emailHelp">                        
                </div>
                <div class="mb-3">
                    <label for="formSignUpPassword" class="form-label">Password</label>
                    <input type="password" class="form-control" id="formSignUpPassword">
                </div>                    
                <button id="btnSignUp" type="submit" class="btn btn-primary">Sign up</button>
            </form>
            <p class="mt-3">
                <small>
                    Already have an account?
                    <a class="text-decoration-none" href="./login.html">Sign in</a>
                </small>
            </p>
        </div>
    </div>
</section>
```

&nbsp;

`signup_confirm.html`

```html
<!-- Main Content -->
<section id="auth-signup-confirm">
    <div class="d-flex justify-content-center min-vh-100">
        <div class="align-self-center">
            <h2>Confirm Email Address</h2>
            <form id="form-auth-signup-confirm">
                <div class="mb-3">
                    <label for="formSignUpConfirmEmail" class="form-label">Email address</label>
                    <input type="email" class="form-control" id="formSignUpConfirmEmail" aria-describedby="emailHelp" value="" readonly> 
                </div>
                <div class="mb-3">
                    <label for="formSignUpConfirmCode" class="form-label">Confirmation Code</label>
                    <input type="text" class="form-control" id="formSignUpConfirmCode">
                </div>                    
                <button id="btnConfirm" type="submit" class="btn btn-primary">Confirm</button>                  
            </form>
            <p class="mt-3">
            <small>
                Didn't get your code? 
                <a id="btnResend" class="text-decoration-none" href="#">Resend</a>
            </small>
        </p>
        </div>
    </div>
</section>
```

&nbsp;

`login.html`

```html
<!-- Main Content -->
<section id="auth-login"> 
    <div class="d-flex justify-content-center min-vh-100">
        <div class="align-self-center">
            <h2>Login</h2>
            <form id="form-auth-login">
                <div class="mb-3">
                    <label for="formLoginEmail" class="form-label">Email address</label>
                    <input type="email" class="form-control" id="formLoginEmail" aria-describedby="emailHelp">                        
                </div>
                <div class="mb-3">
                    <label for="formLoginPassword" class="form-label">Password</label>
                    <input type="password" class="form-control" id="formLoginPassword">
                </div>                    
                <button id="btnLogin" type="submit" class="btn btn-primary">Log in</button>                    
            </form>
            <p class="mt-3 mb-0">
                <small>
                    Don't have an account?
                    <a class="text-decoration-none" href="./signup.html">Sign up</a>
                </small>
            </p>
            <p class="mt-0">
                <small>
                    Forgot password?
                    <a class="text-decoration-none" href="./forgot.html">Reset password</a>
                </small>
            </p>
        </div>
    </div>
</section>
```

&nbsp;

`forgot.html`

```html
<!-- Main Content -->
<section id="auth-forgot-password">
    <div class="d-flex justify-content-center min-vh-100">
        <div class="align-self-center">
            <h2>Reset password</h2>
            <form id="form-auth-forgot-password">
                <div class="mb-3">
                    <label for="formForgotEmail" class="form-label">Email address</label>
                    <input type="email" class="form-control" id="formForgotEmail" aria-describedby="emailHelp">                        
                </div>                            
                <button id="btnForgot" type="submit" class="btn btn-primary">Reset</button>
            </form>
        </div>
    </div>
</section>
```

&nbsp;

`forgot_confirm.html`

```html
<!-- Main Content -->
<section id="auth-forgot-password-confirm">
    <div class="d-flex justify-content-center min-vh-100">
        <div class="align-self-center">
            <h2>Confirm New Password</h2>
            <form id="form-auth-forgot-password-confirm">
                <div class="mb-3">
                    <label for="formForgotConfirmEmail" class="form-label">Email address</label>
                    <input type="email" class="form-control" id="formForgotConfirmEmail" aria-describedby="emailHelp" value="" readonly> 
                </div>
                <div class="mb-3">
                    <label for="formForgotConfirmCode" class="form-label">Confirmation Code (via email)</label>
                    <input type="text" class="form-control" id="formForgotConfirmCode">
                </div>
                <div class="mb-3">
                <label for="formForgotConfirmPassword" class="form-label">New Password</label>
                <input type="password" class="form-control" id="formForgotConfirmPassword">
            </div>             
                <button id="btnConfirmForgot" type="submit" class="btn btn-primary">Confirm</button>                  
            </form>
        </div>
    </div>
</section>
```

&nbsp;

`secret.html`

```html
<!-- Main Content -->
<section id="authenticated-content">
    <div class="d-flex justify-content-center">
    <div class="align-self-center">
        <h1 class="text-success">The Secret Page</h1>
    </div>
    </div>
</section>
```

&nbsp;

## Create the auth flow JavaScript files

To separate the logic per function, I have created .js files for the major user actions, like sign-up, login, etc.  The typical makeup of each file is a function (or two) with the corresponding event listeners.  The event listeners are wrapped in an `if` statement that checks for the existence of a `<section>` id, and thus won't trigger unless that section is present in the DOM.

From the root folder of your project:

```bash
cd src
touch auth_signup.js auth_login.js auth_forgot_password.js auth_user.js auth_logout.js auth_content.js
```

Now copy the contents below to each of the corresponding .js files.

&nbsp;

`auth_signup.js`

```js
console.log("auth_signup.js loaded...");

import { Auth } from 'aws-amplify';

// User Sign Up function
export const signUp = async ({ email, password }) => {
    console.log("signup triggered...");    
    const username = email;    // As username is a required field, even if we use email as the username    
    console.log("sending to Cognito...");
    
    try {
        const { user } = await Auth.signUp({
            username,
            email,
            password,
            attributes: {                
                // other custom attributes 
            }
        });
        console.log(user);
        window.location = '/signup_confirm.html#' + username;
    } catch (error) {
        console.log('error signing up:', error);
        // Redirect to login page if the user already exists
        if (error.name === "UsernameExistsException") {
            alert(error.message);
            window.location.replace("./login.html");
        }        
    }
}


// Event Listeners if user is on the Sign Up page
if (document.querySelector("#auth-signup")) {

    document.querySelector("#form-auth-signup").addEventListener("submit", event => {
        event.preventDefault(); // Prevent the browser from reloading on submit event.
    });
    
    document.querySelector("#btnSignUp").addEventListener("click", () => {
        const email = document.querySelector("#formSignUpEmail").value
        const password = document.querySelector("#formSignUpPassword").value
        signUp({ email, password });
    });
    
};

// Account confirmation function
export const confirmSignUp = async ({username, code}) => {    
    try {
      const {result} = await Auth.confirmSignUp(username, code);
      console.log(result);
      alert("Account created successfully");
      window.location = '/login.html'

    } catch (error) {
        console.log('error confirming sign up', error);
        alert(error.message);
    }
};

// Resend confrimation code function
export const resendConfirmationCode = async (username) => {
    try {
        await Auth.resendSignUp(username);
        console.log('code resent successfully');
        alert('code resent successfully');
    } catch (error) {
        console.log('error resending code: ', error);        
        alert(error.message);
    }
};

// Event Listeners if user is on Account confirmation page
if (document.querySelector("#auth-signup-confirm")) {

    // Populate the email address value
    let username_value = location.hash.substring(1);        
    document.querySelector("#formSignUpConfirmEmail").setAttribute("value", username_value);

    document.querySelector("#form-auth-signup-confirm").addEventListener("click", event => {
        event.preventDefault();
    });

    document.querySelector("#btnConfirm").addEventListener("click", () => {
        let username = document.querySelector("#formSignUpConfirmEmail").value
        const code = document.querySelector("#formSignUpConfirmCode").value
        console.log({username, code});
        confirmSignUp({username, code});
    });

    document.querySelector("#btnResend").addEventListener("click", () => {
        let username = document.querySelector("#formSignUpConfirmEmail").value
        resendConfirmationCode(username);
    });
}
```

&nbsp;

`auth_login.js`

```js
console.log("auth_login.js loaded...");

import { Auth } from 'aws-amplify';

// Sign In function
export const signIn = async ({username, password}) => {
    try {
        const { user } = await Auth.signIn(username, password);
        console.log(user)
        alert("user signed in");
        window.location = '/secret.html'
    } catch (error) {
        console.log('error signing in', error);
        alert(error.message);
        window.location = '/login.html'
    }
}

// Event Listeners if user is on Login page
if (document.querySelector("#auth-login")) {

    document.querySelector("#form-auth-login").addEventListener("click", event => {
        event.preventDefault();
    });

    document.querySelector("#btnLogin").addEventListener("click", () => {
        const username = document.querySelector("#formLoginEmail").value
        const password = document.querySelector("#formLoginPassword").value
        console.log({username, password});
        signIn({username, password});
    });
};
```

&nbsp;

`auth_forgot_password.js`

```js
console.log("auth_forgot_password.js loaded...");

import { Auth } from 'aws-amplify';

// Forgot password function
export const forgotPass = async ({username}) => {    
    try {
        const { user } = await Auth.forgotPassword(username);
        console.log(user)
        alert("Password reset request sent");
        window.location = '/forgot_confirm.html#' + username;
    } catch (error) {
        console.log('error signing in', error);
        alert(error.message);
        window.location = '/login.html'
    }
}

// Event Listeners if user is on Forgot Password page
if (document.querySelector("#auth-forgot-password")) {

    document.querySelector("#form-auth-forgot-password").addEventListener("click", event => {
        event.preventDefault();
    });

    document.querySelector("#btnForgot").addEventListener("click", () => {
        const username = document.querySelector("#formForgotEmail").value                
        forgotPass( {username});
    });

}

// Confirm New Password function
export const confirmForgotPass = async (username, code, new_password) => {    
    try {
        await Auth.forgotPasswordSubmit(username, code, new_password);        
        alert("New password confirmation sent");   
        window.location = '/login.html'     
    } catch (error) {
        console.log('error confirming new password', error);
        alert(error.message);
    }
}

// Event Listeners on the Confirm New Password page (after Forgot Password page)
if (document.querySelector("#auth-forgot-password-confirm")) {

    // Populate the email address value
    let username_value = location.hash.substring(1);        
    document.querySelector("#formForgotConfirmEmail").setAttribute("value", username_value);


    document.querySelector("#form-auth-forgot-password-confirm").addEventListener("click", event => {
        event.preventDefault();
    });

    document.querySelector("#btnConfirmForgot").addEventListener("click", () => {
        const username = document.querySelector("#formForgotConfirmEmail").value
        let code = document.querySelector("#formForgotConfirmCode").value
        let password = document.querySelector("#formForgotConfirmPassword").value
        confirmForgotPass( username, code, password );        
    });

}
```

&nbsp;

`auth_user.js`

```js
console.log("auth_user.js loaded...");

import { Auth } from 'aws-amplify';

// Check if a user is logged or not.
// It will throw an error if there is no user logged in.
export async function userAuthState() {
    return await Auth.currentAuthenticatedUser({
            bypassCache: false // Optional, By default is false. If set to true, this call will send a request to Cognito to get the latest user data
        });
};
```

&nbsp;

`auth_logout.js`

```js
console.log("auth_logout.js loaded...");

import { Auth } from 'aws-amplify';

// Sign Out function
export async function signOut() {
    console.log("signOut triggered...")
    try {
        await Auth.userPool.getCurrentUser().signOut()
        window.location = '/index.html'        
    } catch (error) {
        console.log('error signing out: ', error);
    }
}


// Event Listener for Sign Out button
if (document.querySelector("#nav-logout")) {
    document.querySelector("#nav-logout").addEventListener("click", () => {
        signOut();
    })
}
```

&nbsp;

`auth_content.js`

```js
import { userAuthState } from './auth_user';

export function checkAuthContent() {
// If not authenticated, pages with containing the id of 'authenticated-content' will redirect to login.html.
    if (document.querySelector("#authenticated-content")) {
        userAuthState()
            .then(data => {
                console.log('user is authenticated: ', data);
            })
            .catch(error => {
                console.log('user is not authenticated: ', error);
                // Since this is the secret page and the user is not authenticated, redirect to the login page.
                alert("This user is not authenticated and will be redirected");
                window.location = '/login.html';
            });
    } else {
        // Merely putting this here so that the authentication state of other pages can be seen in Developer Tools
        userAuthState()
            .then(data => {
                console.log('user is authenticated: ', data);
            })
            .catch(error => {
                console.log('user is not authenticated: ', error);
            });
    }
}
```

&nbsp;

Finally, import the modules into `index.js` and perform some basic authentication logic:

```js
console.log("index.js started...");

import Amplify from "aws-amplify";
import { Auth } from 'aws-amplify';
import aws_exports from "./aws-exports.js";

import { userAuthState } from './auth_user';
import { checkAuthContent } from './auth_content';
import { signUp, confirmSignUp, resendConfirmationCode } from './auth_signup';
import { signIn } from './auth_login';
import { forgotPass, confirmForgotPass } from './auth_forgot_password';
import { signOut } from './auth_logout';


Amplify.configure(aws_exports);

checkAuthContent();

console.log("index.js finished...");
```

&nbsp;

## Test it all

From the root folder of your project:

```bash
npm start
```

Your project should compile successfully (no errors or warnings), and your Landing page should be open.  Open Developer Tools as well to view the application logic flow while you are testing.

Navigate to a temporary email provider (there are many) and get a temporary disposable email address.

### Normal sign-up flow

1. Sign up with temporary email address
2. Confirm account with _incorrect_ code.
3. Confirm email account with correct code received via email.
4. Log in.  You should now be directed to the Secret page.
5. Review Developer Tools' Console to ensure that the user is authenticated.
6. Log out.  Review Developer Tools' Console to confirm that the user is **not authenticated**.
7. Attempt to manually access the secret.html file from the address bar.  Should be redirected to the login page.

### Other authentication tidbits

- Attempt to reset your password.
- Attempt to sign up with an existing email address
- Attempt to log in with the incorrect password.
- Test authentication persistence by:
  - Signing in with the correct credentials (confirm this in Developer Tools' Console)
  - Close the browser tab.
  - Close your dev server.
  - Re-run `npm start` and check the Console again.  You should still be authenticated.

## Final thoughts

I spent way too much time on this, but I learnt a lot about how the Amplify and Cognito SDK's work, so it was probably worth it...

Even if this isn't the ideal approach, I hope this will be of use to someone or at least start a discussion around Amplify framework-agnostic approaches.

🥔