@@ -30,12 +30,16 @@ var errBadSecret = xerrors.New("Invalid client secret")
30
30
// errBadCode means the user provided a bad code.
31
31
var errBadCode = xerrors .New ("Invalid code" )
32
32
33
+ // errBadToken means the user provided a bad token.
34
+ var errBadToken = xerrors .New ("Invalid token" )
35
+
33
36
type tokenParams struct {
34
37
clientID string
35
38
clientSecret string
36
39
code string
37
40
grantType codersdk.OAuth2ProviderGrantType
38
41
redirectURL * url.URL
42
+ refreshToken string
39
43
}
40
44
41
45
func extractTokenParams (r * http.Request ,callbackURL * url.URL ) (tokenParams , []codersdk.ValidationError ,error ) {
@@ -44,15 +48,24 @@ func extractTokenParams(r *http.Request, callbackURL *url.URL) (tokenParams, []c
44
48
if err != nil {
45
49
return tokenParams {},nil ,xerrors .Errorf ("parse form: %w" ,err )
46
50
}
47
- p .RequiredNotEmpty ("grant_type" ,"client_secret" ,"client_id" ,"code" )
48
51
49
52
vals := r .Form
53
+ p .RequiredNotEmpty ("grant_type" )
54
+ grantType := httpapi .ParseCustom (p ,vals ,"" ,"grant_type" ,httpapi .ParseEnum [codersdk .OAuth2ProviderGrantType ])
55
+ switch grantType {
56
+ case codersdk .OAuth2ProviderGrantTypeRefreshToken :
57
+ p .RequiredNotEmpty ("refresh_token" )
58
+ case codersdk .OAuth2ProviderGrantTypeAuthorizationCode :
59
+ p .RequiredNotEmpty ("client_secret" ,"client_id" ,"code" )
60
+ }
61
+
50
62
params := tokenParams {
51
63
clientID :p .String (vals ,"" ,"client_id" ),
52
64
clientSecret :p .String (vals ,"" ,"client_secret" ),
53
65
code :p .String (vals ,"" ,"code" ),
66
+ grantType :grantType ,
54
67
redirectURL :p .RedirectURL (vals ,callbackURL ,"redirect_uri" ),
55
- grantType : httpapi . ParseCustom ( p , vals ,"" ,"grant_type" , httpapi . ParseEnum [ codersdk . OAuth2ProviderGrantType ] ),
68
+ refreshToken : p . String ( vals ,"" ,"refresh_token" ),
56
69
}
57
70
58
71
p .ErrorExcessParams (vals )
@@ -89,7 +102,9 @@ func Tokens(db database.Store, defaultLifetime time.Duration) http.HandlerFunc {
89
102
var token oauth2.Token
90
103
//nolint:gocritic,revive // More cases will be added later.
91
104
switch params .grantType {
92
- // TODO: Client creds, device code, refresh.
105
+ // TODO: Client creds, device code.
106
+ case codersdk .OAuth2ProviderGrantTypeRefreshToken :
107
+ token ,err = refreshTokenGrant (ctx ,db ,app ,defaultLifetime ,params )
93
108
default :
94
109
token ,err = authorizationCodeGrant (ctx ,db ,app ,defaultLifetime ,params )
95
110
}
@@ -163,9 +178,6 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
163
178
}
164
179
165
180
// Generate a refresh token.
166
- // The refresh token is not currently used or exposed though as API keys can
167
- // already be refreshed by just using them.
168
- // TODO: However, should we implement the refresh grant anyway?
169
181
refreshToken ,err := GenerateSecret ()
170
182
if err != nil {
171
183
return oauth2.Token {},err
@@ -244,10 +256,115 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
244
256
}
245
257
246
258
return oauth2.Token {
247
- AccessToken :sessionToken ,
248
- TokenType :"Bearer" ,
249
- // TODO: Exclude until refresh grant is implemented.
250
- // RefreshToken: refreshToken.formatted,
251
- // Expiry: key.ExpiresAt,
259
+ AccessToken :sessionToken ,
260
+ TokenType :"Bearer" ,
261
+ RefreshToken :refreshToken .Formatted ,
262
+ Expiry :key .ExpiresAt ,
263
+ },nil
264
+ }
265
+
266
+ func refreshTokenGrant (ctx context.Context ,db database.Store ,app database.OAuth2ProviderApp ,defaultLifetime time.Duration ,params tokenParams ) (oauth2.Token ,error ) {
267
+ // Validate the token.
268
+ token ,err := parseSecret (params .refreshToken )
269
+ if err != nil {
270
+ return oauth2.Token {},errBadToken
271
+ }
272
+ //nolint:gocritic // There is no user yet so we must use the system.
273
+ dbToken ,err := db .GetOAuth2ProviderAppTokenByPrefix (dbauthz .AsSystemRestricted (ctx ), []byte (token .prefix ))
274
+ if errors .Is (err ,sql .ErrNoRows ) {
275
+ return oauth2.Token {},errBadToken
276
+ }
277
+ if err != nil {
278
+ return oauth2.Token {},err
279
+ }
280
+ equal ,err := userpassword .Compare (string (dbToken .RefreshHash ),token .secret )
281
+ if err != nil {
282
+ return oauth2.Token {},xerrors .Errorf ("unable to compare token: %w" ,err )
283
+ }
284
+ if ! equal {
285
+ return oauth2.Token {},errBadToken
286
+ }
287
+
288
+ // Ensure the token has not expired.
289
+ if dbToken .ExpiresAt .Before (dbtime .Now ()) {
290
+ return oauth2.Token {},errBadToken
291
+ }
292
+
293
+ // Grab the user roles so we can perform the refresh as the user.
294
+ //nolint:gocritic // There is no user yet so we must use the system.
295
+ prevKey ,err := db .GetAPIKeyByID (dbauthz .AsSystemRestricted (ctx ),dbToken .APIKeyID )
296
+ if err != nil {
297
+ return oauth2.Token {},err
298
+ }
299
+ //nolint:gocritic // There is no user yet so we must use the system.
300
+ roles ,err := db .GetAuthorizationUserRoles (dbauthz .AsSystemRestricted (ctx ),prevKey .UserID )
301
+ if err != nil {
302
+ return oauth2.Token {},err
303
+ }
304
+ userSubj := rbac.Subject {
305
+ ID :prevKey .UserID .String (),
306
+ Roles :rbac .RoleNames (roles .Roles ),
307
+ Groups :roles .Groups ,
308
+ Scope :rbac .ScopeAll ,
309
+ }
310
+
311
+ // Generate a new refresh token.
312
+ refreshToken ,err := GenerateSecret ()
313
+ if err != nil {
314
+ return oauth2.Token {},err
315
+ }
316
+
317
+ // Generate the new API key.
318
+ // TODO: We are ignoring scopes for now.
319
+ tokenName := fmt .Sprintf ("%s_%s_oauth_session_token" ,prevKey .UserID ,app .ID )
320
+ key ,sessionToken ,err := apikey .Generate (apikey.CreateParams {
321
+ UserID :prevKey .UserID ,
322
+ LoginType :database .LoginTypeOAuth2ProviderApp ,
323
+ // TODO: This is just the lifetime for api keys, maybe have its own config
324
+ // settings. #11693
325
+ DefaultLifetime :defaultLifetime ,
326
+ // For now, we allow only one token per app and user at a time.
327
+ TokenName :tokenName ,
328
+ })
329
+ if err != nil {
330
+ return oauth2.Token {},err
331
+ }
332
+
333
+ // Replace the token.
334
+ err = db .InTx (func (tx database.Store )error {
335
+ ctx := dbauthz .As (ctx ,userSubj )
336
+ err = tx .DeleteAPIKeyByID (ctx ,prevKey .ID )// This cascades to the token.
337
+ if err != nil {
338
+ return xerrors .Errorf ("delete oauth2 app token: %w" ,err )
339
+ }
340
+
341
+ newKey ,err := tx .InsertAPIKey (ctx ,key )
342
+ if err != nil {
343
+ return xerrors .Errorf ("insert oauth2 access token: %w" ,err )
344
+ }
345
+
346
+ _ ,err = tx .InsertOAuth2ProviderAppToken (ctx , database.InsertOAuth2ProviderAppTokenParams {
347
+ ID :uuid .New (),
348
+ CreatedAt :dbtime .Now (),
349
+ ExpiresAt :key .ExpiresAt ,
350
+ HashPrefix : []byte (refreshToken .Prefix ),
351
+ RefreshHash : []byte (refreshToken .Hashed ),
352
+ AppSecretID :dbToken .AppSecretID ,
353
+ APIKeyID :newKey .ID ,
354
+ })
355
+ if err != nil {
356
+ return xerrors .Errorf ("insert oauth2 refresh token: %w" ,err )
357
+ }
358
+ return nil
359
+ },nil )
360
+ if err != nil {
361
+ return oauth2.Token {},err
362
+ }
363
+
364
+ return oauth2.Token {
365
+ AccessToken :sessionToken ,
366
+ TokenType :"Bearer" ,
367
+ RefreshToken :refreshToken .Formatted ,
368
+ Expiry :key .ExpiresAt ,
252
369
},nil
253
370
}