Skip to content

Local authentication

3 min read

Use system.webapi.auth for Argon2id passwords, signed access tokens, and rotating refresh sessions. The account repository and persistent storage remain application-owned.

import system;
import system.webapi;
import system.webapi.auth;

class Accounts: AuthAccounts {
    hash: string;

    public constructor(hash: string) {
        this.hash = hash;
    }

    public async unique fun find_login(login: string): Result<AuthAccount, AuthError> {
        if login != "demo" {
            return Result.Err(AuthError("EAUTH", "unknown account"));
        }
        return Result.Ok(AuthAccount("demo-user", this.hash, "read"));
    }

    public async unique fun link_identity(
        issuer: string,
        subject: string,
        email: string,
        email_verified: bool
    ): Result<AuthAccount, AuthError> {
        return Result.Err(
            AuthError("EAUTH", "external account linking requires application policy")
        );
    }
}

async fun main(): void {
    let key = System.env("API_JWT_KEY").unwrap();
    let password = System.env("DEMO_PASSWORD").unwrap();
    let auth = AuthService(
        JwtOptions("https://api.example.com", "example-api", key, key, JwtAlgorithm.HS256),
        Accounts(Password.hash(password).unwrap()),
        MemoryAuthStore()
    );
    let app = WebApp();
    app.include_router(auth.router().unwrap()).unwrap();
    app.use(auth.middleware()).unwrap();
    app.run("127.0.0.1", 8000).await.unwrap();
}

Set API_JWT_KEY to an explicit secret containing at least 32 bytes and DEMO_PASSWORD to a chosen demonstration password. Do not use this in-memory repository for production accounts.

Endpoints

auth.router() adds these routes under the service prefix, /auth by default.

Route Request body Effect
POST /auth/login {"username": "...", "password": "..."} Checks the password and returns new tokens.
POST /auth/refresh {"refresh_token": "..."} Consumes the refresh token and returns a new pair.
POST /auth/logout {"refresh_token": "..."} Revokes the token's whole session family.

Login and refresh return access_token, refresh_token, token_type, expires_in (seconds), and csrf_token. Send the access token as Authorization: Bearer <token>.

Each refresh token can be consumed once. Replay revokes the entire session family, including already-issued access tokens, because verification checks the backing session's active state.

Service options

AuthService(jwt, accounts, store, ...) takes three required values, then these optional ones in order.

Argument Default Effect
prefix "/auth" Path prefix for the routes above.
access_seconds 900 Access token lifetime.
refresh_seconds 604800 Refresh session lifetime (7 days).
browser_sessions false Also issue a session cookie on login.
browser_origin "" The exact Origin that browser logins must send; required with browser_sessions.
JwtOptions argument Default Meaning
issuer Required The iss claim written and checked.
audience Required The aud claim written and checked.
signing_key Required Private PEM key for RS256, or the shared secret for HS256.
verification_key Required Public PEM key for RS256, or the same shared secret for HS256.
algorithm "RS256" RS256 or HS256.

Issuer, audience, algorithm, signature, expiry, and session activity are verified. Configuration requires explicit keys; the framework never creates an application signing key.

Interfaces you implement

Interface Methods Purpose
AuthAccounts find_login(login), link_identity(issuer, subject, email, email_verified) Look up local accounts and decide how external logins map to them. Return an AuthAccount(subject, password_hash, scopes).
AuthSessionStore Session and login-transaction storage Persist refresh sessions. MemoryAuthStore implements it for tests.

MemoryAuthStore is for tests and single-process examples. Implement AuthSessionStore for production persistence. rotate must atomically consume the old digest, insert its replacement, and revoke the family if the old digest was consumed. A read followed by a separate write is insufficient under concurrency. Store refresh digests rather than bearer secrets.

Passwords use Argon2id PHC strings. Verification returns an error for malformed or unsupported hash parameters and false for a password mismatch. The crypto host bounds password and PHC costs. Protect credential endpoints with deployment-appropriate request limits.

See the API lookup and complete example.

To issue local browser session cookies, set browser_sessions: true and an explicit browser_origin. Browser login requests must send that exact Origin; subsequent cookie-authenticated writes must send the session CSRF token.