Authentication Example
A working login: encrypted cookie sessions, a page action that checks credentials, a logout route, and a protected section guarded in Rust.
npm create giojs@latest my-app -- --auth # a new app
npx create-giojs add auth # an existing apppnpm create giojs my-app --auth # a new app
pnpm dlx create-giojs add auth # an existing appyarn create giojs my-app --auth # a new app
yarn dlx create-giojs add auth # an existing appbun create giojs my-app --auth # a new app
bunx create-giojs add auth # an existing appRun npm run dev, open /dashboard, and the guard sends you to /login. The demo user is in .env.development (DEMO_EMAIL / DEMO_PASSWORD). The building blocks are explained on the Authentication page; this guide walks through the files the feature adds.
The session storage
import { createSessionStorage } from '@gio.js/core';
export interface UserSession {
email: string;
}
export const sessions = createSessionStorage<UserSession>();The session lives in an encrypted, signed gio_session cookie, with keys from GIO_SESSION_SECRET. In development the server makes an ephemeral secret when none is set; in production a missing secret is an error. The .server.ts name keeps the module out of client bundles.
Logging in
app/(site)/login/page.tsx renders a <GioForm> and handles its POST in a page action. Wrong credentials re-render the page with a 422 and the typed email; the right ones commit the session and redirect:
export async function action(req: ActionArgs) {
const form = await req.formData();
const field = (name: string): string => {
const value = form.get(name);
return typeof value === 'string' ? value : '';
};
const email = field('email').trim();
if (!verifyCredentials(email, field('password'))) {
return { status: 422, data: { error: 'Wrong email or password.', email } };
}
const session = sessions.getSession(req);
session.set('email', email);
return redirect('/dashboard', { headers: { 'set-cookie': sessions.commitSession(session) } });
}verifyCredentials (lib/auth.server.ts) compares both the email and the password with timingSafeEqual over SHA-256 digests, always both, so response times reveal neither which field was wrong nor how much of it matched. When DEMO_EMAIL or DEMO_PASSWORD is unset or empty - as in production, which never loads .env.development - nobody can log in. (The first file that sets a variable wins, and .env.local comes before .env.development, which is why .env.example lists them commented out: copied as is, empty values would turn the demo login off.) Replace it with a lookup in your user store and a password-hash check (node:crypto's scrypt, for one) before going live.
The protected section
[[guards]]
path = "/dashboard/*rest"
require_session = true
redirect_to = "/login"
[[rate_limits]]
path = "/login"
per_ip = 10
window_seconds = 60
burst = 5The guard runs in the Rust server before any Node code: a request for /dashboard or anything below it without a valid, unexpired session is redirected to /login. The rate limit answers password guessing with 429, also in Rust. app/(site)/dashboard/page.tsx reads the session in getServerSideProps, which also marks the render personal, so it is never cached and served to someone else.
Logging out
export function POST(): Response {
return new Response(null, {
status: 303,
headers: { location: '/', 'set-cookie': sessions.destroySession() },
});
}The dashboard posts to it with <GioForm action="/logout">. Logout deletes the cookie; a copied cookie stays valid until it expires (see Authentication for revoking every copy).
CSRF
The forms carry no CSRF tokens, and need none: the Rust server refuses cross-site POST, PUT, PATCH and DELETE requests with 403 before an action or route handler runs, and the session cookie is SameSite=Lax. Keep every state change behind one of those methods - never a GET. See Security.
Before you deploy
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"Set the result as GIO_SESSION_SECRET in the server environment or a git-ignored .env.production.local (.env.example lists every variable the feature uses).