Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc43e727

Browse files
committed
feat(oauth2): add configurable refresh token lifetime
Change-Id: I988093e8fc7328a09d2a0b2c5d476bad75e064c8Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parente53bc24 commitc43e727

File tree

12 files changed

+150
-17
lines changed

12 files changed

+150
-17
lines changed

‎cli/testdata/coder_server_--help.golden‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ OPTIONS:
2525
systemd. This directory is NOT safe to be configured as a shared
2626
directory across coderd/provisionerd replicas.
2727

28+
--default-oauth-refresh-lifetime duration, $CODER_DEFAULT_OAUTH_REFRESH_LIFETIME (default: 720h0m0s)
29+
The default lifetime duration for OAuth2 refresh tokens. This controls
30+
how long refresh tokens remain valid after issuance or rotation.
31+
2832
--default-token-lifetime duration, $CODER_DEFAULT_TOKEN_LIFETIME (default: 168h0m0s)
2933
The default lifetime duration for API tokens. This value is used when
3034
creating a token without specifying a duration, such as when

‎cli/testdata/server-config.yaml.golden‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,10 @@ updateCheck: false
454454
# IDE plugin.
455455
# (default: 168h0m0s, type: duration)
456456
defaultTokenLifetime: 168h0m0s
457+
# The default lifetime duration for OAuth2 refresh tokens. This controls how long
458+
# refresh tokens remain valid after issuance or rotation.
459+
# (default: 720h0m0s, type: duration)
460+
defaultOAuthRefreshLifetime: 720h0m0s
457461
# Expose the swagger endpoint via /swagger.
458462
# (default: <unset>, type: bool)
459463
enableSwagger: false

‎cli/vpndaemon_darwin.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/coder/serpent"
1111
)
1212

