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
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user