diff --git a/python/ray/_private/authentication/authentication_constants.py b/python/ray/_private/authentication/authentication_constants.py index 77b090af7afe..eabb96e7bba8 100644 --- a/python/ray/_private/authentication/authentication_constants.py +++ b/python/ray/_private/authentication/authentication_constants.py @@ -24,6 +24,7 @@ AUTHORIZATION_HEADER_NAME = "authorization" AUTHORIZATION_BEARER_PREFIX = "Bearer " +RAY_AUTHORIZATION_HEADER_NAME = "x-ray-authorization" AUTHENTICATION_TOKEN_COOKIE_NAME = "ray-authentication-token" AUTHENTICATION_TOKEN_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 days diff --git a/python/ray/_private/authentication/http_token_authentication.py b/python/ray/_private/authentication/http_token_authentication.py index 58a49f5048eb..5c4b5114b4b9 100644 --- a/python/ray/_private/authentication/http_token_authentication.py +++ b/python/ray/_private/authentication/http_token_authentication.py @@ -43,12 +43,20 @@ async def token_auth_middleware(request, handler): ): return await handler(request) - # Check Authorization header first (for API clients) + # Try to get authentication token from multiple sources (in priority order): + # 1. Standard "Authorization" header (for API clients, SDKs) + # 2. Fallback "X-Ray-Authorization" header (for proxies and KubeRay) + # 3. Cookie (for web dashboard sessions) + auth_header = request.headers.get( authentication_constants.AUTHORIZATION_HEADER_NAME, "" ) - # If no Authorization header, check cookie (for web dashboard) + if not auth_header: + auth_header = request.headers.get( + authentication_constants.RAY_AUTHORIZATION_HEADER_NAME, "" + ) + if not auth_header: token = request.cookies.get( authentication_constants.AUTHENTICATION_TOKEN_COOKIE_NAME diff --git a/python/ray/dashboard/tests/test_dashboard_auth.py b/python/ray/dashboard/tests/test_dashboard_auth.py index 1df2a65d96f0..8ec89a46cfe8 100644 --- a/python/ray/dashboard/tests/test_dashboard_auth.py +++ b/python/ray/dashboard/tests/test_dashboard_auth.py @@ -50,6 +50,54 @@ def test_dashboard_request_requires_auth_invalid_token(setup_cluster_with_token_ assert response.status_code == 403 +def test_dashboard_request_with_ray_auth_header(setup_cluster_with_token_auth): + """Test that requests succeed with valid token in X-Ray-Authorization header.""" + + cluster_info = setup_cluster_with_token_auth + headers = {"X-Ray-Authorization": f"Bearer {cluster_info['token']}"} + + response = requests.get( + f"{cluster_info['dashboard_url']}/api/component_activities", + headers=headers, + ) + + assert response.status_code == 200 + + +def test_authorization_header_takes_precedence(setup_cluster_with_token_auth): + """Test that standard Authorization header takes precedence over X-Ray-Authorization.""" + + cluster_info = setup_cluster_with_token_auth + + # Provide both headers: valid token in Authorization, invalid in X-Ray-Authorization + headers = { + "Authorization": f"Bearer {cluster_info['token']}", + "X-Ray-Authorization": "Bearer invalid_token_000000000000000000000000", + } + + # Should succeed because Authorization header takes precedence + response = requests.get( + f"{cluster_info['dashboard_url']}/api/component_activities", + headers=headers, + ) + + assert response.status_code == 200 + + # Now test with invalid Authorization but valid X-Ray-Authorization + headers = { + "Authorization": "Bearer invalid_token_000000000000000000000000", + "X-Ray-Authorization": f"Bearer {cluster_info['token']}", + } + + # Should fail because Authorization header takes precedence (even though it's invalid) + response = requests.get( + f"{cluster_info['dashboard_url']}/api/component_activities", + headers=headers, + ) + + assert response.status_code == 403 + + def test_dashboard_auth_disabled(setup_cluster_without_token_auth): """Test that auth is not enforced when AUTH_MODE is disabled."""