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

177 lines
5.2 KiB
Markdown

# 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
```yaml
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:
```json
{
"permissions": ["memory:read", "memory:write"]
}
```
## API Request Format
```bash
# 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:
```rust
// 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
```bash
cargo test --test it_jwt_auth
```
Example test:
```rust
#[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
## Related Files
- `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