Skip to content

Auth Integration

Summary

Authentication is a service, not a library. Every Aetos application shares a single identity service — the auth-server — and consumes it through its HTTP contract. An app never implements registration, login, password hashing, or token issuance itself.

The agent does not need an MCP bundle for the auth-server. It reads the pinned OpenAPI contract (below) and declares a bearer security scheme in its own spec; openapi_first then generates the verifying dependency. No auth code is written.


Read the contract

The auth-server OpenAPI document is pinned in the standards bundle and served as an MCP resource:

docs://spec

It is the authoritative client contract (OpenAPI 3.1). Read it to learn the endpoints and the request/response schemas:

Endpoint Purpose
POST /register Create a user (201, returns PublicUser)
POST /login Authenticate and issue a JWT (LoginResponse)
GET /me Current user's PublicUser (bearer required)
POST /logout Stateless logout (LogoutResponse)
POST /introspect Internal token introspection (active, user)

The pinned snapshot is versioned (currently 0.0.5). If the caller has a live auth-server and needs the current contract, they can pass the full endpoint ({AUTH_SERVER}/openapi.json) and read it directly instead of the pin.


Wire it up: declare, don't code

Authentication is added by editing the app's own openapi.yaml — nothing else. Declare a global bearer requirement and point the scheme at the auth server (values come from the environment):

security:
  - bearerAuth: []
paths:
  # ...
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      x-server-url: "{AUTH_SERVER}"
      x-introspect-path: "/introspect"
      x-login-path: "/login"
      x-register-path: "/register"
      x-logout-path: "/logout"
      x-me-path: "/me"

openapi_first reads securitySchemes + security and generates a FastAPI dependency that extracts the bearer token, POSTs {"token": <jwt>} to {AUTH_SERVER}/introspect, and returns the user when active == true. It raises 401 on an invalid or inactive token and 503 when the auth server is unreachable. The generated handlers receive the authenticated user — you author no authentication code.

Public operations

A route that must stay open opts out by overriding security with an empty list:

1
2
3
4
5
6
7
8
paths:
  /health:
    get:
      operationId: health
      security: []        # overrides the global requirement
      responses:
        "200":
          description: OK

Environment

AUTH_SERVER=http://localhost:8000     # auth-server base URL
JWT_SERVER=http://localhost:8000      # jwtlib client base URL (seeding/CLI)

Path 1: jwtlib client (no generator)

Apps that do not use a generated dependency verify requests with the py-jwt client:

1
2
3
4
5
6
7
8
from jwtlib.introspection import authenticate_request

authorized = await authenticate_request(
    should_skip_authentication,  # allow public paths
    method,
    path,
    authorization_token,         # "Bearer <jwt>" or None
)

authenticate_request POSTs to {JWT_SERVER}/introspect (3 second timeout) and treats {"active": true, "user": {...}} as valid. Use this for seeding scripts and tests (jwtlib.register_user, jwtlib.login_user).


Golden rules

  1. Never trust an unverified token. Decode through the generated dependency or jwtlib before handling the request.
  2. Treat active: false as unauthenticated, even if the JWT decodes.
  3. Do not implement your own JWT parsing. Revocation and issuer changes must stay centralized in the auth-server.
  4. Do not add auth until the app's outcome is validated. Ship the app authless first; layer the security scheme once the core behaviour works.

Notes

  • The default template is authless on purpose. Add the securitySchemes block above only when auth is required. Pull the shared slice with get_app_component("auth").
  • The auth-server is deployed as a service (no importable package); the client surface is the spec, plus jwtlib for the low-level path.