@@ -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
}
0 commit comments