A community based topic aggregation platform built on atproto
at main 613 lines 22 kB view raw
1package middleware 2 3import ( 4 "Coves/internal/atproto/oauth" 5 "context" 6 "encoding/json" 7 "log" 8 "net/http" 9 "strings" 10 11 oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth" 12 "github.com/bluesky-social/indigo/atproto/syntax" 13) 14 15// Context keys for storing user information 16type contextKey string 17 18const ( 19 UserDIDKey contextKey = "user_did" 20 OAuthSessionKey contextKey = "oauth_session" 21 UserAccessToken contextKey = "user_access_token" // Backward compatibility: handlers/tests using GetUserAccessToken() 22 IsAggregatorAuthKey contextKey = "is_aggregator_auth" 23 AuthMethodKey contextKey = "auth_method" 24) 25 26// AuthMiddleware is an interface for authentication middleware 27// Both OAuthAuthMiddleware and DualAuthMiddleware implement this 28type AuthMiddleware interface { 29 RequireAuth(next http.Handler) http.Handler 30} 31 32// Auth method constants 33const ( 34 AuthMethodOAuth = "oauth" 35 AuthMethodServiceJWT = "service_jwt" 36 AuthMethodAPIKey = "api_key" 37) 38 39// API key prefix constant 40const APIKeyPrefix = "ckapi_" 41 42// SessionUnsealer is an interface for unsealing session tokens 43// This allows for mocking in tests 44type SessionUnsealer interface { 45 UnsealSession(token string) (*oauth.SealedSession, error) 46} 47 48// AggregatorChecker is an interface for checking if a DID is a registered aggregator 49type AggregatorChecker interface { 50 IsAggregator(ctx context.Context, did string) (bool, error) 51} 52 53// ServiceAuthValidator is an interface for validating service JWTs 54type ServiceAuthValidator interface { 55 Validate(ctx context.Context, tokenString string, lexMethod *syntax.NSID) (syntax.DID, error) 56} 57 58// APIKeyValidator is an interface for validating API keys (used by aggregators) 59type APIKeyValidator interface { 60 // ValidateKey validates an API key and returns the aggregator DID if valid 61 ValidateKey(ctx context.Context, plainKey string) (aggregatorDID string, err error) 62 // RefreshTokensIfNeeded refreshes OAuth tokens for the aggregator if they are expired 63 RefreshTokensIfNeeded(ctx context.Context, aggregatorDID string) error 64} 65 66// OAuthAuthMiddleware enforces OAuth authentication using sealed session tokens. 67type OAuthAuthMiddleware struct { 68 unsealer SessionUnsealer 69 store oauthlib.ClientAuthStore 70} 71 72// NewOAuthAuthMiddleware creates a new OAuth auth middleware using sealed session tokens. 73func NewOAuthAuthMiddleware(unsealer SessionUnsealer, store oauthlib.ClientAuthStore) *OAuthAuthMiddleware { 74 return &OAuthAuthMiddleware{ 75 unsealer: unsealer, 76 store: store, 77 } 78} 79 80// RequireAuth middleware ensures the user is authenticated. 81// Supports sealed session tokens via: 82// - Authorization: Bearer <sealed_token> 83// - Cookie: coves_session=<sealed_token> 84// 85// If not authenticated, returns 401. 86// If authenticated, injects user DID into context. 87func (m *OAuthAuthMiddleware) RequireAuth(next http.Handler) http.Handler { 88 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 89 var token string 90 91 // Try Authorization header first (for mobile/API clients) 92 authHeader := r.Header.Get("Authorization") 93 if authHeader != "" { 94 var ok bool 95 token, ok = extractBearerToken(authHeader) 96 if !ok { 97 writeAuthError(w, "Invalid Authorization header format. Expected: Bearer <token>") 98 return 99 } 100 } 101 102 // If no header, try session cookie (for web clients) 103 if token == "" { 104 if cookie, err := r.Cookie("coves_session"); err == nil { 105 token = cookie.Value 106 } 107 } 108 109 // Must have authentication from either source 110 if token == "" { 111 writeAuthError(w, "Missing authentication") 112 return 113 } 114 115 // Authenticate using sealed token 116 sealedSession, err := m.unsealer.UnsealSession(token) 117 if err != nil { 118 log.Printf("[AUTH_FAILURE] type=unseal_failed ip=%s method=%s path=%s error=%v", 119 r.RemoteAddr, r.Method, r.URL.Path, err) 120 writeAuthError(w, "Invalid or expired token") 121 return 122 } 123 124 // Parse DID 125 did, err := syntax.ParseDID(sealedSession.DID) 126 if err != nil { 127 log.Printf("[AUTH_FAILURE] type=invalid_did ip=%s method=%s path=%s did=%s error=%v", 128 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, err) 129 writeAuthError(w, "Invalid DID in token") 130 return 131 } 132 133 // Load full OAuth session from database 134 session, err := m.store.GetSession(r.Context(), did, sealedSession.SessionID) 135 if err != nil { 136 log.Printf("[AUTH_FAILURE] type=session_not_found ip=%s method=%s path=%s did=%s session_id=%s error=%v", 137 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, sealedSession.SessionID, err) 138 writeAuthError(w, "Session not found or expired") 139 return 140 } 141 142 // Verify session DID matches token DID 143 if session.AccountDID.String() != sealedSession.DID { 144 log.Printf("[AUTH_FAILURE] type=did_mismatch ip=%s method=%s path=%s token_did=%s session_did=%s", 145 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, session.AccountDID.String()) 146 writeAuthError(w, "Session DID mismatch") 147 return 148 } 149 150 log.Printf("[AUTH_SUCCESS] ip=%s method=%s path=%s did=%s session_id=%s", 151 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, sealedSession.SessionID) 152 153 // Inject user info and session into context 154 ctx := context.WithValue(r.Context(), UserDIDKey, sealedSession.DID) 155 ctx = context.WithValue(ctx, OAuthSessionKey, session) 156 // Store access token for backward compatibility 157 ctx = context.WithValue(ctx, UserAccessToken, session.AccessToken) 158 159 // Call next handler 160 next.ServeHTTP(w, r.WithContext(ctx)) 161 }) 162} 163 164// OptionalAuth middleware loads user info if authenticated, but doesn't require it. 165// Useful for endpoints that work for both authenticated and anonymous users. 166// 167// Supports sealed session tokens via: 168// - Authorization: Bearer <sealed_token> 169// - Cookie: coves_session=<sealed_token> 170// 171// If authentication fails, continues without user context (does not return error). 172func (m *OAuthAuthMiddleware) OptionalAuth(next http.Handler) http.Handler { 173 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 174 var token string 175 176 // Try Authorization header first (for mobile/API clients) 177 authHeader := r.Header.Get("Authorization") 178 if authHeader != "" { 179 var ok bool 180 token, ok = extractBearerToken(authHeader) 181 if !ok { 182 // Invalid format - continue without user context 183 next.ServeHTTP(w, r) 184 return 185 } 186 } 187 188 // If no header, try session cookie (for web clients) 189 if token == "" { 190 if cookie, err := r.Cookie("coves_session"); err == nil { 191 token = cookie.Value 192 } 193 } 194 195 // If still no token, continue without authentication 196 if token == "" { 197 next.ServeHTTP(w, r) 198 return 199 } 200 201 // Try to authenticate (don't write errors, just continue without user context on failure) 202 sealedSession, err := m.unsealer.UnsealSession(token) 203 if err != nil { 204 next.ServeHTTP(w, r) 205 return 206 } 207 208 // Parse DID 209 did, err := syntax.ParseDID(sealedSession.DID) 210 if err != nil { 211 log.Printf("[AUTH_WARNING] Optional auth: invalid DID: %v", err) 212 next.ServeHTTP(w, r) 213 return 214 } 215 216 // Load full OAuth session from database 217 session, err := m.store.GetSession(r.Context(), did, sealedSession.SessionID) 218 if err != nil { 219 log.Printf("[AUTH_WARNING] Optional auth: session not found: %v", err) 220 next.ServeHTTP(w, r) 221 return 222 } 223 224 // Verify session DID matches token DID 225 if session.AccountDID.String() != sealedSession.DID { 226 log.Printf("[AUTH_WARNING] Optional auth: DID mismatch") 227 next.ServeHTTP(w, r) 228 return 229 } 230 231 // Build authenticated context 232 ctx := context.WithValue(r.Context(), UserDIDKey, sealedSession.DID) 233 ctx = context.WithValue(ctx, OAuthSessionKey, session) 234 ctx = context.WithValue(ctx, UserAccessToken, session.AccessToken) 235 236 next.ServeHTTP(w, r.WithContext(ctx)) 237 }) 238} 239 240// GetUserDID extracts the user's DID from the request context 241// Returns empty string if not authenticated 242func GetUserDID(r *http.Request) string { 243 val := r.Context().Value(UserDIDKey) 244 did, ok := val.(string) 245 if !ok && val != nil { 246 // SECURITY: Type assertion failed but value exists - this should never happen 247 // Log as error since this could indicate context value corruption 248 log.Printf("[AUTH_ERROR] GetUserDID: type assertion failed, expected string, got %T (value: %v)", 249 val, val) 250 } 251 return did 252} 253 254// GetAuthenticatedDID extracts the authenticated user's DID from the context 255// This is used by service layers for defense-in-depth validation 256// Returns empty string if not authenticated 257func GetAuthenticatedDID(ctx context.Context) string { 258 val := ctx.Value(UserDIDKey) 259 did, ok := val.(string) 260 if !ok && val != nil { 261 // SECURITY: Type assertion failed but value exists - this should never happen 262 // Log as error since this could indicate context value corruption 263 log.Printf("[AUTH_ERROR] GetAuthenticatedDID: type assertion failed, expected string, got %T (value: %v)", 264 val, val) 265 } 266 return did 267} 268 269// GetOAuthSession extracts the OAuth session from the request context 270// Returns nil if not authenticated 271// Handlers can use this to make authenticated PDS calls 272func GetOAuthSession(r *http.Request) *oauthlib.ClientSessionData { 273 val := r.Context().Value(OAuthSessionKey) 274 session, ok := val.(*oauthlib.ClientSessionData) 275 if !ok && val != nil { 276 // SECURITY: Type assertion failed but value exists - this should never happen 277 // Log as error since this could indicate context value corruption 278 log.Printf("[AUTH_ERROR] GetOAuthSession: type assertion failed, expected *ClientSessionData, got %T", 279 val) 280 } 281 return session 282} 283 284// GetUserAccessToken extracts the user's access token from the request context 285// Returns empty string if not authenticated 286func GetUserAccessToken(r *http.Request) string { 287 val := r.Context().Value(UserAccessToken) 288 token, ok := val.(string) 289 if !ok && val != nil { 290 // SECURITY: Type assertion failed but value exists - this should never happen 291 // Log as error since this could indicate context value corruption 292 log.Printf("[AUTH_ERROR] GetUserAccessToken: type assertion failed, expected string, got %T (value: %v)", 293 val, val) 294 } 295 return token 296} 297 298// SetTestUserDID sets the user DID in the context for testing purposes 299// This function should ONLY be used in tests to mock authenticated users 300func SetTestUserDID(ctx context.Context, userDID string) context.Context { 301 return context.WithValue(ctx, UserDIDKey, userDID) 302} 303 304// extractBearerToken extracts the token from a Bearer Authorization header. 305// HTTP auth schemes are case-insensitive per RFC 7235, so "Bearer", "bearer", "BEARER" are all valid. 306// Returns the token and true if valid Bearer scheme, empty string and false otherwise. 307func extractBearerToken(authHeader string) (string, bool) { 308 if authHeader == "" { 309 return "", false 310 } 311 312 // Split on first space: "Bearer <token>" -> ["Bearer", "<token>"] 313 parts := strings.SplitN(authHeader, " ", 2) 314 if len(parts) != 2 { 315 return "", false 316 } 317 318 // Case-insensitive scheme comparison per RFC 7235 319 if !strings.EqualFold(parts[0], "Bearer") { 320 return "", false 321 } 322 323 token := strings.TrimSpace(parts[1]) 324 if token == "" { 325 return "", false 326 } 327 328 return token, true 329} 330 331// writeAuthError writes a JSON error response for authentication failures 332func writeAuthError(w http.ResponseWriter, message string) { 333 w.Header().Set("Content-Type", "application/json") 334 w.WriteHeader(http.StatusUnauthorized) 335 // Use json.NewEncoder to properly escape the message and prevent injection 336 if err := json.NewEncoder(w).Encode(map[string]string{ 337 "error": "AuthenticationRequired", 338 "message": message, 339 }); err != nil { 340 log.Printf("Failed to write auth error response: %v", err) 341 } 342} 343 344// DualAuthMiddleware enforces authentication using either OAuth sealed tokens (for users), 345// PDS service JWTs (for aggregators), or API keys (for aggregators). 346type DualAuthMiddleware struct { 347 unsealer SessionUnsealer 348 store oauthlib.ClientAuthStore 349 serviceValidator ServiceAuthValidator 350 aggregatorChecker AggregatorChecker 351 apiKeyValidator APIKeyValidator // Optional: if nil, API key auth is disabled 352} 353 354// NewDualAuthMiddleware creates a new dual auth middleware that supports both OAuth and service JWT authentication. 355func NewDualAuthMiddleware( 356 unsealer SessionUnsealer, 357 store oauthlib.ClientAuthStore, 358 serviceValidator ServiceAuthValidator, 359 aggregatorChecker AggregatorChecker, 360) *DualAuthMiddleware { 361 return &DualAuthMiddleware{ 362 unsealer: unsealer, 363 store: store, 364 serviceValidator: serviceValidator, 365 aggregatorChecker: aggregatorChecker, 366 } 367} 368 369// WithAPIKeyValidator adds API key validation support to the middleware. 370// Returns the middleware for method chaining. 371func (m *DualAuthMiddleware) WithAPIKeyValidator(validator APIKeyValidator) *DualAuthMiddleware { 372 m.apiKeyValidator = validator 373 return m 374} 375 376// RequireAuth middleware ensures the user is authenticated via either OAuth, service JWT, or API key. 377// Supports: 378// - API keys via Authorization: Bearer ckapi_... (aggregators only, checked first) 379// - OAuth sealed session tokens via Authorization: Bearer <sealed_token> or Cookie: coves_session=<sealed_token> 380// - Service JWTs via Authorization: Bearer <jwt> 381// 382// SECURITY: Service JWT and API key authentication are RESTRICTED to registered aggregators only. 383// Non-aggregator DIDs will be rejected even with valid JWT signatures or API keys. 384// This enforcement happens in handleServiceAuth() via aggregatorChecker.IsAggregator() and 385// in handleAPIKeyAuth() via apiKeyValidator.ValidateKey(). 386// 387// If not authenticated, returns 401. 388// If authenticated, injects user DID and auth method into context. 389func (m *DualAuthMiddleware) RequireAuth(next http.Handler) http.Handler { 390 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 391 var token string 392 var tokenSource string 393 394 // Try Authorization header first (for mobile/API clients and service auth) 395 authHeader := r.Header.Get("Authorization") 396 if authHeader != "" { 397 var ok bool 398 token, ok = extractBearerToken(authHeader) 399 if !ok { 400 writeAuthError(w, "Invalid Authorization header format. Expected: Bearer <token>") 401 return 402 } 403 tokenSource = "header" 404 } 405 406 // If no header, try session cookie (for web clients - OAuth only) 407 if token == "" { 408 if cookie, err := r.Cookie("coves_session"); err == nil { 409 token = cookie.Value 410 tokenSource = "cookie" 411 } 412 } 413 414 // Must have authentication from either source 415 if token == "" { 416 writeAuthError(w, "Missing authentication") 417 return 418 } 419 420 log.Printf("[AUTH_TRACE] ip=%s method=%s path=%s token_source=%s", 421 r.RemoteAddr, r.Method, r.URL.Path, tokenSource) 422 423 // Check for API key first (before JWT/OAuth routing) 424 // API keys start with "ckapi_" prefix 425 if strings.HasPrefix(token, APIKeyPrefix) { 426 m.handleAPIKeyAuth(w, r, next, token) 427 return 428 } 429 430 // Detect token type and route to appropriate handler 431 if isJWTFormat(token) { 432 m.handleServiceAuth(w, r, next, token) 433 } else { 434 m.handleOAuthAuth(w, r, next, token) 435 } 436 }) 437} 438 439// handleServiceAuth handles authentication using PDS service JWTs (aggregators only) 440func (m *DualAuthMiddleware) handleServiceAuth(w http.ResponseWriter, r *http.Request, next http.Handler, token string) { 441 // Validate the service JWT 442 // Note: lexMethod is nil, which allows any lexicon method (endpoint-agnostic validation). 443 // The ServiceAuthValidator skips the lexicon method check when lexMethod is nil. 444 // This is intentional - we want aggregators to authenticate globally, not per-endpoint. 445 did, err := m.serviceValidator.Validate(r.Context(), token, nil) 446 if err != nil { 447 log.Printf("[AUTH_FAILURE] type=service_jwt_invalid ip=%s method=%s path=%s error=%v", 448 r.RemoteAddr, r.Method, r.URL.Path, err) 449 writeAuthError(w, "Invalid or expired service JWT") 450 return 451 } 452 453 // Convert DID to string 454 didStr := did.String() 455 456 // Verify this DID is a registered aggregator 457 isAggregator, err := m.aggregatorChecker.IsAggregator(r.Context(), didStr) 458 if err != nil { 459 log.Printf("[AUTH_FAILURE] type=aggregator_check_failed ip=%s method=%s path=%s did=%s error=%v", 460 r.RemoteAddr, r.Method, r.URL.Path, didStr, err) 461 writeAuthError(w, "Failed to verify aggregator status") 462 return 463 } 464 465 if !isAggregator { 466 log.Printf("[AUTH_FAILURE] type=not_aggregator ip=%s method=%s path=%s did=%s", 467 r.RemoteAddr, r.Method, r.URL.Path, didStr) 468 writeAuthError(w, "Not a registered aggregator") 469 return 470 } 471 472 log.Printf("[AUTH_SUCCESS] type=service_jwt ip=%s method=%s path=%s did=%s", 473 r.RemoteAddr, r.Method, r.URL.Path, didStr) 474 475 // Inject DID and auth method into context 476 ctx := context.WithValue(r.Context(), UserDIDKey, didStr) 477 ctx = context.WithValue(ctx, IsAggregatorAuthKey, true) 478 ctx = context.WithValue(ctx, AuthMethodKey, AuthMethodServiceJWT) 479 480 // Call next handler 481 next.ServeHTTP(w, r.WithContext(ctx)) 482} 483 484// handleAPIKeyAuth handles authentication using Coves API keys (aggregators only) 485func (m *DualAuthMiddleware) handleAPIKeyAuth(w http.ResponseWriter, r *http.Request, next http.Handler, token string) { 486 // Check if API key validation is enabled 487 if m.apiKeyValidator == nil { 488 log.Printf("[AUTH_FAILURE] type=api_key_disabled ip=%s method=%s path=%s", 489 r.RemoteAddr, r.Method, r.URL.Path) 490 writeAuthError(w, "API key authentication is not enabled") 491 return 492 } 493 494 // Validate the API key 495 aggregatorDID, err := m.apiKeyValidator.ValidateKey(r.Context(), token) 496 if err != nil { 497 log.Printf("[AUTH_FAILURE] type=api_key_invalid ip=%s method=%s path=%s error=%v", 498 r.RemoteAddr, r.Method, r.URL.Path, err) 499 writeAuthError(w, "Invalid or revoked API key") 500 return 501 } 502 503 // Refresh OAuth tokens if needed (for PDS operations) 504 if err := m.apiKeyValidator.RefreshTokensIfNeeded(r.Context(), aggregatorDID); err != nil { 505 log.Printf("[AUTH_FAILURE] type=token_refresh_failed ip=%s method=%s path=%s did=%s error=%v", 506 r.RemoteAddr, r.Method, r.URL.Path, aggregatorDID, err) 507 // Token refresh failure means the aggregator cannot perform authenticated PDS operations 508 // This is a critical failure - reject the request so the aggregator knows to re-authenticate 509 writeAuthError(w, "API key authentication failed: unable to refresh OAuth tokens. Please re-authenticate.") 510 return 511 } 512 513 log.Printf("[AUTH_SUCCESS] type=api_key ip=%s method=%s path=%s did=%s", 514 r.RemoteAddr, r.Method, r.URL.Path, aggregatorDID) 515 516 // Inject DID and auth method into context 517 ctx := context.WithValue(r.Context(), UserDIDKey, aggregatorDID) 518 ctx = context.WithValue(ctx, IsAggregatorAuthKey, true) 519 ctx = context.WithValue(ctx, AuthMethodKey, AuthMethodAPIKey) 520 521 // Call next handler 522 next.ServeHTTP(w, r.WithContext(ctx)) 523} 524 525// handleOAuthAuth handles authentication using OAuth sealed session tokens (existing logic) 526func (m *DualAuthMiddleware) handleOAuthAuth(w http.ResponseWriter, r *http.Request, next http.Handler, token string) { 527 // Authenticate using sealed token 528 sealedSession, err := m.unsealer.UnsealSession(token) 529 if err != nil { 530 log.Printf("[AUTH_FAILURE] type=unseal_failed ip=%s method=%s path=%s error=%v", 531 r.RemoteAddr, r.Method, r.URL.Path, err) 532 writeAuthError(w, "Invalid or expired token") 533 return 534 } 535 536 // Parse DID 537 did, err := syntax.ParseDID(sealedSession.DID) 538 if err != nil { 539 log.Printf("[AUTH_FAILURE] type=invalid_did ip=%s method=%s path=%s did=%s error=%v", 540 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, err) 541 writeAuthError(w, "Invalid DID in token") 542 return 543 } 544 545 // Load full OAuth session from database 546 session, err := m.store.GetSession(r.Context(), did, sealedSession.SessionID) 547 if err != nil { 548 log.Printf("[AUTH_FAILURE] type=session_not_found ip=%s method=%s path=%s did=%s session_id=%s error=%v", 549 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, sealedSession.SessionID, err) 550 writeAuthError(w, "Session not found or expired") 551 return 552 } 553 554 // Verify session DID matches token DID 555 if session.AccountDID.String() != sealedSession.DID { 556 log.Printf("[AUTH_FAILURE] type=did_mismatch ip=%s method=%s path=%s token_did=%s session_did=%s", 557 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, session.AccountDID.String()) 558 writeAuthError(w, "Session DID mismatch") 559 return 560 } 561 562 log.Printf("[AUTH_SUCCESS] type=oauth ip=%s method=%s path=%s did=%s session_id=%s", 563 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, sealedSession.SessionID) 564 565 // Inject user info and session into context 566 ctx := context.WithValue(r.Context(), UserDIDKey, sealedSession.DID) 567 ctx = context.WithValue(ctx, OAuthSessionKey, session) 568 ctx = context.WithValue(ctx, UserAccessToken, session.AccessToken) 569 ctx = context.WithValue(ctx, IsAggregatorAuthKey, false) 570 ctx = context.WithValue(ctx, AuthMethodKey, AuthMethodOAuth) 571 572 // Call next handler 573 next.ServeHTTP(w, r.WithContext(ctx)) 574} 575 576// isJWTFormat checks if a token has JWT format (three parts separated by dots). 577// NOTE: This is a format heuristic for routing, not security validation. 578// Actual JWT signature verification happens in ServiceAuthValidator.Validate(). 579func isJWTFormat(token string) bool { 580 parts := strings.Split(token, ".") 581 if len(parts) != 3 { 582 return false 583 } 584 // Ensure all parts are non-empty to prevent misrouting crafted tokens like ".."" 585 return parts[0] != "" && parts[1] != "" && parts[2] != "" 586} 587 588// IsAggregatorAuth checks if the current request was authenticated using aggregator service JWT 589func IsAggregatorAuth(r *http.Request) bool { 590 val := r.Context().Value(IsAggregatorAuthKey) 591 isAggregator, ok := val.(bool) 592 if !ok && val != nil { 593 // SECURITY: Type assertion failed but value exists - this should never happen 594 // Log as error since this could indicate context value corruption 595 log.Printf("[AUTH_ERROR] IsAggregatorAuth: type assertion failed, expected bool, got %T (value: %v)", 596 val, val) 597 } 598 return isAggregator 599} 600 601// GetAuthMethod returns the authentication method used for the current request 602// Returns empty string if not authenticated 603func GetAuthMethod(r *http.Request) string { 604 val := r.Context().Value(AuthMethodKey) 605 method, ok := val.(string) 606 if !ok && val != nil { 607 // SECURITY: Type assertion failed but value exists - this should never happen 608 // Log as error since this could indicate context value corruption 609 log.Printf("[AUTH_ERROR] GetAuthMethod: type assertion failed, expected string, got %T (value: %v)", 610 val, val) 611 } 612 return method 613}