13-
func (r*RootCmd)vpnDaemonRun()*serpent.Command {
13+
func (*RootCmd)vpnDaemonRun()*serpent.Command {
1414
var (
1515
rpcReadFDint64
1616
rpcWriteFDint64

‎coderd/apidoc/docs.go‎

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/apidoc/swagger.json‎

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/oauth2_test.go‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import (
2020
"github.com/coder/coder/v2/coderd/coderdtest"
2121
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
2222
"github.com/coder/coder/v2/coderd/database"
23+
"github.com/coder/coder/v2/coderd/database/dbauthz"
2324
"github.com/coder/coder/v2/coderd/database/dbtestutil"
2425
"github.com/coder/coder/v2/coderd/database/dbtime"
2526
"github.com/coder/coder/v2/coderd/oauth2provider"
2627
"github.com/coder/coder/v2/coderd/userpassword"
2728
"github.com/coder/coder/v2/coderd/util/ptr"
2829
"github.com/coder/coder/v2/codersdk"
2930
"github.com/coder/coder/v2/testutil"
31+
"github.com/coder/serpent"
3032
)
3133

3234
funcTestOAuth2ProviderApps(t*testing.T) {
@@ -1184,6 +1186,71 @@ func TestOAuth2ProviderCrossResourceAudienceValidation(t *testing.T) {
11841186
// For now, this verifies the basic token flow works correctly
11851187
}
11861188

1189+
// TestOAuth2RefreshExpiryOutlivesAccess verifies that refresh token expiry is
1190+
// greater than the provisioned access token (API key) expiry per configuration.
1191+
funcTestOAuth2RefreshExpiryOutlivesAccess(t*testing.T) {
1192+
t.Parallel()
1193+
1194+
// Set explicit lifetimes to make comparison deterministic.
1195+
db,pubsub:=dbtestutil.NewDB(t)
1196+
dv:=coderdtest.DeploymentValues(t,func(d*codersdk.DeploymentValues) {
1197+
d.Sessions.DefaultDuration=serpent.Duration(1*time.Hour)
1198+
d.Sessions.RefreshDefaultDuration=serpent.Duration(48*time.Hour)
1199+
})
1200+
ownerClient:=coderdtest.New(t,&coderdtest.Options{
1201+
Database:db,
1202+
Pubsub:pubsub,
1203+
DeploymentValues:dv,
1204+
})
1205+
_=coderdtest.CreateFirstUser(t,ownerClient)
1206+
ctx:=testutil.Context(t,testutil.WaitLong)
1207+
1208+
// Create app and secret
1209+
// Keep suffix short to satisfy name validation (<=32 chars, alnum + hyphens).
1210+
apps:=generateApps(ctx,t,ownerClient,"ref-exp")
1211+
//nolint:gocritic // Owner permission required for app secret creation
1212+
secret,err:=ownerClient.PostOAuth2ProviderAppSecret(ctx,apps.Default.ID)
1213+
require.NoError(t,err)
1214+
1215+
cfg:=&oauth2.Config{
1216+
ClientID:apps.Default.ID.String(),
1217+
ClientSecret:secret.ClientSecretFull,
1218+
Endpoint: oauth2.Endpoint{
1219+
AuthURL:apps.Default.Endpoints.Authorization,
1220+
DeviceAuthURL:apps.Default.Endpoints.DeviceAuth,
1221+
TokenURL:apps.Default.Endpoints.Token,
1222+
AuthStyle:oauth2.AuthStyleInParams,
1223+
},
1224+
RedirectURL:apps.Default.CallbackURL,
1225+
Scopes: []string{},
1226+
}
1227+
1228+
// Authorization and token exchange
1229+
code,err:=authorizationFlow(ctx,ownerClient,cfg)
1230+
require.NoError(t,err)
1231+
tok,err:=cfg.Exchange(ctx,code)
1232+
require.NoError(t,err)
1233+
require.NotEmpty(t,tok.AccessToken)
1234+
require.NotEmpty(t,tok.RefreshToken)
1235+
1236+
// Parse refresh token prefix (coder_<prefix>_<secret>)
1237+
parts:=strings.Split(tok.RefreshToken,"_")
1238+
require.Len(t,parts,3)
1239+
prefix:=parts[1]
1240+
1241+
// Look up refresh token row and associated API key
1242+
dbToken,err:=db.GetOAuth2ProviderAppTokenByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(prefix))
1243+
require.NoError(t,err)
1244+
apiKey,err:=db.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx),dbToken.APIKeyID)
1245+
require.NoError(t,err)
1246+
1247+
// Assert refresh token expiry is strictly after access token expiry
1248+
require.Truef(t,dbToken.ExpiresAt.After(apiKey.ExpiresAt),
1249+
"expected refresh expiry %s to be after access expiry %s",
1250+
dbToken.ExpiresAt,apiKey.ExpiresAt,
1251+
)
1252+
}
1253+
11871254
// customTokenExchange performs a custom OAuth2 token exchange with support for resource parameter
11881255
// This is needed because golang.org/x/oauth2 doesn't support custom parameters in token requests
11891256
funccustomTokenExchange(ctx context.Context,baseURL,clientID,clientSecret,code,redirectURI,resourcestring) (*oauth2.Token,error) {

‎coderd/oauth2provider/tokens.go‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,8 @@ func extractTokenParams(r *http.Request, callbackURL *url.URL) (tokenParams, []c
9292
}
9393

9494
// Tokens
95-
// TODO: the sessions lifetime config passed is for coder api tokens.
96-
// Should there be a separate config for oauth2 tokens? They are related,
97-
// but they are not the same.
95+
// Uses Sessions.DefaultDuration for access token (API key) TTL and
96+
// Sessions.RefreshDefaultDuration for refresh token TTL.
9897
funcTokens(db database.Store,lifetimes codersdk.SessionLifetime) http.HandlerFunc {
9998
returnfunc(rw http.ResponseWriter,r*http.Request) {
10099
ctx:=r.Context()
@@ -280,6 +279,14 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
280279
}
281280

282281
// Do the actual token exchange in the database.
282+
// Determine refresh token expiry independently from the access token.
283+
refreshLifetime:=lifetimes.RefreshDefaultDuration.Value()
284+
ifrefreshLifetime==0 {
285+
// Fallback to session default duration if not configured.
286+
refreshLifetime=lifetimes.DefaultDuration.Value()
287+
}
288+
refreshExpiresAt:=dbtime.Now().Add(refreshLifetime)
289+
283290
err=db.InTx(func(tx database.Store)error {
284291
ctx:=dbauthz.As(ctx,actor)
285292
err=tx.DeleteOAuth2ProviderAppCodeByID(ctx,dbCode.ID)
@@ -307,7 +314,7 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
307314
_,err=tx.InsertOAuth2ProviderAppToken(ctx, database.InsertOAuth2ProviderAppTokenParams{
308315
ID:uuid.New(),
309316
CreatedAt:dbtime.Now(),
310-
ExpiresAt:key.ExpiresAt,
317+
ExpiresAt:refreshExpiresAt,
311318
HashPrefix: []byte(refreshToken.Prefix),
312319
RefreshHash: []byte(refreshToken.Hashed),
313320
AppSecretID:dbSecret.ID,
@@ -401,6 +408,14 @@ func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAut
401408
}
402409

403410
// Replace the token.
411+
// Determine refresh token expiry independently from the access token.
412+
refreshLifetime:=lifetimes.RefreshDefaultDuration.Value()
413+
ifrefreshLifetime==0 {
414+
// Fallback to session default duration if not configured.
415+
refreshLifetime=lifetimes.DefaultDuration.Value()
416+
}
417+
refreshExpiresAt:=dbtime.Now().Add(refreshLifetime)
418+
404419
err=db.InTx(func(tx database.Store)error {
405420
ctx:=dbauthz.As(ctx,actor)
406421
err=tx.DeleteAPIKeyByID(ctx,prevKey.ID)// This cascades to the token.
@@ -416,7 +431,7 @@ func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAut
416431
_,err=tx.InsertOAuth2ProviderAppToken(ctx, database.InsertOAuth2ProviderAppTokenParams{
417432
ID:uuid.New(),
418433
CreatedAt:dbtime.Now(),
419-
ExpiresAt:key.ExpiresAt,
434+
ExpiresAt:refreshExpiresAt,
420435
HashPrefix: []byte(refreshToken.Prefix),
421436
RefreshHash: []byte(refreshToken.Hashed),
422437
AppSecretID:dbToken.AppSecretID,

‎codersdk/deployment.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,11 @@ type SessionLifetime struct {
566566
// DefaultDuration is only for browser, workspace app and oauth sessions.
567567
DefaultDuration serpent.Duration`json:"default_duration" typescript:",notnull"`
568568

569+
// RefreshDefaultDuration is the default lifetime for OAuth2 refresh tokens.
570+
// This should generally be longer than access token lifetimes to allow
571+
// refreshing after access token expiry.
572+
RefreshDefaultDuration serpent.Duration`json:"refresh_default_duration,omitempty" typescript:",notnull"`
573+
569574
DefaultTokenDuration serpent.Duration`json:"default_token_lifetime,omitempty" typescript:",notnull"`
570575

571576
MaximumTokenDuration serpent.Duration`json:"max_token_lifetime,omitempty" typescript:",notnull"`
@@ -2464,6 +2469,16 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
24642469
YAML:"defaultTokenLifetime",
24652470
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration,"true"),
24662471
},
2472+
{
2473+
Name:"Default OAuth Refresh Lifetime",
2474+
Description:"The default lifetime duration for OAuth2 refresh tokens. This controls how long refresh tokens remain valid after issuance or rotation.",
2475+
Flag:"default-oauth-refresh-lifetime",
2476+
Env:"CODER_DEFAULT_OAUTH_REFRESH_LIFETIME",
2477+
Default: (30*24*time.Hour).String(),
2478+
Value:&c.Sessions.RefreshDefaultDuration,
2479+
YAML:"defaultOAuthRefreshLifetime",
2480+
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration,"true"),
2481+
},
24672482
{
24682483
Name:"Enable swagger endpoint",
24692484
Description:"Expose the swagger endpoint via /swagger.",

‎docs/reference/api/general.md‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎docs/reference/api/schemas.md‎

Lines changed: 14 additions & 10 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp