@@ -22,9 +22,14 @@ import (
22
22
"github.com/coder/coder/v2/coderd/rbac"
23
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 = errors .New ("Invalid client secret" )
29
+
30
+ // errBadCode means the user provided a bad code.
31
+ var errBadCode = errors .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,59 +114,68 @@ 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 , err := userpassword . Hash ( clientSecret )
119
+ secretPrefix , originalSecret , err := parseSecret ( params . clientSecret )
115
120
if err != nil {
116
- return oauth2.Token {}, err
121
+ return oauth2.Token {}, errBadSecret
122
+ }
123
+ //nolint:gocritic // Users cannot read secrets so we must use the system.
124
+ secret , err := db .GetOAuth2ProviderAppSecretByPrefix (dbauthz .AsSystemRestricted (ctx ), []byte (secretPrefix ))
125
+ if errors .Is (err , sql .ErrNoRows ) {
126
+ return oauth2.Token {}, errBadSecret
117
127
}
118
- secret , err := db .GetOAuth2ProviderAppSecretByAppIDAndSecret (
119
- //nolint:gocritic // Users cannot read secrets so we must use the system.
120
- dbauthz .AsSystemRestricted (ctx ),
121
- database.GetOAuth2ProviderAppSecretByAppIDAndSecretParams {
122
<
E377
/td>- AppID : app .ID ,
123
- HashedSecret : []byte (secretHash ),
124
- })
125
128
if err != nil {
126
129
return oauth2.Token {}, err
127
130
}
131
+ equal , err := userpassword .Compare (string (secret .HashedSecret ), originalSecret )
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
+ }
128
138
129
139
// Validate the authorization code.
130
- codeHash , err := userpassword . Hash ( code )
140
+ codePrefix , originalCode , err := parseSecret ( params . code )
131
141
if err != nil {
132
- 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
+ code , err := db .GetOAuth2ProviderAppCodeByPrefix (dbauthz .AsSystemRestricted (ctx ), []byte (codePrefix ))
146
+ if errors .Is (err , sql .ErrNoRows ) {
147
+ return oauth2.Token {}, errBadCode
133
148
}
134
- dbCode , err := db .GetOAuth2ProviderAppCodeByAppIDAndSecret (
135
- //nolint:gocritic // There is no user yet so we must use the system.
136
- dbauthz .AsSystemRestricted (ctx ),
137
- database.GetOAuth2ProviderAppCodeByAppIDAndSecretParams {
138
- AppID : app .ID ,
139
- HashedSecret : []byte (codeHash ),
140
- })
141
149
if err != nil {
142
150
return oauth2.Token {}, err
143
151
}
152
+ equal , err = userpassword .Compare (string (code .HashedSecret ), originalCode )
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
+ }
144
159
145
- // Ensure the code has not expired. Make it look like no code.
146
- if dbCode .ExpiresAt .Before (dbtime .Now ()) {
147
- return oauth2.Token {}, sql . ErrNoRows
160
+ // Ensure the code has not expired.
161
+ if code .ExpiresAt .Before (dbtime .Now ()) {
162
+ return oauth2.Token {}, errBadCode
148
163
}
149
164
150
165
// Generate a refresh token.
151
166
// The refresh token is not currently used or exposed though as API keys can
152
167
// already be refreshed by just using them.
153
168
// TODO: However, should we implement the refresh grant anyway?
154
- // 40 characters matches the length of GitHub's client secrets.
155
- rawRefreshToken , err := cryptorand .String (40 )
169
+ refreshToken , err := GenerateSecret ()
156
170
if err != nil {
157
171
return oauth2.Token {}, err
158
172
}
159
173
160
174
// Generate the API key we will swap for the code.
161
175
// TODO: We are ignoring scopes for now.
162
- tokenName := fmt .Sprintf ("%s_%s_oauth_session_token" , dbCode .UserID , app .ID )
176
+ tokenName := fmt .Sprintf ("%s_%s_oauth_session_token" , code .UserID , app .ID )
163
177
key , sessionToken , err := apikey .Generate (apikey.CreateParams {
164
- UserID : dbCode .UserID ,
178
+ UserID : code .UserID ,
165
179
LoginType : database .LoginTypeOAuth2ProviderApp ,
166
180
// TODO: This is just the lifetime for api keys, maybe have its own config
167
181
// settings. #11693
@@ -175,12 +189,12 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
175
189
176
190
// Grab the user roles so we can perform the exchange as the user.
177
191
//nolint:gocritic // In the token exchange, there is no user actor.
178
- roles , err := db .GetAuthorizationUserRoles (dbauthz .AsSystemRestricted (ctx ), dbCode .UserID )
192
+ roles , err := db .GetAuthorizationUserRoles (dbauthz .AsSystemRestricted (ctx ), code .UserID )
179
193
if err != nil {
180
194
return oauth2.Token {}, err
181
195
}
182
196
userSubj := rbac.Subject {
183
- ID : dbCode .UserID .String (),
197
+ ID : code .UserID .String (),
184
198
Roles : rbac .RoleNames (roles .Roles ),
185
199
Groups : roles .Groups ,
186
200
Scope : rbac .ScopeAll ,
@@ -189,14 +203,14 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
189
203
// Do the actual token exchange in the database.
190
204
err = db .InTx (func (tx database.Store ) error {
191
205
ctx := dbauthz .As (ctx , userSubj )
192
- err = tx .DeleteOAuth2ProviderAppCodeByID (ctx , dbCode .ID )
206
+ err = tx .DeleteOAuth2ProviderAppCodeByID (ctx , code .ID )
193
207
if err != nil {
194
208
return xerrors .Errorf ("delete oauth2 app code: %w" , err )
195
209
}
196
210
197
211
// Delete the previous key, if any.
198
212
prevKey , err := tx .GetAPIKeyByName (ctx , database.GetAPIKeyByNameParams {
199
- UserID : dbCode .UserID ,
213
+ UserID : code .UserID ,
200
214
TokenName : tokenName ,
201
215
})
202
216
if err == nil {
@@ -211,15 +225,12 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
211
225
return xerrors .Errorf ("insert oauth2 access token: %w" , err )
212
226
}
213
227
214
- refreshHash , err := userpassword .Hash (rawRefreshToken )
215
- if err != nil {
216
- return xerrors .Errorf ("hash oauth2 refresh token: %w" , err )
217
- }
218
228
_ , err = tx .InsertOAuth2ProviderAppToken (ctx , database.InsertOAuth2ProviderAppTokenParams {
219
229
ID : uuid .New (),
220
230
CreatedAt : dbtime .Now (),
221
231
ExpiresAt : key .ExpiresAt ,
222
- RefreshHash : []byte (refreshHash ),
232
+ HashPrefix : []byte (refreshToken .Prefix ),
233
+ RefreshHash : []byte (refreshToken .Hashed ),
223
234
AppSecretID : secret .ID ,
224
235
APIKeyID : newKey .ID ,
225
236
})
@@ -236,7 +247,7 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
236
247
AccessToken : sessionToken ,
237
248
TokenType : "Bearer" ,
238
249
// TODO: Exclude until refresh grant is implemented.
239
- // RefreshToken: rawRefreshToken ,
250
+ // RefreshToken: refreshToken.formatted ,
240
251
// Expiry: key.ExpiresAt,
241
252
}, nil
242
253
}
0 commit comments