Skip to content

Auth Block

The auth {} block is a top-level language construct in Tova that provides complete authentication for your application. Declare your providers, and the compiler generates server endpoints, browser components, route guards, and token management -- with zero runtime dependencies and compile-time security validation.

Why a Dedicated Auth Block?

Without first-class auth support, adding authentication to a web app means:

  • Manual endpoint wiring -- writing signup, login, logout, token refresh, password reset, email confirmation, and OAuth callback routes by hand
  • Security footguns everywhere -- timing-safe comparison, PBKDF2 tuning, PKCE for OAuth, refresh token rotation, replay detection -- one mistake and your auth is broken
  • Scattered UI boilerplate -- login forms, signup forms, auth guards, loading states, cross-tab session sync all written manually in every project
  • OAuth complexity -- each provider has different URLs, scopes, profile shapes, and token formats

The auth {} block solves this:

  • One block, complete auth -- declare providers and the compiler generates everything: server endpoints, database tables, browser components, and route guards
  • Always-on security -- PBKDF2 with 100k iterations, timing-safe comparison, PKCE on every OAuth flow, refresh token rotation with replay detection, brute-force lockout
  • Compile-time validation -- the analyzer catches hardcoded secrets, weak passwords, short tokens, missing providers, and undefined roles before your code runs
  • Zero boilerplate -- auto-generated <LoginForm />, <SignupForm />, <AuthGuard />, and $currentUser / $isAuthenticated reactive signals
  • Cross-block integration -- works with security {} for role-based route protection and server {} for with auth route guards

Syntax Overview

tova
auth {
  secret: env("AUTH_SECRET")
  token_expires: 900
  refresh_expires: 604800
  storage: "cookie"

  provider email {
    confirm_email: true
    password_min: 8
    max_attempts: 5
    lockout_duration: 900
  }

  provider google {
    client_id: env("GOOGLE_CLIENT_ID")
    client_secret: env("GOOGLE_CLIENT_SECRET")
    scopes: ["email", "profile"]
  }

  provider github {
    client_id: env("GITHUB_CLIENT_ID")
    client_secret: env("GITHUB_CLIENT_SECRET")
    scopes: ["user:email"]
  }

  provider magic_link {
    send: fn(email, link) {
      send_email(email, "Login to MyApp", "Click here: " ++ link)
    }
    expires: 600
  }

  on signup fn(user) {
    send_welcome_email(user.email)
  }

  on login fn(user) {
    update_last_login(user.id)
  }

  protected_route "/dashboard" { redirect: "/login" }
  protected_route "/admin/*" { require: Admin, redirect: "/unauthorized" }

  loading_component: fn() { <div class="spinner" /> }
}

Minimal Example

The simplest auth block -- email/password authentication with sensible defaults:

tova
auth {
  provider email {
    confirm_email: true
  }
}

This generates: signup, login, logout, refresh, me, forgot-password, and reset-password endpoints; a built-in users table; JWT tokens stored in HttpOnly cookies; $currentUser and $isAuthenticated signals; and <LoginForm /> / <SignupForm /> components.

Config Fields

FieldTypeDefaultDescription
secretExpressionenv("AUTH_SECRET")JWT signing secret. Use env() -- hardcoded strings trigger a warning
token_expiresInt900Access token lifetime in seconds (15 minutes)
refresh_expiresInt604800Refresh token lifetime in seconds (7 days)
storageString"cookie"Token storage: "cookie" (HttpOnly) or "local" (localStorage)

Providers

Email/Password

The email provider generates signup and login endpoints with password hashing, brute-force lockout, email confirmation, and password reset.

tova
auth {
  provider email {
    confirm_email: true
    password_min: 8
    max_attempts: 5
    lockout_duration: 900
  }
}
FieldTypeDefaultDescription
confirm_emailBoolfalseRequire email confirmation before login
password_minInt8Minimum password length
max_attemptsInt5Failed login attempts before lockout
lockout_durationInt900Lockout duration in seconds (15 minutes)

Passwords are hashed with PBKDF2 (100,000 iterations, SHA-512, random salt). All comparisons use crypto.timingSafeEqual.

OAuth Providers

Built-in support for Google, GitHub, Apple, and Discord. Each requires a client_id and client_secret:

