Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

chore!: ensure consistent secret token generation and hashing#20388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Emyrk merged 23 commits intomainfromstevenmasley/refresh_hashing
Oct 23, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
23 commits
Select commitHold shift + click to select a range
d1cf114
chore: oauth2 refresh tokens to use sha256 hashed secrets
EmyrkOct 20, 2025
55ef6ed
chore: refactor RegistrationAccessToken from string to []byte
EmyrkOct 20, 2025
41e1c4a
make gen
EmyrkOct 20, 2025
feb3df7
reuse apikey generate for more places
EmyrkOct 21, 2025
2cfc267
bump migration
EmyrkOct 21, 2025
44744f7
fmt
EmyrkOct 21, 2025
8e853f8
fmt
EmyrkOct 21, 2025
914de0f
fix test compile
EmyrkOct 21, 2025
168c0a8
fixup! fix test compile
EmyrkOct 21, 2025
7d9b034
Linting
EmyrkOct 21, 2025
b954782
test fixes
EmyrkOct 21, 2025
91f6f3e
linting
EmyrkOct 21, 2025
d2c1d28
linting
EmyrkOct 21, 2025
9a5c796
linting
EmyrkOct 21, 2025
96dc3e1
linting
EmyrkOct 21, 2025
bc613c0
Merge remote-tracking branch 'origin/main' into stevenmasley/refresh_…
EmyrkOct 23, 2025
1df23e2
bump migration
EmyrkOct 23, 2025
499b121
fix hash check
EmyrkOct 23, 2025
fe29e85
switch from string to hash
EmyrkOct 23, 2025
6c1c64d
compile issue
EmyrkOct 23, 2025
db61128
pr feedback
EmyrkOct 23, 2025
6e7cdb1
Merge remote-tracking branch 'origin/main' into stevenmasley/refresh_…
EmyrkOct 23, 2025
b2a8fc3
migration bump
EmyrkOct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletioncoderd/apidoc/docs.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

5 changes: 4 additions & 1 deletioncoderd/apidoc/swagger.json
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

43 changes: 28 additions & 15 deletionscoderd/apikey/apikey.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ package apikey

import (
"crypto/sha256"
"crypto/subtle"
"fmt"
"net"
"time"
Expand DownExpand Up@@ -44,12 +45,17 @@ type CreateParams struct {
// database representation. It is the responsibility of the caller to insert it
// into the database.
func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error) {
keyID, keySecret, err := generateKey()
// Length of an API Key ID.
keyID, err := cryptorand.String(10)
if err != nil {
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("generate API key: %w", err)
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("generate API key ID: %w", err)
}

hashed := sha256.Sum256([]byte(keySecret))
// Length of an API Key secret.
keySecret, hashedSecret, err := GenerateSecret(22)
if err != nil {
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("generate API key secret: %w", err)
}

// Default expires at to now+lifetime, or use the configured value if not
// set.
Expand DownExpand Up@@ -120,25 +126,32 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
ExpiresAt: params.ExpiresAt.UTC(),
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
HashedSecret:hashed[:],
HashedSecret:hashedSecret,
LoginType: params.LoginType,
Scopes: scopes,
AllowList: params.AllowList,
TokenName: params.TokenName,
}, token, nil
}

// generateKey a new ID and secret for an API key.
func generateKey() (id string, secret string, err error) {
// Length of an API Key ID.
id, err = cryptorand.String(10)
if err != nil {
return "", "", err
}
// Length of an API Key secret.
secret, err = cryptorand.String(22)
func GenerateSecret(length int) (secret string, hashed []byte, err error) {
secret, err = cryptorand.String(length)
if err != nil {
return "","", err
return "",nil, err
}
return id, secret, nil
hash := HashSecret(secret)
return secret, hash, nil
}

// ValidateHash compares a secret against an expected hashed secret.
func ValidateHash(hashedSecret []byte, secret string) bool {
hash := HashSecret(secret)
return subtle.ConstantTimeCompare(hashedSecret, hash) == 1
}

// HashSecret is the single function used to hash API key secrets.
// Use this to ensure a consistent hashing algorithm.
func HashSecret(secret string) []byte {
hash := sha256.Sum256([]byte(secret))
return hash[:]
}
19 changes: 16 additions & 3 deletionscoderd/apikey/apikey_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
package apikey_test

import (
"crypto/sha256"
"strings"
"testing"
"time"
Expand DownExpand Up@@ -126,8 +125,8 @@ func TestGenerate(t *testing.T) {
require.Equal(t,key.ID,keytokens[0])

// Assert that the hashed secret is correct.
hashed:=sha256.Sum256([]byte(keytokens[1]))
assert.ElementsMatch(t,hashed,key.HashedSecret)
equal:=apikey.ValidateHash(key.HashedSecret,keytokens[1])
require.True(t,equal,"valid secret")

assert.Equal(t,tc.params.UserID,key.UserID)
assert.WithinDuration(t,dbtime.Now(),key.CreatedAt,time.Second*5)
Expand DownExpand Up@@ -173,3 +172,17 @@ func TestGenerate(t *testing.T) {
})
}
}

