Skip to content

Commit ec5ef51

Browse files
authored
feat: add session token injection to provisioner (#7461)
1 parent 00a2413 commit ec5ef51

File tree

13 files changed

+577
-218
lines changed

13 files changed

+577
-218
lines changed

coderd/apikey.go

+19-99
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,13 +80,14 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
8380
return
8481
}
8582

86-
cookie, key, err := api.createAPIKey(ctx, createAPIKeyParams{
87-
UserID: user.ID,
88-
LoginType: database.LoginTypeToken,
89-
ExpiresAt: database.Now().Add(lifeTime),
90-
Scope: scope,
91-
LifetimeSeconds: int64(lifeTime.Seconds()),
92-
TokenName: tokenName,
83+
cookie, key, err := api.createAPIKey(ctx, apikey.CreateParams{
84+
UserID: user.ID,
85+
LoginType: database.LoginTypeToken,
86+
DeploymentValues: api.DeploymentValues,
87+
ExpiresAt: database.Now().Add(lifeTime),
88+
Scope: scope,
89+
LifetimeSeconds: int64(lifeTime.Seconds()),
90+
TokenName: tokenName,
9391
})
9492
if err != nil {
9593
if database.IsUniqueViolation(err, database.UniqueIndexApiKeyName) {
@@ -127,10 +125,11 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
127125
user := httpmw.UserParam(r)
128126

129127
lifeTime := time.Hour * 24 * 7
130-
cookie, _, err := api.createAPIKey(ctx, createAPIKeyParams{
131-
UserID: user.ID,
132-
LoginType: database.LoginTypePassword,
133-
RemoteAddr: r.RemoteAddr,
128+
cookie, _, err := api.createAPIKey(ctx, apikey.CreateParams{
129+
UserID: user.ID,
130+
DeploymentValues: api.DeploymentValues,
131+
LoginType: database.LoginTypePassword,
132+
RemoteAddr: r.RemoteAddr,
134133
// All api generated keys will last 1 week. Browser login tokens have
135134
// a shorter life.
136135
ExpiresAt: database.Now().Add(lifeTime),
@@ -359,33 +358,6 @@ func (api *API) tokenConfig(rw http.ResponseWriter, r *http.Request) {
359358
)
360359
}
361360

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-
377-
type createAPIKeyParams struct {
378-
UserID uuid.UUID
379-
RemoteAddr string
380-
LoginType database.LoginType
381-
382-
// Optional.
383-
ExpiresAt time.Time
384-
LifetimeSeconds int64
385-
Scope database.APIKeyScope
386-
TokenName string
387-
}
388-
389361
func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
390362
if lifetime <= 0 {
391363
return xerrors.New("lifetime must be positive number greater than 0")
@@ -401,79 +373,27 @@ func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
401373
return nil
402374
}
403375

404-
func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, *database.APIKey, error) {
405-
keyID, keySecret, err := GenerateAPIKeyIDSecret()
376+
func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (*http.Cookie, *database.APIKey, error) {
377+
key, sessionToken, err := apikey.Generate(params)
406378
if err != nil {
407379
return nil, nil, xerrors.Errorf("generate API key: %w", err)
408380
}
409-
hashed := sha256.Sum256([]byte(keySecret))
410-
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
430381

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-
})
382+
newkey, err := api.Database.InsertAPIKey(ctx, key)
461383
if err != nil {
462384
return nil, nil, xerrors.Errorf("insert API key: %w", err)
463385
}
464386

465387
api.Telemetry.Report(&telemetry.Snapshot{
466-
APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(key)},
388+
APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(newkey)},
467389
})
468390

469-
// This format is consumed by the APIKey middleware.
470-
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
471391
return &http.Cookie{
472392
Name: codersdk.SessionTokenCookie,
473393
Value: sessionToken,
474394
Path: "/",
475395
HttpOnly: true,
476396
SameSite: http.SameSiteLaxMode,
477397
Secure: api.SecureAuthCookie,
478-
}, &key, nil
398+
}, &newkey, nil
479399
}

coderd/apikey/apikey.go

+110
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) (database.InsertAPIKeyParams, string, 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+
token := fmt.Sprintf("%s-%s", keyID, keySecret)
74+
75+
return 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+
}, token, 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+
}

0 commit comments

Comments
 (0)