A community based topic aggregation platform built on atproto
at main 623 lines 23 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// 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}