Skip to content

Commit 40a5c04

Browse files
authored
feat: add flag for token lifetime (#5385)
1 parent 760419a commit 40a5c04

File tree

9 files changed

+144
-58
lines changed

9 files changed

+144
-58
lines changed

cli/deployment/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,12 @@ func newConfig() *codersdk.DeploymentConfig {
424424
Flag: "update-check",
425425
Default: flag.Lookup("test.v") == nil && !buildinfo.IsDev(),
426426
},
427+
MaxTokenLifetime: &codersdk.DeploymentConfigField[time.Duration]{
428+
Name: "Max Token Lifetime",
429+
Usage: "The maximum lifetime duration for any user creating a token.",
430+
Flag: "max-token-lifetime",
431+
Default: 24 * 30 * time.Hour,
432+
},
427433
}
428434
}
429435

cli/testdata/coder_server_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ Flags:
6565
production.
6666
Consumes $CODER_EXPERIMENTAL
6767
-h, --help help for server
68+
--max-token-lifetime duration The maximum lifetime duration for any
69+
user creating a token.
70+
Consumes $CODER_MAX_TOKEN_LIFETIME
71+
(default 720h0m0s)
6872
--oauth2-github-allow-everyone Allow all logins, setting this option
6973
means allowed orgs and teams must be
7074
empty.

cli/tokens.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/spf13/cobra"
99
"golang.org/x/xerrors"
1010

11+
"github.com/coder/coder/cli/cliflag"
1112
"github.com/coder/coder/cli/cliui"
1213
"github.com/coder/coder/codersdk"
1314
)
@@ -46,6 +47,9 @@ func tokens() *cobra.Command {
4647
}
4748

4849
func createToken() *cobra.Command {
50+
var (
51+
tokenLifetime time.Duration
52+
)
4953
cmd := &cobra.Command{
5054
Use: "create",
5155
Short: "Create a tokens",
@@ -55,7 +59,9 @@ func createToken() *cobra.Command {
5559
return xerrors.Errorf("create codersdk client: %w", err)
5660
}
5761

58-
res, err := client.CreateToken(cmd.Context(), codersdk.Me, codersdk.CreateTokenRequest{})
62+
res, err := client.CreateToken(cmd.Context(), codersdk.Me, codersdk.CreateTokenRequest{
63+
Lifetime: tokenLifetime,
64+
})
5965
if err != nil {
6066
return xerrors.Errorf("create tokens: %w", err)
6167
}
@@ -74,6 +80,8 @@ func createToken() *cobra.Command {
7480
},
7581
}
7682

83+
cliflag.DurationVarP(cmd.Flags(), &tokenLifetime, "lifetime", "", "CODER_TOKEN_LIFETIME", 30*24*time.Hour, "Specify a duration for the lifetime of the token.")
84+
7785
return cmd
7886
}
7987

coderd/apikey.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,21 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
4444
scope = database.APIKeyScope(createToken.Scope)
4545
}
4646

47-
// tokens last 100 years
48-
lifeTime := time.Hour * 876000
47+
// default lifetime is 30 days
48+
lifeTime := 30 * 24 * time.Hour
49+
if createToken.Lifetime != 0 {
50+
lifeTime = createToken.Lifetime
51+
}
52+
53+
err := api.validateAPIKeyLifetime(lifeTime)
54+
if err != nil {
55+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
56+
Message: "Failed to validate create API key request.",
57+
Detail: err.Error(),
58+
})
59+
return
60+
}
61+
4962
cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
5063
UserID: user.ID,
5164
LoginType: database.LoginTypeToken,
@@ -65,7 +78,6 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
6578
}
6679

6780
// Creates a new session key, used for logging in via the CLI.
68-
// DEPRECATED: use postToken instead.
6981
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
7082
ctx := r.Context()
7183
user := httpmw.UserParam(r)
@@ -214,6 +226,18 @@ type createAPIKeyParams struct {
214226
Scope database.APIKeyScope
215227
}
216228

