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// SetTestOAuthSession sets the OAuth session in the context for testing purposes
305// This function should ONLY be used in tests to mock authenticated sessions
306func SetTestOAuthSession(ctx context.Context, session *oauthlib.ClientSessionData) context.Context {
307 ctx = context.WithValue(ctx, OAuthSessionKey, session)
308 if session != nil {
309 ctx = context.WithValue(ctx, UserAccessToken, session.AccessToken)
310 }
311 return ctx
312}
313
314// extractBearerToken extracts the token from a Bearer Authorization header.
315// HTTP auth schemes are case-insensitive per RFC 7235, so "Bearer", "bearer", "BEARER" are all valid.
316// Returns the token and true if valid Bearer scheme, empty string and false otherwise.
317func extractBearerToken(authHeader string) (string, bool) {
318 if authHeader == "" {
319 return "", false
320 }
321
322 // Split on first space: "Bearer <token>" -> ["Bearer", "<token>"]
323 parts := strings.SplitN(authHeader, " ", 2)
324 if len(parts) != 2 {
325 return "", false
326 }
327
328 // Case-insensitive scheme comparison per RFC 7235
329 if !strings.EqualFold(parts[0], "Bearer") {
330 return "", false
331 }
332
333 token := strings.TrimSpace(parts[1])
334 if token == "" {
335 return "", false
336 }
337
338 return token, true
339}
340
341// writeAuthError writes a JSON error response for authentication failures
342func writeAuthError(w http.ResponseWriter, message string) {
343 w.Header().Set("Content-Type", "application/json")
344 w.WriteHeader(http.StatusUnauthorized)
345 // Use json.NewEncoder to properly escape the message and prevent injection
346 if err := json.NewEncoder(w).Encode(map[string]string{
347 "error": "AuthenticationRequired",
348 "message": message,
349 }); err != nil {
350 log.Printf("Failed to write auth error response: %v", err)
351 }
352}
353
354// DualAuthMiddleware enforces authentication using either OAuth sealed tokens (for users),
355// PDS service JWTs (for aggregators), or API keys (for aggregators).
356type DualAuthMiddleware struct {
357 unsealer SessionUnsealer
358 store oauthlib.ClientAuthStore
359 serviceValidator ServiceAuthValidator
360 aggregatorChecker AggregatorChecker
361 apiKeyValidator APIKeyValidator // Optional: if nil, API key auth is disabled
362}
363
364// NewDualAuthMiddleware creates a new dual auth middleware that supports both OAuth and service JWT authentication.
365func NewDualAuthMiddleware(
366 unsealer SessionUnsealer,
367 store oauthlib.ClientAuthStore,
368 serviceValidator ServiceAuthValidator,
369 aggregatorChecker AggregatorChecker,
370) *DualAuthMiddleware {
371 return &DualAuthMiddleware{
372 unsealer: unsealer,
373 store: store,
374 serviceValidator: serviceValidator,
375 aggregatorChecker: aggregatorChecker,
376 }
377}
378
379// WithAPIKeyValidator adds API key validation support to the middleware.
380// Returns the middleware for method chaining.
381func (m *DualAuthMiddleware) WithAPIKeyValidator(validator APIKeyValidator) *DualAuthMiddleware {
382 m.apiKeyValidator = validator
383 return m
384}
385
386// RequireAuth middleware ensures the user is authenticated via either OAuth, service JWT, or API key.
387// Supports:
388// - API keys via Authorization: Bearer ckapi_... (aggregators only, checked first)
389// - OAuth sealed session tokens via Authorization: Bearer <sealed_token> or Cookie: coves_session=<sealed_token>
390// - Service JWTs via Authorization: Bearer <jwt>
391//
392// SECURITY: Service JWT and API key authentication are RESTRICTED to registered aggregators only.
393// Non-aggregator DIDs will be rejected even with valid JWT signatures or API keys.
394// This enforcement happens in handleServiceAuth() via aggregatorChecker.IsAggregator() and
395// in handleAPIKeyAuth() via apiKeyValidator.ValidateKey().
396//
397// If not authenticated, returns 401.
398// If authenticated, injects user DID and auth method into context.
399func (m *DualAuthMiddleware) RequireAuth(next http.Handler) http.Handler {
400 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
401 var token string
402 var tokenSource string
403
404 // Try Authorization header first (for mobile/API clients and service auth)
405 authHeader := r.Header.Get("Authorization")
406 if authHeader != "" {
407 var ok bool
408 token, ok = extractBearerToken(authHeader)
409 if !ok {
410 writeAuthError(w, "Invalid Authorization header format. Expected: Bearer <token>")
411 return
412 }
413 tokenSource = "header"
414 }
415
416 // If no header, try session cookie (for web clients - OAuth only)
417 if token == "" {
418 if cookie, err := r.Cookie("coves_session"); err == nil {
419 token = cookie.Value
420 tokenSource = "cookie"
421 }
422 }
423
424 // Must have authentication from either source
425 if token == "" {
426 writeAuthError(w, "Missing authentication")
427 return
428 }
429
430 log.Printf("[AUTH_TRACE] ip=%s method=%s path=%s token_source=%s",
431 r.RemoteAddr, r.Method, r.URL.Path, tokenSource)
432
433 // Check for API key first (before JWT/OAuth routing)
434 // API keys start with "ckapi_" prefix
435 if strings.HasPrefix(token, APIKeyPrefix) {
436 m.handleAPIKeyAuth(w, r, next, token)
437 return
438 }
439
440 // Detect token type and route to appropriate handler
441 if isJWTFormat(token) {
442 m.handleServiceAuth(w, r, next, token)
443 } else {
444 m.handleOAuthAuth(w, r, next, token)
445 }
446 })
447}
448
449// handleServiceAuth handles authentication using PDS service JWTs (aggregators only)
450func (m *DualAuthMiddleware) handleServiceAuth(w http.ResponseWriter, r *http.Request, next http.Handler, token string) {
451 // Validate the service JWT
452 // Note: lexMethod is nil, which allows any lexicon method (endpoint-agnostic validation).
453 // The ServiceAuthValidator skips the lexicon method check when lexMethod is nil.
454 // This is intentional - we want aggregators to authenticate globally, not per-endpoint.
455 did, err := m.serviceValidator.Validate(r.Context(), token, nil)
456 if err != nil {
457 log.Printf("[AUTH_FAILURE] type=service_jwt_invalid ip=%s method=%s path=%s error=%v",
458 r.RemoteAddr, r.Method, r.URL.Path, err)
459 writeAuthError(w, "Invalid or expired service JWT")
460 return
461 }
462
463 // Convert DID to string
464 didStr := did.String()
465
466 // Verify this DID is a registered aggregator
467 isAggregator, err := m.aggregatorChecker.IsAggregator(r.Context(), didStr)
468 if err != nil {
469 log.Printf("[AUTH_FAILURE] type=aggregator_check_failed ip=%s method=%s path=%s did=%s error=%v",
470 r.RemoteAddr, r.Method, r.URL.Path, didStr, err)
471 writeAuthError(w, "Failed to verify aggregator status")
472 return
473 }
474
475 if !isAggregator {
476 log.Printf("[AUTH_FAILURE] type=not_aggregator ip=%s method=%s path=%s did=%s",
477 r.RemoteAddr, r.Method, r.URL.Path, didStr)
478 writeAuthError(w, "Not a registered aggregator")
479 return
480 }
481
482 log.Printf("[AUTH_SUCCESS] type=service_jwt ip=%s method=%s path=%s did=%s",
483 r.RemoteAddr, r.Method, r.URL.Path, didStr)
484
485 // Inject DID and auth method into context
486 ctx := context.WithValue(r.Context(), UserDIDKey, didStr)
487 ctx = context.WithValue(ctx, IsAggregatorAuthKey, true)
488 ctx = context.WithValue(ctx, AuthMethodKey, AuthMethodServiceJWT)
489
490 // Call next handler
491 next.ServeHTTP(w, r.WithContext(ctx))
492}
493
494// handleAPIKeyAuth handles authentication using Coves API keys (aggregators only)
495func (m *DualAuthMiddleware) handleAPIKeyAuth(w http.ResponseWriter, r *http.Request, next http.Handler, token string) {
496 // Check if API key validation is enabled
497 if m.apiKeyValidator == nil {
498 log.Printf("[AUTH_FAILURE] type=api_key_disabled ip=%s method=%s path=%s",
499 r.RemoteAddr, r.Method, r.URL.Path)
500 writeAuthError(w, "API key authentication is not enabled")
501 return
502 }
503
504 // Validate the API key
505 aggregatorDID, err := m.apiKeyValidator.ValidateKey(r.Context(), token)
506 if err != nil {
507 log.Printf("[AUTH_FAILURE] type=api_key_invalid ip=%s method=%s path=%s error=%v",
508 r.RemoteAddr, r.Method, r.URL.Path, err)
509 writeAuthError(w, "Invalid or revoked API key")
510 return
511 }
512
513 // Refresh OAuth tokens if needed (for PDS operations)
514 if err := m.apiKeyValidator.RefreshTokensIfNeeded(r.Context(), aggregatorDID); err != nil {
515 log.Printf("[AUTH_FAILURE] type=token_refresh_failed ip=%s method=%s path=%s did=%s error=%v",
516 r.RemoteAddr, r.Method, r.URL.Path, aggregatorDID, err)
517 // Token refresh failure means the aggregator cannot perform authenticated PDS operations
518 // This is a critical failure - reject the request so the aggregator knows to re-authenticate
519 writeAuthError(w, "API key authentication failed: unable to refresh OAuth tokens. Please re-authenticate.")
520 return
521 }
522
523 log.Printf("[AUTH_SUCCESS] type=api_key ip=%s method=%s path=%s did=%s",
524 r.RemoteAddr, r.Method, r.URL.Path, aggregatorDID)
525
526 // Inject DID and auth method into context
527 ctx := context.WithValue(r.Context(), UserDIDKey, aggregatorDID)
528 ctx = context.WithValue(ctx, IsAggregatorAuthKey, true)
529 ctx = context.WithValue(ctx, AuthMethodKey, AuthMethodAPIKey)
530
531 // Call next handler
532 next.ServeHTTP(w, r.WithContext(ctx))
533}
534
535// handleOAuthAuth handles authentication using OAuth sealed session tokens (existing logic)
536func (m *DualAuthMiddleware) handleOAuthAuth(w http.ResponseWriter, r *http.Request, next http.Handler, token string) {
537 // Authenticate using sealed token
538 sealedSession, err := m.unsealer.UnsealSession(token)
539 if err != nil {
540 log.Printf("[AUTH_FAILURE] type=unseal_failed ip=%s method=%s path=%s error=%v",
541 r.RemoteAddr, r.Method, r.URL.Path, err)
542 writeAuthError(w, "Invalid or expired token")
543 return
544 }
545
546 // Parse DID
547 did, err := syntax.ParseDID(sealedSession.DID)
548 if err != nil {
549 log.Printf("[AUTH_FAILURE] type=invalid_did ip=%s method=%s path=%s did=%s error=%v",
550 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, err)
551 writeAuthError(w, "Invalid DID in token")
552 return
553 }
554
555 // Load full OAuth session from database
556 session, err := m.store.GetSession(r.Context(), did, sealedSession.SessionID)
557 if err != nil {
558 log.Printf("[AUTH_FAILURE] type=session_not_found ip=%s method=%s path=%s did=%s session_id=%s error=%v",
559 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, sealedSession.SessionID, err)
560 writeAuthError(w, "Session not found or expired")
561 return
562 }
563
564 // Verify session DID matches token DID
565 if session.AccountDID.String() != sealedSession.DID {
566 log.Printf("[AUTH_FAILURE] type=did_mismatch ip=%s method=%s path=%s token_did=%s session_did=%s",
567 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, session.AccountDID.String())
568 writeAuthError(w, "Session DID mismatch")
569 return
570 }
571
572 log.Printf("[AUTH_SUCCESS] type=oauth ip=%s method=%s path=%s did=%s session_id=%s",
573 r.RemoteAddr, r.Method, r.URL.Path, sealedSession.DID, sealedSession.SessionID)
574
575 // Inject user info and session into context
576 ctx := context.WithValue(r.Context(), UserDIDKey, sealedSession.DID)
577 ctx = context.WithValue(ctx, OAuthSessionKey, session)
578 ctx = context.WithValue(ctx, UserAccessToken, session.AccessToken)
579 ctx = context.WithValue(ctx, IsAggregatorAuthKey, false)
580 ctx = context.WithValue(ctx, AuthMethodKey, AuthMethodOAuth)
581
582 // Call next handler
583 next.ServeHTTP(w, r.WithContext(ctx))
584}
585
586// isJWTFormat checks if a token has JWT format (three parts separated by dots).
587// NOTE: This is a format heuristic for routing, not security validation.
588// Actual JWT signature verification happens in ServiceAuthValidator.Validate().
589func isJWTFormat(token string) bool {
590 parts := strings.Split(token, ".")
591 if len(parts) != 3 {
592 return false
593 }
594 // Ensure all parts are non-empty to prevent misrouting crafted tokens like "..""
595 return parts[0] != "" && parts[1] != "" && parts[2] != ""
596}
597
598// IsAggregatorAuth checks if the current request was authenticated using aggregator service JWT
599func IsAggregatorAuth(r *http.Request) bool {
600 val := r.Context().Value(IsAggregatorAuthKey)
601 isAggregator, ok := val.(bool)
602 if !ok && val != nil {
603 // SECURITY: Type assertion failed but value exists - this should never happen
604 // Log as error since this could indicate context value corruption
605 log.Printf("[AUTH_ERROR] IsAggregatorAuth: type assertion failed, expected bool, got %T (value: %v)",
606 val, val)
607 }
608 return isAggregator
609}
610
611// GetAuthMethod returns the authentication method used for the current request
612// Returns empty string if not authenticated
613func GetAuthMethod(r *http.Request) string {
614 val := r.Context().Value(AuthMethodKey)
615 method, ok := val.(string)
616 if !ok && val != nil {
617 // SECURITY: Type assertion failed but value exists - this should never happen
618 // Log as error since this could indicate context value corruption
619 log.Printf("[AUTH_ERROR] GetAuthMethod: type assertion failed, expected string, got %T (value: %v)",
620 val, val)
621 }
622 return method
623}