Skip to content

Commit 43477bf

Browse files
committed
feat!: add --default-token-lifetime
1 parent 7ea8a22 commit 43477bf

File tree

14 files changed

+165
-38
lines changed

14 files changed

+165
-38
lines changed

cli/testdata/coder_server_--help.golden

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

28+
--default-token-lifetime duration, $CODER_DEFAULT_TOKEN_LIFETIME (default: 168h0m0s)
29+
The default lifetime duration for API tokens. This value is used when
30+
creating a token without specifying a duration, such as when
31+
authenticating the CLI or an IDE plugin.
32+
2833
--disable-owner-workspace-access bool, $CODER_DISABLE_OWNER_WORKSPACE_ACCESS
2934
Remove the permission for the 'owner' role to have workspace execution
3035
on all workspaces. This prevents the 'owner' from ssh, apps, and

cli/testdata/server-config.yaml.golden

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,11 @@ experiments: []
423423
# performed once per day.
424424
# (default: false, type: bool)
425425
updateCheck: false
426+
# The default lifetime duration for API tokens. This value is used when creating a
427+
# token without specifying a duration, such as when authenticating the CLI or an
428+
# IDE plugin.
429+
# (default: 168h0m0s, type: duration)
430+
defaultTokenLifetime: 168h0m0s
426431
# Expose the swagger endpoint via /swagger.
427432
# (default: <unset>, type: bool)
428433
enableSwagger: false

coderd/apidoc/docs.go

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

coderd/apidoc/swagger.json

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

coderd/apikey.go

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"github.com/coder/coder/v2/codersdk"
2424
)
2525

26-
// Creates a new token API key that effectively doesn't expire.
26+
// Creates a new token API key with the given scope and lifetime.
2727
//
2828
// @Summary Create token API key
2929
// @ID create-token-api-key
@@ -60,36 +60,34 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
6060
scope = database.APIKeyScope(createToken.Scope)
6161
}
6262

63-
// default lifetime is 30 days
64-
lifeTime := 30 * 24 * time.Hour
65-
if createToken.Lifetime != 0 {
66-
lifeTime = createToken.Lifetime
67-
}
68-
6963
tokenName := namesgenerator.GetRandomName(1)
7064

7165
if len(createToken.TokenName) != 0 {
7266
tokenName = createToken.TokenName
7367
}
7468

75-
err := api.validateAPIKeyLifetime(lifeTime)
76-
if err != nil {
77-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
78-
Message: "Failed to validate create API key request.",
79-
Detail: err.Error(),
80-
})
81-
return
82-
}
83-
84-
cookie, key, err := api.createAPIKey(ctx, apikey.CreateParams{
69+
params := apikey.CreateParams{
8570
UserID: user.ID,
8671
LoginType: database.LoginTypeToken,
87-
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
88-
ExpiresAt: dbtime.Now().Add(lifeTime),
72+
DefaultLifetime: api.DeploymentValues.Sessions.DefaultTokenDuration.Value(),
8973
Scope: scope,
90-
LifetimeSeconds: int64(lifeTime.Seconds()),
9174
TokenName: tokenName,
92-
})
75+
}
76+
77+
if createToken.Lifetime != 0 {
78+
err := api.validateAPIKeyLifetime(createToken.Lifetime)
79+
if err != nil {
80+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
81+
Message: "Failed to validate create API key request.",
82+
Detail: err.Error(),
83+
})
84+
return
85+
}
86+
params.ExpiresAt = dbtime.Now().Add(createToken.Lifetime)
87+
params.LifetimeSeconds = int64(createToken.Lifetime.Seconds())
88+
}
89+
90+
cookie, key, err := api.createAPIKey(ctx, params)
9391
if err != nil {
9492
if database.IsUniqueViolation(err, database.UniqueIndexAPIKeyName) {
9593
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
@@ -125,16 +123,11 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
125123
ctx := r.Context()
126124
user := httpmw.UserParam(r)
127125

128-
lifeTime := time.Hour * 24 * 7
129126
cookie, _, err := api.createAPIKey(ctx, apikey.CreateParams{
130127
UserID: user.ID,
131-
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
128+
DefaultLifetime: api.DeploymentValues.Sessions.DefaultTokenDuration.Value(),
132129
LoginType: database.LoginTypePassword,
133130
RemoteAddr: r.RemoteAddr,
134-
// All api generated keys will last 1 week. Browser login tokens have
135-
// a shorter life.
136-
ExpiresAt: dbtime.Now().Add(lifeTime),
137-
LifetimeSeconds: int64(lifeTime.Seconds()),
138131
})
139132
if err != nil {
140133
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{

coderd/apikey_test.go

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ func TestTokenCRUD(t *testing.T) {
4545
require.EqualValues(t, len(keys), 1)
4646
require.Contains(t, res.Key, keys[0].ID)
4747
// expires_at should default to 30 days
48-
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*29*24))
49-
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*31*24))
48+
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*6))
49+
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*8))
5050
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
5151