229+
func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
230+
if lifetime <= 0 {
231+
return xerrors.New("lifetime must be positive number greater than 0")
232+
}
233+
234+
if lifetime > api.DeploymentConfig.MaxTokenLifetime.Value {
235+
return xerrors.Errorf("lifetime must be less than %s", api.DeploymentConfig.MaxTokenLifetime.Value)
236+
}
237+
238+
return nil
239+
}
240+
217241
func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, error) {
218242
keyID, keySecret, err := generateAPIKeyIDSecret()
219243
if err != nil {

coderd/apikey_test.go

Lines changed: 90 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,63 +12,101 @@ import (
1212
"github.com/coder/coder/testutil"
1313
)
1414

15-
func TestTokens(t *testing.T) {
15+
func TestTokenCRUD(t *testing.T) {
1616
t.Parallel()
1717

18-
t.Run("CRUD", func(t *testing.T) {
19-
t.Parallel()
20-
21-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
22-
defer cancel()
23-
client := coderdtest.New(t, nil)
24-
_ = coderdtest.CreateFirstUser(t, client)
25-
keys, err := client.GetTokens(ctx, codersdk.Me)
26-
require.NoError(t, err)
27-
require.Empty(t, keys)
28-
29-
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
30-
require.NoError(t, err)
31-
require.Greater(t, len(res.Key), 2)
32-
33-
keys, err = client.GetTokens(ctx, codersdk.Me)
34-
require.NoError(t, err)
35-
require.EqualValues(t, len(keys), 1)
36-
require.Contains(t, res.Key, keys[0].ID)
37-
// expires_at must be greater than 50 years
38-
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
39-
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
40-
41-
// no update
42-
43-
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
44-
require.NoError(t, err)
45-
keys, err = client.GetTokens(ctx, codersdk.Me)
46-
require.NoError(t, err)
47-
require.Empty(t, keys)
18+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
19+
defer cancel()
20+
client := coderdtest.New(t, nil)
21+
_ = coderdtest.CreateFirstUser(t, client)
22+
keys, err := client.GetTokens(ctx, codersdk.Me)
23+
require.NoError(t, err)
24+
require.Empty(t, keys)
25+
26+
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
27+
require.NoError(t, err)
28+
require.Greater(t, len(res.Key), 2)
29+
30+
keys, err = client.GetTokens(ctx, codersdk.Me)
31+
require.NoError(t, err)
32+
require.EqualValues(t, len(keys), 1)
33+
require.Contains(t, res.Key, keys[0].ID)
34+
// expires_at should default to 30 days
35+
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*29*24))
36+
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*31*24))
37+
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
38+
39+
// no update
40+
41+
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
42+
require.NoError(t, err)
43+
keys, err = client.GetTokens(ctx, codersdk.Me)
44+
require.NoError(t, err)
45+
require.Empty(t, keys)
46+
}
47+
48+
func TestTokenScoped(t *testing.T) {
49+
t.Parallel()
50+
51+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
52+
defer cancel()
53+
client := coderdtest.New(t, nil)
54+
_ = coderdtest.CreateFirstUser(t, client)
55+
56+
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
57+
Scope: codersdk.APIKeyScopeApplicationConnect,
4858
})
59+
require.NoError(t, err)
60+
require.Greater(t, len(res.Key), 2)
61+
62+
keys, err := client.GetTokens(ctx, codersdk.Me)
63+
require.NoError(t, err)
64+
require.EqualValues(t, len(keys), 1)
65+
require.Contains(t, res.Key, keys[0].ID)
66+
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
67+
}
68+
69+
func TestTokenDuration(t *testing.T) {
70+
t.Parallel()
71+
72+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
73+
defer cancel()
74+
client := coderdtest.New(t, nil)
75+
_ = coderdtest.CreateFirstUser(t, client)
76+
77+
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
78+
Lifetime: time.Hour * 24 * 7,
79+
})
80+
require.NoError(t, err)
81+
keys, err := client.GetTokens(ctx, codersdk.Me)
82+
require.NoError(t, err)
83+
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*6*24))
84+
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*8*24))
85+
}
86+
87+
func TestTokenMaxLifetime(t *testing.T) {
88+
t.Parallel()
89+
90+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
91+
defer cancel()
92+
dc := coderdtest.DeploymentConfig(t)
93+
dc.MaxTokenLifetime.Value = time.Hour * 24 * 7
94+
client := coderdtest.New(t, &coderdtest.Options{
95+
DeploymentConfig: dc,
96+
})
97+
_ = coderdtest.CreateFirstUser(t, client)
98+
99+
// success
100+
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
101+
Lifetime: time.Hour * 24 * 6,
102+
})
103+
require.NoError(t, err)
49104

