GioJSdocs
On this page

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 app

Run 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

lib/session.server.ts
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:

tsx
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

gio.toml
[[guards]]
path = "/dashboard/*rest"
require_session = true
redirect_to = "/login"

[[rate_limits]]
path = "/login"
per_ip = 10
window_seconds = 60
burst = 5

The 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

app/logout/route.ts
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

bash
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).