MCPcopy Create free account
hub / github.com/bytebase/bytebase / handleRefreshTokenGrant

Method handleRefreshTokenGrant

backend/api/oauth2/token.go:166–225  ·  view source on GitHub ↗
(c *echo.Context, client *store.OAuth2ClientMessage, req *tokenRequest)

Source from the content-addressed store, hash-verified

164}
165
166func (s *Service) handleRefreshTokenGrant(c *echo.Context, client *store.OAuth2ClientMessage, req *tokenRequest) error {
167 ctx := c.Request().Context()
168
169 // Validate grant type is allowed
170 if !slices.Contains(client.Config.GrantTypes, "refresh_token") {
171 return oauth2Error(c, http.StatusBadRequest, "unauthorized_client", "client not authorized for refresh_token grant")
172 }
173
174 // Validate refresh token
175 if req.RefreshToken == "" {
176 return oauth2Error(c, http.StatusBadRequest, "invalid_request", "refresh_token is required")
177 }
178
179 tokenHash := auth.HashToken(req.RefreshToken)
180 refreshToken, err := s.store.GetOAuth2RefreshToken(ctx, client.ClientID, tokenHash)
181 if err != nil {
182 return oauth2Error(c, http.StatusInternalServerError, "server_error", "failed to lookup refresh token")
183 }
184 if refreshToken == nil {
185 return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "invalid refresh token")
186 }
187
188 // Validate token belongs to this client BEFORE deleting
189 // This prevents DoS where attacker with stolen token invalidates it for legitimate client
190 if refreshToken.ClientID != client.ClientID {
191 return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "refresh token was not issued to this client")
192 }
193
194 // Validate not expired
195 if time.Now().After(refreshToken.ExpiresAt) {
196 // Delete expired token
197 if err := s.store.DeleteOAuth2RefreshToken(ctx, client.ClientID, tokenHash); err != nil {
198 slog.Warn("failed to delete expired OAuth2 refresh token", log.BBError(err))
199 }
200 return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "refresh token has expired")
201 }
202
203 // Delete token after validations pass (single use, will issue new one)
204 if err := s.store.DeleteOAuth2RefreshToken(ctx, client.ClientID, tokenHash); err != nil {
205 slog.Warn("failed to delete OAuth2 refresh token after use", log.BBError(err))
206 }
207
208 // Get user
209 user, err := s.store.GetUserByEmail(ctx, refreshToken.UserEmail)
210 if err != nil || user == nil {
211 return oauth2Error(c, http.StatusBadRequest, "invalid_grant", "user not found")
212 }
213
214 // Preserve the workspace binding from the refresh token. Fall back paths
215 // mirror the auth-code grant for pre-migration tokens. Membership is
216 // re-checked on every refresh so a user removed from the workspace
217 // after consent loses access at most one access-token lifetime later
218 // rather than waiting out the refresh token's 30-day expiry.
219 workspaceID, err := s.resolveBoundWorkspace(ctx, refreshToken.Workspace, client.Workspace, user.Email)
220 if err != nil {
221 return workspaceResolutionError(c, err)
222 }
223

Callers 1

handleTokenMethod · 0.95

Calls 9

resolveBoundWorkspaceMethod · 0.95
issueTokensMethod · 0.95
HashTokenFunction · 0.92
BBErrorFunction · 0.92
oauth2ErrorFunction · 0.85
workspaceResolutionErrorFunction · 0.85
GetOAuth2RefreshTokenMethod · 0.80
GetUserByEmailMethod · 0.80

Tested by

no test coverage detected