Password-less Authentication
Allow user to login using email + verification code
2025.07
This is a sample design document for our technical design consulting service.
Requirements Summary
- Users login with email + confirmation code.
- Email address works as user account identifier
- Login form does not distinguish between signup and login
- Confirmation code must be entered with 10 minutes
- Three maximum failed attempts, after which a new code must be requested
- Session token does not expire, but should be renewed often
UI Components
Account Component
Account Component to be placed on the right most side of the topbar.
If the user is logged in, the account component shows the user profile image and his name. When clicked, navigates to the user profile page.
If the user is not logged in, the account component is a "Login" button which navigates to the login page when clicked.
Before navigation to the login form, the current url is stored in
sessionStorage
under key login_redirect_url
so it can be restored
after successful login.
Profile page
The profile page should show the user's name, email, and profile picture.
The profile page includes a logout button.
Login page
The login page shows a login form which can be in two states:
Email Input
- Email field with basic validation
- Action button: "Request login code"
Code Input
- Login code field
- Action button: "Login"
- Email is retained, but not editable
Post-Login Redirection
After logging in, the login_redirect_url
key from sessionStorage
is
inspected. If a value is present there, the key is cleared and the page
is redirected to the given route. If it's empty, the page is redirected
to the home page.
Database tables
For efficient storage on the database, we will internally use a numeric user_id, but it will not be exposed to the UI side.
Session
The backend generates a session token by filling a 16 byte buffer with cryptographically secure random data, then encodes it in base64.
For the client side, the session token is stored in local storage using key "session_token".
Every API request expects the session token to be included in the header "X-Session-Token".
The front end has a globally accessible instance of UserProfile
type UserProfile = {
email: string
name: string
picture_url: string
}
Processes
There are types of events we care about:
-
Interacting with the login form
-
Checking session status on first load
As such, the following processes will be explained:
-
Login Code Request
-
Login Code Validation
-
Session Token Validation
-
Session Token Invalidation
Login Code Request
Occurs when the user enters their email into the login form and clicks the "Request login code" button.
API:
POST /api/request_login_code
Request:
{
email: string
}
OK Response (status 200)
{
}
Error Response (status 400)
{
}
Frontend flow:
-
Minimal validation is applied to the email address to validate it looks like an email address. There should at least be an '@' sign, one character before it, three characters after it, including a dot.
-
If the email is invalid, an error message is presented prompting the user to enter a valid email.
- Form error reporting UX is outside the scope of this document
-
If the email is valid, a request is sent to the backend
request_login_code
endpoint. -
The backend will perform the same validation on the email and return an error response if the given address does not pass validation.
-
If the backend returns an ok response, the login form moves to the login code validation state.
Backend flow:
-
Minimal validation is applied to the email address. Same logic as frontend. If invalid, returns an empty error response.
-
A short code is generated randomly
- It uses only numbers and capital letters.
- Avoid ambiguous letters and numbers
0 O I 1
. - The length of the code should be 6 characters.
-
The short code is stored in the
login_codes
table, with thecreated_at
column set to the current timestamp. -
An email is formatted such that the login code is included in both the title and the body, and the phrase "login code" should be clearly present in both the title and the body.
-
The email should be short so as to not distract the user.
-
The email should not contain other things that could look like the login code.
-
-
The email is sent to the given address.
- The details of email sending mechanism is outside the scope of this document
-
OK response is sent to the frontend.
- Ideally, the process of formatting the email and sending it should not block sending the OK response.
Below we demonstrate the basic case with a sequence diagram.
Login Code Verification
Occurs when the user enters the login code into the login form
API:
POST /api/verify_login_code
Request:
{
email: string
code: string
}
OK Response (status 200)
{
session_token: string
user_profile: UserProfile
}
Error Response (status 400)
{
}
Frontend flow:
-
With the login form in the code validation state, the user enters the login code, obtained via email
-
The code is sent to the server for verification.
-
If the code is correct, the response will include the session token and the user profile.
-
The session token is stored into localStorage under key
session_token
. -
The global user profile instance is updated with the data returned by the backend
-
-
If the code is incorrect, a counter is incremented
- If the counter reaches 3, an error message is presented, prompting the user to restart the process, along with a link that when clicked resets the form to its initial state
Backend flow:
-
The
login_codes
table is queries for the entry keyed by the given email, and the login code in the form is compared against the login code in the table. -
The code is accepted if the entry is found and the code matches, and the entry is not expired
-
An entry is considered to be expired if:
failed_count
is >= 3created_at
is more than 10 minutes ago
-
If the entry exists but the code does not match,
failed_count
is incremented by one -
If the entry exists but is expired, it's deleted
-
The error response is always empty; no need to report the reason.
-
If the code is accepted:
-
Entry is deleted from
login_codes
-
The
user_accounts
table is queries for the given email -
If no match is found, this is considered a new account creation
-
A new entry is created in
user_profiles
with a new id -
A new entry is created in
user_accounts
associating the email address with the new account id -
A short welcome email is sent to the email address
-
-
If a match was found, the corresponding entry from user_profiles is loaded
-
A new session token is generated, and an entry in the sessions table is created.
-
The ok response is prepared, containing the session token and the user profile
-
Below we demonstrate various cases with sequence diagrams.
Session Token Validation
Occurs when the page is first loaded during the initialization process
API:
POST /api/verify_session_token
Request:
{
session_token: string
}
OK Response (status 200)
{
session_token: string
user_profile: UserProfile
}
Error Response (status 400)
{
}
Frontend flow:
- During initial page load, session_token is read from localStorage
- If a token is found, it's validated with the backend using the api
validate_session_token
- If the response is ok, the session_token is updated based on the response, and a global user profile instance is set
- If the response is invalid, the session token is deleted from localStorage, and the global user profile is set to null
Backend flow:
- The backend verifies the token by querying the sessions table for the
token
- If nothing is found, the session is considered invalid, and an error response is returned
- If the session token was created more than 24 hours ago, it is renewed:
- A new token is generated
- The current session is deleted from the database
- A new session is created using the new token, with the created_at set to the current timestamp
- Backend loads user profile to prepare the API response
- If the user profile cannot be found, it's an unexpected situation but in that case, the session is deleted from the sessions table, and the backend returns an error response
- After both token and user profile are loaded, the response is formatted and sent back to the frontend
Below we demonstrate various cases with sequence diagrams.
Session Token Invalidation
Triggered when user hits logout button. The browser deletes the session token from local storage, and tells the backend to delete it from the database as well.
API:
POST /api/delete_session_token
Request:
{
session_token: string
}
OK Response (status 200)
{
}
Frontend flow:
-
When user hits log out button, the session token is read from the localStorage into a variable, before calling
removeItem
. -
The frontend sends the token to the backend so it can delete it as well.
-
Without checking for the response, the frontend redirects to the home page and forces the browser to reload, in order to make sure none of the user data is retained anywhere.
Backend flow:
-
Send query to the database to delete session entry matching the token
-
Send an empty ok response to the browser
There's no need for any error response. The browser does not even check the response.
Conclusion
In order to support the implementation of password less implementation, we have outlined the following:
- Data models
- Processing logic
- Interface boundaries