177 lines
5.2 KiB
Markdown
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
|