ValidateAPIKey extracts and validates the API key from the request. It performs all security-critical checks: - Token extraction and parsing - Database lookup + secret hash validation - Expiry check - OIDC/OAuth token refresh (if applicable) - API key LastUsed / ExpiresAt DB updates - User role look
(ctx context.Context, cfg ValidateAPIKeyConfig, r *http.Request)
| 248 | // |
| 249 | // Returns (result, nil) on success or (nil, error) on failure. |
| 250 | func ValidateAPIKey(ctx context.Context, cfg ValidateAPIKeyConfig, r *http.Request) (*ValidateAPIKeyResult, *ValidateAPIKeyError) { |
| 251 | key, valErr := apiKeyFromRequestValidate(ctx, cfg.DB, cfg.SessionTokenFunc, r) |
| 252 | if valErr != nil { |
| 253 | return nil, valErr |
| 254 | } |
| 255 | |
| 256 | // Log the API key ID for all requests that have a valid key |
| 257 | // format and secret, regardless of whether subsequent validation |
| 258 | // (expiry, user status, etc.) succeeds. |
| 259 | if rl := loggermw.RequestLoggerFromContext(ctx); rl != nil { |
| 260 | rl.WithFields(slog.F("api_key_id", key.ID)) |
| 261 | } |
| 262 | |
| 263 | now := dbtime.Now() |
| 264 | if key.ExpiresAt.Before(now) { |
| 265 | return nil, &ValidateAPIKeyError{ |
| 266 | Code: http.StatusUnauthorized, |
| 267 | Response: codersdk.Response{ |
| 268 | Message: SignedOutErrorMessage, |
| 269 | Detail: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()), |
| 270 | }, |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | // Refresh OIDC/GitHub tokens if applicable. |
| 275 | if key.LoginType == database.LoginTypeGithub || key.LoginType == database.LoginTypeOIDC { |
| 276 | //nolint:gocritic // System needs to fetch UserLink to check if it's valid. |
| 277 | link, err := cfg.DB.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx), database.GetUserLinkByUserIDLoginTypeParams{ |
| 278 | UserID: key.UserID, |
| 279 | LoginType: key.LoginType, |
| 280 | }) |
| 281 | if errors.Is(err, sql.ErrNoRows) { |
| 282 | return nil, &ValidateAPIKeyError{ |
| 283 | Code: http.StatusUnauthorized, |
| 284 | Response: codersdk.Response{ |
| 285 | Message: SignedOutErrorMessage, |
| 286 | Detail: "You must re-authenticate with the login provider.", |
| 287 | }, |
| 288 | } |
| 289 | } |
| 290 | if err != nil { |
| 291 | return nil, &ValidateAPIKeyError{ |
| 292 | Code: http.StatusInternalServerError, |
| 293 | Response: codersdk.Response{ |
| 294 | Message: "A database error occurred", |
| 295 | Detail: fmt.Sprintf("get user link by user ID and login type: %s", err.Error()), |
| 296 | }, |
| 297 | Hard: true, |
| 298 | } |
| 299 | } |
| 300 | // Check if the OAuth token is expired. |
| 301 | if !link.OAuthExpiry.IsZero() && link.OAuthExpiry.Before(now) { |
| 302 | if cfg.OAuth2Configs.IsZero() { |
| 303 | return nil, &ValidateAPIKeyError{ |
| 304 | Code: http.StatusInternalServerError, |
| 305 | Response: codersdk.Response{ |
| 306 | Message: internalErrorMessage, |
| 307 | Detail: fmt.Sprintf("Unable to refresh OAuth token for login type %q. "+ |
no test coverage detected