tova
auth {
  provider google {
    client_id: env("GOOGLE_CLIENT_ID")
    client_secret: env("GOOGLE_CLIENT_SECRET")
    scopes: ["email", "profile"]
  }

  provider github {
    client_id: env("GITHUB_CLIENT_ID")
    client_secret: env("GITHUB_CLIENT_SECRET")
    scopes: ["user:email"]
  }

  provider apple {
    client_id: env("APPLE_CLIENT_ID")
    client_secret: env("APPLE_CLIENT_SECRET")
  }

  provider discord {
    client_id: env("DISCORD_CLIENT_ID")
    client_secret: env("DISCORD_CLIENT_SECRET")
    scopes: ["identify", "email"]
  }
}

All OAuth flows use PKCE (S256 code challenge) and a cryptographic state parameter. The code verifier is stored in an HttpOnly cookie during the redirect.

Apple is handled specially: Apple does not expose a profile URL, so user data is extracted from the id_token JWT returned in the token response.

Account linking: When a user signs in via OAuth with the same email as an existing account, the accounts are linked automatically.

Custom OAuth Providers

For any OAuth 2.0 provider not built in, use provider custom:

tova
auth {
  provider custom "gitlab" {
    client_id: env("GITLAB_CLIENT_ID")
    client_secret: env("GITLAB_CLIENT_SECRET")
    auth_url: "https://gitlab.com/oauth/authorize"
    token_url: "https://gitlab.com/oauth/token"
    profile_url: "https://gitlab.com/api/v4/user"
    scopes: ["read_user"]
  }
}

The custom provider generates the same PKCE-secured OAuth flow as built-in providers, using the URLs you specify.

Passwordless authentication via email link:

tova
auth {
  provider magic_link {
    send: fn(email, link) {
      send_email(email, "Login to MyApp", "Click here: " ++ link)
    }
    expires: 600
  }
}
FieldTypeDefaultDescription
sendFunctionrequiredCalled with (email, link) to deliver the magic link
expiresInt600Token lifetime in seconds (10 minutes)

The send function is your responsibility -- use it to call your email service. The compiler generates the token, hashes it, stores it, and verifies it on click.

Event Hooks

React to authentication lifecycle events:

tova
auth {
  on signup fn(user) {
    send_welcome_email(user.email)
  }

  on login fn(user) {
    update_last_login(user.id)
  }

  on logout fn(user) {
    // cleanup
  }

  on oauth_link fn(user, provider, profile) {
    user.avatar = profile.picture
  }
}
EventArgumentsWhen
signup(user)After a new user is created (any provider)
login(user)After successful authentication (any provider)
logout(user)After logout
oauth_link(user, provider, profile)When an OAuth login links to an existing user

Protected Routes

Guard browser routes by authentication status or role:

tova
auth {
  protected_route "/dashboard" { redirect: "/login" }
  protected_route "/settings/*" { redirect: "/login" }
  protected_route "/admin/*" { require: Admin, redirect: "/unauthorized" }
}
  • redirect -- where to send unauthenticated users
  • require -- role name (validated against security {} block roles at compile time)
  • * -- wildcard matching within path segments

The route guard checks $isAuthenticated and optionally the user's role. During the initial auth check, the loading_component is shown to prevent a flash of protected content.

Generated Server Endpoints

The auth block generates these endpoints automatically:

Email/Password

EndpointMethodDescription
POST /auth/signupCreates user, hashes password, returns tokens (or sends confirmation)
POST /auth/loginValidates credentials, lockout check, returns access + refresh tokens
POST /auth/logoutInvalidates session, clears cookie
POST /auth/refreshRotates refresh token, issues new access token
GET /auth/meReturns current user from token
POST /auth/confirmConfirms email address (when confirm_email: true)
POST /auth/forgot-passwordGenerates password reset token
POST /auth/reset-passwordValidates reset token, updates password

OAuth

EndpointMethodDescription
GET /auth/oauth/:providerRedirects to provider consent screen (with PKCE)
GET /auth/oauth/:provider/callbackHandles callback, creates/links user, issues tokens
EndpointMethodDescription
POST /auth/magic-linkGenerates token, calls your send function
GET /auth/magic-link/verify/:tokenValidates token, creates session, redirects

Token Strategy

  • Dual-token: Short-lived access token (default 15 min) + long-lived refresh token (default 7 days)
  • Rotation: Each refresh issues a new refresh token; the old one is invalidated
  • Replay detection: Used refresh tokens are tracked. If a used token is presented, the entire token family is invalidated -- this detects token theft
  • Storage: HttpOnly cookies by default (storage: "cookie"). Optional localStorage with a compile-time XSS warning (storage: "local")

