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

Commit20db7ba

Browse files
committed
Use userpassword.Hash
1 parent3c2176a commit20db7ba

File tree

5 files changed

+201
-68
lines changed

5 files changed

+201
-68
lines changed

‎enterprise/coderd/identityprovider/authorize.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/coder/coder/v2/coderd/httpapi"
1616
"github.com/coder/coder/v2/coderd/httpmw"
1717
"github.com/coder/coder/v2/codersdk"
18-
"github.com/coder/coder/v2/cryptorand"
1918
)
2019

2120
typeauthorizeParamsstruct {
@@ -80,15 +79,13 @@ func Authorize(db database.Store, accessURL *url.URL) http.HandlerFunc {
8079
}
8180

8281
// TODO: Ignoring scope for now, but should look into implementing.
83-
// 40 characters matches the length of GitHub's client secrets.
84-
rawCode,err:=cryptorand.String(40)
82+
code,err:=GenerateSecret()
8583
iferr!=nil {
8684
httpapi.Write(r.Context(),rw,http.StatusInternalServerError, codersdk.Response{
8785
Message:"Failed to generate OAuth2 app authorization code.",
8886
})
8987
return
9088
}
91-
hashedCode:=Hash(rawCode,app.ID)
9289
err=db.InTx(func(tx database.Store)error {
9390
// Delete any previous codes.
9491
err=tx.DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx, database.DeleteOAuth2ProviderAppCodesByAppAndUserIDParams{
@@ -105,7 +102,8 @@ func Authorize(db database.Store, accessURL *url.URL) http.HandlerFunc {
105102
CreatedAt:dbtime.Now(),
106103
// TODO: Configurable expiration? Ten minutes matches GitHub.
107104
ExpiresAt:dbtime.Now().Add(time.Duration(10)*time.Minute),
108-
HashedSecret:hashedCode[:],
105+
SecretPrefix: []byte(code.Prefix),
106+
HashedSecret: []byte(code.Hashed),
109107
AppID:app.ID,
110108
UserID:apiKey.UserID,
111109
})
@@ -124,7 +122,7 @@ func Authorize(db database.Store, accessURL *url.URL) http.HandlerFunc {
124122
}
125123

126124
newQuery:=params.redirectURL.Query()
127-
newQuery.Add("code",rawCode)
125+
newQuery.Add("code",code.Formatted)
128126
newQuery.Add("state",params.state)
129127
params.redirectURL.RawQuery=newQuery.Encode()
130128

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package identityprovider
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/v2/coderd/userpassword"
10+
"github.com/coder/coder/v2/cryptorand"
11+
)
12+
13+
typeOAuth2ProviderAppSecretstruct {
14+
Formattedstring
15+
Prefixstring
16+
Hashedstring
17+
}
18+
19+
// GenerateSecret generates a secret to be used as a client secret, refresh
20+
// token, or authorization code.
21+
funcGenerateSecret() (OAuth2ProviderAppSecret,error) {
22+
// 40 characters matches the length of GitHub's client secrets.
23+
secret,err:=cryptorand.String(40)
24+
iferr!=nil {
25+
returnOAuth2ProviderAppSecret{},err
26+
}
27+
28+
// This ID is prefixed to the secret so it can be used to look up the secret
29+
// when the user provides it, since we cannot just re-hash it to match as we
30+
// will not have the salt.
31+
prefix,err:=cryptorand.String(10)
32+
iferr!=nil {
33+
returnOAuth2ProviderAppSecret{},err
34+
}
35+
36+
hashed,err:=userpassword.Hash(secret)
37+
iferr!=nil {
38+
returnOAuth2ProviderAppSecret{},err
39+
}
40+
41+
returnOAuth2ProviderAppSecret{
42+
Formatted:fmt.Sprintf("coder_%s_%s",prefix,secret),
43+
Prefix:prefix,
44+
Hashed:hashed,
45+
},nil
46+
}
47+
48+
typeparsedSecretstruct {
49+
prefixstring
50+
secretstring
51+
}
52+
53+
// parseSecret extracts the ID and original secret from a secret.
54+
funcparseSecret(secretstring) (parsedSecret,error) {
55+
parts:=strings.Split(secret,"_")
56+
iflen(parts)!=3 {
57+
returnparsedSecret{},xerrors.Errorf("incorrect number of parts: %d",len(parts))
58+
}
59+
ifparts[0]!="coder" {
60+
returnparsedSecret{},xerrors.Errorf("incorrect scheme: %s",parts[0])
61+
}
62+
iflen(parts[1])==0 {
63+
returnparsedSecret{},xerrors.Errorf("prefix is invalid")
64+
}
65+
iflen(parts[2])==0 {
66+
returnparsedSecret{},xerrors.Errorf("invalid")
67+
}
68+
returnparsedSecret{parts[1],parts[2]},nil
69+
}

‎enterprise/coderd/identityprovider/tokens.go

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"time"
1111

1212
"github.com/google/uuid"
13-
"golang.org/x/crypto/argon2"
1413
"golang.org/x/oauth2"
1514
"golang.org/x/xerrors"
1615

@@ -21,10 +20,16 @@ import (
2120
"github.com/coder/coder/v2/coderd/httpapi"
2221
"github.com/coder/coder/v2/coderd/httpmw"
2322
"github.com/coder/coder/v2/coderd/rbac"
23+
"github.com/coder/coder/v2/coderd/userpassword"
2424
"github.com/coder/coder/v2/codersdk"
25-
"github.com/coder/coder/v2/cryptorand"
2625
)
2726

27+
// errBadSecret means the user provided a bad secret.
28+
varerrBadSecret=xerrors.New("Invalid client secret")
29+
30+
// errBadCode means the user provided a bad code.
31+
varerrBadCode=xerrors.New("Invalid code")
32+
2833
typetokenParamsstruct {
2934
clientIDstring
3035
clientSecretstring
@@ -86,12 +91,12 @@ func Tokens(db database.Store, defaultLifetime time.Duration) http.HandlerFunc {
8691
switchparams.grantType {
8792
// TODO: Client creds, device code, refresh.
8893
default:
89-
token,err=authorizationCodeGrant(ctx,db,app,defaultLifetime,params.clientSecret,params.code)
94+
token,err=authorizationCodeGrant(ctx,db,app,defaultLifetime,params)
9095
}
9196

92-
iferr!=nil&&errors.Is(err,sql.ErrNoRows) {
97+
iferrors.Is(err,errBadCode)||errors.Is(err,errBadSecret) {
9398
httpapi.Write(r.Context(),rw,http.StatusUnauthorized, codersdk.Response{
94-
Message:"Invalid client secret or code",
99+
Message:err.Error(),
95100
})
96101
return
97102
}
@@ -109,47 +114,59 @@ func Tokens(db database.Store, defaultLifetime time.Duration) http.HandlerFunc {
109114
}
110115
}
111116

112-
funcauthorizationCodeGrant(ctx context.Context,db database.Store,app database.OAuth2ProviderApp,defaultLifetime time.Duration,clientSecret,codestring) (oauth2.Token,error) {
117+
funcauthorizationCodeGrant(ctx context.Context,db database.Store,app database.OAuth2ProviderApp,defaultLifetime time.Duration,paramstokenParams) (oauth2.Token,error) {
113118
// Validate the client secret.
114-
secretHash:=Hash(clientSecret,app.ID)
115-
secret,err:=db.GetOAuth2ProviderAppSecretByAppIDAndSecret(
116-
//nolint:gocritic // Users cannot read secrets so we must use the system.
117-
dbauthz.AsSystemRestricted(ctx),
118-
database.GetOAuth2ProviderAppSecretByAppIDAndSecretParams{
119-
AppID:app.ID,
120-
HashedSecret:secretHash[:],
121-
})
119+
secret,err:=parseSecret(params.clientSecret)
120+
iferr!=nil {
121+
return oauth2.Token{},errBadSecret
122+
}
123+
//nolint:gocritic // Users cannot read secrets so we must use the system.
124+
dbSecret,err:=db.GetOAuth2ProviderAppSecretByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(secret.prefix))
125+
iferrors.Is(err,sql.ErrNoRows) {
126+
return oauth2.Token{},errBadSecret
127+
}
122128
iferr!=nil {
123129
return oauth2.Token{},err
124130
}
131+
equal,err:=userpassword.Compare(string(dbSecret.HashedSecret),secret.secret)
132+
iferr!=nil {
133+
return oauth2.Token{},xerrors.Errorf("unable to compare secret: %w",err)
134+
}
135+
if!equal {
136+
return oauth2.Token{},errBadSecret
137+
}
125138

126139
// Validate the authorization code.
127-
codeHash:=Hash(code,app.ID)
140+
code,err:=parseSecret(params.code)
128141
iferr!=nil {
129-
return oauth2.Token{},err
142+
return oauth2.Token{},errBadCode
143+
}
144+
//nolint:gocritic // There is no user yet so we must use the system.
145+
dbCode,err:=db.GetOAuth2ProviderAppCodeByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(code.prefix))
146+
iferrors.Is(err,sql.ErrNoRows) {
147+
return oauth2.Token{},errBadCode
130148
}
131-
dbCode,err:=db.GetOAuth2ProviderAppCodeByAppIDAndSecret(
132-
//nolint:gocritic // There is no user yet so we must use the system.
133-
dbauthz.AsSystemRestricted(ctx),
134-
database.GetOAuth2ProviderAppCodeByAppIDAndSecretParams{
135-
AppID:app.ID,
136-
HashedSecret:codeHash[:],
137-
})
138149
iferr!=nil {
139150
return oauth2.Token{},err
140151
}
152+
equal,err=userpassword.Compare(string(dbCode.HashedSecret),code.secret)
153+
iferr!=nil {
154+
return oauth2.Token{},xerrors.Errorf("unable to compare code: %w",err)
155+
}
156+
if!equal {
157+
return oauth2.Token{},errBadCode
158+
}
141159

142-
// Ensure the code has not expired. Make it look like no code.
160+
// Ensure the code has not expired.
143161
ifdbCode.ExpiresAt.Before(dbtime.Now()) {
144-
return oauth2.Token{},sql.ErrNoRows
162+
return oauth2.Token{},errBadCode
145163
}
146164

147165
// Generate a refresh token.
148166
// The refresh token is not currently used or exposed though as API keys can
149167
// already be refreshed by just using them.
150168
// TODO: However, should we implement the refresh grant anyway?
151-
// 40 characters matches the length of GitHub's client secrets.
152-
rawRefreshToken,err:=cryptorand.String(40)
169+
refreshToken,err:=GenerateSecret()
153170
iferr!=nil {
154171
return oauth2.Token{},err
155172
}
@@ -208,13 +225,13 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
208225
returnxerrors.Errorf("insert oauth2 access token: %w",err)
209226
}
210227

211-
hashed:=Hash(rawRefreshToken,app.ID)
212228
_,err=tx.InsertOAuth2ProviderAppToken(ctx, database.InsertOAuth2ProviderAppTokenParams{
213229
ID:uuid.New(),
214230
CreatedAt:dbtime.Now(),
215231
ExpiresAt:key.ExpiresAt,
216-
RefreshHash:hashed[:],
217-
AppSecretID:secret.ID,
232+
HashPrefix: []byte(refreshToken.Prefix),
233+
RefreshHash: []byte(refreshToken.Hashed),
234+
AppSecretID:dbSecret.ID,
218235
APIKeyID:newKey.ID,
219236
})
220237
iferr!=nil {
@@ -230,16 +247,7 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
230247
AccessToken:sessionToken,
231248
TokenType:"Bearer",
232249
// TODO: Exclude until refresh grant is implemented.
233-
// RefreshToken:rawRefreshToken,
250+
// RefreshToken:refreshToken.formatted,
234251
// Expiry: key.ExpiresAt,
235252
},nil
236253
}
237-
238-
/**
239-
* Hash uses argon2 to hash the secret using the ID as the salt.
240-
*/
241-
funcHash(secretstring,id uuid.UUID) []byte {
242-
b:= []byte(secret)
243-
// TODO: Expose iterations, memory, and threads as configuration values?
244-
returnargon2.IDKey(b, []byte(id.String()),1,64*1024,2,uint32(len(b)))
245-
}

‎enterprise/coderd/oauth2.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/coder/coder/v2/coderd/httpapi"
1414
"github.com/coder/coder/v2/coderd/httpmw"
1515
"github.com/coder/coder/v2/codersdk"
16-
"github.com/coder/coder/v2/cryptorand"
1716
"github.com/coder/coder/v2/enterprise/coderd/identityprovider"
1817
)
1918

@@ -233,24 +232,23 @@ func (api *API) oAuth2ProviderAppSecrets(rw http.ResponseWriter, r *http.Request
233232
func (api*API)postOAuth2ProviderAppSecret(rw http.ResponseWriter,r*http.Request) {
234233
ctx:=r.Context()
235234
app:=httpmw.OAuth2ProviderApp(r)
236-
// 40 characters matches the length of GitHub's client secrets.
237-
rawSecret,err:=cryptorand.String(40)
235+
secret,err:=identityprovider.GenerateSecret()
238236
iferr!=nil {
239-
httpapi.Write(r.Context(),rw,http.StatusInternalServerError, codersdk.Response{
237+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
240238
Message:"Failed to generate OAuth2 client secret.",
239+
Detail:err.Error(),
241240
})
242241
return
243242
}
244-
hashed:=identityprovider.Hash(rawSecret,app.ID)
245-
secret,err:=api.Database.InsertOAuth2ProviderAppSecret(ctx, database.InsertOAuth2ProviderAppSecretParams{
243+
dbSecret,err:=api.Database.InsertOAuth2ProviderAppSecret(ctx, database.InsertOAuth2ProviderAppSecretParams{
246244
ID:uuid.New(),
247245
CreatedAt:dbtime.Now(),
248-
SecretPrefix: []byte(""),// TODO: Currently unused.
249-
HashedSecret:hashed[:],
246+
SecretPrefix: []byte(secret.Prefix),
247+
HashedSecret:[]byte(secret.Hashed),
250248
// DisplaySecret is the last six characters of the original unhashed secret.
251249
// This is done so they can be differentiated and it matches how GitHub
252250
// displays their client secrets.
253-
DisplaySecret:rawSecret[len(rawSecret)-6:],
251+
DisplaySecret:secret.Formatted[len(secret.Formatted)-6:],
254252
AppID:app.ID,
255253
})
256254
iferr!=nil {
@@ -261,8 +259,8 @@ func (api *API) postOAuth2ProviderAppSecret(rw http.ResponseWriter, r *http.Requ
261259
return
262260
}
263261
httpapi.Write(ctx,rw,http.StatusOK, codersdk.OAuth2ProviderAppSecretFull{
264-
ID:secret.ID,
265-
ClientSecretFull:rawSecret,
262+
ID:dbSecret.ID,
263+
ClientSecretFull:secret.Formatted,
266264
})
267265
}
268266

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp