Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 913c0f5

Browse files
authored
feat: Longer lived api keys for cli (#1935)
* feat: Longer lived api keys for cli * feat: Refresh tokens based on their lifetime set in the db * test: Add unit test for refreshing
1 parent bb400a4 commit 913c0f5

File tree

10 files changed

+147
-8
lines changed

10 files changed

+147
-8
lines changed

coderd/database/databasefake/databasefake.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,9 +1128,14 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
11281128
q.mutex.Lock()
11291129
defer q.mutex.Unlock()
11301130

1131+
if arg.LifetimeSeconds == 0 {
1132+
arg.LifetimeSeconds = 86400
1133+
}
1134+
11311135
//nolint:gosimple
11321136
key := database.APIKey{
11331137
ID: arg.ID,
1138+
LifetimeSeconds: arg.LifetimeSeconds,
11341139
HashedSecret: arg.HashedSecret,
11351140
UserID: arg.UserID,
11361141
ExpiresAt: arg.ExpiresAt,

coderd/database/dump.sql

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE api_keys DROP COLUMN lifetime_seconds;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Default lifetime is 24hours.
2+
ALTER TABLE api_keys ADD COLUMN lifetime_seconds bigint default 86400 NOT NULL;

coderd/database/models.go

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

coderd/database/queries.sql.go

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/apikeys.sql

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ LIMIT
1212
INSERT INTO
1313
api_keys (
1414
id,
15+
lifetime_seconds,
1516
hashed_secret,
1617
user_id,
1718
last_used,
@@ -25,7 +26,13 @@ INSERT INTO
2526
oauth_expiry
2627
)
2728
VALUES
28-
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *;
29+
(@id,
30+
-- If the lifetime is set to 0, default to 24hrs
31+
CASE @lifetime_seconds::bigint
32+
WHEN 0 THEN 86400
33+
ELSE @lifetime_seconds::bigint
34+
END
35+
, @hashed_secret, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type, @oauth_access_token, @oauth_refresh_token, @oauth_id_token, @oauth_expiry) RETURNING *;
2936

3037
-- name: UpdateAPIKeyByID :exec
3138
UPDATE

coderd/httpmw/apikey.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h
166166
}
167167
// Only update the ExpiresAt once an hour to prevent database spam.
168168
// We extend the ExpiresAt to reduce re-authentication.
169-
apiKeyLifetime := 24 * time.Hour
169+
apiKeyLifetime := time.Duration(key.LifetimeSeconds) * time.Second
170170
if key.ExpiresAt.Sub(now) <= apiKeyLifetime-time.Hour {
171171
key.ExpiresAt = now.Add(apiKeyLifetime)
172172
changed = true

coderd/users.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -660,9 +660,14 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
660660
return
661661
}
662662

663+
lifeTime := time.Hour * 24 * 7
663664
sessionToken, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
664665
UserID: user.ID,
665666
LoginType: database.LoginTypePassword,
667+
// All api generated keys will last 1 week. Browser login tokens have
668+
// a shorter life.
669+
ExpiresAt: database.Now().Add(lifeTime),
670+
LifetimeSeconds: int64(lifeTime.Seconds()),
666671
})
667672
if !created {
668673
return
@@ -723,10 +728,21 @@ func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat
723728
}
724729
hashed := sha256.Sum256([]byte(keySecret))
725730

731+
// Default expires at to now+lifetime, or just 24hrs if not set
732+
if params.ExpiresAt.IsZero() {
733+
if params.LifetimeSeconds != 0 {
734+
params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
735+
} else {
736+
params.ExpiresAt = database.Now().Add(24 * time.Hour)
737+
}
738+
}
739+
726740
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
727-
ID: keyID,
728-
UserID: params.UserID,
729-
ExpiresAt: database.Now().Add(24 * time.Hour),
741+
ID: keyID,
742+
UserID: params.UserID,
743+
LifetimeSeconds: params.LifetimeSeconds,
744+
// Make sure in UTC time for common time zone
745+
ExpiresAt: params.ExpiresAt.UTC(),
730746
CreatedAt: database.Now(),
731747
UpdatedAt: database.Now(),
732748
HashedSecret: hashed[:],