5252
// no update
@@ -115,8 +115,8 @@ func TestDefaultTokenDuration(t *testing.T) {
115115
require.NoError(t, err)
116116
keys, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
117117
require.NoError(t, err)
118-
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*29*24))
119-
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*31*24))
118+
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*6))
119+
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*8))
120120
}
121121

122122
func TestTokenUserSetMaxLifetime(t *testing.T) {
@@ -144,6 +144,27 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
144144
require.ErrorContains(t, err, "lifetime must be less")
145145
}
146146

147+
func TestTokenUserSetDefaultLifetime(t *testing.T) {
148+
t.Parallel()
149+
150+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
151+
defer cancel()
152+
dc := coderdtest.DeploymentValues(t)
153+
dc.Sessions.DefaultTokenDuration = serpent.Duration(time.Hour * 12)
154+
client := coderdtest.New(t, &coderdtest.Options{
155+
DeploymentValues: dc,
156+
})
157+
_ = coderdtest.CreateFirstUser(t, client)
158+
159+
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
160+
require.NoError(t, err)
161+
162+
tokens, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
163+
require.NoError(t, err)
164+
require.Len(t, tokens, 1)
165+
require.EqualValues(t, dc.Sessions.DefaultTokenDuration.Value().Seconds(), tokens[0].LifetimeSeconds)
166+
}
167+
147168
func TestSessionExpiry(t *testing.T) {
148169
t.Parallel()
149170

@@ -224,3 +245,66 @@ func TestAPIKey_Deleted(t *testing.T) {
224245
require.ErrorAs(t, err, &apiErr)
225246
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
226247
}
248+
249+
func TestAPIKey_Refresh(t *testing.T) {
250+
t.Parallel()
251+
252+
db, pubsub := dbtestutil.NewDB(t)
253+
client := coderdtest.New(t, &coderdtest.Options{
254+
Database: db,
255+
Pubsub: pubsub,
256+
})
257+
owner := coderdtest.CreateFirstUser(t, client)
258+
259+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
260+
defer cancel()
261+
262+
token, err := client.CreateAPIKey(ctx, owner.UserID.String())
263+
require.NoError(t, err)
264+
split := strings.Split(token.Key, "-")
265+
apiKey1, err := db.GetAPIKeyByID(ctx, split[0])
266+
require.NoError(t, err)
267+
require.Equal(t, int64(604800), apiKey1.LifetimeSeconds, "default should be 7 days")
268+
269+
err = db.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
270+
ID: apiKey1.ID,
271+
LastUsed: apiKey1.LastUsed,
272+
// Cross the no-refresh threshold
273+
ExpiresAt: apiKey1.ExpiresAt.Add(time.Hour * -2),
274+
IPAddress: apiKey1.IPAddress,
275+
})
276+
require.NoError(t, err, "update login key")
277+
278+
// Refresh the token
279+
client.SetSessionToken(token.Key)
280+
_, err = client.User(ctx, codersdk.Me)
281+
require.NoError(t, err)
282+
283+
apiKey2, err := client.APIKeyByID(ctx, owner.UserID.String(), split[0])
284+
require.NoError(t, err)
285+
require.True(t, apiKey2.ExpiresAt.After(apiKey1.ExpiresAt), "token should have a later expiry")
286+
}
287+
288+
func TestAPIKey_SetDefault(t *testing.T) {
289+
t.Parallel()
290+
291+
db, pubsub := dbtestutil.NewDB(t)
292+
dc := coderdtest.DeploymentValues(t)
293+
dc.Sessions.DefaultTokenDuration = serpent.Duration(time.Hour * 12)
294+
client := coderdtest.New(t, &coderdtest.Options{
295+
Database: db,
296+
Pubsub: pubsub,
297+
DeploymentValues: dc,
298+
})
299+
owner := coderdtest.CreateFirstUser(t, client)
300+
301+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
302+
defer cancel()
303+
304+
token, err := client.CreateAPIKey(ctx, owner.UserID.String())
305+
require.NoError(t, err)
306+
split := strings.Split(token.Key, "-")
307+
apiKey1, err := db.GetAPIKeyByID(ctx, split[0])
308+
require.NoError(t, err)
309+
require.EqualValues(t, dc.Sessions.DefaultTokenDuration.Value().Seconds(), apiKey1.LifetimeSeconds)
310+
}