// TestInvalid just ensures the false case is asserted by some tests.
// Otherwise, a function that just `returns true` might pass all tests incorrectly.
funcTestInvalid(t*testing.T) {
t.Parallel()

require.Falsef(t,apikey.ValidateHash([]byte{},"secret"),"empty hash")

secret,hash,err:=apikey.GenerateSecret(10)
require.NoError(t,err)

require.Falsef(t,apikey.ValidateHash(hash,secret+"_"),"different secret")
require.Falsef(t,apikey.ValidateHash(hash[:len(hash)-1],secret),"different hash length")
}
2 changes: 1 addition & 1 deletioncoderd/database/dbauthz/dbauthz.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2475,7 +2475,7 @@ func (q *querier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (d
returnq.db.GetOAuth2ProviderAppByID(ctx,id)
}

func (q*querier)GetOAuth2ProviderAppByRegistrationToken(ctx context.Context,registrationAccessTokensql.NullString) (database.OAuth2ProviderApp,error) {
func (q*querier)GetOAuth2ProviderAppByRegistrationToken(ctx context.Context,registrationAccessToken[]byte) (database.OAuth2ProviderApp,error) {
iferr:=q.authorizeContext(ctx,policy.ActionRead,rbac.ResourceOauth2App);err!=nil {
return database.OAuth2ProviderApp{},err
}
Expand Down
4 changes: 2 additions & 2 deletionscoderd/database/dbauthz/dbauthz_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3925,9 +3925,9 @@ func (s *MethodTestSuite) TestOAuth2ProviderApps() {
}))
s.Run("GetOAuth2ProviderAppByRegistrationToken", s.Subtest(func(db database.Store, check *expects) {
app := dbgen.OAuth2ProviderApp(s.T(), db, database.OAuth2ProviderApp{
RegistrationAccessToken:sql.NullString{String:"test-token", Valid: true},
RegistrationAccessToken:[]byte("test-token"),
})
check.Args(sql.NullString{String:"test-token", Valid: true}).Asserts(rbac.ResourceOauth2App, policy.ActionRead).Returns(app)
check.Args([]byte("test-token")).Asserts(rbac.ResourceOauth2App, policy.ActionRead).Returns(app)
}))
}

Expand Down
17 changes: 8 additions & 9 deletionscoderd/database/dbgen/dbgen.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,6 @@ package dbgen
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
Expand All@@ -20,6 +19,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
Expand DownExpand Up@@ -161,8 +161,8 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.

