(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, req codersdk.OAuth2TokenRequest)
| 379 | } |
| 380 | |
| 381 | func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, req codersdk.OAuth2TokenRequest) (codersdk.OAuth2TokenResponse, error) { |
| 382 | // Validate the token. |
| 383 | token, err := ParseFormattedSecret(req.RefreshToken) |
| 384 | if err != nil { |
| 385 | return codersdk.OAuth2TokenResponse{}, errBadToken |
| 386 | } |
| 387 | //nolint:gocritic // OAuth2 system context — no authenticated user during refresh |
| 388 | dbToken, err := db.GetOAuth2ProviderAppTokenByPrefix(dbauthz.AsSystemOAuth2(ctx), []byte(token.Prefix)) |
| 389 | if errors.Is(err, sql.ErrNoRows) { |
| 390 | return codersdk.OAuth2TokenResponse{}, errBadToken |
| 391 | } |
| 392 | if err != nil { |
| 393 | return codersdk.OAuth2TokenResponse{}, err |
| 394 | } |
| 395 | equal := apikey.ValidateHash(dbToken.RefreshHash, token.Secret) |
| 396 | if !equal { |
| 397 | return codersdk.OAuth2TokenResponse{}, errBadToken |
| 398 | } |
| 399 | |
| 400 | // Ensure the token has not expired. |
| 401 | if dbToken.ExpiresAt.Before(dbtime.Now()) { |
| 402 | return codersdk.OAuth2TokenResponse{}, errBadToken |
| 403 | } |
| 404 | |
| 405 | // Verify resource parameter consistency for refresh tokens (RFC 8707) |
| 406 | if req.Resource != "" { |
| 407 | // If resource is provided in refresh request, it must match the original token's audience |
| 408 | if !dbToken.Audience.Valid || dbToken.Audience.String != req.Resource { |
| 409 | return codersdk.OAuth2TokenResponse{}, errInvalidResource |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | // Grab the user roles so we can perform the refresh as the user. |
| 414 | //nolint:gocritic // OAuth2 system context — need to read the previous API key |
| 415 | prevKey, err := db.GetAPIKeyByID(dbauthz.AsSystemOAuth2(ctx), dbToken.APIKeyID) |
| 416 | if err != nil { |
| 417 | return codersdk.OAuth2TokenResponse{}, err |
| 418 | } |
| 419 | |
| 420 | actor, _, err := httpmw.UserRBACSubject(ctx, db, prevKey.UserID, rbac.ScopeAll) |
| 421 | if err != nil { |
| 422 | return codersdk.OAuth2TokenResponse{}, xerrors.Errorf("fetch user actor: %w", err) |
| 423 | } |
| 424 | |
| 425 | // Generate a new refresh token. |
| 426 | refreshToken, err := GenerateSecret() |
| 427 | if err != nil { |
| 428 | return codersdk.OAuth2TokenResponse{}, err |
| 429 | } |
| 430 | |
| 431 | // Generate the new API key. |
| 432 | // TODO: We are ignoring scopes for now. |
| 433 | tokenName := fmt.Sprintf("%s_%s_oauth_session_token", prevKey.UserID, app.ID) |
| 434 | key, sessionToken, err := apikey.Generate(apikey.CreateParams{ |
| 435 | UserID: prevKey.UserID, |
| 436 | LoginType: database.LoginTypeOAuth2ProviderApp, |
| 437 | DefaultLifetime: lifetimes.DefaultDuration.Value(), |
| 438 | // For now, we allow only one token per app and user at a time. |
no test coverage detected