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
- Set up a project
- Initialising Amplify
- Adding Auth
- Create the auth flow html pages
- Create the auth flow JavaScript files
- Test it all
- Final thoughts
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
Setup Amplify
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
.
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
Then proceed to open in your code editor of choice (VS Code in my case):
code .
The directory structure should be:
amplify-vanilla-auth-flowsrc
├── src
│ └── index.js
├── index.html
├── package.json
└── webpack.config.js
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"
}
}
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
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()
]
};
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
+ }
+ }
+ }
]
},
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>
Before we continue, let's confirm that our environment is working.
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. Ctrl+C when done.
Initialising Amplify
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
Adding Auth
Now to add authentication to our Amplify app. From the root folder of your project, run the following command:
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
Once done, you'll have to push these changes to the Amplify service:
amplify push
Review your Cognito settings (optional)
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.
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
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>
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>
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>
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>
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>
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>
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
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);
});
}
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});
});
};
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 );
});
}
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
});
};
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();
})
}
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);
});
}
}
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...");
Test it all
From the root folder of your project:
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
- Sign up with temporary email address
- Confirm account with incorrect code.
- Confirm email account with correct code received via email.
- Log in. You should now be directed to the Secret page.
- Review Developer Tools' Console to ensure that the user is authenticated.
- Log out. Review Developer Tools' Console to confirm that the user is not authenticated.
- 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.
🥔
Top comments (4)
This is a really good tutorial Willem. Unfortunately I had a few issues getting it working on my environment.
There are a few issues with some of the sample files - the packages.json provided is invalid, even after removing the 'deleted' line and the plus signs. Should there be a "script" tag there, with an opening curly brace?
Also the webpack file is throwing errors:
[webpack-cli] Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
By removing the DevServer element I was able to get it to compile and run, but then am striking Javascript errors on the Index page:
Uncaught TypeError: (0 , auth_userWEBPACK_IMPORTED_MODULE_0_.userAuthState) is not a function
at checkAuthContent (main.bundle.js:143934)
But thank you for your article anyway - it was valuable in getting to understand the Amplify stack.
Thank you for the kind words and picking up on those issues.
For future readers:
I've updated the tutorial slightly, including the missing
"scrips:"
tag in the package.json file.The error regarding the Dev Server has to do with the
npm install ...
command installing the latest versions of packages listed under"devDependencies:"
. If one were to replace the latest versions with those of the tutorial, it will work. Just remember to runnpm install
again when replacing anydevDependancies
items.As I recall, this is fundementally a problem with Amplify and breaking changes in Webpack 5, which in turn has to do with changes in standards in module loading that have yet to be resolved in many underlying packages that Amplify uses, although don't quote me on that.
One can confirm that this is an ongoing issue as even AWS haven't updated their JS Amplify tutorials (see docs.amplify.aws/start/getting-sta...), where they currently have the devDependancies listed as:
If anyone has a resolution to this, pelase feel free to leave a comment, I'd love to keep this tutorial updated for future users.
note that for amplify v6, you cannot use this style anymore
you have to import the function themselves, like
Thank you very much for this tutorial. I have been trying to do this for quite some time as I am learning Amplify :)
However, I am surprised the secret page is still loaded on the user side before the redirection happens. This is definitely a no-go for me as this means that if someone deactivate JavaScript, the secret page is displayed (big security issue :)
Is there a way to restrict page access on the server to be more secured? (a bit like Azure SWA does with a config file ?)