- 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
5.2 KiB
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 browsingmemory: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
- Algorithm Pinning: Only RS256 accepted (defense against algorithm confusion attacks)
- Signature Validation: All tokens verified against Authentik's public keys
- Claim Pinning:
iss(issuer) andaud(audience) must match configured values - Expiry Check: Expired tokens rejected (60s clock skew tolerance)
- JWKS Caching: Keys cached with TTL; refreshed on key ID miss (handles rotation)
- Token TTL: Memory service does not cache validation results; each request re-validates
Backward Compatibility
Default MEM_AUTH_MODE=apikey preserves old behavior:
- Checks
apikeyheader againstMEM_API_KEYenv 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_ISSUERenv var - Verify audience matches
AUTHENTIK_AUDIENCEenv var
"Missing capability: memory:write"
- Token's
permissionsclaim must includememory:writeor* - Check Authentik app scope configuration includes
permissionsclaim
JWKS fetch timeout
- Ensure Authentik is reachable from Memory pod
- Check network policies / firewall rules
- Verify
AUTHENTIK_ISSUERURL is correct
Related Files
crates/mem-cli/src/jwt_validator.rs— Token validation logiccrates/mem-cli/src/http_server.rs— Handler integrationtests/it_jwt_auth.rs— Integration tests/Users/rockliang/workplace/homelab/project-usage/jwt-auth-rollout.md— Cluster-wide OIDC setup