You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(token)!: add per-client token lifetime profiles
- Add TokenProfile field on OAuthApplication with short/standard/long presets
- Resolve per-client TTL at token issuance across device, auth code, client credentials, and refresh flows
- Add configurable TTL caps (JWT_EXPIRATION_MAX, REFRESH_TOKEN_EXPIRATION_MAX) enforced at startup
- Expose Token Lifetime dropdown in the admin client form and detail page
- Log profile changes with WARNING severity in the audit log
- Extend TokenProvider interface methods to accept an explicit TTL (0 preserves provider default)
BREAKING CHANGE: TokenProvider interface methods GenerateToken, GenerateRefreshToken, and GenerateClientCredentialsToken now take a trailing ttl time.Duration argument; RefreshAccessToken takes accessTTL, refreshTTL time.Duration. Pass 0 on each to preserve previous behavior (use config default).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(token): address Copilot review feedback on token profiles
- Trim whitespace in normalizeTokenProfile so " standard " is accepted.
- Normalize empty previous TokenProfile before comparing, so migrated rows
going "" → short/long now produce a WARNING audit event.
- ttlForClient returns 0 when the standard profile mirrors base JWT config,
restoring JWT_EXPIRATION_JITTER for the default path; explicit short/long
and diverged standard still override as before.
- IssueClientCredentialsToken passes ttl=0 so CLIENT_CREDENTIALS_TOKEN_EXPIRATION
remains the authority for M2M token lifetime, keeping it independently
constrained from user-delegated tokens.
- Admin "Token Lifetime" dropdown: drop hard-coded TTL numbers; wording now
reflects configurability and points to server configuration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(token): document per-client token lifetime profiles
- CONFIGURATION.md: add TOKEN_PROFILE_* env vars and JWT_EXPIRATION_MAX /
REFRESH_TOKEN_EXPIRATION_MAX caps to the env snippet; add a Token Lifetime
Profiles section covering preset defaults, hard caps, jitter semantics,
client_credentials independence, and audit behavior on profile changes.
- ARCHITECTURE.md: describe TokenProfile resolution under Refresh Token
Architecture — when each preset fits, why standard returns zero TTLs to
preserve jitter, and how client_credentials remains separately governed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(token): apply simplify + security review feedback
- Centralize empty→standard TokenProfile fallback in
models.ResolveTokenProfile; used by normalizeTokenProfile, UpdateClient
audit path, and ttlForClient resolver.
- Log a WARNING when a client row carries an unknown token_profile value,
and fall back to the standard profile's TTLs rather than base JWT config
so a corrupted row can't quietly grant longer-than-intended tokens.
- Thread the already-loaded client through ExchangeAuthorizationCode for
parity with the device-flow path, removing a redundant cached lookup.
- Build ErrInvalidTokenProfile from the models constants so the message
can't drift from IsValidTokenProfile.
- Seed the default "AuthGate CLI" client with TokenProfile=standard
explicitly instead of relying on the GORM column default.
- Audit previous_token_profile now records the normalized value so readers
don't see "" alongside "standard".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(token): trim whitespace in ResolveTokenProfile
The earlier refactor to centralize "" → standard fallback inadvertently
dropped the trim-then-validate semantics that normalizeTokenProfile
originally had, so inputs like " standard " from form posts or API
clients would fail validation and fall back to the standard TTLs.
Trim once inside ResolveTokenProfile so both validation (in
normalizeTokenProfile) and resolution (in ttlForClient) see the canonical
value. Add test coverage for the whitespace cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(token): gate jitter-zeroing to standard profile
The previous ttlForClient zeroed out profile TTLs whenever they matched
JWT_EXPIRATION or REFRESH_TOKEN_EXPIRATION, which incorrectly affected
short/long profiles if an operator set them equal to the base config.
Zeroing is a standard-only affordance for preserving jitter on the default
path; explicit short/long profiles must now return their TTLs verbatim.
Added TestResolveClientTTL_ShortProfileNeverZeroes to lock this in.
Also captured a reference start time before token generation in the
TTL-override tests so the assertions aren't flaky on slower CI runners.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -109,6 +109,7 @@ In addition to Device Code Flow, AuthGate supports Authorization Code Flow with
109
109
110
110
-`OAuthApplication` - Supports both Device Flow and Auth Code Flow with per-client toggles (`EnableDeviceFlow`, `EnableAuthCodeFlow`)
111
111
-`OAuthApplication.ClientType` - "confidential" (with secret) or "public" (PKCE only)
112
+
-`OAuthApplication.TokenProfile` - Per-client token lifetime preset: `short` (15min / 1d), `standard` (default, 10h / 30d), or `long` (24h / 90d). Preset TTLs are defined in `config.TokenProfiles` and overridable via `TOKEN_PROFILE_*` env vars; hard caps are enforced via `JWT_EXPIRATION_MAX` / `REFRESH_TOKEN_EXPIRATION_MAX`. Changes to a client's profile take effect on the next token issuance and refresh.
112
113
-`UserAuthorization` - Per-app consent grants (one record per user+app pair)
113
114
-`AccessToken` - Unified storage for both access and refresh tokens (distinguished by `token_category` field)
114
115
-`User.IsActive` - Boolean (default `true`) controlling whether a user may log in. Disabling revokes all of the user's tokens and `RequireAuth` clears any live session on the next request. Guards prevent self-disable and disabling the last *active* admin.
Each `OAuthApplication` carries a `token_profile` field (one of `short`, `standard`, `long`; defaults to `standard`) so admins can tune token lifetimes per client without editing base config. Resolution happens on every issuance and every refresh, so a profile change takes effect on the next token issued to that client — in-flight tokens keep the lifetime they were issued with.
345
+
346
+
-**`short`** — 15m access / 24h refresh. For admin consoles and other high-value clients where a leaked token's blast radius must be tightly bounded.
347
+
-**`standard`** — defaults to `JWT_EXPIRATION` / `REFRESH_TOKEN_EXPIRATION`. For typical web and SPA clients. Because it mirrors the base config by default, the token service returns `(0, 0)` from its TTL resolver here — which tells the local provider to take its normal issuance path, including `JWT_EXPIRATION_JITTER`. Explicit short/long profiles (and any `standard` that has been diverged from the base) bypass jitter and use the profile TTL exactly.
348
+
-**`long`** — 24h access / 90d refresh. For CLI tools, IoT devices, and long-lived automation where frequent re-auth is user-hostile.
349
+
350
+
All three profiles are bounded by `JWT_EXPIRATION_MAX` and `REFRESH_TOKEN_EXPIRATION_MAX`; the server refuses to start if any configured profile exceeds its cap.
351
+
352
+
**Scope:** TokenProfile applies to access and refresh tokens issued through the device-code and authorization-code grants. The `client_credentials` grant is governed separately by `CLIENT_CREDENTIALS_TOKEN_EXPIRATION` so M2M token lifetimes can stay tight regardless of per-client profile choices made for user-facing flows.
353
+
354
+
**Auditability:** Every profile change is logged at `WARNING` severity with the prior value (`previous_token_profile`) so incident response can trace lifetime changes across the fleet.
# CLIENT_CREDENTIALS_TOKEN_EXPIRATION=1h # Access token lifetime for client_credentials grant (default: 1h)
85
86
# # Keep short — no refresh token means no rotation mechanism
87
+
# # Governed independently from per-client TokenProfile (see below)
88
+
89
+
# Per-Client Token Lifetime Profiles
90
+
# Each OAuth client selects one of three presets: "short", "standard" (default), or "long".
91
+
# "standard" defaults to JWT_EXPIRATION / REFRESH_TOKEN_EXPIRATION above; overrides below
92
+
# let you tailor the short/long presets without touching the base defaults.
93
+
# TOKEN_PROFILE_SHORT_ACCESS_TTL=15m # Short profile access token lifetime (default: 15m)
94
+
# TOKEN_PROFILE_SHORT_REFRESH_TTL=24h # Short profile refresh token lifetime (default: 24h)
95
+
# TOKEN_PROFILE_STANDARD_ACCESS_TTL=10h # Standard profile access TTL (default: JWT_EXPIRATION)
96
+
# TOKEN_PROFILE_STANDARD_REFRESH_TTL=720h # Standard profile refresh TTL (default: REFRESH_TOKEN_EXPIRATION)
97
+
# TOKEN_PROFILE_LONG_ACCESS_TTL=24h # Long profile access TTL (default: 24h)
98
+
# TOKEN_PROFILE_LONG_REFRESH_TTL=2160h # Long profile refresh TTL (default: 90 days)
99
+
#
100
+
# Hard caps — enforced at startup. No profile may exceed these values.
101
+
# JWT_EXPIRATION_MAX=24h # Upper bound for any access-token profile (default: 24h)
102
+
# REFRESH_TOKEN_EXPIRATION_MAX=2160h # Upper bound for any refresh-token profile (default: 90d)
86
103
87
104
# OAuth Configuration (optional - for third-party login)
88
105
# GitHub OAuth
@@ -354,6 +371,38 @@ If `JWT_KEY_ID` is not set, it is automatically derived from the SHA-256 hash of
354
371
355
372
---
356
373
374
+
## Token Lifetime Profiles
375
+
376
+
AuthGate assigns every OAuth client one of three **token lifetime presets** so admins can tune access and refresh token durations to each client's risk profile without touching the base JWT configuration. The preset is selectable from the admin UI (**Admin → OAuth Clients → Token Lifetime**) and recorded on the client as `token_profile`.
377
+
378
+
### Profiles
379
+
380
+
| Profile | When to use | Default access TTL | Default refresh TTL |
|`short`| High-security apps (admin consoles, financial dashboards) | 15 min | 24 h |
383
+
|`standard`| Typical web/SPA clients (default for new clients) |`JWT_EXPIRATION` (10 h) |`REFRESH_TOKEN_EXPIRATION` (30 d) |
384
+
|`long`| CLI tools, IoT devices, long-lived background jobs | 24 h | 90 d |
385
+
386
+
Defaults are overridable per environment via the `TOKEN_PROFILE_*` variables listed in [Environment Variables](#environment-variables).
387
+
388
+
### Hard caps
389
+
390
+
`JWT_EXPIRATION_MAX` and `REFRESH_TOKEN_EXPIRATION_MAX` bound every profile's TTL. The server refuses to start if any configured profile exceeds its cap — this guarantees that a stray env override cannot issue tokens longer than the operator intends.
391
+
392
+
### Jitter behavior
393
+
394
+
`JWT_EXPIRATION_JITTER` is applied only when the resolved access-token TTL matches the base `JWT_EXPIRATION` (the `standard`-profile default). Explicit `short`/`long` overrides — and a `standard` profile that has been explicitly diverged from the base config — use the profile's TTL exactly, with no jitter added. This keeps jitter working for the high-volume default path (preventing refresh thundering herds) while respecting operator-chosen short/long lifetimes precisely.
395
+
396
+
### Client Credentials independence
397
+
398
+
The `client_credentials` grant is governed by `CLIENT_CREDENTIALS_TOKEN_EXPIRATION` and **ignores** the client's TokenProfile. M2M tokens carry a larger blast radius than user-delegated tokens (no refresh, no user-revoke UI), so their lifetime is managed separately and is typically kept much shorter than user-facing tokens. If you need per-client M2M TTLs, open an issue — it will require a dedicated field on TokenProfile rather than overloading the existing access TTL.
399
+
400
+
### Changing a profile
401
+
402
+
Updates take effect on the **next token issuance or refresh**. Existing tokens retain the lifetime they were originally issued with; AuthGate does not retroactively shorten live tokens. Every TokenProfile change is recorded in the audit log at `WARNING` severity with the previous value (`previous_token_profile`) for forensic traceability.
403
+
404
+
---
405
+
357
406
## Default Test Data
358
407
359
408
The server initializes with default test accounts:
0 commit comments