funcAPIKey(t testing.TB,db database.Store,seed database.APIKey,munge...func(*database.InsertAPIKeyParams)) (key database.APIKey,tokenstring) {
id,_:=cryptorand.String(10)
secret,_:=cryptorand.String(22)
hashed:=sha256.Sum256([]byte(secret))
secret,hashed,err:=apikey.GenerateSecret(22)
require.NoError(t,err)

ip:=seed.IPAddress
if!ip.Valid {
Expand All@@ -179,7 +179,7 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
ID:takeFirst(seed.ID,id),
// 0 defaults to 86400 at the db layer
LifetimeSeconds:takeFirst(seed.LifetimeSeconds,0),
HashedSecret:takeFirstSlice(seed.HashedSecret,hashed[:]),
HashedSecret:takeFirstSlice(seed.HashedSecret,hashed),
IPAddress:ip,
UserID:takeFirst(seed.UserID,uuid.New()),
LastUsed:takeFirst(seed.LastUsed,dbtime.Now()),
Expand All@@ -194,7 +194,7 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
for_,fn:=rangemunge {
fn(&params)
}
key,err:=db.InsertAPIKey(genCtx,params)
key,err=db.InsertAPIKey(genCtx,params)
require.NoError(t,err,"insert api key")
returnkey,fmt.Sprintf("%s-%s",key.ID,secret)
}
Expand DownExpand Up@@ -980,16 +980,15 @@ func WorkspaceResourceMetadatums(t testing.TB, db database.Store, seed database.
}

funcWorkspaceProxy(t testing.TB,db database.Store,orig database.WorkspaceProxy) (database.WorkspaceProxy,string) {
secret,err:=cryptorand.HexString(64)
secret,hashedSecret,err:=apikey.GenerateSecret(64)
require.NoError(t,err,"generate secret")
hashedSecret:=sha256.Sum256([]byte(secret))

proxy,err:=db.InsertWorkspaceProxy(genCtx, database.InsertWorkspaceProxyParams{
ID:takeFirst(orig.ID,uuid.New()),
Name:takeFirst(orig.Name,testutil.GetRandomName(t)),
DisplayName:takeFirst(orig.DisplayName,testutil.GetRandomName(t)),
Icon:takeFirst(orig.Icon,testutil.GetRandomName(t)),
TokenHashedSecret:hashedSecret[:],
TokenHashedSecret:hashedSecret,
CreatedAt:takeFirst(orig.CreatedAt,dbtime.Now()),
UpdatedAt:takeFirst(orig.UpdatedAt,dbtime.Now()),
DerpEnabled:takeFirst(orig.DerpEnabled,false),
Expand DownExpand Up@@ -1259,7 +1258,7 @@ func OAuth2ProviderApp(t testing.TB, db database.Store, seed database.OAuth2Prov
Jwks:seed.Jwks,// pqtype.NullRawMessage{} is not comparable, use existing value
SoftwareID:takeFirst(seed.SoftwareID, sql.NullString{}),
SoftwareVersion:takeFirst(seed.SoftwareVersion, sql.NullString{}),
RegistrationAccessToken:takeFirst(seed.RegistrationAccessToken, sql.NullString{}),
RegistrationAccessToken:seed.RegistrationAccessToken,
RegistrationClientUri:takeFirst(seed.RegistrationClientUri, sql.NullString{}),
})
require.NoError(t,err,"insert oauth2 app")
Expand Down
3 changes: 1 addition & 2 deletionscoderd/database/dbmetrics/querymetrics.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

3 changes: 1 addition & 2 deletionscoderd/database/dbmock/dbmock.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

2 changes: 1 addition & 1 deletioncoderd/database/dump.sql
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
ALTERTABLE oauth2_provider_apps
ALTER COLUMN registration_access_token
SET DATA TYPEtext
USING encode(registration_access_token,'escape');
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
ALTERTABLE oauth2_provider_apps
ALTER COLUMN registration_access_token
SET DATA TYPEbytea
USING decode(registration_access_token,'escape');
2 changes: 1 addition & 1 deletioncoderd/database/models.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

3 changes: 1 addition & 2 deletionscoderd/database/querier.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

4 changes: 2 additions & 2 deletionscoderd/database/queries.sql.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

6 changes: 2 additions & 4 deletionscoderd/httpmw/apikey.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,6 @@ package httpmw

import (
"context"
"crypto/sha256"
"crypto/subtle"
"database/sql"
"errors"
"fmt"
Expand All@@ -20,6 +18,7 @@ import (
"golang.org/x/xerrors"

"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
Expand DownExpand Up@@ -188,8 +187,7 @@ func APIKeyFromRequest(ctx context.Context, db database.Store, sessionTokenFunc
}

// Checking to see if the secret is valid.
hashedSecret:=sha256.Sum256([]byte(keySecret))
ifsubtle.ConstantTimeCompare(key.HashedSecret,hashedSecret[:])!=1 {
if!apikey.ValidateHash(key.HashedSecret,keySecret) {
returnnil, codersdk.Response{
Message:SignedOutErrorMessage,
Detail:"API key secret is invalid.",
Expand Down
15 changes: 8 additions & 7 deletionscoderd/httpmw/apikey_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,7 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/oauth2"

"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
Expand All@@ -32,10 +33,10 @@ import (
"github.com/coder/coder/v2/testutil"
)

func randomAPIKeyParts() (id string, secret string) {
func randomAPIKeyParts() (id string, secret string, hashedSecret []byte) {
id, _ = cryptorand.String(10)
secret, _ =cryptorand.String(22)
return id, secret
secret,hashedSecret,_ =apikey.GenerateSecret(22)
return id, secret, hashedSecret
}

func TestAPIKey(t *testing.T) {
Expand DownExpand Up@@ -171,10 +172,10 @@ func TestAPIKey(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
var (
db, _ = dbtestutil.NewDB(t)
id, secret = randomAPIKeyParts()
r = httptest.NewRequest("GET", "/", nil)
rw = httptest.NewRecorder()
db, _= dbtestutil.NewDB(t)
id, secret, _ = randomAPIKeyParts()
r= httptest.NewRequest("GET", "/", nil)
rw= httptest.NewRecorder()
)
r.Header.Set(codersdk.SessionTokenHeader, fmt.Sprintf("%s-%s", id, secret))

Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp