Skip to content

Commit 3ff4539

Browse files
committedMay 16, 2023
move api key generation to its own package
1 parent b49d18b commit 3ff4539

File tree

5 files changed

+147
-136
lines changed

5 files changed

+147
-136
lines changed
 

‎coderd/apikey.go

Lines changed: 9 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,24 @@ package coderd
22

33
import (
44
"context"
5-
"crypto/sha256"
65
"fmt"
7-
"net"
86
"net/http"
97
"strconv"
108
"time"
119

1210
"github.com/go-chi/chi/v5"
1311
"github.com/google/uuid"
1412
"github.com/moby/moby/pkg/namesgenerator"
15-
"github.com/tabbed/pqtype"
1613
"golang.org/x/xerrors"
1714

15+
"github.com/coder/coder/coderd/apikey"
1816
"github.com/coder/coder/coderd/audit"
1917
"github.com/coder/coder/coderd/database"
2018
"github.com/coder/coder/coderd/httpapi"
2119
"github.com/coder/coder/coderd/httpmw"
2220
"github.com/coder/coder/coderd/rbac"
2321
"github.com/coder/coder/coderd/telemetry"
2422
"github.com/coder/coder/codersdk"
25-
"github.com/coder/coder/cryptorand"
2623
)
2724

2825
// Creates a new token API key that effectively doesn't expire.
@@ -83,7 +80,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
8380
return
8481
}
8582

