In this task, you will add authentication to the Posts app. You will implement user sign-up, sign-in, and sign-out functionality using the Lucia Auth library. You will also secure the API routes to require authentication for creating, updating, and deleting posts.
Authentication is the process of verifying the identity of a user. It involves validating the user’s credentials, such as a username and password, to grant access to a system or application. Authentication is essential for securing user data and ensuring that only authorized users can access protected resources.
Authentication involves the following steps:
Authentication is often confused with authorization, but they are distinct concepts:
In this task, you will focus on implementing authentication for the Posts app.
In the context of web applications, authentication is typically implemented using sessions, tokens, or cookies.
Cookies are small pieces of data stored in the user’s browser that are sent with each request to identify the user. Cookies are commonly used to store session IDs, authentication tokens, and other user-related information. Cookies can be set with an expiration time, domain, and path to control their behavior.
To send a cookie in an HTTP response, you set the Set-Cookie
header with the cookie’s name and value. The browser automatically includes the cookie in subsequent requests to the same domain.
In this task, you will use sessions and cookies to manage user authentication in the Posts app.
Lucia Auth is a library that provides simple and secure user authentication and session management for web applications. It abstracts away the complexity of handling sessions and provides an API that’s easy to use, understand, and extend. Lucia Auth supports various adapters for different databases and session storage mechanisms.
We will use Lucia Auth with the Drizzle SQLite adapter to manage user sessions and authentication in the Posts app.
In this step, you will create the authentication routes for sign-up, sign-in, and sign-out. Create a new file api/src/routes/auth.ts
and add the following code:
Typically, a successful POST request results in a 201 Created
status code, indicating that a new resource has been created. However, in the case of authentication routes, it is common to return a 200 Ok
status code for sign-in
and sign-out
routes, and a 201 Created
status code for the sign-up
route.
Moreover, while the general wisdom is to use plural nouns for routes (such as posts
, comments
, users
, etc.), for authentication routes, it is common to use endpoints such as login
, sign-in
, register
, sign-up
, logout
, sign-out
, etc. If we wanted to align more closely with RESTful principles while still being clear, we could consider nesting these actions under a noun. For example:
/sessions
for sign-in (creating a new session)/sessions
for sign-out (ending a session)/users
for sign-up (creating a new user resource)Update the api/src/app.ts
file to include the authentication routes:
Try these endpoints in Postman to ensure they are working as expected.
It might be helpful to add logging to the Hono instance to see the incoming requests and outgoing responses. You can do this by adding the following code to the api/src/app.ts
file:
If you now restart the server and make a request to the authentication routes, you should see the incoming requests and outgoing responses in the console.
Let’s add some validation to the sign-up and sign-in routes. Add the following code to the api/src/validators/schemas.ts
file:
Notice the refine
method in the password
field of the signUpSchema
. This method allows you to add custom validation logic to the schema. In this case, we are checking that the password contains at least one lowercase letter, one uppercase letter, and one number.
The /[a-z]/.test(value)
is a regular expression that checks if the password contains at least one lowercase letter. Similarly, /[A-Z]/.test(value)
checks for an uppercase letter, and /[0-9]/.test(value)
checks for a number.
Add validation to the sign-up and sign-in routes in the api/src/routes/auth.ts
file:
Now, if you try to sign up with invalid data, you should receive an error message indicating what went wrong.
In this step, you will add a SQLite table for storing user data. Create a new file api/src/db/schema.ts
and add the following code:
Notice that we are storing the password hash instead of the plain text password. This is a common practice to enhance security. Moreover, we are using the unique
constraint on the username
field to ensure that each username is unique.
Next, run the following command to update the SQLite table:
This command will create the users
table in the SQLite database.
In this step, you will hash the password before storing it in the database. We will use the Argon2 password hashing algorithm, which is considered one of the best choices for password hashing due to its resistance to GPU attacks and side-channel attacks.
First, install the @node-rs/argon2
package:
Next, update the api/src/routes/auth.ts
file to hash the password before storing it in the database:
The hash
function takes the password and a configuration object as arguments. The configuration object specifies the memory cost, time cost, output length, and parallelism. These parameters determine the security and performance of the hashing algorithm.
Now, try signing up with a new user in Postman.
You should see the user data stored in the database with the password hashed.
In this step, you will update the sign-in route to verify the password hash during authentication. We will use the verify
function from the @node-rs/argon2
package to compare the password hash stored in the database with the password provided during sign-in. Update the api/src/routes/auth.ts
file to verify the password during sign-in:
Notice that we are using the verify
function to compare the password hash stored in the database with the password provided during sign-in. If the passwords match, the user is successfully signed in; otherwise, an error message is returned.
Try signing in with the user you created earlier in Postman.
Also try signing in with an incorrect password to see the error message.
In the following steps, you will add the Lucia Auth library to handle user sessions. Lucia Auth provides a simple and secure way to manage user authentication and sessions in your application.
First, install the lucia
and @lucia-auth/adapter-drizzle
packages:
Next, add the following code to the api/src/db/schema.ts
file to create a sessions
table:
Notice that the id
field is of type text
and is the primary key for the sessions
table. This is because Lucia Auth requires the session ID to be a string. The sessions table also includes a foreign key userId
that references the id
field in the users
table. This establishes a relationship between the sessions
and users
tables. The expiresAt
field stores the timestamp when the session expires. These fields are essential for managing user sessions and are required by Lucia Auth.
Next, run the following command to update the SQLite database with the new sessions
table: (You may need to delete the existing db.sqlite
file before running this command)
You should now see the sessions
table in the SQLite database.
In this step, you will create a new file api/src/db/auth.ts
to set up the Lucia Auth adapter:
The DrizzleSQLiteAdapter
class is used to create an adapter for the Lucia Auth library that interacts with the SQLite database through the Drizzle ORM. The adapter requires the database connection, the sessions
table, and the users
table as arguments. The lucia
instance is created with the adapter and an empty configuration object.
The Register
interface is extended to include the Lucia
instance and the UserId
type. This allows us to use a numeric user ID when interacting with the lucia
instance.
The syntax declare module "lucia" {...}
is used to extend the lucia
module with additional types and properties. In this case, we are adding the Lucia
instance and the UserId
type to the Register
interface.
In this step, you will update the sign-in route handler to create a session using Lucia Auth. Add the following code to the api/src/routes/auth.ts
file:
Try signing in with a user in Postman to see the session data in the response.
You can also inspect the database in Drizzle Studio to see the session data stored in the sessions
table.
It is a common practice to store session data in cookies to manage user sessions in web applications. In this step, you will update the sign-in routes to set a session cookie in the response headers. Add the following code to the api/src/routes/auth.ts
file:
Notice that we are not returning the session data in the response anymore. Instead, we are setting a session cookie in the response headers using the Set-Cookie
header. The createSessionCookie
method is used to create a session cookie with the session ID. The serialize
method is used to serialize the cookie, which is then set in the response headers.
Try signing in with a user in Postman to see the session cookie in the response headers.
You can also add a console.log statement to see the session cookie in the console. The output should look similar to the following:
It is a common practice to create a session cookie for new users when they sign up. This allows users to be automatically signed in after signing up. In this step, you will update the sign-up route to create a session cookie for new users. Add the following code to the api/src/routes/auth.ts
file:
Now, when a user signs up or signs in, a session is created, and the session cookie is set in the response headers.
Try signing up with a user in Postman to see the session cookie in the response headers.
When a cookie is set in the response headers, the browser automatically includes the cookie in subsequent requests to the same domain. This is how user sessions are maintained across multiple requests. Postman also automatically includes the session cookie in subsequent requests.
To sign out a user, you need to invalidate the session and clear the session cookie.
In this step, you will update the sign-out route to invalidate the session and clear the session cookie. Add the following code to the api/src/routes/auth.ts
file:
Let’s break down the changes:
The c.req.header("Cookie")
method is used to read the cookie from the request headers. The session ID is extracted from the session cookie using the readSessionCookie
method. If no session ID is found, an error is thrown.
If no session ID is found, an error is thrown.
The invalidateSession
method is used to invalidate the session with the given session ID. This is done by removing the session data from the sessions
table.
The createBlankSessionCookie
method is used to create a blank session cookie. This cookie is then set in the response headers to clear the session cookie.
Now, when a user signs out, the session is invalidated, and the session cookie is cleared.
Try signing out in Postman to see the session cookie cleared in the response headers. Note that Postman’s cookie manager enables you to view and edit cookies that are associated with different domains. So when you signed in, the session cookie was stored in Postman’s cookie manager. When you send any subsequent requests, Postman automatically includes the session cookie in the request headers.
In this task, you added authentication to the Posts app using the Lucia Auth library. You implemented user sign-up, sign-in, and sign-out functionality. You also learned about password hashing, input validation, and session management. In the next task, we will update the Posts app UI to allow users to sign up, sign in, and sign out.