Fix F-02: replace password-in-hidden-field with nonce
- ui/ui.go: add pendingLogin struct and pendingLogins sync.Map to UIServer; add issueTOTPNonce (generates 128-bit random nonce, stores accountID with 90s TTL) and consumeTOTPNonce (single-use, expiry-checked LoadAndDelete); add dummyHash() method - ui/handlers_auth.go: split handleLoginPost into step 1 (password verify → issue nonce) and step 2 (handleTOTPStep, consume nonce → validate TOTP) via a new finishLogin helper; password never transmitted or stored after step 1 - ui/ui_test.go: refactor newTestMux to reuse new newTestUIServer; add TestTOTPNonceIssuedAndConsumed, TestTOTPNonceUnknownRejected, TestTOTPNonceExpired, and TestLoginPostPasswordNotInTOTPForm; 11/11 tests pass - web/templates/fragments/totp_step.html: replace 'name=password' hidden field with 'name=totp_nonce' - db/accounts.go: add GetAccountByID for TOTP step lookup - AUDIT.md: mark F-02 as fixed Security: the plaintext password previously survived two HTTP round-trips and lived in the browser DOM during the TOTP step. The nonce approach means the password is verified once and immediately discarded; only an opaque random token tied to an account ID (never a credential) crosses the wire on step 2. Nonces are single-use and expire after 90 seconds to limit the window if one is captured.
This commit is contained in:
571
INTEGRATION.md
Normal file
571
INTEGRATION.md
Normal file
@@ -0,0 +1,571 @@
|
||||
# MCIAS Integration Guide
|
||||
|
||||
MCIAS is an HTTP+TLS authentication service. It issues Ed25519-signed JWTs and
|
||||
provides account, role, and credential management. This guide covers everything
|
||||
needed to integrate a new system with MCIAS as its authentication backend.
|
||||
|
||||
---
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Concepts](#concepts)
|
||||
2. [Quick Start — Human Login](#quick-start--human-login)
|
||||
3. [Quick Start — System Account](#quick-start--system-account)
|
||||
4. [Verifying a Token in Your Service](#verifying-a-token-in-your-service)
|
||||
5. [TOTP (Two-Factor Authentication)](#totp-two-factor-authentication)
|
||||
6. [Setting Up a System Account](#setting-up-a-system-account)
|
||||
7. [Role-Based Authorization](#role-based-authorization)
|
||||
8. [Client Libraries](#client-libraries)
|
||||
9. [Token Lifecycle](#token-lifecycle)
|
||||
10. [Error Reference](#error-reference)
|
||||
11. [API Reference](#api-reference)
|
||||
12. [Security Notes for Integrators](#security-notes-for-integrators)
|
||||
|
||||
---
|
||||
|
||||
## Concepts
|
||||
|
||||
**Human account** — A person who logs in with username + password (+ optional
|
||||
TOTP). The server returns a JWT valid for 30 days (8 hours for admins). The
|
||||
token is bearer-only; there are no cookies or server-side sessions.
|
||||
|
||||
**System account** — A non-interactive identity for a service, daemon, or
|
||||
automated process. It has no password. An admin issues a long-lived bearer token
|
||||
(1 year) that the service stores and sends with every request.
|
||||
|
||||
**JWT** — All tokens are signed with Ed25519 (algorithm `EdDSA`). They contain
|
||||
`sub` (account UUID), `roles`, `iss`, `iat`, `exp`, and `jti`. Tokens can be
|
||||
validated offline using the public key, or online via `/v1/token/validate`.
|
||||
|
||||
**Role** — An arbitrary string scoped to your deployment (e.g. `admin`,
|
||||
`editor`, `readonly`). MCIAS enforces `admin` internally; your services enforce
|
||||
any other roles by inspecting the `roles` claim.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start — Human Login
|
||||
|
||||
```sh
|
||||
# Login
|
||||
RESPONSE=$(curl -sS -X POST https://auth.example.com:8443/v1/auth/login \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"alice","password":"s3cr3t"}')
|
||||
|
||||
TOKEN=$(echo "$RESPONSE" | jq -r .token)
|
||||
EXPIRES=$(echo "$RESPONSE" | jq -r .expires_at)
|
||||
|
||||
# Use the token
|
||||
curl -sS https://my-app.example.com/api/resource \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# Renew before expiry (extends the window, invalidates the old token)
|
||||
NEW_TOKEN=$(curl -sS -X POST https://auth.example.com:8443/v1/auth/renew \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H "Authorization: Bearer $TOKEN" | jq -r .token)
|
||||
|
||||
# Logout (revokes the token immediately)
|
||||
curl -sS -X POST https://auth.example.com:8443/v1/auth/logout \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### Login with TOTP
|
||||
|
||||
If the user has enrolled TOTP, include the current 6-digit code. Omitting it
|
||||
returns HTTP 401 with code `totp_required`.
|
||||
|
||||
```sh
|
||||
curl -sS -X POST https://auth.example.com:8443/v1/auth/login \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"alice","password":"s3cr3t","totp_code":"123456"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start — System Account
|
||||
|
||||
A system account authenticates non-interactively. An admin creates it, issues a
|
||||
token, and the service includes that token in every request.
|
||||
|
||||
```sh
|
||||
# 1. Admin: create the system account
|
||||
TOKEN=$(curl -sS -X POST https://auth.example.com:8443/v1/auth/login \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"..."}' | jq -r .token)
|
||||
|
||||
ACCOUNT=$(curl -sS -X POST https://auth.example.com:8443/v1/accounts \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"my-service","account_type":"system"}')
|
||||
|
||||
SVC_UUID=$(echo "$ACCOUNT" | jq -r .id)
|
||||
|
||||
# 2. Admin: issue a bearer token for the service (valid 1 year)
|
||||
SVC_TOKEN=$(curl -sS -X POST https://auth.example.com:8443/v1/token/issue \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"account_id\":\"$SVC_UUID\"}" | jq -r .token)
|
||||
|
||||
# 3. Store SVC_TOKEN in the service's secret store (env var, Vault, etc.)
|
||||
# 4. Service: use it in every outbound or MCIAS API call
|
||||
curl -sS https://protected-api.example.com/v1/resource \
|
||||
-H "Authorization: Bearer $SVC_TOKEN"
|
||||
```
|
||||
|
||||
To rotate the token (e.g. after exposure), reissue it. The old token is
|
||||
automatically revoked.
|
||||
|
||||
```sh
|
||||
curl -sS -X POST https://auth.example.com:8443/v1/token/issue \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"account_id\":\"$SVC_UUID\"}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verifying a Token in Your Service
|
||||
|
||||
Your service receives a Bearer token from a client. It must verify the token
|
||||
before trusting any claims. There are two approaches:
|
||||
|
||||
### Option A: Offline verification (recommended)
|
||||
|
||||
Fetch the public key once at startup and cache it. Verify the JWT locally
|
||||
without a round-trip to MCIAS.
|
||||
|
||||
```
|
||||
GET /v1/keys/public
|
||||
```
|
||||
|
||||
Response (JWK, RFC 8037):
|
||||
|
||||
```json
|
||||
{
|
||||
"kty": "OKP",
|
||||
"crv": "Ed25519",
|
||||
"use": "sig",
|
||||
"alg": "EdDSA",
|
||||
"x": "<base64url-encoded public key>"
|
||||
}
|
||||
```
|
||||
|
||||
Verification steps your code must perform (in order):
|
||||
|
||||
1. **Check `alg` header** — must be exactly `"EdDSA"`. Reject `"none"` and all
|
||||
other values before touching the signature.
|
||||
2. **Verify the Ed25519 signature** using the public key.
|
||||
3. **Validate claims:** `exp` > now, `iat` <= now, `iss` matches your configured
|
||||
issuer.
|
||||
4. **Check for revocation** — if your service needs real-time revocation, call
|
||||
`/v1/token/validate` instead (Option B). Offline verification will accept a
|
||||
token until it expires, even if it was explicitly revoked.
|
||||
|
||||
```python
|
||||
# Python example using PyJWT
|
||||
import jwt # pip install PyJWT[crypto]
|
||||
|
||||
PUBLIC_KEY = load_ed25519_jwk(mcias_client.get_public_key())
|
||||
|
||||
def verify_token(bearer: str) -> dict:
|
||||
token = bearer.removeprefix("Bearer ").strip()
|
||||
# Step 1 is performed by PyJWT when algorithms=["EdDSA"] is specified.
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
PUBLIC_KEY,
|
||||
algorithms=["EdDSA"],
|
||||
issuer="https://auth.example.com",
|
||||
options={"require": ["exp", "iat", "iss", "sub", "jti"]},
|
||||
)
|
||||
return payload # {"sub": "...", "roles": [...], "exp": ..., "jti": "..."}
|
||||
```
|
||||
|
||||
```go
|
||||
// Go example using golang-jwt/jwt/v5
|
||||
import jwtlib "github.com/golang-jwt/jwt/v5"
|
||||
|
||||
func verifyToken(tokenStr string, pubKey ed25519.PublicKey, issuer string) (*Claims, error) {
|
||||
parsed, err := jwtlib.ParseWithClaims(tokenStr, &Claims{},
|
||||
func(t *jwtlib.Token) (any, error) {
|
||||
// Step 1: algorithm must be EdDSA
|
||||
if _, ok := t.Method.(*jwtlib.SigningMethodEd25519); !ok {
|
||||
return nil, fmt.Errorf("unexpected alg: %v", t.Header["alg"])
|
||||
}
|
||||
return pubKey, nil
|
||||
},
|
||||
jwtlib.WithIssuedAt(),
|
||||
jwtlib.WithIssuer(issuer),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parsed.Claims.(*Claims), nil
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: Online validation
|
||||
|
||||
Call MCIAS for every request. This reflects revocations immediately but adds
|
||||
latency. Suitable for high-security paths.
|
||||
|
||||
```
|
||||
POST /v1/token/validate
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
or equivalently with a JSON body:
|
||||
|
||||
```json
|
||||
{ "token": "<JWT>" }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"sub": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"roles": ["editor"],
|
||||
"expires_at": "2026-04-10T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
On failure: `{ "valid": false }` (HTTP 200, never 401). Do not branch on the
|
||||
HTTP status code; always inspect `valid`.
|
||||
|
||||
---
|
||||
|
||||
## TOTP (Two-Factor Authentication)
|
||||
|
||||
TOTP enrollment is a two-step process and must be completed in one session.
|
||||
Abandoning after step 1 does **not** lock the account.
|
||||
|
||||
### Step 1: Enroll
|
||||
|
||||
```
|
||||
POST /v1/auth/totp/enroll
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"secret": "JBSWY3DPEHPK3PXP",
|
||||
"otpauth_uri": "otpauth://totp/MCIAS:alice?secret=JBSWY3DPEHPK3PXP&issuer=MCIAS"
|
||||
}
|
||||
```
|
||||
|
||||
Show the `otpauth_uri` as a QR code (e.g. using a TOTP app like Authenticator).
|
||||
The secret is shown **once**; it is not retrievable after this call.
|
||||
|
||||
### Step 2: Confirm
|
||||
|
||||
```
|
||||
POST /v1/auth/totp/confirm
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{ "code": "123456" }
|
||||
```
|
||||
|
||||
HTTP 204 on success. After this call, all future logins for this account require
|
||||
a TOTP code.
|
||||
|
||||
### Admin: Remove TOTP
|
||||
|
||||
```
|
||||
DELETE /v1/auth/totp
|
||||
Authorization: Bearer <admin-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{ "account_id": "<UUID>" }
|
||||
```
|
||||
|
||||
Use this for account recovery when a user loses their TOTP device.
|
||||
|
||||
---
|
||||
|
||||
## Setting Up a System Account
|
||||
|
||||
Full walkthrough using `mciasctl`:
|
||||
|
||||
```sh
|
||||
export MCIAS_TOKEN=$(curl -sS -X POST https://auth.example.com:8443/v1/auth/login \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"..."}' | jq -r .token)
|
||||
|
||||
# 1. Create the account
|
||||
mciasctl -server https://auth.example.com:8443 \
|
||||
account create -username my-service -type system
|
||||
|
||||
# 2. Note the UUID printed, then assign roles
|
||||
mciasctl role set -id <UUID> -roles readonly
|
||||
|
||||
# 3. Issue the service token
|
||||
mciasctl token issue -id <UUID>
|
||||
# Prints: token=eyJ... expires_at=2027-03-11T...
|
||||
|
||||
# 4. Store the token securely (e.g. in a Kubernetes secret, Vault, .env)
|
||||
echo "MCIAS_SVC_TOKEN=eyJ..." >> /etc/my-service/env
|
||||
|
||||
# 5. Rotate (when token is compromised or at scheduled intervals)
|
||||
mciasctl token issue -id <UUID>
|
||||
# New token issued; old token automatically revoked
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Role-Based Authorization
|
||||
|
||||
Roles are arbitrary strings. MCIAS enforces only `admin` internally. Your
|
||||
services enforce any other roles by inspecting the `roles` JWT claim.
|
||||
|
||||
### Assigning roles
|
||||
|
||||
```sh
|
||||
# Grant roles (replaces existing)
|
||||
mciasctl role set -id <UUID> -roles editor,readonly
|
||||
|
||||
# Or via the API
|
||||
curl -sS -X PUT https://auth.example.com:8443/v1/accounts/<UUID>/roles \
|
||||
--cacert /etc/mcias/server.crt \
|
||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"roles":["editor","readonly"]}'
|
||||
```
|
||||
|
||||
### Checking roles in your service
|
||||
|
||||
Roles are in the `roles` claim of the decoded JWT:
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "550e8400-...",
|
||||
"roles": ["editor", "readonly"],
|
||||
"exp": 1775000000,
|
||||
"iss": "https://auth.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
def require_role(payload: dict, role: str) -> None:
|
||||
if role not in payload.get("roles", []):
|
||||
raise PermissionError(f"role '{role}' required")
|
||||
```
|
||||
|
||||
```go
|
||||
func hasRole(claims *Claims, role string) bool {
|
||||
for _, r := range claims.Roles {
|
||||
if r == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client Libraries
|
||||
|
||||
Four language implementations are available in the `clients/` directory. All
|
||||
expose the same API surface:
|
||||
|
||||
| Language | Location | Install |
|
||||
|----------|----------|---------|
|
||||
| Go | `clients/go/` | `go get git.wntrmute.dev/kyle/mcias/clients/go` |
|
||||
| Python | `clients/python/` | `pip install ./clients/python` |
|
||||
| Rust | `clients/rust/` | `cargo add mcias-client` |
|
||||
| Common Lisp | `clients/lisp/` | ASDF `mcias-client` |
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
import mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
||||
|
||||
c, err := mcias.New("https://auth.example.com:8443", "/etc/mcias/server.crt", "")
|
||||
if err != nil { ... }
|
||||
|
||||
token, expiresAt, err := c.Login("alice", "s3cr3t", "")
|
||||
if err != nil { ... }
|
||||
|
||||
result, err := c.ValidateToken(token)
|
||||
if err != nil || !result.Valid { ... }
|
||||
fmt.Println(result.Sub, result.Roles)
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
from mcias_client import Client, MciasAuthError
|
||||
|
||||
c = Client("https://auth.example.com:8443", ca_cert="/etc/mcias/server.crt")
|
||||
token, expires_at = c.login("alice", "s3cr3t")
|
||||
|
||||
try:
|
||||
result = c.validate_token(token)
|
||||
if not result.valid:
|
||||
raise MciasAuthError("token invalid")
|
||||
print(result.sub, result.roles)
|
||||
except MciasAuthError as e:
|
||||
print("auth error:", e)
|
||||
```
|
||||
|
||||
### Error types (all languages)
|
||||
|
||||
| Name | HTTP | Meaning |
|
||||
|------|------|---------|
|
||||
| `MciasAuthError` | 401 | Token missing, invalid, or expired |
|
||||
| `MciasForbiddenError` | 403 | Insufficient role |
|
||||
| `MciasNotFoundError` | 404 | Resource does not exist |
|
||||
| `MciasInputError` | 400 | Malformed request |
|
||||
| `MciasConflictError` | 409 | Conflict (e.g. duplicate username) |
|
||||
| `MciasServerError` | 5xx | Unexpected server error |
|
||||
| `MciasTransportError` | — | Network or TLS failure |
|
||||
|
||||
---
|
||||
|
||||
## Token Lifecycle
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ MCIAS │
|
||||
POST /v1/auth/login │ │
|
||||
User ─────────────────────► Argon2id verify │
|
||||
◄─────────────────── │ Ed25519 sign JWT │
|
||||
JWT (30d / 8h) │ Track JTI in DB │
|
||||
│ │
|
||||
Service ──► POST /v1/token/validate ──► Check sig + JTI │
|
||||
◄── {valid, sub, roles} │
|
||||
│ │
|
||||
User ──► POST /v1/auth/renew ─────────► Revoke old JTI │
|
||||
◄── New JWT Issue new JWT │
|
||||
│ │
|
||||
User ──► POST /v1/auth/logout ────────► Revoke JTI │
|
||||
│ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Expiry defaults:**
|
||||
|
||||
| Account type | Default expiry | Notes |
|
||||
|---|---|---|
|
||||
| Human | 30 days | Configurable via `tokens.default_expiry` |
|
||||
| Admin (`admin` role) | 8 hours | Configurable via `tokens.admin_expiry` |
|
||||
| System account | 1 year | Configurable via `tokens.service_expiry` |
|
||||
|
||||
Tokens are revoked on explicit logout, account deletion, or admin revocation.
|
||||
Expired-but-not-revoked tokens can be pruned from the database with
|
||||
`mciasdb prune tokens`.
|
||||
|
||||
---
|
||||
|
||||
## Error Reference
|
||||
|
||||
All error responses are JSON with `error` (human-readable) and `code` (machine-readable):
|
||||
|
||||
```json
|
||||
{ "error": "invalid credentials", "code": "unauthorized" }
|
||||
```
|
||||
|
||||
| HTTP | `code` | When |
|
||||
|------|--------|------|
|
||||
| 400 | `bad_request` | Missing required field or malformed JSON |
|
||||
| 401 | `unauthorized` | Wrong credentials, expired or invalid token |
|
||||
| 401 | `totp_required` | Correct password but TOTP code not provided |
|
||||
| 403 | `forbidden` | Token valid but lacks required role |
|
||||
| 404 | `not_found` | Account or token does not exist |
|
||||
| 409 | `conflict` | Username already taken |
|
||||
| 429 | `rate_limited` | Too many login or validate requests |
|
||||
| 500 | `internal_error` | Unexpected server error |
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
See [openapi.yaml](openapi.yaml) for the machine-readable OpenAPI 3.1 spec.
|
||||
A Swagger UI is served at `https://<server>/docs` when the server is running.
|
||||
|
||||
### Authentication header
|
||||
|
||||
All authenticated endpoints require:
|
||||
|
||||
```
|
||||
Authorization: Bearer <JWT>
|
||||
```
|
||||
|
||||
### Endpoints summary
|
||||
|
||||
#### Public (no auth)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/v1/health` | Health check |
|
||||
| GET | `/v1/keys/public` | Ed25519 public key (JWK) |
|
||||
| POST | `/v1/auth/login` | Login; returns JWT |
|
||||
| POST | `/v1/token/validate` | Validate a JWT |
|
||||
|
||||
#### Authenticated
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/v1/auth/logout` | Revoke current token |
|
||||
| POST | `/v1/auth/renew` | Exchange token for a fresh one |
|
||||
| POST | `/v1/auth/totp/enroll` | Begin TOTP enrollment |
|
||||
| POST | `/v1/auth/totp/confirm` | Confirm TOTP enrollment |
|
||||
|
||||
#### Admin only (`admin` role required)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/v1/accounts` | List all accounts |
|
||||
| POST | `/v1/accounts` | Create account |
|
||||
| GET | `/v1/accounts/{id}` | Get account |
|
||||
| PATCH | `/v1/accounts/{id}` | Update account status |
|
||||
| DELETE | `/v1/accounts/{id}` | Soft-delete account |
|
||||
| GET | `/v1/accounts/{id}/roles` | Get roles |
|
||||
| PUT | `/v1/accounts/{id}/roles` | Replace roles |
|
||||
| GET | `/v1/accounts/{id}/pgcreds` | Get Postgres credentials |
|
||||
| PUT | `/v1/accounts/{id}/pgcreds` | Set Postgres credentials |
|
||||
| POST | `/v1/token/issue` | Issue service account token |
|
||||
| DELETE | `/v1/token/{jti}` | Revoke token by JTI |
|
||||
| DELETE | `/v1/auth/totp` | Remove TOTP from account |
|
||||
| GET | `/v1/audit` | Query audit log |
|
||||
|
||||
---
|
||||
|
||||
## Security Notes for Integrators
|
||||
|
||||
**Always validate `alg` before the signature.** Never use a JWT library that
|
||||
accepts `alg: none` or falls back to HMAC. MCIAS only issues `EdDSA` tokens.
|
||||
|
||||
**Cache the public key, but refresh it.** Fetch `/v1/keys/public` at startup.
|
||||
Refresh it if signature verification starts failing (key rotation). Do not
|
||||
hard-code the key bytes.
|
||||
|
||||
**Use the CA certificate.** MCIAS typically runs with a self-signed or
|
||||
private-CA cert. Pass the cert path to your HTTP client; do not disable TLS
|
||||
verification.
|
||||
|
||||
**Do not log tokens.** Strip the `Authorization` header from access logs.
|
||||
A leaked token is valid until it expires or is explicitly revoked.
|
||||
|
||||
**For revocation-sensitive paths, use online validation.** The `/v1/token/validate`
|
||||
endpoint reflects revocations instantly. Offline verification is fine for
|
||||
low-sensitivity reads; use online validation for writes, privilege escalation,
|
||||
and any action with irreversible effects.
|
||||
|
||||
**System accounts have one active token at a time.** Issuing a new token via
|
||||
`/v1/token/issue` revokes the previous one. Do not share a system account
|
||||
across services that rotate independently; create one system account per service.
|
||||
|
||||
**Roles are not hierarchical.** `admin` does not imply any other role and vice
|
||||
versa. Check each required role explicitly.
|
||||
Reference in New Issue
Block a user