A community based topic aggregation platform built on atproto
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}