Files
poimen-memory/docs/JWT_AUTH.md
T
Story Crater Bot 47e55afae3 feat: JWT auth validation with Authentik OIDC
- Add jwt_validator module with JWKS caching (TTL + refresh-on-miss)
- Implement RS256 algorithm pinning + claim validation
- Replace apikey with Bearer token validation in http_server
- Add capability-based access control (memory:read/write/*)
- Backward compatible: MEM_AUTH_MODE=jwt|apikey (default: apikey)
- 16 tests passing (7 unit + 9 integration)
- Docs: JWT_AUTH.md with deployment guide

Config via env vars:
- MEM_AUTH_MODE=jwt
- AUTHENTIK_ISSUER=https://authentik.riotpiao.com/application/o/poimen-memory/
- AUTHENTIK_AUDIENCE=poimen-memory
- JWT_CACHE_TTL_SECS=3600 (optional)

Gw passes Authorization: Bearer <token> header
Memory validates + checks permissions claim
2026-08-27 12:29:23 -07:00

5.2 KiB

JWT Authentication for Poimen Memory Service

Overview

Memory service validates incoming requests using JWT tokens issued by Authentik OIDC provider. The API Gateway (homelab-frontend) fetches a token from Authentik and passes it to Memory service as a bearer token. Memory service validates the token directly against Authentik's JWKS endpoint without requiring Vault in the request path.

Architecture

Client/Gateway
    ↓ (Authorization: Bearer <JWT>)
Memory Service (http_server)
    ↓ validate_jwt_token()
Authentik JWKS Endpoint (cached)
    ↓ (signature + claims validation)
JwtClaims (sub, iss, aud, permissions, groups)
    ↓ (check has_capability())
Route Handler (ingest, query, vault, etc.)

Configuration

Environment Variables

Required for JWT mode:

  • MEM_AUTH_MODE=jwt — Enable JWT validation (default: apikey)
  • AUTHENTIK_ISSUER — Authentik OIDC issuer, e.g. https://authentik.riotpiao.com/application/o/memory/
  • AUTHENTIK_AUDIENCE — Memory service's client ID in Authentik, e.g. poimen-memory

Optional:

  • JWT_CACHE_TTL_SECS — JWKS cache TTL in seconds (default: 3600)

K8s Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: poimen-memory
  namespace: poimen
spec:
  template:
    spec:
      containers:
      - name: memory
        image: registry/poimen-memory:latest
        env:
        - name: MEM_AUTH_MODE
          value: "jwt"
        - name: AUTHENTIK_ISSUER
          valueFrom:
            configMapKeyRef:
              name: poimen-config
              key: authentik-issuer-url
        - name: AUTHENTIK_AUDIENCE
          valueFrom:
            configMapKeyRef:
              name: poimen-config
              key: memory-client-id
        - name: JWT_CACHE_TTL_SECS
          value: "3600"
        # ... other env vars

Capabilities

JWT tokens must include a permissions claim with one of:

  • memory:read — Read-only: query, skills, projects, vault browsing
  • memory:write — Write: ingest, source sync, vault generation
  • * — Wildcard: all capabilities (typically for homelab-admins group)

Example permissions claim in token:

{
  "permissions": ["memory:read", "memory:write"]
}

API Request Format

# Fetch JWT from Authentik (typically done by API Gateway)
TOKEN=$(curl -s -X POST https://authentik.riotpiao.com/application/o/token/ \
  -d "grant_type=client_credentials&client_id=...&client_secret=...")

# Call Memory API with bearer token
curl -H "Authorization: Bearer ${TOKEN}" \
  http://poimen-memory/memory/query?project=myproject&query=topic

Security Considerations

  1. Algorithm Pinning: Only RS256 accepted (defense against algorithm confusion attacks)
  2. Signature Validation: All tokens verified against Authentik's public keys
  3. Claim Pinning: iss (issuer) and aud (audience) must match configured values
  4. Expiry Check: Expired tokens rejected (60s clock skew tolerance)
  5. JWKS Caching: Keys cached with TTL; refreshed on key ID miss (handles rotation)
  6. Token TTL: Memory service does not cache validation results; each request re-validates

Backward Compatibility

Default MEM_AUTH_MODE=apikey preserves old behavior:

  • Checks apikey header against MEM_API_KEY env var
  • Grants synthetic * permission
  • Useful for local dev/test

Switch to JWT by setting MEM_AUTH_MODE=jwt.

Capability Checking in Handlers

Each handler checks for required capability:

// In ingest_handler (write operation)
if !has_capability(&claims, "memory:write") {
    return HttpResponse::Forbidden().json(...);
}

// In query_handler (read operation)
if !has_capability(&claims, "memory:read") {
    return HttpResponse::Forbidden().json(...);
}

Wildcard permission * grants all.

Testing

Unit tests in tests/it_jwt_auth.rs:

  • Bearer token extraction
  • JWT claims structures
  • Permission validation
  • Wildcard permission handling
cargo test --test it_jwt_auth

Example test:

#[test]
fn test_jwt_permissions_claim() {
    let claims = JwtClaims {
        permissions: Some(vec!["memory:read".to_string()]),
        ...
    };
    assert!(claims.permissions.unwrap().contains(&"memory:read".to_string()));
}

Troubleshooting

"Invalid Authorization header format"

  • Ensure request includes Authorization: Bearer <token> (capital B)
  • Token must not be empty

"JWT validation failed: Token validation failed"

  • Check token signature: ensure Authentik JWKS endpoint is reachable
  • Verify issuer matches AUTHENTIK_ISSUER env var
  • Verify audience matches AUTHENTIK_AUDIENCE env var

"Missing capability: memory:write"

  • Token's permissions claim must include memory:write or *
  • Check Authentik app scope configuration includes permissions claim

JWKS fetch timeout

  • Ensure Authentik is reachable from Memory pod
  • Check network policies / firewall rules
  • Verify AUTHENTIK_ISSUER URL is correct
  • crates/mem-cli/src/jwt_validator.rs — Token validation logic
  • crates/mem-cli/src/http_server.rs — Handler integration
  • tests/it_jwt_auth.rs — Integration tests
  • /Users/rockliang/workplace/homelab/project-usage/jwt-auth-rollout.md — Cluster-wide OIDC setup