Skip to content

Commit 08ac32f

Browse files
appleboyclaude
andauthored
feat(token): add per-client token lifetime profiles (#168)
* 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>
1 parent 1e42045 commit 08ac32f

28 files changed

Lines changed: 1004 additions & 86 deletions

.env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ SESSION_SECRET=session-secret-change-in-production
2929
# JWT_EXPIRATION_JITTER=30m # Max random jitter added to access token expiry (default: 30m)
3030
# # Prevents thundering herd when many tokens expire simultaneously
3131
# # Example: JWT_EXPIRATION=8h + JWT_EXPIRATION_JITTER=30m → token lifetime [8h, 8h30m)
32+
# JWT_EXPIRATION_MAX=24h # Hard cap on any token profile's access TTL (default: 24h). Startup fails if a profile exceeds this.
33+
# REFRESH_TOKEN_EXPIRATION_MAX=2160h # Hard cap on any token profile's refresh TTL (default: 2160h / 90 days).
34+
35+
# Token Profiles — per-OAuthApplication TTL presets (short / standard / long).
36+
# Each OAuth client selects one profile via the "Token Lifetime" admin UI; tokens
37+
# issued for that client use the preset's access & refresh TTLs. The "standard"
38+
# profile inherits JWT_EXPIRATION / REFRESH_TOKEN_EXPIRATION by default.
39+
# TOKEN_PROFILE_SHORT_ACCESS_TTL=15m # Short access token TTL (default: 15m)
40+
# TOKEN_PROFILE_SHORT_REFRESH_TTL=24h # Short refresh token TTL (default: 24h)
41+
# TOKEN_PROFILE_STANDARD_ACCESS_TTL= # Defaults to JWT_EXPIRATION
42+
# TOKEN_PROFILE_STANDARD_REFRESH_TTL= # Defaults to REFRESH_TOKEN_EXPIRATION
43+
# TOKEN_PROFILE_LONG_ACCESS_TTL=24h # Long access token TTL (default: 24h)
44+
# TOKEN_PROFILE_LONG_REFRESH_TTL=2160h # Long refresh token TTL (default: 2160h / 90 days)
3245

3346
# Database
3447
DATABASE_DRIVER=sqlite

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ In addition to Device Code Flow, AuthGate supports Authorization Code Flow with
109109

110110
- `OAuthApplication` - Supports both Device Flow and Auth Code Flow with per-client toggles (`EnableDeviceFlow`, `EnableAuthCodeFlow`)
111111
- `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.
112113
- `UserAuthorization` - Per-app consent grants (one record per user+app pair)
113114
- `AccessToken` - Unified storage for both access and refresh tokens (distinguished by `token_category` field)
114115
- `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.

docs/ARCHITECTURE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,20 @@ AuthGate supports refresh tokens following RFC 6749 with configurable rotation m
339339
- `ENABLE_REFRESH_TOKENS=true` - Feature flag (default: enabled)
340340
- `ENABLE_TOKEN_ROTATION=false` - Enable rotation mode (default: disabled, uses fixed mode)
341341

342+
### Per-Client Token Lifetime Profiles
343+
344+
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.
355+
342356
### Grant Type Support
343357

344358
- `urn:ietf:params:oauth:grant-type:device_code` - Device authorization flow (returns access + refresh)

docs/CONFIGURATION.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This guide covers all configuration options for AuthGate, including environment
88
- [TLS / HTTPS](#tls--https)
99
- [Bootstrap and Shutdown Timeouts](#bootstrap-and-shutdown-timeouts)
1010
- [Generate Strong Secrets](#generate-strong-secrets)
11+
- [Token Lifetime Profiles](#token-lifetime-profiles)
1112
- [Default Test Data](#default-test-data)
1213
- [OAuth Third-Party Login](#oauth-third-party-login)
1314
- [Service-to-Service Authentication](#service-to-service-authentication)
@@ -83,6 +84,22 @@ ENABLE_TOKEN_ROTATION=false # Enable rotation mode (default: fixed mode)
8384
# Client Credentials Flow (RFC 6749 §4.4)
8485
# CLIENT_CREDENTIALS_TOKEN_EXPIRATION=1h # Access token lifetime for client_credentials grant (default: 1h)
8586
# # 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)
86103

87104
# OAuth Configuration (optional - for third-party login)
88105
# GitHub OAuth
@@ -354,6 +371,38 @@ If `JWT_KEY_ID` is not set, it is automatically derived from the SHA-256 hash of
354371

355372
---
356373

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 |
381+
| ------------ | -------------------------------------------------------------- | ---------------------------- | ----------------------------- |
382+
| `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+
357406
## Default Test Data
358407

359408
The server initializes with default test accounts:

internal/config/config.go

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"strings"
88
"time"
99

10+
"github.com/go-authgate/authgate/internal/models"
11+
1012
"github.com/joho/godotenv"
1113
)
1214

@@ -36,6 +38,14 @@ const (
3638
AlgES256 = "ES256"
3739
)
3840

41+
// TokenProfile defines the access and refresh token lifetimes for a named preset.
42+
// Clients reference a profile by name via OAuthApplication.TokenProfile (see
43+
// models.TokenProfile* constants) and the TTL is resolved at token issuance.
44+
type TokenProfile struct {
45+
AccessTokenTTL time.Duration
46+
RefreshTokenTTL time.Duration
47+
}
48+
3949
type Config struct {
4050
// Server settings
4151
ServerAddr string
@@ -100,6 +110,17 @@ type Config struct {
100110
EnableRefreshTokens bool // Feature flag to enable/disable refresh tokens (default: true)
101111
EnableTokenRotation bool // Enable token rotation mode (default: false, fixed mode)
102112

113+
// Token lifetime hard caps. Any TokenProfile value that exceeds these is rejected
114+
// during Validate(). Prevents a misconfigured profile from silently extending token
115+
// lifetime far beyond the security intent.
116+
JWTExpirationMax time.Duration // env: JWT_EXPIRATION_MAX (default: 24h)
117+
RefreshTokenExpirationMax time.Duration // env: REFRESH_TOKEN_EXPIRATION_MAX (default: 2160h / 90d)
118+
119+
// TokenProfiles maps a profile name ("short" / "standard" / "long") to its TTLs.
120+
// Populated in Load() from the TOKEN_PROFILE_*_ACCESS_TTL / TOKEN_PROFILE_*_REFRESH_TTL env
121+
// vars; the "standard" profile falls back to JWTExpiration / RefreshTokenExpiration.
122+
TokenProfiles map[string]TokenProfile
123+
103124
// Client Credentials Flow settings (RFC 6749 §4.4)
104125
ClientCredentialsTokenExpiration time.Duration // Access token lifetime for client_credentials grant (default: 1h, same as JWTExpiration)
105126

@@ -251,6 +272,31 @@ func Load() *Config {
251272
dsn = getEnv("DATABASE_DSN", "")
252273
}
253274

275+
// Resolve base JWT settings first — the "standard" profile inherits these,
276+
// so the map must be built after the values are known.
277+
jwtExpiration := getEnvDuration("JWT_EXPIRATION", 10*time.Hour)
278+
refreshTokenExpiration := getEnvDuration("REFRESH_TOKEN_EXPIRATION", 720*time.Hour)
279+
tokenProfiles := map[string]TokenProfile{
280+
models.TokenProfileShort: {
281+
AccessTokenTTL: getEnvDuration("TOKEN_PROFILE_SHORT_ACCESS_TTL", 15*time.Minute),
282+
RefreshTokenTTL: getEnvDuration("TOKEN_PROFILE_SHORT_REFRESH_TTL", 24*time.Hour),
283+
},
284+
models.TokenProfileStandard: {
285+
AccessTokenTTL: getEnvDuration("TOKEN_PROFILE_STANDARD_ACCESS_TTL", jwtExpiration),
286+
RefreshTokenTTL: getEnvDuration(
287+
"TOKEN_PROFILE_STANDARD_REFRESH_TTL",
288+
refreshTokenExpiration,
289+
),
290+
},
291+
models.TokenProfileLong: {
292+
AccessTokenTTL: getEnvDuration("TOKEN_PROFILE_LONG_ACCESS_TTL", 24*time.Hour),
293+
RefreshTokenTTL: getEnvDuration(
294+
"TOKEN_PROFILE_LONG_REFRESH_TTL",
295+
2160*time.Hour,
296+
), // 90 days
297+
},
298+
}
299+
254300
return &Config{
255301
ServerAddr: getEnv("SERVER_ADDR", ":8080"),
256302
BaseURL: getEnv("BASE_URL", "http://localhost:8080"),
@@ -259,7 +305,7 @@ func Load() *Config {
259305
IsProduction: getEnvBool("ENVIRONMENT", false) ||
260306
getEnv("ENVIRONMENT", "") == "production",
261307
JWTSecret: getEnv("JWT_SECRET", "your-256-bit-secret-change-in-production"),
262-
JWTExpiration: getEnvDuration("JWT_EXPIRATION", 10*time.Hour),
308+
JWTExpiration: jwtExpiration,
263309
JWTSigningAlgorithm: getEnv("JWT_SIGNING_ALGORITHM", AlgHS256),
264310
JWTPrivateKeyPath: getEnv("JWT_PRIVATE_KEY_PATH", ""),
265311
JWTKeyID: getEnv("JWT_KEY_ID", ""),
@@ -302,12 +348,12 @@ func Load() *Config {
302348
HTTPAPIMaxRetryDelay: getEnvDuration("HTTP_API_MAX_RETRY_DELAY", 10*time.Second),
303349

304350
// Refresh Token settings
305-
RefreshTokenExpiration: getEnvDuration(
306-
"REFRESH_TOKEN_EXPIRATION",
307-
720*time.Hour,
308-
), // 30 days
309-
EnableRefreshTokens: getEnvBool("ENABLE_REFRESH_TOKENS", true),
310-
EnableTokenRotation: getEnvBool("ENABLE_TOKEN_ROTATION", false),
351+
RefreshTokenExpiration: refreshTokenExpiration,
352+
EnableRefreshTokens: getEnvBool("ENABLE_REFRESH_TOKENS", true),
353+
EnableTokenRotation: getEnvBool("ENABLE_TOKEN_ROTATION", false),
354+
JWTExpirationMax: getEnvDuration("JWT_EXPIRATION_MAX", 24*time.Hour),
355+
RefreshTokenExpirationMax: getEnvDuration("REFRESH_TOKEN_EXPIRATION_MAX", 2160*time.Hour),
356+
TokenProfiles: tokenProfiles,
311357

312358
// Client Credentials Flow settings
313359
ClientCredentialsTokenExpiration: getEnvDuration(
@@ -680,5 +726,67 @@ func (c *Config) Validate() error {
680726
)
681727
}
682728

729+
return c.validateTokenProfiles()
730+
}
731+
732+
// validateTokenProfiles checks that every profile has positive TTLs and that
733+
// no profile's TTL exceeds the configured hard caps (JWT_EXPIRATION_MAX /
734+
// REFRESH_TOKEN_EXPIRATION_MAX). The standard / short / long profiles must all
735+
// be present. When TokenProfiles and both caps are left at their zero values
736+
// (e.g. a hand-built *Config used in an ad-hoc unit test) validation is
737+
// skipped — Load() always populates these, so this gate only affects real
738+
// startup, not consumers who only care about unrelated fields.
739+
func (c *Config) validateTokenProfiles() error {
740+
if len(c.TokenProfiles) == 0 && c.JWTExpirationMax == 0 && c.RefreshTokenExpirationMax == 0 {
741+
return nil
742+
}
743+
if c.JWTExpirationMax <= 0 {
744+
return fmt.Errorf(
745+
"JWT_EXPIRATION_MAX must be a positive duration (got %s)",
746+
c.JWTExpirationMax,
747+
)
748+
}
749+
if c.RefreshTokenExpirationMax <= 0 {
750+
return fmt.Errorf(
751+
"REFRESH_TOKEN_EXPIRATION_MAX must be a positive duration (got %s)",
752+
c.RefreshTokenExpirationMax,
753+
)
754+
}
755+
756+
requiredProfiles := []string{
757+
models.TokenProfileShort,
758+
models.TokenProfileStandard,
759+
models.TokenProfileLong,
760+
}
761+
for _, name := range requiredProfiles {
762+
profile, ok := c.TokenProfiles[name]
763+
if !ok {
764+
return fmt.Errorf("token profile %q is missing from TokenProfiles", name)
765+
}
766+
if profile.AccessTokenTTL <= 0 {
767+
return fmt.Errorf(
768+
"token profile %q access TTL must be a positive duration (got %s)",
769+
name, profile.AccessTokenTTL,
770+
)
771+
}
772+
if profile.RefreshTokenTTL <= 0 {
773+
return fmt.Errorf(
774+
"token profile %q refresh TTL must be a positive duration (got %s)",
775+
name, profile.RefreshTokenTTL,
776+
)
777+
}
778+
if profile.AccessTokenTTL > c.JWTExpirationMax {
779+
return fmt.Errorf(
780+
"token profile %q access TTL %s exceeds JWT_EXPIRATION_MAX %s",
781+
name, profile.AccessTokenTTL, c.JWTExpirationMax,
782+
)
783+
}
784+
if profile.RefreshTokenTTL > c.RefreshTokenExpirationMax {
785+
return fmt.Errorf(
786+
"token profile %q refresh TTL %s exceeds REFRESH_TOKEN_EXPIRATION_MAX %s",
787+
name, profile.RefreshTokenTTL, c.RefreshTokenExpirationMax,
788+
)
789+
}
790+
}
683791
return nil
684792
}

0 commit comments

Comments
 (0)