Built-in Users Table

The auth block auto-creates SQLite tables when present:

__auth_users
  id              TEXT PRIMARY KEY (UUID)
  email           TEXT UNIQUE NOT NULL
  password_hash   TEXT
  email_confirmed INTEGER DEFAULT 0
  role            TEXT DEFAULT 'user'
  provider        TEXT
  provider_id     TEXT
  locked_until    INTEGER
  failed_attempts INTEGER DEFAULT 0
  created_at      INTEGER NOT NULL
  updated_at      INTEGER NOT NULL

__auth_refresh_tokens
  id         TEXT PRIMARY KEY
  user_id    TEXT NOT NULL
  token_hash TEXT NOT NULL
  family     TEXT NOT NULL
  expires_at INTEGER NOT NULL
  used       INTEGER DEFAULT 0
  created_at INTEGER NOT NULL

__auth_magic_tokens
  id         TEXT PRIMARY KEY
  email      TEXT NOT NULL
  token_hash TEXT NOT NULL
  expires_at INTEGER NOT NULL
  used       INTEGER DEFAULT 0

__auth_email_confirmations
  id         TEXT PRIMARY KEY
  user_id    TEXT NOT NULL
  token_hash TEXT NOT NULL
  expires_at INTEGER NOT NULL

__auth_password_resets
  id         TEXT PRIMARY KEY
  user_id    TEXT NOT NULL
  token_hash TEXT NOT NULL
  expires_at INTEGER NOT NULL
  used       INTEGER DEFAULT 0

All tokens (confirmation, reset, magic link, refresh) are SHA-256 hashed before storage. Raw tokens are never persisted.

Browser-Side Generation

Reactive Signals

The auth block injects three $-prefixed reactive signals into your browser scope:

SignalTypeDescription
$currentUserUser | nilThe authenticated user object, or nil
$isAuthenticatedBoolWhether a user is logged in
$authLoadingBooltrue during initial auth check and token refresh

The $ prefix denotes framework-managed reactive state. These are regular Tova signals -- use them in JSX like any other state:

tova
browser {
  component Header {
    <div>
      if $isAuthenticated {
        <p>"Welcome, {$currentUser.email}"</p>
        <button onclick={fn() logout()}>"Log out"</button>
      } else {
        <a href="/login">"Sign in"</a>
      }
    </div>
  }
}

The logout() function is also injected and handles clearing the session, updating signals, and notifying other tabs.

Auto-Generated Components

When the email provider is present, these components are generated:

ComponentDescription
<LoginForm />Email/password form with OAuth provider buttons
<SignupForm />Registration form
<ForgotPasswordForm />Email input, sends reset link
<ResetPasswordForm />New password input from reset link
<AuthGuard />Wraps content requiring authentication

All components accept onSuccess and redirect props:

tova
browser {
  component LoginPage {
    <div class="login-page">
      <h1>"Sign In"</h1>
      <LoginForm redirect="/dashboard" />
    </div>
  }
}

OAuth buttons are automatically added to <LoginForm /> based on the configured OAuth providers.

AuthGuard

Wrap content that requires authentication:

tova
browser {
  component Dashboard {
    <AuthGuard require="Admin" fallback={<p>"Access denied"</p>}>
      <h1>"Admin Dashboard"</h1>
      <AdminPanel />
    </AuthGuard>
  }
}
PropTypeDescription
requireString?Role name required for access
fallbackElement?Shown when not authenticated or wrong role
loadingElement?Shown during initial auth check

Cross-Tab Session Sync

The auth block uses BroadcastChannel to synchronize authentication state across browser tabs. When a user logs out in one tab, all other tabs update automatically. When a user logs in, other tabs refresh their auth state.

Security Guarantees

These protections are always on -- they cannot be disabled:

ProtectionImplementation
Password hashingPBKDF2, 100,000 iterations, SHA-512, random salt
Timing-safe comparisoncrypto.timingSafeEqual for all token and password checks
PKCE for OAuthS256 code challenge on every OAuth flow
Token rotationRefresh tokens are single-use, rotated on each refresh
Replay detectionUsed refresh tokens tracked; reuse invalidates entire family
Rate limitingLogin: 5 attempts / 15 min / IP (configurable)
Brute-force lockoutAccount locked after N failed attempts (configurable)
Secure cookiesHttpOnly, Secure, SameSite=Lax (when storage: "cookie")
State parameterCrypto-random state in OAuth for CSRF prevention
Token hashingAll tokens (confirmation, reset, magic, refresh) SHA-256 hashed in DB

