Skip to content

Authorization and scopes

1 min read

Authentication establishes identity. A protected handler injects AuthService.current_user and can require permission scopes.

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

@router("/reports")
class Routes {
    @requires_scopes("read")
    @get("/")
    public static fun reports(@dep(AuthService.current_user) user: AuthPrincipal): string {
        return "Reports for " + user.subject;
    }
}

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 auth = AuthService(
        JwtOptions("issuer", "reports", key, key, JwtAlgorithm.HS256),
        Accounts(Password.hash(System.env("DEMO_PASSWORD").unwrap()).unwrap()),
        MemoryAuthStore()
    );
    let app = WebApp();
    app.include_router(auth.router().unwrap()).unwrap();
    app.include_router(Routes.router()).unwrap();
    app.use(auth.middleware()).unwrap();
    app.run("127.0.0.1", 8000).await.unwrap();
}
Name Kind Effect
@dep(AuthService.current_user) Dependency Injects the AuthPrincipal that AuthService.middleware() verified. Returns 401 when there is none.
@requires_scopes("read write") Route attribute Requires every listed scope. Returns 403 if any is missing. Also listed in OpenAPI.
AuthPrincipal.subject Field The account's stable identifier.
AuthPrincipal.scopes Field The granted scopes, space-separated.
AuthPrincipal.has_scope(scope) Method Whether one scope is granted.

Scope strings are assigned by the account repository (AuthAccount.scopes) and included in issued tokens.

Keep authorization requirements on the route that injects AuthService.current_user. Merely adding a metadata attribute does not authenticate a request. The generated security scheme adds Swagger Authorize support; actual enforcement occurs in the middleware and dependency.

AuthPrincipal.has_scope checks an individual permission. For resource-specific policy, define another dependency that injects AuthService.current_user, loads the resource, checks ownership, and returns Result<Resource, HttpStatus>. Avoid accepting an identity or permission set from request headers without verification.

See the API lookup and complete example.