User Sign-up Flow
Intro
In this article, we will lay out what an ideal user sign-up flow looks like, including our recommendations for data security and better user experience.
Use cases
Web apps, Mobile apps, Desktop apps
Prerequisites
Creating user sign-up flows just require entry-level knowledge of these:
How to create user interfaces (e.g. HTML form, input, buttons)
How to create an HTTP web server.
How to create HTTP requests (method, headers, body)
How to create HTTP responses (status, headers, body)
How to use HTTP cookies and sessions.
How to use a relational database (like PostgreSQL, or MySQL)
How to create, read, update, and delete rows.
What is authentication and authorization
Authentication and Authorization are the core concepts of Identity and Access Management.
Authentication is how we verify someone’s identity. It involves user id’s, passwords, two-factor authentication, multi-factor authentication.
Authorization is how we verify someone’s access to resources. It involves roles and permissions.
In this workflow we will just focus on the account sign-ups, but keep in mind that other account sign-up flows might involve assigning the user’s default roles and permissions (e.g. as an admin, staff, or user). That is where authentication and authorization go hand-in-hand.
What is password hashing
Long story short, storing our user’s passwords in plain text is not recommended because if our database gets hacked and the hackers gain access to our user’s plain text passwords, the hackers may now also access their accounts on other sites where they may have used the same email address and password.
Password hashing solves this. It is the use of cryptographic hash functions and cryptographic salts to create password hashes that let’s us verify that the user has entered their correct password, even without actually storing their passwords.
Create the users table
Create the "users" table.
Create the "id", "email", "password_salt", "password_key", "created" columns.
“id” can be an auto-incrementing integer, or a UUIDv4 string
"password_salt" is our generated salt for our password hashing process.
"password_key" is our generated key derived from our "password" and "password_salt".
CREATE TABLE "users" (
"id" serial PRIMARY KEY,
"email" text NOT NULL,
"password_salt" text,
"password_key" text,
UNIQUE("email")
);
Create the sign-up page
Email input
Password input
Sign-up button
Create the sign-up button action
Email must not be empty.
Password must not be empty.
Create the HTTP request
POST /sign-up HTTP/1.1
Host: example.com
Content-Type: application/json; charset=utf-8
{ email, password }
Show the progress indicators
Disable the inputs and buttons.
Show an animated loader / spinner.
Show a message, e.g. "Creating your account..".
Create the HTTP request handler
Extract the user's credentials from the HTTP request body.
Sanitize the user's credentials
Sanitizing prevents unwanted user inputs, i.e. XSS attacks.
Normalize the user's credentials
Normalization helps us accurately check if an email address is already used. This is normally done through case-insensitive comparisons.
If you only allow ASCII characters, just lower-case the values.
If you also allow Unicode characters, case-folding and normalization with NFKC can improve our case-insensitive comparisons.
Case Folding
Normalization with NFKC
Validate the user's credentials
Email must not be empty.
Email must not exist.
Password must not be empty.
Password must be strong.
Decide on a password hashing algorithm, like Scrypt
Create the “password_salt” and “password_key”
Generate a 256-bit random key as your “password_salt”
Generate the “password_key” derived from your “password” and “password_salt”
Create the user account
user = {
id: null, // auto-increment, or uuidv4
email,
password_salt,
password_key,
}
Sign-in the user in the current session
This can improve the user experience by not requiring the user to re-enter the same credentials they have used to sign-up.
session.user_id = user.id;
Create the HTTP response
Example sign-up error message:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{ message: "Error, email already used." }
Example sign-up success message.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{ message: "Account created!" }
Show the error and success messages
We want to show these messages to the user so they can amend as needed.
"Network error."
"Error, email invalid."
"Error, email already used."
"Account created!"
Redirect the user
This can improve the user experience by redirecting the user to a welcome page, an on-boarding page, or their user dashboard.
Improvements
Allow sign-up with usernames.
Allow sign-up with phone numbers.
Enforce Transport Layer Security (TLS). User credentials can be easily sniffed when sent over non-secure connections.
Enforce IP-based rate limits, or challenge-response captchas, to prevent automated sign-ups.
Enforce sign-up verification, by sending a verification link to the user’s email address, or by sending a randomly generated 6-digit code to the user’s phone number.
Enforce restriction of usernames to ASCII characters, or alphanumeric characters.
Enforce disallowed usernames.
Check if password has been leaked in data breaches.
Add a link to the sign-in page.
Already have an account? Sign-in here.
Add a link to the account recovery page.
Forgot your password? Recover your account here.
Allow sign-ups through OAuth.
Sign-up with Twitter
Sign-up with GitHub
Some code examples
Example JavaScript client-side code:
try {
const response = await fetch('/sign-up', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password }),
});
const response_body = await response.json();
alert(response_body.message);
if (response.status === 200) {
// success, redirect
} else if (response.status >= 400) {
// error, do nothing
}
} catch (e) {
// request error
// network error
}
Example regex for ASCII-only restriction.
/^[A-Za-z0-9!@#$%^&*()]+$/
Example JavaScript code demonstrating normalization.
// Latin Capital Letter I
// https://unicode-table.com/en/0049/
const latin = 'I';
// Roman Numeral One
// https://unicode-table.com/en/2160/
const roman = 'Ⅰ';
latin.normalize('NFKC'); // 'I'
roman.normalize('NFKC'); // 'I'
latin.normalize('NFKC') === roman.normalize('NFKC'); // true
Feedback
For corrections and improvements in this workflow, feel free to comment. I’ll update as necessary, thank you very much.