50-
t.Run("Scoped", func(t *testing.T) {
51-
t.Parallel()
52-
53-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
54-
defer cancel()
55-
client := coderdtest.New(t, nil)
56-
_ = coderdtest.CreateFirstUser(t, client)
57-
58-
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
59-
Scope: codersdk.APIKeyScopeApplicationConnect,
60-
})
61-
require.NoError(t, err)
62-
require.Greater(t, len(res.Key), 2)
63-
64-
keys, err := client.GetTokens(ctx, codersdk.Me)
65-
require.NoError(t, err)
66-
require.EqualValues(t, len(keys), 1)
67-
require.Contains(t, res.Key, keys[0].ID)
68-
// expires_at must be greater than 50 years
69-
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
70-
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
105+
// fail
106+
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
107+
Lifetime: time.Hour * 24 * 8,
71108
})
109+
require.ErrorContains(t, err, "lifetime must be less")
72110
}
73111

74112
func TestAPIKey(t *testing.T) {

coderd/users_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,8 @@ func TestPostLogin(t *testing.T) {
313313
apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0])
314314
require.NoError(t, err, "fetch api key")
315315

316-
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*438300)), "tokens lasts more than 50 years")
316+
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*29)), "default tokens lasts more than 29 days")
317+
require.True(t, apiKey.ExpiresAt.Before(time.Now().Add(time.Hour*24*31)), "default tokens lasts less than 31 days")
317318
require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "token should have longer lifetime")
318319
})
319320
}

codersdk/apikey.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ const (
4040
)
4141

4242
type CreateTokenRequest struct {
43-
Scope APIKeyScope `json:"scope"`
43+
Lifetime time.Duration `json:"lifetime"`
44+
Scope APIKeyScope `json:"scope"`
4445
}
4546

4647
// GenerateAPIKeyResponse contains an API key for a user.

codersdk/deploymentconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type DeploymentConfig struct {
4242
APIRateLimit *DeploymentConfigField[int] `json:"api_rate_limit" typescript:",notnull"`
4343
Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"`
4444
UpdateCheck *DeploymentConfigField[bool] `json:"update_check" typescript:",notnull"`
45+
MaxTokenLifetime *DeploymentConfigField[time.Duration] `json:"max_token_lifetime" typescript:",notnull"`
4546
}
4647

4748
type DERP struct {

site/src/api/typesGenerated.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ export interface CreateTestAuditLogRequest {
206206

207207
// From codersdk/apikey.go
208208
export interface CreateTokenRequest {
209+
// This is likely an enum in an external package ("time.Duration")
210+
readonly lifetime: number
209211
readonly scope: APIKeyScope
210212
}
211213

@@ -303,6 +305,7 @@ export interface DeploymentConfig {
303305
readonly api_rate_limit: DeploymentConfigField<number>
304306
readonly experimental: DeploymentConfigField<boolean>
305307
readonly update_check: DeploymentConfigField<boolean>
308+
readonly max_token_lifetime: DeploymentConfigField<number>
306309
}
307310

308311
// From codersdk/deploymentconfig.go

0 commit comments

Comments
 (0)