Authentication
JWT-based authentication with protected routes, role-based access, and user context in templates.
Auth Configuration
Enable authentication in what.toml. The framework reads JWT tokens from a cookie, decodes the claims, and makes them available as template variables.
[auth]
enabled = true
jwt_secret = "${JWT_SECRET}"
jwt_cookie_name = "w_token"
login_path = "/login"
after_login = "/dashboard"
protected_paths = ["/dashboard/*", "/admin/**"]
jwt_claims = ["id", "email", "full_name", "role"]
| Key | Default | Description |
|---|---|---|
enabled | false | Enable auth middleware |
jwt_secret | auto-generated | Secret for JWT signature validation |
jwt_cookie_name | w_token | Cookie name containing the JWT |
login_path | /login | Where to redirect unauthenticated users |
after_login | / | Where to redirect after successful login |
login_endpoint | — | Backend API URL for login (proxied via /w-auth/login) |
logout_endpoint | — | Backend API URL for logout (proxied via /w-auth/logout) |
protected_paths | [] | Glob patterns for routes requiring auth |
jwt_claims | ["id", "user_id", "email", "full_name"] | Claims to extract from the JWT |
jwt_secret is configured, a random secret is generated at startup. Tokens signed by external services will be rejected. Always set the secret in production via the JWT_SECRET environment variable.
Protected Pages
Protect individual pages with the auth directive in the <what> block:
<what>
auth: user
</what>
<h1>Dashboard</h1>
<p>Welcome back, #user.full_name#!</p>
For role-based access, list the allowed roles separated by commas:
<what>
auth: admin, editor
</what>
<h1>Settings</h1>
<p>Only admins and editors can see this.</p>
| Value | Meaning |
|---|---|
auth: all | Public (no authentication required) |
auth: user | Any authenticated user |
auth: admin | Users with the admin role |
auth: admin, editor | Users with admin or editor role |
Directory-Level Auth
Protect an entire directory by adding an application.what file. All pages in that directory (and subdirectories) inherit the auth setting.
auth = "admin"
layout = "admin-layout"
Pages can override the directory-level setting:
<what>
auth: all
</what>
<h1>Public Report</h1>
<p>This page is accessible to everyone.</p>
Protected Paths (Config Level)
In addition to page-level auth: directives, you can protect paths globally in what.toml:
[auth]
enabled = true
protected_paths = ["/dashboard/*", "/admin/**", "/api/**"]
Pattern matching rules:
| Pattern | Matches |
|---|---|
/admin | Exact path only |
/admin/* | One level deep: /admin/users but not /admin/users/123 |
/admin/** | All depths: /admin/users, /admin/users/123, etc. |
User Variables
When a user is authenticated, their JWT claims are available in templates with the user. prefix:
<p>Hello, #user.full_name#</p>
<p>Email: #user.email#</p>
<p>Role: #user.role#</p>
<p>User ID: #user.id#</p>
<p>Authenticated: #user.authenticated#</p>
The user.authenticated variable is always available and returns true or false. Other variables depend on what claims are in the JWT and what's listed in jwt_claims.
Login Form
Create a login page that posts to /w-auth/login. The framework proxies credentials to your login_endpoint and sets the JWT cookie on success.
<what>
auth: all
layout: "site-layout"
</what>
<form action="/w-auth/login" method="post">
<input type="hidden" name="_csrf" value="#_csrf#">
<label>Email</label>
<input type="email" name="email" required>
<label>Password</label>
<input type="password" name="password" required>
<if flash.error>
<div class="alert alert-error">#flash.error#</div>
</if>
<button type="submit" class="btn btn-primary">Log in</button>
</form>
On success, the user is redirected to after_login (default: /). On failure, a flash error is set and the user is returned to the login page.
Logout
Post to /w-auth/logout to clear the JWT cookie and end the session:
<form action="/w-auth/logout" method="post">
<input type="hidden" name="_csrf" value="#_csrf#">
<button type="submit" class="btn btn-ghost">Log out</button>
</form>
If a logout_endpoint is configured, the framework also notifies your backend.
Conditional UI
Show or hide content based on authentication state:
<if user.authenticated>
<nav>
<a href="/dashboard">Dashboard</a>
<span>#user.full_name#</span>
</nav>
</if>
<else/>
<nav>
<a href="/login">Log in</a>
<a href="/signup">Sign up</a>
</nav>
</else>
Role-based UI:
<if user.role == admin>
<a href="/admin">Admin Panel</a>
</if>
JWT Claims Mapping
The jwt_claims array in your config controls which claims are extracted from the JWT and made available as #user.*# variables:
[auth]
enabled = true
jwt_claims = ["id", "email", "full_name", "role", "organization_id"]
Any claim in the JWT payload that matches a name in this list becomes a template variable. For example, a JWT with this payload:
{
"id": 42,
"email": "[email protected]",
"full_name": "Alice Smith",
"role": "admin",
"organization_id": "org_123",
"internal_flag": true
}
Makes these variables available: #user.id#, #user.email#, #user.full_name#, #user.role#, #user.organization_id#. The internal_flag claim is ignored because it's not in the jwt_claims list.
WHAT_AUTH_JWT_SECRET, WHAT_AUTH_LOGIN_ENDPOINT, WHAT_AUTH_JWT_COOKIE_NAME, WHAT_AUTH_LOGIN_PATH, and WHAT_AUTH_AFTER_LOGIN.