Amplify authentication flow without any front end frameworks ("Vanilla" JavaScript)

illusivemilkman

Willem Booysen

Posted on February 18, 2021

Amplify authentication flow without any front end frameworks ("Vanilla" JavaScript)

Alt Text

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.

 

Index

 

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.

# To install Amplify CLI
npm install -g @aws-amplify/cli 
Enter fullscreen mode Exit fullscreen mode

Setup Amplify

amplify configure
Enter fullscreen mode Exit fullscreen mode

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.

 

Set up a project

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

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

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

code .
Enter fullscreen mode Exit fullscreen mode

The directory structure should be:

amplify-vanilla-auth-flowsrc
├── src
│   └── index.js
├── index.html
├── package.json
└── webpack.config.js
Enter fullscreen mode Exit fullscreen mode

Add the following to the package.json file:

{
  "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"
  }
}

Enter fullscreen mode Exit fullscreen mode

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):

npm install
Enter fullscreen mode Exit fullscreen mode

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

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

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:

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

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

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

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

npm start
Enter fullscreen mode Exit fullscreen mode

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. Ctrl+C when done.

 

Initialising Amplify

amplify init
Enter fullscreen mode Exit fullscreen mode

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

 

Adding Auth

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

amplify add auth
Enter fullscreen mode Exit fullscreen mode

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

 

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

amplify push
Enter fullscreen mode Exit fullscreen mode

 

Review your Cognito settings (optional)

amplify console
Enter fullscreen mode Exit fullscreen mode

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.

 

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:

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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:

cd src
touch auth_signup.js auth_login.js auth_forgot_password.js auth_user.js auth_logout.js auth_content.js
Enter fullscreen mode Exit fullscreen mode

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

 

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

 

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

 

auth_forgot_password.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 );        
    });

}
Enter fullscreen mode Exit fullscreen mode

 

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

 

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

 

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

 

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

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

 

Test it all

From the root folder of your project:

npm start
Enter fullscreen mode Exit fullscreen mode

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.

🥔

💖 💪 🙅 🚩
illusivemilkman
Willem Booysen

Posted on February 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related