coderd/provisionerdserver/provisionerdserver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1958,7 +1958,7 @@ func (s *server) regenerateSessionToken(ctx context.Context, user database.User,
19581958
UserID: user.ID,
19591959
LoginType: user.LoginType,
19601960
TokenName: workspaceSessionTokenName(workspace),
1961-
DefaultLifetime: s.DeploymentValues.Sessions.DefaultDuration.Value(),
1961+
DefaultLifetime: s.DeploymentValues.Sessions.DefaultTokenDuration.Value(),
19621962
LifetimeSeconds: int64(s.DeploymentValues.Sessions.MaximumTokenDuration.Value().Seconds()),
19631963
})
19641964
if err != nil {

coderd/users_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,8 @@ func TestPostLogin(t *testing.T) {
299299
apiKey, err := client.APIKeyByID(ctx, owner.UserID.String(), split[0])
300300
require.NoError(t, err, "fetch api key")
301301

302-
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*29)), "default tokens lasts more than 29 days")
303-
require.True(t, apiKey.ExpiresAt.Before(time.Now().Add(time.Hour*24*31)), "default tokens lasts less than 31 days")
302+
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*6)), "default tokens lasts more than 6 days")
303+
require.True(t, apiKey.ExpiresAt.Before(time.Now().Add(time.Hour*24*8)), "default tokens lasts less than 8 days")
304304
require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "token should have longer lifetime")
305305
})
306306
}

codersdk/deployment.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,9 +454,11 @@ type SessionLifetime struct {
454454
// creation is the lifetime of the api key.
455455
DisableExpiryRefresh serpent.Bool `json:"disable_expiry_refresh,omitempty" typescript:",notnull"`
456456

457-
// DefaultDuration is for api keys, not tokens.
457+
// DefaultDuration is only for browser, workspace app and oauth sessions.
458458
DefaultDuration serpent.Duration `json:"default_duration" typescript:",notnull"`
459459

460+
DefaultTokenDuration serpent.Duration `json:"default_token_lifetime,omitempty" typescript:",notnull"`
461+
460462
MaximumTokenDuration serpent.Duration `json:"max_token_lifetime,omitempty" typescript:",notnull"`
461463
}
462464

@@ -1998,6 +2000,16 @@ when required by your organization's security policy.`,
19982000
YAML: "maxTokenLifetime",
19992001
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
20002002
},
2003+
{
2004+
Name: "Default Token Lifetime",
2005+
Description: "The default lifetime duration for API tokens. This value is used when creating a token without specifying a duration, such as when authenticating the CLI or an IDE plugin.",
2006+
Flag: "default-token-lifetime",
2007+
Env: "CODER_DEFAULT_TOKEN_LIFETIME",
2008+
Default: (7 * 24 * time.Hour).String(),
2009+
Value: &c.Sessions.DefaultTokenDuration,
2010+
YAML: "defaultTokenLifetime",
2011+
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
2012+
},
20012013
{
20022014
Name: "Enable swagger endpoint",
20032015
Description: "Expose the swagger endpoint via /swagger.",

docs/reference/api/general.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy