Skip to content

Commit 19fa030

Browse files
committed
Implement refresh grant
1 parent de037d3 commit 19fa030

File tree

8 files changed

+350
-44
lines changed

8 files changed

+350
-44
lines changed

coderd/apidoc/docs.go

Lines changed: 14 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 13 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codersdk/oauth2.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,12 @@ type OAuth2ProviderGrantType string
184184

185185
const (
186186
OAuth2ProviderGrantTypeAuthorizationCode OAuth2ProviderGrantType = "authorization_code"
187+
OAuth2ProviderGrantTypeRefreshToken OAuth2ProviderGrantType = "refresh_token"
187188
)
188189

189190
func (e OAuth2ProviderGrantType) Valid() bool {
190-
//nolint:gocritic,revive // More cases will be added later.
191191
switch e {
192-
case OAuth2ProviderGrantTypeAuthorizationCode:
192+
case OAuth2ProviderGrantTypeAuthorizationCode, OAuth2ProviderGrantTypeRefreshToken:
193193
return true
194194
}
195195
return false

docs/api/enterprise.md

Lines changed: 10 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/coderd/identityprovider/tokens.go

Lines changed: 128 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@ var errBadSecret = xerrors.New("Invalid client secret")
3030
// errBadCode means the user provided a bad code.
3131
var errBadCode = xerrors.New("Invalid code")
3232

33+
// errBadToken means the user provided a bad token.
34+
var errBadToken = xerrors.New("Invalid token")
35+
3336
type tokenParams struct {
3437
clientID string
3538
clientSecret string
3639
code string
3740
grantType codersdk.OAuth2ProviderGrantType
3841
redirectURL *url.URL
42+
refreshToken string
3943
}
4044

4145
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
4448
if err != nil {
4549
return tokenParams{}, nil, xerrors.Errorf("parse form: %w", err)
4650
}
47-
p.RequiredNotEmpty("grant_type", "client_secret", "client_id", "code")
4851

4952
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+
5062
params := tokenParams{
5163
clientID: p.String(vals, "", "client_id"),
5264
clientSecret: p.String(vals, "", "client_secret"),
5365
code: p.String(vals, "", "code"),
66+
grantType: grantType,
5467
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"),
5669
}
5770

5871
p.ErrorExcessParams(vals)
@@ -89,7 +102,9 @@ func Tokens(db database.Store, defaultLifetime time.Duration) http.HandlerFunc {
89102
var token oauth2.Token
90103
//nolint:gocritic,revive // More cases will be added later.
91104
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)
93108
default:
94109
token, err = authorizationCodeGrant(ctx, db, app, defaultLifetime, params)
95110
}
@@ -163,9 +178,6 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
163178
}
164179

165180
// 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?
169181
refreshToken, err := GenerateSecret()
170182
if err != nil {
171183
return oauth2.Token{}, err
@@ -244,10 +256,115 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
244256
}
245257

246258
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,
252369
}, nil
253370
}

enterprise/coderd/oauth2.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,10 @@ func (api *API) postOAuth2ProviderAppAuthorize() http.HandlerFunc {
300300
// @ID oauth2-token-exchange
301301
// @Produce json
302302
// @Tags Enterprise
303-
// @Param client_id formData string true "Client ID"
304-
// @Param client_secret formData string true "Client secret"
305-
// @Param code formData string true "Authorization code"
303+
// @Param client_id formData string false "Client ID, required if grant_type=authorization_code"
304+
// @Param client_secret formData string false "Client secret, required if grant_type=authorization_code"
305+
// @Param code formData string false "Authorization code, required if grant_type=authorization_code"
306+
// @Param refresh_token formData string false "Refresh token, required if grant_type=refresh_token"
306307
// @Param grant_type formData codersdk.OAuth2ProviderGrantType true "Grant type"
307308
// @Success 200 {object} oauth2.Token
308309
// @Router /login/oauth2/tokens [post]

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy