Skip to main content

Login, session and route protection

The client side of authentication in @archibald/auth is four hooks — useLogin, useUser, useIsLoggedIn, useLogOut — backed by the SessionClient; on the server, getSession() reads the session of the current request. Setup and concepts are in the authentication guide; this recipe wires them into a working flow.

Login form

useLogin returns a login(credentials) function plus the usual mutation state. Credentials are a typed AuthCredentials implementation — CookieAuthCredentialsPassword for username/password, CookieAuthCredentialsOidc for the OIDC redirect flow:

import { type CookieAuthCredentialsPassword, useLogin } from '@archibald/auth';

function LoginForm() {
const { login, isLoading, error } = useLogin();

async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = new FormData(event.currentTarget);
const credentials: CookieAuthCredentialsPassword = {
authProtocol: 'password',
username: String(form.get('username')),
password: String(form.get('password'))
};
await login(credentials);
}

return (
<form onSubmit={handleSubmit}>
<input autoComplete="username" name="username" type="text" />
<input autoComplete="current-password" name="password" type="password" />
{error && <ErrorNote error={error} />}
<button disabled={isLoading} type="submit">
Sign in
</button>
</form>
);
}

On success, useLogin invalidates the cached session user, so every useUser/useIsLoggedIn consumer updates without a manual refetch.

Reading the session on the client

import { useIsLoggedIn, useLogOut, useUser } from '@archibald/auth';

function AccountMenu() {
const isLoggedIn = useIsLoggedIn();
const { data: user, isLoading } = useUser();
const { logout } = useLogOut();

if (!isLoggedIn) {
return <a href="/login">Sign in</a>;
}

return (
<>
<span>{isLoading ? '…' : user?.name}</span>
<button onClick={() => logout()}>Sign out</button>
</>
);
}

useIsLoggedIn is synchronous state from the SessionClient; useUser fetches (and caches) the user object via useFetch under the hood.

Reading the session on the server

In controllers, providers, and other server code, getSession() returns the session bound to the current request context:

import { getSession } from '@archibald/auth';

export class AccountController {
public async getAccountInfo() {
const { user } = getSession<User>();
return this.accountService.getInfo(user);
}
}

It throws when called outside a request context (including on the client), so keep it to server-side code paths. Whether a route requires a session is declared on the route itself — options.auth.mode: 'required' | 'optional' | 'try' in the module's RouteConfig.

Protecting client routes

Wrap protected content in a RestrictedRoute that redirects anonymous visitors to the login page:

import { useIsLoggedIn } from '@archibald/auth';
import { Navigate, useMatchPattern } from '@archibald/core';
import { type PropsWithChildren, Suspense } from 'react';

function RestrictedRoute({ children }: PropsWithChildren) {
const isLoggedIn = useIsLoggedIn();
const alreadyOnLoginPath = useMatchPattern('login');

if (!isLoggedIn && !alreadyOnLoginPath) {
return <Navigate to="/login" />;
}

return <Suspense>{children}</Suspense>;
}

Client-side protection is a UX measure, not a security boundary — the data behind the page must still be guarded server-side with auth.mode: 'required'. Both halves, including the native (expo-router) variant, are covered in Route Protection.