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.
| Interface | Authentication 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 methods | x-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:
| Input | Behavior |
|---|---|
Limit omitted or 0 | Use the default limit of 10 |
Limit from 1 through 100 | Return at most that many users |
| Offset omitted | Start at 0 |
Offset from 0 through 2147483647 | Skip that many users |
| Negative or out-of-range values | Reject with HTTP 400 or gRPC InvalidArgument |
| Valid offset beyond the available records | Return 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.
| Request | Input | Expected successful result |
|---|---|---|
POST /auth/signup | email, password | 201 user record; creates the account |
POST /auth/login | email, password | 200 with access_token, refresh_token, and expires_in |
GET /users/me | Bearer access token | 200 with the authenticated user's UUID, roles, and permissions |
POST /auth/refresh | token: the current refresh token | 200 with a new token pair |
POST /auth/logout | refresh_token: the current refresh token | Empty 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.
| Status | Investigate |
|---|---|
400 | Invalid input, pagination, email/password rules, or rejected login credentials |
401 | Missing/invalid access credentials, expired or revoked session, or invalid/reused refresh token |
403 | Recognized credentials lack the route's permission; an ordinary user cannot use administrative routes |
404 | Incorrect route/prefix or missing resource; distinguish routing from an entity lookup |
409 | User email already exists |
429 | Authentication or transport rate limit; inspect the configured limiter and recent request volume |
503 | Authentication dependency unavailable; inspect database/cache errors and readiness |
500 | Unhandled 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.