@@ -10,7 +10,6 @@ import (
10
10
"time"
11
11
12
12
"github.com/google/uuid"
13
- "golang.org/x/crypto/argon2"
14
13
"golang.org/x/oauth2"
15
14
"golang.org/x/xerrors"
16
15
@@ -21,10 +20,16 @@ import (
21
20
"github.com/coder/coder/v2/coderd/httpapi"
22
21
"github.com/coder/coder/v2/coderd/httpmw"
23
22
"github.com/coder/coder/v2/coderd/rbac"
23
+ "github.com/coder/coder/v2/coderd/userpassword"
24
24
"github.com/coder/coder/v2/codersdk"
25
- "github.com/coder/coder/v2/cryptorand"
26
25
)
27
26
27
+ // errBadSecret means the user provided a bad secret.
28
+ var errBadSecret = xerrors .New ("Invalid client secret" )
29
+
30
+ // errBadCode means the user provided a bad code.
31
+ var errBadCode = xerrors .New ("Invalid code" )
32
+
28
33
type tokenParams struct {
29
34
clientID string
30
35
clientSecret string
@@ -86,12 +91,12 @@ func Tokens(db database.Store, defaultLifetime time.Duration) http.HandlerFunc {
86
91
switch params .grantType {
87
92
// TODO: Client creds, device code, refresh.
88
93
default :
89
- token ,err = authorizationCodeGrant (ctx ,db ,app ,defaultLifetime ,params . clientSecret , params . code )
94
+ token ,err = authorizationCodeGrant (ctx ,db ,app ,defaultLifetime ,params )
90
95
}
91
96
92
- if err != nil && errors .Is (err ,sql . ErrNoRows ) {
97
+ if errors . Is ( err , errBadCode ) || errors .Is (err ,errBadSecret ) {
93
98
httpapi .Write (r .Context (),rw ,http .StatusUnauthorized , codersdk.Response {
94
- Message :"Invalid client secret or code" ,
99
+ Message :err . Error () ,
95
100
})
96
101
return
97
102
}
@@ -109,47 +114,59 @@ func Tokens(db database.Store, defaultLifetime time.Duration) http.HandlerFunc {
109
114
}
110
115
}
111
116
112
- func authorizationCodeGrant (ctx context.Context ,db database.Store ,app database.OAuth2ProviderApp ,defaultLifetime time.Duration ,clientSecret , code string ) (oauth2.Token ,error ) {
117
+ func authorizationCodeGrant (ctx context.Context ,db database.Store ,app database.OAuth2ProviderApp ,defaultLifetime time.Duration ,params tokenParams ) (oauth2.Token ,error ) {
113
118
// 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
+ if err != 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
+ if errors .Is (err ,sql .ErrNoRows ) {
126
+ return oauth2.Token {},errBadSecret
127
+ }
122
128
if err != nil {
123
129
return oauth2.Token {},err
124
130
}
131
+ equal ,err := userpassword .Compare (string (dbSecret .HashedSecret ),secret .secret )
132
+ if err != nil {
133
+ return oauth2.Token {},xerrors .Errorf ("unable to compare secret: %w" ,err )
134
+ }
135
+ if ! equal {
136
+ return oauth2.Token {},errBadSecret
137
+ }
125
138
126
139
// Validate the authorization code.
127
- codeHash := Hash ( code , app . ID )
140
+ code , err := parseSecret ( params . code )
128
141
if err != 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
+ if errors .Is (err ,sql .ErrNoRows ) {
147
+ return oauth2.Token {},errBadCode
130
148
}
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
- })
138
149
if err != nil {
139
150
return oauth2.Token {},err
140
151
}
152
+ equal ,err = userpassword .Compare (string (dbCode .HashedSecret ),code .secret )
153
+ if err != nil {
154
+ return oauth2.Token {},xerrors .Errorf ("unable to compare code: %w" ,err )
155
+ }
156
+ if ! equal {
157
+ return oauth2.Token {},errBadCode
158
+ }
141
159
142
- // Ensure the code has not expired. Make it look like no code.
160
+ // Ensure the code has not expired.
143
161
if dbCode .ExpiresAt .Before (dbtime .Now ()) {
144
- return oauth2.Token {},sql . ErrNoRows
162
+ return oauth2.Token {},errBadCode
145
163
}
146
164
147
165
// Generate a refresh token.
148
166
// The refresh token is not currently used or exposed though as API keys can
149
167
// already be refreshed by just using them.
150
168
// 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 ()
153
170
if err != nil {
154
171
return oauth2.Token {},err
155
172
}
@@ -208,13 +225,13 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
208
225
return xerrors .Errorf ("insert oauth2 access token: %w" ,err )
209
226
}
210
227
211
- hashed := Hash (rawRefreshToken ,app .ID )
212
228
_ ,err = tx .InsertOAuth2ProviderAppToken (ctx , database.InsertOAuth2ProviderAppTokenParams {
213
229
ID :uuid .New (),
214
230
CreatedAt :dbtime .Now (),
215
231
ExpiresAt :key .ExpiresAt ,
216
- RefreshHash :hashed [:],
217
- AppSecretID :secret .ID ,
232
+ HashPrefix : []byte (refreshToken .Prefix ),
233
+ RefreshHash : []byte (refreshToken .Hashed ),
234
+ AppSecretID :dbSecret .ID ,
218
235
APIKeyID :newKey .ID ,
219
236
})
220
237
if err != nil {
@@ -230,16 +247,7 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
230
247
AccessToken :sessionToken ,
231
248
TokenType :"Bearer" ,
232
249
// TODO: Exclude until refresh grant is implemented.
233
- // RefreshToken:rawRefreshToken ,
250
+ // RefreshToken:refreshToken.formatted ,
234
251
// Expiry: key.ExpiresAt,
235
252
},nil
236
253
}
237
-
238
- /**
239
- * Hash uses argon2 to hash the secret using the ID as the salt.
240
- */
241
- func Hash (secret string ,id uuid.UUID ) []byte {
242
- b := []byte (secret )
243
- // TODO: Expose iterations, memory, and threads as configuration values?
244
- return argon2 .IDKey (b , []byte (id .String ()),1 ,64 * 1024 ,2 ,uint32 (len (b )))
245
- }