coderd/users_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import (
88
"sort"
99
"strings"
1010
"testing"
11+
"time"
1112

1213
"github.com/google/uuid"
1314
"github.com/stretchr/testify/require"
1415

1516
"github.com/coder/coder/coderd/coderdtest"
17+
"github.com/coder/coder/coderd/database"
1618
"github.com/coder/coder/coderd/database/databasefake"
1719
"github.com/coder/coder/coderd/httpmw"
1820
"github.com/coder/coder/coderd/rbac"
@@ -130,6 +132,99 @@ func TestPostLogin(t *testing.T) {
130132
})
131133
require.NoError(t, err)
132134
})
135+
136+
t.Run("Lifetime&Expire", func(t *testing.T) {
137+
t.Parallel()
138+
var (
139+
ctx = context.Background()
140+
)
141+
client, api := coderdtest.NewWithAPI(t, nil)
142+
admin := coderdtest.CreateFirstUser(t, client)
143+
144+
split := strings.Split(client.SessionToken, "-")
145+
loginKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
146+
require.NoError(t, err, "fetch login key")
147+
require.Equal(t, int64(86400), loginKey.LifetimeSeconds, "default should be 86400")
148+
149+
// Generated tokens have a longer life
150+
token, err := client.CreateAPIKey(ctx, admin.UserID.String())
151+
require.NoError(t, err, "make new api key")
152+
split = strings.Split(token.Key, "-")
153+
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
154+
require.NoError(t, err, "fetch api key")
155+
156+
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*6)), "api key lasts more than 6 days")
157+
require.True(t, apiKey.ExpiresAt.After(loginKey.ExpiresAt.Add(time.Hour)), "api key should be longer expires")
158+
require.Greater(t, apiKey.LifetimeSeconds, loginKey.LifetimeSeconds, "api key should have longer lifetime")
159+
})
160+
161+
t.Run("APIKeyExtend", func(t *testing.T) {
162+
t.Parallel()
163+
var (
164+
ctx = context.Background()
165+
)
166+
client, api := coderdtest.NewWithAPI(t, nil)
167+
admin := coderdtest.CreateFirstUser(t, client)
168+
169+
token, err := client.CreateAPIKey(ctx, admin.UserID.String())
170+
require.NoError(t, err, "make new api key")
171+
client.SessionToken = token.Key
172+
split := strings.Split(token.Key, "-")
173+
174+
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
175+
require.NoError(t, err, "fetch api key")
176+
177+
err = api.Database.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
178+
ID: apiKey.ID,
179+
LastUsed: apiKey.LastUsed,
180+
// This should cause a refresh
181+
ExpiresAt: apiKey.ExpiresAt.Add(time.Hour * -2),
182+
OAuthAccessToken: apiKey.OAuthAccessToken,
183+
OAuthRefreshToken: apiKey.OAuthRefreshToken,
184+
OAuthExpiry: apiKey.OAuthExpiry,
185+
})
186+
require.NoError(t, err, "update api key")
187+
188+
_, err = client.User(ctx, codersdk.Me)
189+
require.NoError(t, err, "fetch user")
190+
191+
apiKey, err = api.Database.GetAPIKeyByID(ctx, split[0])
192+
require.NoError(t, err, "fetch refreshed api key")
193+
// 1 minute tolerance
194+
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*7).Add(time.Minute*-1)), "api key lasts 7 days")
195+
})
196+
197+
t.Run("LoginKeyExtend", func(t *testing.T) {
198+
t.Parallel()
199+
var (
200+
ctx = context.Background()
201+
)
202+
client, api := coderdtest.NewWithAPI(t, nil)
203+
_ = coderdtest.CreateFirstUser(t, client)
204+
split := strings.Split(client.SessionToken, "-")
205+
206+
apiKey, err := api.Database.GetAPIKeyByID(ctx, split[0])
207+
require.NoError(t, err, "fetch login key")
208+
209+
err = api.Database.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
210+
ID: apiKey.ID,
211+
LastUsed: apiKey.LastUsed,
212+
// This should cause a refresh
213+
ExpiresAt: apiKey.ExpiresAt.Add(time.Hour * -2),
214+
OAuthAccessToken: apiKey.OAuthAccessToken,
215+
OAuthRefreshToken: apiKey.OAuthRefreshToken,
216+
OAuthExpiry: apiKey.OAuthExpiry,
217+
})
218+
require.NoError(t, err, "update login key")
219+
220+
_, err = client.User(ctx, codersdk.Me)
221+
require.NoError(t, err, "fetch user")
222+
223+
apiKey, err = api.Database.GetAPIKeyByID(ctx, split[0])
224+
require.NoError(t, err, "fetch refreshed login key")
225+
// 1 minute tolerance
226+
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24).Add(time.Minute*-1)), "login key lasts 24 hrs")
227+
})
133228
}
134229

135230
func TestPostLogout(t *testing.T) {

0 commit comments

Comments
 (0)