Limiting Access to Authenticated Users
Learning Objectives
- You know how to use user information in the server-side code.
- You know how to limit shown content based on user authentication.
With authentication in place, we can now limit access to certain parts of the application to authenticated users. In this chapter, we will expand the chat application from the chapter on WebSockets to require authentication.
Identifying user on the server
When using Better Auth, the key routing functionality catches GET and POST requests to /api/auth/**
, handling the requests using the auth.handler
function. This function authenticates the user.
// other imports
import { auth } from "./auth.js";
const app = new Hono();
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
// other routes etc
With the above in place, users are authenticated, and a cookie is added to the response when the user logs in.
Retrieving user information from request headers
When the cookie has been set, the user information is available in the request headers of the subsequent requests. Using information from the request headers, user details can be retrieved using the asynchronous function auth.api.getSession
that takes the request headers as an argument.
The function can be used as a part of a middleware that captures incoming requests, as shown below.
app.use("*", async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
return next();
}
c.set("user", session.user.name);
return next();
});
Now, if the user is authenticated and the user makes a request to the server, information about the user is available in the context object. The user name is set as a key-value pair in the context object.
Limiting access to chat
With the above middleware in place, we can create another middleware that limits access to the chat application to authenticated users.
As an example, the following middleware retrieves user information from the context object for GET requests to /api/ws-chat
. If the user information is not available, the middleware responds with an HTTP status code of 401. Otherwise, the middleware passes the request forward.
app.use("/api/ws-chat", async (c, next) => {
const user = c.get("user");
if (!user) {
c.status(401);
return c.json({ message: "Unauthorized" });
}
return next();
});
The above middleware must come after the middleware that sets the user information on the context object, but before the concrete chat functionality that establishes the WebSocket connection.
Including user name in chat messages
Now, as the user name is available in the context object when the request reaches the functionality for upgrading the connection to a WebSocket connection, the user name can be retrieved from the context object when the WebSocket connection is opened. This way, the user name can be included in the chat messages.
This is shown in the example below.
const sockets = new Set();
app.get(
"/api/ws-chat",
upgradeWebSocket((c) => {
const user = c.get("user");
return {
onOpen: (event, ws) => {
sockets.add(ws);
},
onMessage(event, ws) {
const message = JSON.parse(event.data);
message.date = Date.now();
message.message = `${user}: ${message.message}`;
for (const socket of sockets) {
socket.send(
JSON.stringify(message),
);
}
},
onClose: (event, ws) => {
sockets.delete(ws);
ws.close();
},
onError: (event, ws) => {
sockets.delete(ws);
ws.close();
},
};
}),
);
Now, on the server, only authenticated users can chat, and their messages include their user name.
Client-side implementation
On the client, we can use the user state to determine whether the user is authenticated. If the user is not authenticated, we can show them links to the login and registration pages. On the other hand, if the user is authenticated, we can show them the chat functionality.
Note that in addition to the restriction, the chat connection is opened only if the user has an email (i.e., the user is authenticated). The $effect rune is used to monitor for state changes in the user state — once the state changes to include an email, the chat connection is opened.
<script>
import { useUserState } from "../states/userState.svelte.js";
let userState = useUserState();
let message = $state("");
let messages = $state([]);
let connection = "";
const reconnect = () => {
setTimeout(() => {
connection.close();
openConnection();
}, 500);
};
const openConnection = async () => {
connection = new WebSocket("/api/ws-chat");
connection.onmessage = (event) => {
const newMessage = JSON.parse(event.data);
messages = [newMessage, ...messages];
messages = messages.slice(0, 10);
};
connection.onclose = () => {
reconnect();
};
connection.onerror = () => {
reconnect();
};
};
const sendMessage = async () => {
connection.send(JSON.stringify({ message }));
message = "";
};
$effect(() => {
if (userState.email) {
openConnection();
}
});
</script>
{#if !userState.email}
<p>
Chatting is only for authenticated users. <a href="/auth/login">Login</a> or
<a href="/auth/register">Register</a>.
</p>
{:else}
<p>Chat!</p>
<input type="text" bind:value={message} />
<button onclick={() => sendMessage()}>Send</button>
<ul>
{#each messages as message}
<li>{message.message}</li>
{/each}
</ul>
{/if}
With the above code, the chat functionality is only available to authenticated users. If the user is not authenticated, they are shown links to the login and registration pages. If the user is authenticated, the chat functionality is shown.
All chatters would now have their user names included in the chat messages, and the names would be added to the messages on the server side.