Skip to content

Authentication

Introduction

Many web applications provide a way for their users to authenticate and "log in". Arvel ships an authentication system built around guards and user providers, plus a Hash facade for password hashing and an optional set of ready-made auth endpoints.

Note

Arvel's Auth facade is intentionally smaller than Laravel's. It covers the essential operations — attempt, login, logout, user, check, id — and every one of them takes the current request as an argument, because guards resolve state from the request rather than from global helpers. Laravel conveniences like Auth.guest(), Auth.once(), via_remember, and login_using_id are not implemented.

Registering the Provider

Authentication is opt-in. Add AuthServiceProvider to bootstrap/providers.py:

# bootstrap/providers.py
from arvel.auth.provider import AuthServiceProvider

providers = [
    # ...other providers...
    AuthServiceProvider,
]

Without it, the Auth facade, the Gate, and the built-in /api/auth/* routes are all unavailable. The provider binds the AuthManager (wiring the Auth facade), registers the Gate singleton, and — when config.routes.enabled is true — mounts the auth endpoints.

Guards & Providers

Authentication has two halves:

  • Guards define how users are authenticated for each request — a session guard reads a cookie-backed session; a JWT guard reads a bearer token.
  • User providers define how users are retrieved from storage — typically a database lookup against your User model.

The Auth Facade

Import the facade from its module and pass the request:

from arvel.facades.auth import Auth
Method Async Description
Auth.attempt(credentials, request) yes Attempt to authenticate with credentials; returns bool
Auth.login(user, request) yes Log a user in on the default guard
Auth.logout(request) yes Log the current user out
Auth.user(request) yes The authenticated user, or None
Auth.check(request) yes True if a user is authenticated
Auth.id(request) yes The authenticated user's id as a string, or None
Auth.guard(name) no Get a specific guard
Auth.set_manager(manager) no Bind the AuthManager (done by the provider)
from arvel import Route, UnauthenticatedException
from arvel.facades.auth import Auth
from starlette.requests import Request


@Route.post("/login")
async def login(request: Request) -> dict[str, str]:
    body = await request.json()
    ok = await Auth.attempt(
        {"email": body["email"], "password": body["password"]},
        request,
    )
    if not ok:
        raise UnauthenticatedException()
    return {"status": "ok"}

Note

Auth.user(request) always asks the guard — it does not read request.state.user. After the Authenticate middleware runs, the resolved user is also stored on request.state.user for direct access in a handler.

Guards

The AuthManager holds the configured guards and a default. Three guard types ship with the framework.

Session Guard

The session guard authenticates against the session. attempt looks the user up via the provider, verifies the password with Hash.check, and on success regenerates the session and stores the user's id. logout forgets it.

guard = Auth.guard("web")   # session guard
await Auth.attempt({"email": "[email protected]", "password": "secret"}, request)

JWT Guard

The JWT guard authenticates a request from an Authorization: Bearer <token> header. It decodes and validates the token (signature, exp, optional aud/iss), requires a string sub claim, and rejects tokens whose typ isn't access. An invalid or expired token resolves to None rather than raising.

guard = Auth.guard("api")   # JWT guard
token = await guard.issue_token(subject="42", expires_in=timedelta(minutes=15))
user = await guard.user(request)   # reads the bearer token

Note

The JWT guard requires the arvel[jwt] extra (PyJWT). The signing secret must be at least 32 characters, and the algorithm cannot be none — both are enforced when the guard is built.

Token Guard

The token guard authenticates a hashed personal-access token from a bearer header and can scope abilities (token.can("posts:write")).

Warning

The token guard is not fully wired in the framework. It depends on auth.token_repository and auth.user_repository bindings that the framework does not register, so configuring the token driver without supplying those bindings yourself will fail at runtime. Prefer the session or JWT guards unless you provide the repositories.

User Providers

The only built-in provider is the database provider, which looks users up against a model. It finds users by primary key (by_id) and by a username field (by_credentials, default email):

# config/auth.py (published by `arvel auth:install`)
providers = {
    "users": {"driver": "database", "model": "app.models.user.User"},
}

The Authenticatable Mixin

A user model becomes authenticatable through the Authenticatable mixin, which exposes get_auth_id() and get_auth_password(). The password column defaults to password_hash, but the shipped User model overrides it to password:

from arvel.auth.mixins import Authenticatable
from arvel.database import Model, Timestamps, id_, string


class User(Model, Timestamps, Authenticatable):
    __tablename__ = "users"
    _auth_password_field = "password"

    id: int = id_()
    email: str = string(255, unique=True)
    password: str = string(255)

Protecting Routes

Attach the Authenticate middleware to require an authenticated user. It resolves the named guard, stores the user on request.state.user, binds the user id into the logging context, and raises an unauthenticated exception (translated to 401) when no user is present:

from arvel.http.middleware import Authenticate


@Route.get("/dashboard", middleware=[Authenticate("web")])
async def dashboard(request: Request) -> dict[str, Any]:
    user = request.state.user
    return {"email": user.email}

Related middleware: OptionalAuthenticate (non-blocking — sets the user when present), GuestMiddleware (redirects authenticated users away from guest-only pages), VerifiedMiddleware (requires a verified email), and CanMiddleware (a gate check).

Password Hashing

The Hash facade hashes and verifies passwords. The default algorithm is argon2id:

from arvel.facades.hash import Hash

hashed = Hash.make("plain-text-password")
ok = Hash.check("plain-text-password", hashed)
if Hash.needs_rehash(hashed):
    hashed = Hash.make("plain-text-password")

Hash.make_bcrypt(password, rounds=12) is available if you install the arvel[bcrypt] extra. All Hash methods are synchronous.

Note

Although a HashConfig exists with a bcrypt default, the Hash facade does not read it — it uses argon2id directly. Ignore the hashing block in the published config stub.

Built-in Auth Routes

With AuthServiceProvider registered and config.routes.enabled true (the default), the provider mounts a set of JSON auth endpoints under /api/auth:

Method Path Purpose
POST /api/auth/register Register a user (201)
POST /api/auth/login Log in; sets refresh + CSRF cookies
POST /api/auth/refresh Rotate the refresh token
POST /api/auth/logout Log out (204)
GET /api/auth/me The current user (bearer JWT)
POST /api/auth/forgot-password Begin a password reset (202)
POST /api/auth/reset-password Complete a password reset
GET /api/auth/verify/{signed} Verify an email via signed URL
POST /api/auth/verify/resend Resend verification (rate-limited)

These are backed by AuthService (register/login/refresh/logout/me), PasswordService (forgot/reset), and EmailVerificationService. Access tokens are JWTs; refresh tokens are opaque, stored as SHA-256 digests in a refresh_tokens table and rotated on use.

Note

"Remember me" is not implemented. LoginRequest accepts a remember flag and the User model has a remember_token column, but no guard or flow uses them.

Run arvel auth:install to publish the auth config, views, route stub, and migrations.

Configuration

Authentication is configured through AuthConfig (published as config/auth.py): the default guard, the guards map (web → session, api → JWT), the providers map, JWT settings, and route options. The AuthServiceProvider validates the JWT secret length and algorithm when it registers.