what

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.

what.toml
[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"]
KeyDefaultDescription
enabledfalseEnable auth middleware
jwt_secretauto-generatedSecret for JWT signature validation
jwt_cookie_namew_tokenCookie name containing the JWT
login_path/loginWhere to redirect unauthenticated users
after_login/Where to redirect after successful login
login_endpointBackend API URL for login (proxied via /w-auth/login)
logout_endpointBackend 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
Important: If no 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:

site/dashboard.html
<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:

site/admin/settings.html
<what>
auth: admin, editor
</what>

<h1>Settings</h1>
<p>Only admins and editors can see this.</p>
ValueMeaning
auth: allPublic (no authentication required)
auth: userAny authenticated user
auth: adminUsers with the admin role
auth: admin, editorUsers 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.

site/admin/application.what
auth = "admin"
layout = "admin-layout"

Pages can override the directory-level setting:

site/admin/public-report.html
<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:

PatternMatches
/adminExact 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.

site/login.html
<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:

what.toml
[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.

Tip: Auth settings can also be overridden with environment variables: WHAT_AUTH_JWT_SECRET, WHAT_AUTH_LOGIN_ENDPOINT, WHAT_AUTH_JWT_COOKIE_NAME, WHAT_AUTH_LOGIN_PATH, and WHAT_AUTH_AFTER_LOGIN.