In this task, you will add authorization to the Posts API. You will enforce authorization rules to allow users to create, update, and delete posts and comments only if they are authenticated and are the creators of the posts or comments. You will also associate posts and comments with the users who created them and implement authorization guards to enforce these rules.
Step 1: Create a new middleware
We start by creating an auth middleware that will be responsible for extracting the session cookie from the request headers, validating the session, and setting the user and session variables in the context.
Create a new file api/src/middleware/auth.ts with the following content:
Let’s break down the code above:
We import the Context and Next types from Hono and the lucia instance from the auth module.
We define an auth middleware function that takes the context c and the next function as arguments.
We extract the session cookie from the request headers using the Cookie header.
We read the session ID from the session cookie using the readSessionCookie method from the lucia instance.
If no session ID is found, we set the user and session variables in the context to null and call the next function.
If a session ID is found, we validate the session using the validateSession method from the lucia instance.
If the session is not found, we create a blank session cookie and set it in the response headers.
If the session is found and is fresh, we create a new session cookie and set it in the response headers.
We set the session and user variables in the context and call the next function.
If we apply this middleware to all routes, it will ensure that the user and session variables are set in the context for each request. This will allow us to check if a user is authenticated and retrieve the user’s information from the context.
Since we are adding session and user variables to the context, we need to update the Context type to include these variables. We’ll do this in the next step.
Step 2: Update the Context type
Let’s create a new file api/src/lib/context.ts with the following content:
In the code above:
We import the Env type from Hono and the Session and User types from the lucia module.
We define an Context interface that extends the Env interface.
We define a Variables object that contains the user and session variables with types User | null and Session | null, respectively.
Next, we need to let Hono know about this new Context type. We’ll do this in the next step.
Step 3: Update the Hono instance
To take advantage of Hono’s improved support for context-aware middleware, we need to update all instances where a Hono instance is created to use the Context type.
For example, in the api/src/app.ts file, you’ll want to modify the code as follows:
Notice that we’ve added the <Context> type parameter to the Hono instance creation. This tells Hono to use the Context type we defined earlier. You’ll need to make similar changes in other files where Hono instances are created.
Once you’ve updated all instances of Hono creation, you are ready to apply the auth middleware to all routes. We’ll do this in the next step.
Step 4: Apply the auth middleware to all routes
To apply the auth middleware to all routes, you can use the app.use method in the api/src/app.ts file. Add the following line to the file, before defining any routes:
This line tells Hono to apply the auth middleware to all routes. The /* pattern matches all routes, ensuring that the auth middleware is executed for every request.
With the auth middleware in place, you can now access the user and session variables in your route handlers. We will try this out in the next step.
Step 5: Access the session variables in a route handler
Let’s update the POST /sign-out route in the api/src/routes/auth.ts file to access the session variables from the context:
In the code above:
We access the session variable from the context using c.get("session").
We check if a session is found. If not, we throw an HTTP 401 exception.
Otherwise, we invalidate the session using the invalidateSession method from the lucia instance.
We create a blank session cookie and set it in the response headers to remove the session cookie from the client.
We return a JSON response with a message indicating that the user has been signed out.
Now that we can access the session and user variables in the context, we can use them to implement authorization for the Posts API. We’ll do this in the next steps.
Step 6: Implement authorization guard middleware
Instead of checking for the session variable in each route handler, we can create another middleware, authGuard, that will check if the user is authenticated and throw an HTTP 401 exception if not.
Create a new file api/src/middleware/auth-guard.ts with the following content:
In the code above:
We import the Context and Next types from Hono and the HTTPException class from the hono/http-exception module.
We define an authGuard middleware function that takes the context c and the next function as arguments.
We access the session variable from the context using c.get("session").
If no session is found, we throw an HTTP 401 exception with a message indicating that the user is unauthorized.
Otherwise, we call the next function to proceed to the next middleware or route handler.
With the authGuard middleware in place, we can now apply it to the routes that require authentication. We’ll do this in the next step.
Step 7: Apply the authGuard middleware to the routes
Add the authGuard middleware to the POST /posts route in api/src/routes/posts.ts:
Notice that we added the authGuard middleware to the POST /posts route. This ensures that only authenticated users can create new posts. You can apply the authGuard middleware to other routes that require authentication in a similar manner.
Open Postman and try performing any CRUD operations on posts without being signed in. You should receive an HTTP 401 Unauthorized response.
Adjust the session expiration time
If you want to adjust the time to expire the session, you can do so in api/src/db/auth.ts:
The sessionExpiresIn option accepts a TimeSpan object that specifies the duration after which the session will expire. You can adjust the duration as needed.
Step 8: Associate posts and comments with users
As part of our authorization plan, we need to associate posts and comments with the users who created them. This will allow us to enforce authorization rules such as allowing users to delete only their own posts and comments.
To associate posts and comments with users, we need to add a userId column to the posts and comments tables in the database schema.
Open the api/src/db/schema.ts file and update the posts and comments tables as follows:
Notice we added a userId column to both the posts and comments tables. This column references the id column in the users table, establishing a relationship between the posts/comments and the users who created them. Moreover, we added a cascade option to the references method to ensure that when a user is deleted, their associated posts and comments are also deleted.
Now that we have associated posts and comments with users, we can enforce authorization rules based on the user who created them. We’ll do this in the next steps. But first, we need to update the database!
Delete the sqlite.db file and run the following command to recreate the database:
We should also seed the database with some sample data to test the authorization rules. We’ll do this in the next step.
Step 9: Seed the database with sample data
To test the authorization rules, we need some sample data in the database. We’ll seed the database with sample users, posts, and comments.
Update the api/src/db/seed.ts file with the following content:
Notice that we added sample users, posts, and comments to the database. Each post is associated with a random user, and each comment is associated with a random user and post. This will allow us to test the authorization rules based on the user who created the post or comment.
To seed the database with the sample data, run the following command:
You can inspect the database to verify that the sample data has been inserted correctly. Open Drizzle Studio to view the data:
With the sample data in place, we can now enforce authorization rules based on the user who created the posts and comments. We’ll do this in the next steps.
Step 10: Implement authorization rules for posts
The general strategy for enforcing authorization rules is to check if the user is the creator of the post or comment before allowing them to perform certain actions. We’ll start by implementing authorization rules for posts.
Create a new post
Update the POST /posts route in the api/src/routes/posts.ts file to associate the new post with the authenticated user:
Notice that we added the userId: user!.id line to associate the new post with the authenticated user. This ensures that the user who created the post is correctly recorded in the database.
Delete a post
Update the DELETE /posts/:id route in the api/src/routes/posts.ts file to enforce authorization rules:
Notice that we added a check to ensure that the user who created the post is the same as the authenticated user. If not, we throw an HTTP 403 Forbidden exception, indicating that the user is unauthorized to delete the post.
Update a post
Update the PATCH /posts/:id route in the api/src/routes/posts.ts file to enforce authorization rules:
Notice that we added a check to ensure that the user who created the post is the same as the authenticated user. If not, we throw an HTTP 403 Forbidden exception, indicating that the user is unauthorized to update the post.
Get a single post
As part of our authroization strategy, we want to allow users to view posts even if they are not signed in. We’ll remove the authGuard middleware from the GET /posts/:id route in the api/src/routes/posts.ts file:
Notice that we removed the authGuard middleware from the route. This allows users to view posts even if they are not signed in.
Moreover, we use a leftJoin to include the author information in the response. This allows us to display the author’s name and username along with the post content.
Join Operations
In relational databases, a join operation is used to combine rows from two or more tables based on a related column between them. In this case, we are performing a leftJoin operation to combine the posts and users tables based on the userId column in the posts table and the id column in the users table.
The equivalent SQL query for the join operation would look like this:
There are different types of join operations, such as INNER JOIN, LEFT JOIN, RIGHT JOIN, and FULL JOIN, each with its own behavior. In this case, we are using a LEFT JOIN to include all posts, even if there is no corresponding user record (although this should not happen in practice).
It is beyond the scope of this guide to cover all the nuances of join operations, but understanding the basics of joins is essential for working with relational databases.
Get all posts
Similar to the GET /posts/:id route, we’ll remove the authGuard middleware from the GET /posts route in the api/src/routes/posts.ts file. We also perform a leftJoin operation to include the author information in the response. Moreover, we allow users to search for posts by the author’s username. To that aim, we should first update the input schema in api/src/validators/schemas.ts to include the username field:
Next, update the GET /posts route in the api/src/routes/posts.ts file:
Notice if the username query parameter is provided, we look up the user by the username and filter the posts by the user’s id. This allows users to search for posts by a specific author’s username.
Step 11: Implement authorization rules for comments
Similar to posts, we need to enforce authorization rules for comments. We’ll start by implementing authorization rules for comments.
Create a new comment
Update the POST /posts/:postId/comments route in the api/src/routes/comments.ts file to associate the new comment with the authenticated user:
Notice that we added the userId: user!.id line to associate the new comment with the authenticated user. This ensures that the user who created the comment is correctly recorded in the database.
Delete a comment
Update the DELETE /posts/:postId/comments/:commentId route in the api/src/routes/comments.ts file to enforce authorization rules:
Notice that we added a check to ensure that the user who created the comment is the same as the authenticated user. If not, we throw an HTTP 403 Forbidden exception, indicating that the user is unauthorized to delete the comment.
Update a comment
Update the PATCH /posts/:postId/comments/:commentId route in the api/src/routes/comments.ts file to enforce authorization rules:
Notice that we added a check to ensure that the user who created the comment is the same as the authenticated user. If not, we throw an HTTP 403 Forbidden exception, indicating that the user is unauthorized to update the comment.
Get a single comment
Unlike posts, we will not allow users to view comments without being signed in. We’ll keep the authGuard middleware in the GET /posts/:postId/comments/:commentId route in the api/src/routes/comments.ts file:
Notice that we kept the authGuard middleware in the route to ensure that users are signed in before viewing comments. This allows us to enforce authorization rules based on the authenticated user.
Moreover, we use a leftJoin to include the author information in the response. This allows us to display the author’s name and username along with the comment content.
Get all comments for a post
Similar to the GET /posts/:postId/comments/:commentId route, we’ll keep the authGuard middleware in the GET /posts/:postId/comments route in the api/src/routes/comments.ts file. We also perform a leftJoin operation to include the author information in the response. Moreover, we allow users to search for comments by the author’s username.
Update GET /posts/:postId/comments/ route in the api/src/routes/comments.ts file:
Notice that we kept the authGuard middleware in the route to ensure that users are signed in before viewing comments. Moreover, if the username query parameter is provided, we look up the user by the username and filter the comments by the user’s id. This allows users to search for comments by a specific author’s username.
Step 12: Test the authorization rules
With the authorization rules in place, you can now test the API to ensure that users can only perform actions on posts and comments that they created. Here are some test scenarios you can try:
View all posts: Try fetching all posts without being signed in. You should be able to view all posts.
View all posts for a specific author: Try fetching all posts for a specific author by providing the username query parameter. You should be able to view posts by the specified author.
Delete a post: Sign in as a sample user and try deleting a post created by another user. You should receive an HTTP 403 Forbidden response.
Update a post: Sign in as a sample user and try updating a post created by another user. You should receive an HTTP 403 Forbidden response.
Create a new post: Sign in as a sample user and try creating a post. The post should be associated with the signed-in user.
Repeat the above steps for comments.
Conclusion
In this task, you learned how to implement authorization in a RESTful API using Hono, Lucia, and SQLite. You enforced primary authorization rules by checking if a user is authenticated. You also implemented secondary authorization rules by associating posts and comments with users and enforcing rules based on the user who created them. By following these steps, you should have a good understanding of how to implement authorization in a RESTful API using Hono and Lucia.