# 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/mc/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/mc/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.
