Skip to main content

Using the API

This guide describes the implemented authentication and user interfaces. Use it to verify a local instance or diagnose a failed request. Follow feature implementation when changing the interface.

Contracts and authentication​

HTTP business routes use /api/v1; probes and metrics are at the root. The authentication OpenAPI source and Protobuf source describe the contracts. HTTP handlers and error mappings are handwritten. The bundled dummy.yaml contract is a tooling placeholder and has no application handler.

InterfaceAuthentication and authorization
HTTP /api/v1/auth/*Signup/login use credentials; refresh/logout use a refresh token in the body
HTTP /api/v1/users/*Authorization: Bearer ACCESS_TOKEN or X-API-Key; each route also checks its permission
gRPC user methodsx-api-key metadata when authorization is enabled; each method needs its registered permissions

A normal signed-up user has self-service permissions and cannot list or administer other users. Use /users/me to verify that user's access. The inherited administrative test key is intended for administrative methods and does not grant users:read_self. gRPC does not use the HTTP login JWT as its authentication credential.

Permission enforcement is defined in the HTTP adapter, HTTP authentication middleware, and gRPC authentication and access interceptors.

User list pagination​

HTTP GET /api/v1/users and gRPC ListUsers use the shared API pagination rules:

InputBehavior
Limit omitted or 0Use the default limit of 10
Limit from 1 through 100Return at most that many users
Offset omittedStart at 0
Offset from 0 through 2147483647Skip that many users
Negative or out-of-range valuesReject with HTTP 400 or gRPC InvalidArgument
Valid offset beyond the available recordsReturn a successful response with an empty user list

HTTP pagination values must parse as integers. Present but empty values, such as ?limit= or ?offset=, and malformed values return 400. HTTP offsets above 2147483647 are rejected, matching the gRPC int32 range. These checks tighten the earlier HTTP behavior, which treated empty values as omitted and accepted larger offsets on 64-bit platforms.

Both adapters decode input, call NormalizePagination, and map validation errors before invoking the service. This helper owns the project-wide defaults, bounds, and pagination error; reuse it for other paginated APIs. The service forwards the normalized values to the repository. Keep the OpenAPI and Protobuf contracts aligned when changing these rules.

Session sequence​

All paths below have the /api/v1 prefix. Send JSON request bodies with Content-Type: application/json.

RequestInputExpected successful result
POST /auth/signupemail, password201 user record; creates the account
POST /auth/loginemail, password200 with access_token, refresh_token, and expires_in
GET /users/meBearer access token200 with the authenticated user's UUID, roles, and permissions
POST /auth/refreshtoken: the current refresh token200 with a new token pair
POST /auth/logoutrefresh_token: the current refresh tokenEmpty 200; ends the session

Use the newly returned token pair after refresh. Refresh tokens are single-use; reusing one revokes its session, including successor tokens. A retry with an old refresh token is not a validity check. After logout, access and refresh using that session are rejected. The authentication service owns these transitions.

Signup currently returns a model without reloaded role associations, so its response can contain empty roles and permissions even though the user role was assigned. Read /users/me after login to inspect the persisted user's permissions.

Check an authenticated request locally​

Use a dedicated local test instance with the inherited self-service role configured. The following Python 3 check needs no additional packages. It creates a disposable account, logs in, verifies read-self, and logs out. Credentials and tokens remain in memory. The account remains in the test database; a default in-memory instance discards it on restart.

python3 - <<'PY'
import json
import secrets
import urllib.request

base = "http://127.0.0.1:8080/api/v1"
client = urllib.request.build_opener(urllib.request.ProxyHandler({}))

def call(method, path, body=None, token=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
data = json.dumps(body).encode() if body is not None else None
request = urllib.request.Request(base + path, data=data, headers=headers, method=method)
with client.open(request, timeout=10) as response:
content = response.read()
return response.status, json.loads(content) if content else None

credentials = {
"email": "docs-" + secrets.token_hex(12) + "@example.invalid",
"password": secrets.token_urlsafe(32),
}
status, user = call("POST", "/auth/signup", credentials)
assert status == 201
status, tokens = call("POST", "/auth/login", credentials)
assert status == 200
try:
status, current = call("GET", "/users/me", token=tokens["access_token"])
assert status == 200 and current["uuid"] == user["uuid"]
finally:
status, _ = call("POST", "/auth/logout", {"refresh_token": tokens["refresh_token"]})
assert status == 200
print("PASS: signup, login, read-self, and logout")
PY

Change the loopback port if your test instance uses another listener. A failed HTTP request stops the check; inspect its status with the table below. This verifies one local session flow, not administrative access, external backends, or load.

Diagnose HTTP failures​

Application errors use application/problem+json with type, title, and status; validation can add an errors list. The current renderer puts the request URI in type. Capture the status, route, time, and request ID without credentials or tokens. Compare with the HTTP error mapping.

StatusInvestigate
400Invalid input, pagination, email/password rules, or rejected login credentials
401Missing/invalid access credentials, expired or revoked session, or invalid/reused refresh token
403Recognized credentials lack the route's permission; an ordinary user cannot use administrative routes
404Incorrect route/prefix or missing resource; distinguish routing from an entity lookup
409User email already exists
429Authentication or transport rate limit; inspect the configured limiter and recent request volume
503Authentication dependency unavailable; inspect database/cache errors and readiness
500Unhandled server error; correlate request ID and component logs

Login returns the same generic 400 problem response for an unknown email, incorrect password, or inactive account. It does not expose account status through a separate response. Protected user operations explicitly document 403 for authenticated credentials lacking the required permission, including the two /users/me operations.

Signup, administrative user creation, and both user-update operations return 409 when another user has the normalized email. Normalization trims surrounding whitespace and lowercases the address. Rejected updates preserve the user's email, password, and existing sessions.

For PUT /users/me, a non-null email or password requires the correct current_password, even when the submitted email is unchanged. Missing, empty, null, or incorrect proof returns 400. Omitted or null email/password fields are ignored; {} and requests containing only null credential fields succeed without changing credentials or sessions. When neither credential is supplied with a non-null value, current_password is ignored.

Refresh token and logout refresh_token must be nonempty strings. Missing, null, or empty values return 400; nonempty invalid tokens, including whitespace-only strings, return 401. Logout's optional access_token is ignored and may be empty.

For gRPC, inspect the returned status rather than HTTP codes: missing credentials map to Unauthenticated, missing permissions to PermissionDenied, and authentication dependency failures to Unavailable. The gRPC error mapping covers service errors. Use operations for evidence collection and recovery checks.


Source: docs/handbook/api.md. Revision: 3841815ceb9e.