Compile-Time Warnings

The analyzer validates your auth configuration and produces warnings:

CodeTrigger
W_AUTH_HARDCODED_SECRETsecret: "literal" instead of env(...)
W_AUTH_SHORT_TOKENtoken_expires < 300 seconds
W_AUTH_LONG_REFRESHrefresh_expires > 30 days
W_AUTH_WEAK_PASSWORDpassword_min < 8
W_AUTH_NO_CONFIRMEmail provider without confirm_email: true
W_AUTH_LOCAL_STORAGEstorage: "local" (XSS risk)
W_AUTH_MISSING_PROVIDERauth {} block with no providers
W_AUTH_PROTECTED_NO_REDIRECTprotected_route without redirect
W_AUTH_DUPLICATE_PROVIDERSame provider type declared twice
W_AUTH_UNKNOWN_HOOKUnrecognized event name in on declaration
W_AUTH_UNKNOWN_ROLEprotected_route { require: RoleName } references undefined role

Integration with Other Blocks

security block

The auth block reads roles from the security {} block for role-based route protection. Role references in protected_route { require: RoleName } and <AuthGuard require="RoleName"> are cross-validated at compile time:

tova
security {
  role Admin {
    can: [manage_users, view_analytics]
  }

  role User {
    can: [view_profile]
  }
}

auth {
  provider email {}

  // "Admin" is validated against security block roles
  protected_route "/admin/*" { require: Admin, redirect: "/login" }
}

server block

Server routes with with auth use the auth block's __authenticate() function. The authenticated user is available as auth.user in route handlers:

tova
auth {
  provider email {}
}

server {
  route GET "/api/profile" with auth => fn(req) {
    respond(200, { user: req.user })
  }
}

browser block

The $currentUser, $isAuthenticated, and $authLoading signals are injected into all browser components. Auth components (<LoginForm />, <SignupForm />, etc.) are available globally. Route guards integrate with the SPA router.

edge block

The auth block's JWT configuration is shared with edge codegen for token verification on edge runtimes using the Web Crypto API.

Full Example

A complete app with email/password auth, Google OAuth, role-based access, and protected routes:

tova
security {
  role Admin {
    can: [manage_users, view_analytics]
  }

  role User {
    can: [view_profile, edit_profile]
  }
}

auth {
  secret: env("AUTH_SECRET")
  token_expires: 900
  refresh_expires: 604800
  storage: "cookie"

  provider email {
    confirm_email: true
    password_min: 10
    max_attempts: 5
    lockout_duration: 900
  }

  provider google {
    client_id: env("GOOGLE_CLIENT_ID")
    client_secret: env("GOOGLE_CLIENT_SECRET")
    scopes: ["email", "profile"]
  }

  on signup fn(user) {
    send_welcome_email(user.email)
  }

  on login fn(user) {
    db.run("UPDATE users SET last_login = ? WHERE id = ?", [Date.now(), user.id])
  }

  protected_route "/dashboard" { redirect: "/login" }
  protected_route "/admin/*" { require: Admin, redirect: "/unauthorized" }
}

server {
  db { adapter: "sqlite", database: "app.db" }

  fn get_dashboard(req) {
    respond(200, { message: "Welcome, {req.user.email}" })
  }

  route GET "/api/dashboard" with auth => get_dashboard
}

browser {
  component App {
    <div>
      if $authLoading {
        <div class="spinner" />
      } elif $isAuthenticated {
        <div>
          <h1>"Welcome, {$currentUser.email}"</h1>
          <button onclick={fn() logout()}>"Log out"</button>
        </div>
      } else {
        <div>
          <h1>"Please sign in"</h1>
          <LoginForm redirect="/dashboard" />
        </div>
      }
    </div>
  }
}

Auth Block vs Manual Auth

Tova supports two approaches to authentication:

Auth Block (auth {})Manual (in server {})
SetupDeclarative, one blockWrite every endpoint manually
EndpointsAuto-generated (12+ routes)Hand-coded per route
OAuthBuilt-in PKCE, state, providersManual OAuth flow implementation
BrowserAuto $currentUser, componentsManual signals and forms
SecurityAlways-on (PBKDF2, timing-safe, replay detection)Your responsibility
FlexibilityHooks for customizationFull control

Use the auth {} block for most applications. Use manual auth (documented in Server Authentication) when you need complete control over the authentication flow or are integrating with an external auth service.

Released under the MIT License.