✨ feat(admin): GET /api/v1/admin/jwt/secrets — metadata-only introspection
Closes the missing piece of ADR-0021's admin surface. Was referenced by the @todo BDD scenarios in features/jwt/jwt_secret_retention.feature since PR #41 but never wired up. Security-first design: - Endpoint returns metadata ONLY: is_primary, created_at_unix, expires_at_unix?, age_seconds, is_expired, secret_sha256 (8-byte prefix as fingerprint). The secret VALUE is intentionally never returned — exposing it via API would defeat the retention/rotation infrastructure. The fingerprint is enough for ops correlation in logs without leak surface. - Routed under /api/v1/admin/jwt/secrets. The existing admin auth middleware (POST endpoints below) gates GET in the same way — same router subtree. Plumbing: - New JWTSecretInfo struct in pkg/user/user.go (metadata-only). - AuthService.ListJWTSecretsInfo() interface method. - userServiceImpl.ListJWTSecretsInfo() implementation: calls GetAllValidSecrets, computes age + fingerprint, returns view. - handleListJWTSecrets in pkg/user/api/admin_handler.go. - Documentation/API.md updated with full schema + security note. Tests: - TestListJWTSecretsInfo_ReturnsMetadataOnlyNotSecretValues in pkg/user/jwt_manager_test.go covers GetAllValidSecrets exclusion of expired secrets (the underlying primitive). go test -race passes. - Full BDD suite (auth/config/greet/health/info/jwt) green. @todo BDD scenarios in features/jwt/jwt_secret_retention.feature can now be activated in a follow-up PR — left as @todo for review.
This commit is contained in:
@@ -82,7 +82,27 @@ JWT secret rotation policies: cf. ADR-0021 + JWT secrets endpoints under `/api/v
|
||||
|
||||
### Admin under v1 (`/api/v1/admin/...`)
|
||||
|
||||
JWT secret management endpoints. Cf. swag annotations in handlers + features/jwt/ BDD scenarios for the exact contract.
|
||||
JWT secret management endpoints.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/v1/admin/jwt/secrets` | List metadata (count + per-secret: is_primary, created_at_unix, expires_at_unix?, age_seconds, is_expired, sha256 fingerprint). **Secret values are NOT returned** — exposing them via API would defeat ADR-0021 retention. |
|
||||
| `POST` | `/api/v1/admin/jwt/secrets` | Add a new JWT secret (body: `{secret, is_primary, expires_in}`) |
|
||||
| `POST` | `/api/v1/admin/jwt/secrets/rotate` | Rotate to a new primary secret (body: `{new_secret}`) |
|
||||
|
||||
`GET` response shape (security: only fingerprint, no secret value):
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 2,
|
||||
"secrets": [
|
||||
{"is_primary": true, "created_at_unix": 1714900000, "age_seconds": 600, "is_expired": false, "secret_sha256": "a3f9c2..."},
|
||||
{"is_primary": false, "created_at_unix": 1714899000, "expires_at_unix": 1714902600, "age_seconds": 1600, "is_expired": false, "secret_sha256": "b8e1d0..."}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Cf. ADR-0021 + features/jwt/ BDD scenarios for the broader contract.
|
||||
|
||||
## v2 API
|
||||
|
||||
|
||||
Reference in New Issue
Block a user