86-
cookie, key, err := api.createAPIKey(ctx, createAPIKeyParams{
83+
cookie, key, err := api.createAPIKey(ctx, apikey.CreateParams{
8784
UserID: user.ID,
8885
LoginType: database.LoginTypeToken,
8986
ExpiresAt: database.Now().Add(lifeTime),
@@ -127,7 +124,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
127124
user := httpmw.UserParam(r)
128125

129126
lifeTime := time.Hour * 24 * 7
130-
cookie, _, err := api.createAPIKey(ctx, createAPIKeyParams{
127+
cookie, _, err := api.createAPIKey(ctx, apikey.CreateParams{
131128
UserID: user.ID,
132129
LoginType: database.LoginTypePassword,
133130
RemoteAddr: r.RemoteAddr,
@@ -359,21 +356,6 @@ func (api *API) tokenConfig(rw http.ResponseWriter, r *http.Request) {
359356
)
360357
}
361358

362-
// Generates a new ID and secret for an API key.
363-
func GenerateAPIKeyIDSecret() (id string, secret string, err error) {
364-
// Length of an API Key ID.
365-
id, err = cryptorand.String(10)
366-
if err != nil {
367-
return "", "", err
368-
}
369-
// Length of an API Key secret.
370-
secret, err = cryptorand.String(22)
371-
if err != nil {
372-
return "", "", err
373-
}
374-
return id, secret, nil
375-
}
376-
377359
type createAPIKeyParams struct {
378360
UserID uuid.UUID
379361
RemoteAddr string
@@ -401,79 +383,27 @@ func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
401383
return nil
402384
}
403385

404-
func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, *database.APIKey, error) {
405-
keyID, keySecret, err := GenerateAPIKeyIDSecret()
386+
func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (*http.Cookie, *database.APIKey, error) {
387+
secret, key, err := apikey.Generate(params)
406388
if err != nil {
407389
return nil, nil, xerrors.Errorf("generate API key: %w", err)
408390
}
409-
hashed := sha256.Sum256([]byte(keySecret))
410391

411-
// Default expires at to now+lifetime, or use the configured value if not
412-
// set.
413-
if params.ExpiresAt.IsZero() {
414-
if params.LifetimeSeconds != 0 {
415-
params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
416-
} else {
417-
params.ExpiresAt = database.Now().Add(api.DeploymentValues.SessionDuration.Value())
418-
params.LifetimeSeconds = int64(api.DeploymentValues.SessionDuration.Value().Seconds())
419-
}
420-
}
421-
if params.LifetimeSeconds == 0 {
422-
params.LifetimeSeconds = int64(time.Until(params.ExpiresAt).Seconds())
423-
}
424-
425-
ip := net.ParseIP(params.RemoteAddr)
426-
if ip == nil {
427-
ip = net.IPv4(0, 0, 0, 0)
428-
}
429-
bitlen := len(ip) * 8
430-
431-
scope := database.APIKeyScopeAll
432-
if params.Scope != "" {
433-
scope = params.Scope
434-
}
435-
switch scope {
436-
case database.APIKeyScopeAll, database.APIKeyScopeApplicationConnect:
437-
default:
438-
return nil, nil, xerrors.Errorf("invalid API key scope: %q", scope)
439-
}
440-
441-
key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{
442-
ID: keyID,
443-
UserID: params.UserID,
444-
LifetimeSeconds: params.LifetimeSeconds,
445-
IPAddress: pqtype.Inet{
446-
IPNet: net.IPNet{
447-
IP: ip,
448-
Mask: net.CIDRMask(bitlen, bitlen),
449-
},
450-
Valid: true,
451-
},
452-
// Make sure in UTC time for common time zone
453-
ExpiresAt: params.ExpiresAt.UTC(),
454-
CreatedAt: database.Now(),
455-
UpdatedAt: database.Now(),
456-
HashedSecret: hashed[:],
457-
LoginType: params.LoginType,
458-
Scope: scope,
459-
TokenName: params.TokenName,
460-
})
392+
newkey, err := api.Database.InsertAPIKey(ctx, key)
461393
if err != nil {
462394
return nil, nil, xerrors.Errorf("insert API key: %w", err)
463395
}
464396

465397
api.Telemetry.Report(&telemetry.Snapshot{
466-
APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(key)},
398+
APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(newkey)},
467399
})
468400

469-
// This format is consumed by the APIKey middleware.
470-
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
471401
return &http.Cookie{
472402
Name: codersdk.SessionTokenCookie,
473-
Value: sessionToken,
403+
Value: secret,
474404
Path: "/",
475405
HttpOnly: true,
476406
SameSite: http.SameSiteLaxMode,
477407
Secure: api.SecureAuthCookie,
478-
}, &key, nil
408+
}, &newkey, nil
479409
}

‎coderd/apikey/apikey.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package apikey
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"net"
7+
"time"
8+
9+
"github.com/google/uuid"
10+
"github.com/tabbed/pqtype"
11+
"golang.org/x/xerrors"
12+
13+
"github.com/coder/coder/coderd/database"
14+
"github.com/coder/coder/codersdk"
15+
"github.com/coder/coder/cryptorand"
16+
)
17+
18+
type CreateParams struct {
19+
UserID uuid.UUID
20+
LoginType database.LoginType
21+
DeploymentValues *codersdk.DeploymentValues
22+
23+
// Optional.
24+
ExpiresAt time.Time
25+
LifetimeSeconds int64
26+
Scope database.APIKeyScope
27+
TokenName string
28+
RemoteAddr string
29+
}
30+
31+
// Generate generates an API key, returning the key as a string as well as the
32+
// database representation. It is the responsibility of the caller to insert it
33+
// into the database.
34+
func Generate(params CreateParams) (string, database.InsertAPIKeyParams, error) {
35+
keyID, keySecret, err := generateKey()
36+
if err != nil {
37+
return "", database.InsertAPIKeyParams{}, xerrors.Errorf("generate API key: %w", err)
38+
}
39+
40+
hashed := sha256.Sum256([]byte(keySecret))
41+
42+
// Default expires at to now+lifetime, or use the configured value if not
43+
// set.
44+
if params.ExpiresAt.IsZero() {
45+
if params.LifetimeSeconds != 0 {
46+
params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
47+
} else {
48+
params.ExpiresAt = database.Now().Add(params.DeploymentValues.SessionDuration.Value())
49+
params.LifetimeSeconds = int64(params.DeploymentValues.SessionDuration.Value().Seconds())
50+
}
51+
}
52+
if params.LifetimeSeconds == 0 {
53+
params.LifetimeSeconds = int64(time.Until(params.ExpiresAt).Seconds())
54+
}
55+
56+
ip := net.ParseIP(params.RemoteAddr)
57+
if ip == nil {
58+
ip = net.IPv4(0, 0, 0, 0)
59+
}
60+
61+
bitlen := len(ip) * 8
62+
63+
scope := database.APIKeyScopeAll
64+
if params.Scope != "" {
65+
scope = params.Scope
66+
}
67+
switch scope {
68+
case database.APIKeyScopeAll, database.APIKeyScopeApplicationConnect:
69+
default:
70+
return "", database.InsertAPIKeyParams{}, xerrors.Errorf("invalid API key scope: %q", scope)
71+
}
72+
73+
keyStr := fmt.Sprintf("%s-%s", keyID, keySecret)
74+
75+
return keyStr, database.InsertAPIKeyParams{
76+
ID: keyID,
77+
UserID: params.UserID,
78+
LifetimeSeconds: params.LifetimeSeconds,
79+
IPAddress: pqtype.Inet{
80+
IPNet: net.IPNet{
81+
IP: ip,
82+
Mask: net.CIDRMask(bitlen, bitlen),
83+
},
84+
Valid: true,
85+
},
86+
// Make sure in UTC time for common time zone
87+
ExpiresAt: params.ExpiresAt.UTC(),
88+
CreatedAt: database.Now(),
89+
UpdatedAt: database.Now(),
90+
HashedSecret: hashed[:],
91+
LoginType: params.LoginType,
92+
Scope: scope,
93+
TokenName: params.TokenName,
94+
}, nil
95+
}
96+
97+
// generateKey a new ID and secret for an API key.
98+
func generateKey() (id string, secret string, err error) {
99+
// Length of an API Key ID.
100+
id, err = cryptorand.String(10)
101+
if err != nil {
102+
return "", "", err
103+
}
104+
// Length of an API Key secret.
105+
secret, err = cryptorand.String(22)
106+
if err != nil {
107+
return "", "", err
108+
}
109+
return id, secret, nil
110+
}

‎coderd/provisionerdserver/provisionerdserver.go

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ package provisionerdserver
22

33
import (
44
"context"
5-
"crypto/sha256"
65
"database/sql"
76
"encoding/json"
87
"errors"
98
"fmt"
10-
"net"
119
"net/http"
1210
"net/url"
1311
"reflect"
@@ -29,6 +27,7 @@ import (
2927

3028
"cdr.dev/slog"
3129

30+
"github.com/coder/coder/coderd/apikey"
3231
"github.com/coder/coder/coderd/audit"
3332
"github.com/coder/coder/coderd/database"
3433
"github.com/coder/coder/coderd/database/dbauthz"
@@ -39,7 +38,6 @@ import (
3938
"github.com/coder/coder/coderd/telemetry"
4039
"github.com/coder/coder/coderd/tracing"
4140
"github.com/coder/coder/codersdk"
42-
"github.com/coder/coder/cryptorand"
4341
"github.com/coder/coder/provisioner"
4442
"github.com/coder/coder/provisionerd/proto"
4543
"github.com/coder/coder/provisionersdk"
@@ -1424,28 +1422,16 @@ func workspaceSessionTokenName(workspace database.Workspace) string {
14241422
return fmt.Sprintf("%s_%s_session_token", workspace.OwnerID, workspace.ID)
14251423
}
14261424

1427-
// Generates a new ID and secret for an API key.
1428-
// TODO put API key logic in separate package.
1429-
func GenerateAPIKeyIDSecret() (id string, secret string, err error) {
1430-
// Length of an API Key ID.
1431-
id, err = cryptorand.String(10)
1432-
if err != nil {
1433-
return "", "", err
1434-
}
1435-
// Length of an API Key secret.
1436-
secret, err = cryptorand.String(22)
1437-
if err != nil {
1438-
return "", "", err
1439-
}
1440-
return id, secret, nil
1441-
}
1442-
14431425
func (server *Server) regenerateSessionToken(ctx context.Context, user database.User, workspace database.Workspace) (string, error) {
1444-
id, secret, err := GenerateAPIKeyIDSecret()
1426+
secret, newkey, err := apikey.Generate(apikey.CreateParams{
1427+
UserID: user.ID,
1428+
LoginType: user.LoginType,
1429+
DeploymentValues: server.DeploymentValues,
1430+
TokenName: workspaceSessionTokenName(workspace),
1431+
})
14451432
if err != nil {
14461433
return "", xerrors.Errorf("generate API key: %w", err)
14471434
}
1448-
hashed := sha256.Sum256([]byte(secret))
14491435

14501436
err = server.Database.InTx(
14511437
func(tx database.Store) error {
@@ -1463,27 +1449,7 @@ func (server *Server) regenerateSessionToken(ctx context.Context, user database.
14631449
return xerrors.Errorf("get api key by name: %w", err)
14641450
}
14651451

1466-
ip := net.IPv4(0, 0, 0, 0)
1467-
bitlen := len(ip) * 8
1468-
_, err = tx.InsertAPIKey(ctx, database.InsertAPIKeyParams{
1469-
ID: id,
1470-
UserID: workspace.OwnerID,
1471-
LifetimeSeconds: int64(server.DeploymentValues.MaxTokenLifetime.Value().Seconds()),
1472-
ExpiresAt: database.Now().Add(server.DeploymentValues.MaxTokenLifetime.Value()).UTC(),
1473-
IPAddress: pqtype.Inet{
1474-
IPNet: net.IPNet{
1475-
IP: ip,
1476-
Mask: net.CIDRMask(bitlen, bitlen),
1477-
},
1478-
Valid: true,
1479-
},
1480-
CreatedAt: database.Now(),
1481-
UpdatedAt: database.Now(),
1482-
HashedSecret: hashed[:],
1483-
LoginType: user.LoginType,
1484-
Scope: database.APIKeyScopeAll,
1485-
TokenName: workspaceSessionTokenName(workspace),
1486-
})
1452+
_, err = tx.InsertAPIKey(ctx, newkey)
14871453
if err != nil {
14881454
return xerrors.Errorf("insert API key: %w", err)
14891455
}
@@ -1493,7 +1459,7 @@ func (server *Server) regenerateSessionToken(ctx context.Context, user database.
14931459
if err != nil {
14941460
return "", xerrors.Errorf("regenerate API key: %w", err)
14951461
}
1496-
return fmt.Sprintf("%s-%s", id, secret), nil
1462+
return secret, nil
14971463
}
14981464

14991465
// obtainOIDCAccessToken returns a valid OpenID Connect access token

0 commit comments

Comments
 (0)