Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f0c063b
[core] Support token based auth in ray dashboard UI
Nov 3, 2025
0d3316c
fix lint issues
Nov 3, 2025
7660ade
address comments
Nov 3, 2025
5234bfe
fix lint
Nov 3, 2025
b30bf04
Merge remote-tracking branch 'upstream/master' into token_auth_8
Nov 4, 2025
f61c27f
fix typo
Nov 4, 2025
2da0b9b
[core] Configure an interceptor to pass auth token in python direct g…
Nov 4, 2025
1f1897d
fix lint issues
Nov 4, 2025
6aa4ce0
reduce code duplication
Nov 4, 2025
8186c09
empty commit
Nov 4, 2025
d90ffd3
[Core] Add Service Interceptor to support token authentication in das…
Nov 5, 2025
b464cd5
[core] Token auth usability improvements
Nov 5, 2025
29f511a
address comment
Nov 5, 2025
26de367
add test_grpc_authentication_server_interceptor to BUILD.bazel
Nov 5, 2025
03e5c52
Merge branch 'token_auth_10' into token_auth_11
sampan-s-nayak Nov 5, 2025
2b5e01b
fix typo
Nov 5, 2025
f38591d
Merge branch 'token_auth_10' into token_auth_11
sampan-s-nayak Nov 5, 2025
c018203
[core] use client interceptor for adding auth token in c++ client calls
Nov 6, 2025
d66af16
fix lint issues
Nov 6, 2025
fbcfea4
separate out intererceptor code
Nov 6, 2025
d59d8ea
Merge branch 'master' into token_auth_10
edoakes Nov 10, 2025
267b440
Merge branch 'token_auth_10' into token_auth_11
sampan-s-nayak Nov 11, 2025
4e85216
Merge branch 'master' into token_auth_10
sampan-s-nayak Nov 11, 2025
e0b8b93
Merge branch 'token_auth_10' into token_auth_11
sampan-s-nayak Nov 11, 2025
214b0e1
Merge branch 'token_auth_11' into token_auth_12
sampan-s-nayak Nov 11, 2025
1240a8c
empty commit
Nov 11, 2025
c857c11
Merge branch 'token_auth_10' into token_auth_11
sampan-s-nayak Nov 11, 2025
3e473ba
address comment
Nov 11, 2025
86f62f2
Merge branch 'token_auth_11' into token_auth_12
sampan-s-nayak Nov 11, 2025
6fe7473
Merge branch 'master' into token_auth_11
edoakes Nov 11, 2025
1b0b89b
Merge branch 'master' of https://github.com/ray-project/ray into toke…
edoakes Nov 11, 2025
0a473bf
Merge branch 'token_auth_11' into token_auth_12
sampan-s-nayak Nov 11, 2025
ab8ea56
Merge branch 'master' into token_auth_11
edoakes Nov 12, 2025
790150c
Merge branch 'token_auth_11' into token_auth_12
sampan-s-nayak Nov 12, 2025
215b625
[Core] support token auth in ray client server
Nov 12, 2025
4a0566b
fix lint
Nov 12, 2025
ca93ce0
Merge branch 'master' into token_auth_12
sampan-s-nayak Nov 12, 2025
99513f7
fix import after merge
Nov 12, 2025
0317fda
fix lint issues
Nov 12, 2025
a2f92ba
Merge branch 'token_auth_12' into token_auth_14
sampan-s-nayak Nov 12, 2025
fb3d6cf
fix imports
Nov 12, 2025
a7d5cee
Merge remote-tracking branch 'upstream/token_auth_14' into token_auth_14
Nov 12, 2025
d7428a0
Merge branch 'master' into token_auth_14
edoakes Nov 12, 2025
2fd9386
make grpc_utils imports lazy
Nov 12, 2025
417f575
add BUILD.bazel
Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def ensure_token_if_auth_enabled(
3. Generate and save a default token for new local clusters if one doesn't already exist.

Args:
system_config: Ray raises an error if you set auth_mode in system_config instead of the environment.
system_config: Ray raises an error if you set AUTH_MODE in system_config instead of the environment.
create_token_if_missing: Generate a new token if one doesn't already exist.

Raises:
Expand All @@ -79,11 +79,11 @@ def ensure_token_if_auth_enabled(
if get_authentication_mode() != AuthenticationMode.TOKEN:
if (
system_config
and "auth_mode" in system_config
and system_config["auth_mode"] != "disabled"
and "AUTH_MODE" in system_config
and system_config["AUTH_MODE"] != "disabled"
):
raise RuntimeError(
"Set authentication mode can only be set with the `RAY_auth_mode` environment variable, not using the system_config."
"Set authentication mode can only be set with the `RAY_AUTH_MODE` environment variable, not using the system_config."
)
return

Expand Down
2 changes: 1 addition & 1 deletion python/ray/_private/authentication/authentication_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def is_token_auth_enabled() -> bool:
"""Check if token authentication is enabled.

Returns:
bool: True if auth_mode is set to "token", False otherwise
bool: True if AUTH_MODE is set to "token", False otherwise
"""
if not _RAYLET_AVAILABLE:
return False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def _get_authentication_metadata_tuple() -> Tuple[Tuple[str, str], ...]:
return tuple((k, v) for k, v in headers.items())


class AuthenticationMetadataClientInterceptor(
class SyncAuthenticationMetadataClientInterceptor(
grpc.UnaryUnaryClientInterceptor,
grpc.UnaryStreamClientInterceptor,
grpc.StreamUnaryClientInterceptor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"""gRPC server interceptor for token-based authentication."""

import logging
from typing import Awaitable, Callable

import grpc
from grpc import aio as aiogrpc

from ray._private.authentication.authentication_constants import (
AUTHORIZATION_HEADER_NAME,
)
from ray._private.authentication.authentication_utils import (
is_token_auth_enabled,
validate_request_token,
)

logger = logging.getLogger(__name__)


def _authenticate_request(metadata: tuple) -> bool:
"""Authenticate incoming request. currently only supports token authentication.

Args:
metadata: gRPC metadata tuple of (key, value) pairs
Returns:
True if authentication succeeds or is not required, False otherwise
"""
if not is_token_auth_enabled():
return True

# Extract authorization header from metadata
auth_header = None
for key, value in metadata:
if key.lower() == AUTHORIZATION_HEADER_NAME:
auth_header = value
break

if not auth_header:
logger.warning("Authentication required but no authorization header provided")
return False

# Validate the token format and value
# validate_request_token returns bool (True if valid, False otherwise)
return validate_request_token(auth_header)


class AsyncAuthenticationServerInterceptor(aiogrpc.ServerInterceptor):
"""Async gRPC server interceptor that validates authentication tokens.

This interceptor checks the "authorization" metadata header for a valid
Bearer token when token authentication is enabled via RAY_AUTH_MODE=token.
If the token is missing or invalid, the request is rejected with UNAUTHENTICATED status.
"""

async def intercept_service(
self,
continuation: Callable[
[grpc.HandlerCallDetails], Awaitable[grpc.RpcMethodHandler]
],
handler_call_details: grpc.HandlerCallDetails,
) -> grpc.RpcMethodHandler:
"""Intercept service calls to validate authentication.

This method is called once per RPC to get the handler. We wrap the handler
to validate authentication before executing the actual RPC method.
"""
# Get the actual handler
handler = await continuation(handler_call_details)

if handler is None:
return None

# Wrap the RPC behavior with authentication check
def wrap_rpc_behavior(behavior):
"""Wrap an RPC method to validate authentication first."""
if behavior is None:
return None

async def wrapped(request_or_iterator, context):
if not _authenticate_request(context.invocation_metadata()):
await context.abort(
grpc.StatusCode.UNAUTHENTICATED,
"Invalid or missing authentication token",
)
return await behavior(request_or_iterator, context)

return wrapped

# Create a wrapper class that implements RpcMethodHandler interface
class AuthenticatedHandler:
"""Wrapper handler that validates authentication."""

def __init__(self, original_handler, wrapper_func):
self._original = original_handler
self._wrap = wrapper_func

@property
def request_streaming(self):
return self._original.request_streaming

@property
def response_streaming(self):
return self._original.response_streaming

@property
def request_deserializer(self):
return self._original.request_deserializer

@property
def response_serializer(self):
return self._original.response_serializer

@property
def unary_unary(self):
return self._wrap(self._original.unary_unary)

@property
def unary_stream(self):
return self._wrap(self._original.unary_stream)

@property
def stream_unary(self):
return self._wrap(self._original.stream_unary)

@property
def stream_stream(self):
return self._wrap(self._original.stream_stream)

return AuthenticatedHandler(handler, wrap_rpc_behavior)


class SyncAuthenticationServerInterceptor(grpc.ServerInterceptor):
"""Synchronous gRPC server interceptor that validates authentication tokens.

This interceptor checks the "authorization" metadata header for a valid
Bearer token when token authentication is enabled via RAY_AUTH_MODE=token.
If the token is missing or invalid, the request is rejected with UNAUTHENTICATED status.
"""

def intercept_service(
self,
continuation: Callable[[grpc.HandlerCallDetails], grpc.RpcMethodHandler],
handler_call_details: grpc.HandlerCallDetails,
) -> grpc.RpcMethodHandler:
"""Intercept service calls to validate authentication.

This method is called once per RPC to get the handler. We wrap the handler
to validate authentication before executing the actual RPC method.
"""
# Get the actual handler
handler = continuation(handler_call_details)

if handler is None:
return None

# Wrap the RPC behavior with authentication check
def wrap_rpc_behavior(behavior):
"""Wrap an RPC method to validate authentication first."""
if behavior is None:
return None

def wrapped(request_or_iterator, context):
if not _authenticate_request(context.invocation_metadata()):
context.abort(
grpc.StatusCode.UNAUTHENTICATED,
"Invalid or missing authentication token",
)
return behavior(request_or_iterator, context)

return wrapped

# Create a wrapper class that implements RpcMethodHandler interface
class AuthenticatedHandler:
"""Wrapper handler that validates authentication."""

def __init__(self, original_handler, wrapper_func):
self._original = original_handler
self._wrap = wrapper_func

@property
def request_streaming(self):
return self._original.request_streaming

@property
def response_streaming(self):
return self._original.response_streaming

@property
def request_deserializer(self):
return self._original.request_deserializer

@property
def response_serializer(self):
return self._original.response_serializer

@property
def unary_unary(self):
return self._wrap(self._original.unary_unary)

@property
def unary_stream(self):
return self._wrap(self._original.unary_stream)

@property
def stream_unary(self):
return self._wrap(self._original.stream_unary)

@property
def stream_stream(self):
return self._wrap(self._original.stream_stream)

return AuthenticatedHandler(handler, wrap_rpc_behavior)
2 changes: 1 addition & 1 deletion python/ray/_private/gcs_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def create_gcs_channel(address: str, aio=False):
Returns:
grpc.Channel or grpc.aio.Channel to GCS
"""
from ray._private.utils import init_grpc_channel
from ray._private.grpc_utils import init_grpc_channel

return init_grpc_channel(address, options=_GRPC_OPTIONS, asynchronous=aio)

Expand Down
Loading