As our MVP (Minimum Viable Product) prototype of the task list app undergoes user testing, it’s time to build the backend infrastructure.
Currently, the app operates solely on the client side, with all data stored in the user’s browser.
To enable usage of the app across multiple devices and potentially allow different users to access shared task lists, we need to move this data to the cloud.
With this change, all tasks from all users will be stored in a centralised database. This raises an essential question: how do we determine which tasks each user can access? This is where authentication comes into play.
Since all tasks are stored in a single database, they need to be paired with users so that user A cannot see user B's tasks. However, enabling collaboration between users (e.g., user A and user B sharing tasks) would require significant rework.
By introducing a higher-level abstraction and associating both users and tasks with organisations, we decouple tasks from individual users, making it easier to let users share task lists in the future. The best part is that this change requires minimal effort - rather than linking entities directly to users, we simply associate them with organisations instead.
Here's a high level overview of what we will implement:
Users will register using their email address. The email address serves multiple purposes:
To prevent unauthorised sign-ups, we must confirm that the email address truly belongs to the person registering. We achieve this with an email verification process:
After setting up their credentials, users can log in. However, authentication presents a challenge:
In case users forgot their password, we need to provide a secure way for them to regain access to their accounts.
Upon successful login, we generate a pair of tokens:
To prevent security risks:
We split the project into parts:
This is a high level diagram of what we are going to build:
We will create services, namely the comms service, and the auth service. They will be defined as packages for now to be used within the API, but designed in a way in which they can be easily extracted to their own HTTP microservices.
This may seem complicated at first, but it becomes much clearer and simpler when seen in practice.
We created a new package @repo/comms-service
. The purpose of it will be to communicate with the users externally, outside of the app.
To make the process of constructing emails easier, we created a reusable email HTML template that takes care of formatting, and branding the emails. We have predefined some common elements in EmailElements
like:
This makes it easy to customise emails. The utility function createEmail
receives the title, content, domain, and optional extra styles for the email.
For now the purpose of the comms service is to send emails, and we will be using nodemailer
and AWS SES to send emails in production. In development we will just log the props received by the handlers for easier debugging.
Based on the authentication flow, we will need to send emails in two cases:
sendVerificationEmail
- send the verification link with verification token to the user to verify email ownershipsendPasswordResetEmail
- send the password reset link to verify email ownership, and set a new password with a password tokenBoth receiving the recipient’s email
address and the url
to include in the email.
For the authentication process we need to store information persistently on the database, and in order to do that, we need to create new database schemas, and validation schemas for:
User
- the user schema will store information required to identify users, and their properties like credentials, and which organisation they belong toOrganisation
- the organisation groups together users, and tasks, so users belonging to an organisation can share tasksPasswordToken
- belongs to a user, allows them to set a new passwordVerficiationToken
- belongs to an email address, and binds it to a user when usedRefreshToken
- storing refresh tokens on the server (along with the client) allows to remotely revoke and validate themHere's a demonstration of the high level relations between the schemas:
The job of the auth service is to handle every authentication related operation, so that the API only needs to handle the routing of inputs and outputs.
For the context of the handlers we will initialise resources:
connection
with the connection string (DB_CONNECTION_STRING
) coming from the configcommsService
to be able to send emailsOrganisationModel
UserModel
VerificationTokenModel
PasswordTokenModel
RefreshTokenModel
generateTokens
to generate access and refresh token pairsWe defined the following handlers:
createVerificationToken
- Generates a verification token for a new user based on their email, deletes any existing tokens for the email, and sends a verification email with a URL using the comms-service
's sendVerificationEmail
.verifyEmail
- Validates the verification token to see if it exists and not expired, then creates an organisation if necessary, registers the user, and generates a password token.verifyPasswordToken
- Checks if a given password reset token exists and is still valid (exists, and not expired).setPassword
- Validates a password token, hashes and updates the user's password, marks them as verified, and deletes any existing refresh tokens.resetPassword
- Generates a password token for a user, deletes any existing tokens, and sends a password reset email with a URL using the comms-service
's sendPasswordResetEmail
.signIn
- Authenticates a user by validating their email and password, then generates and returns access and refresh tokens.refreshTokens
- Validates and refreshes authentication tokens using a valid refresh token, deleting the old one and issuing new tokens.removeRefreshToken
- Deletes a given refresh token if valid, ensuring the token cannot be reused.While token generation and management are handled by the authentication service, we still need a way to securely issue, store, and forward tokens between the server and the client. This allows users to remain authenticated across requests while maintaining security best practices.
To integrate authentication into the API, we have:
authService
as a plugin, making authentication methods available throughout the API.auth
plugin that introduces several key methods for handling authentication tokens efficiently.These are methods attached to Fastify’s reply
object, allowing responses to manipulate authentication tokens:
setRefreshToken(refreshToken)
httpOnly
cookie for security.refreshToken(refreshToken)
authService.refreshTokens()
to generate new access and refresh tokens.setRefreshToken()
.These extend the request
object, enabling authentication enforcement:
requireUser()
To automatically authenticate users when they send requests, we added a preHandler
hook:
preHandler
HookAuthorization
header.request.user
if valid.Finally, we need to wrap everything together in GraphQL operations that are accessible by the client. We created the relevant mutations with their input shapes, and their corresponding resolvers:
signUpResolver
- calls authService.createVerificationToken
to generate a verification token for the user and send a verification email.verifyEmailResolver
- calls authService.verifyEmail
to verify a user's email using a verification token and returns a password reset token.setPasswordResolver
- calls authService.setPassword
to validate the password reset token and update the user's password.resetPasswordResolver
- calls authService.resetPassword
to generate a password reset token and send a password reset email.signInResolver
- calls authService.signIn
to authenticate the user using their email and password, issues authentication tokens, and sets the refresh token in cookies.refreshTokensResolver
- calls response.refreshToken
to refresh authentication tokens using a refresh token from cookies or request data, then returns them, and sets the refresh token in cookies.logoutResolver
- calls authService.removeRefreshToken
to revoke the refresh token and clears the refresh token cookie.Based on the ticket description, we need to add an additional method that checks if the password token is valid before it is used. Password tokens come into play when the user is resetting their password, and click on the link in the email.
verifyPasswordTokenArgsSchema
for the handler verifyPasswordToken
in the auth service to validate its arguments using zod. We only need the passwordTokenId
.verifyPasswordToken
handler, which returns false
if the token doesn’t exist, or is expired, and true
otherwise.VerifyPasswordToken
that takes the passwordTokenId
as inputverifyPasswordTokenResolver
which is forwarding the input to the auth service, and returns its result.With this release, we have implemented a complete user management system, including authentication, email verification, password recovery, and session management.
We will continue to build our API, and implement task management including:
Here's how our timeline look like now: