From c6a452ffa805cbbe0b5333e834db991b6a9931c6 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 8 Oct 2025 13:49:39 +0900 Subject: [PATCH 1/5] Add doc for client metadata interceptor Signed-off-by: Anuraag Agrawal --- docs/interceptors.md | 66 ++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/docs/interceptors.md b/docs/interceptors.md index f722c6e..4625ec0 100644 --- a/docs/interceptors.md +++ b/docs/interceptors.md @@ -175,28 +175,40 @@ to be able to intercept RPC messages. However, many interceptors, such as for au tracing, only need access to headers and not messages. Connect provides a metadata interceptor protocol that can be implemented to work with any RPC type. -An authentication interceptor checking bearer tokens may look like this: +An authentication interceptor checking bearer tokens and storing them to a context variable may look like this: === "ASGI" ```python - class AuthInterceptor: + from contextvars import ContextVar, Token + + _auth_token = ContextVar["auth_token"]("current_auth_token") + + class ServerAuthInterceptor: def __init__(self, valid_tokens: list[str]): self._valid_tokens = valid_tokens - async def on_start(self, ctx: RequestContext): + async def on_start(self, ctx: RequestContext) -> Token["auth_token"]: authorization = ctx.request_headers().get("authorization") if not authorization or not authorization.startswith("Bearer "): raise ConnectError(Code.UNAUTHENTICATED) token = authorization[len("Bearer "):] if token not in valid_tokens: raise ConnectError(Code.PERMISSION_DENIED) + return _auth_token.set(token) + + async def on_end(self, token: Token["auth_token"], ctx: RequestContext): + _auth_token.reset(token) ``` === "WSGI" ```python - class AuthInterceptor: + from contextvars import ContextVar, Token + + _auth_token = ContextVar["auth_token"]("current_auth_token") + + class ServerAuthInterceptor: def __init__(self, valid_tokens: list[str]): self._valid_tokens = valid_tokens @@ -207,33 +219,47 @@ An authentication interceptor checking bearer tokens may look like this: token = authorization[len("Bearer "):] if token not in valid_tokens: raise ConnectError(Code.PERMISSION_DENIED) + return _auth_token.set(token) + + def on_end(self, token: Token["auth_token"], ctx: RequestContext): + _auth_token.reset(token) ``` -`on_start` can return any value, which is passed to the optional `on_end` method. This can be -used, for example, to record the time of execution for the method. +`on_start` can return any value, which is passed to the optional `on_end` method. Here, we +return the token to reset the context variable. -=== "ASGI" +Clients can add an interceptor that reads the token from the context variable and populates +the authorization header. + +=== "Async" ```python - import time + from contextvars import ContextVar, Token - class TimingInterceptor: - async def on_start(self, ctx: RequestContext) -> float: - return time.perf_counter() + _auth_token = ContextVar["auth_token"]("current_auth_token") - async def on_end(self, token: float, ctx: RequestContext): - print(f"Method took {} seconds.", token - time.perf_counter()) + class ClientAuthInterceptor: + async def on_start(self, ctx: RequestContext) -> Token["auth_token"]: + auth_token = _auth_token.get(None) + if auth_token: + ctx.request_headers()["authorization"] = f"Bearer {auth_token}" ``` -=== "WSGI" +=== "Sync" ```python - import time + from contextvars import ContextVar, Token - class TimingInterceptor: - def on_start(self, ctx: RequestContext): - return time.perf_counter() + _auth_token = ContextVar["auth_token"]("current_auth_token") - def on_end(self, token: float, ctx: RequestContext): - print(f"Method took {} seconds.", token - time.perf_counter()) + class ClientAuthInterceptor: + def on_start(self, ctx: RequestContext) -> Token["auth_token"]: + auth_token = _auth_token.get(None) + if auth_token: + ctx.request_headers()["authorization"] = f"Bearer {auth_token}" ``` + +Note that in the client interceptor, we do not need to define `on_end`. + +The above interceptors would allow a server to receive and validate an auth token and automatically +propagate it to the authorization header of backend calls. From 73572f7a058e5a2912465fa062f3a61b47917f89 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 8 Oct 2025 13:52:23 +0900 Subject: [PATCH 2/5] Fix type annotation Signed-off-by: Anuraag Agrawal --- docs/interceptors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/interceptors.md b/docs/interceptors.md index 4625ec0..53c0a2c 100644 --- a/docs/interceptors.md +++ b/docs/interceptors.md @@ -239,7 +239,7 @@ the authorization header. _auth_token = ContextVar["auth_token"]("current_auth_token") class ClientAuthInterceptor: - async def on_start(self, ctx: RequestContext) -> Token["auth_token"]: + async def on_start(self, ctx: RequestContext): auth_token = _auth_token.get(None) if auth_token: ctx.request_headers()["authorization"] = f"Bearer {auth_token}" @@ -253,7 +253,7 @@ the authorization header. _auth_token = ContextVar["auth_token"]("current_auth_token") class ClientAuthInterceptor: - def on_start(self, ctx: RequestContext) -> Token["auth_token"]: + def on_start(self, ctx: RequestContext): auth_token = _auth_token.get(None) if auth_token: ctx.request_headers()["authorization"] = f"Bearer {auth_token}" From cb03b29a5b88860c936f5a3277ff4788c7888158 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 8 Oct 2025 13:53:13 +0900 Subject: [PATCH 3/5] Fix import Signed-off-by: Anuraag Agrawal --- docs/interceptors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/interceptors.md b/docs/interceptors.md index 53c0a2c..f8b7a0b 100644 --- a/docs/interceptors.md +++ b/docs/interceptors.md @@ -234,7 +234,7 @@ the authorization header. === "Async" ```python - from contextvars import ContextVar, Token + from contextvars import ContextVar _auth_token = ContextVar["auth_token"]("current_auth_token") @@ -248,7 +248,7 @@ the authorization header. === "Sync" ```python - from contextvars import ContextVar, Token + from contextvars import ContextVar _auth_token = ContextVar["auth_token"]("current_auth_token") From a4bf7c0c2550ea49fabfa2fb5efe50c1a801a39d Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Wed, 8 Oct 2025 13:57:11 +0900 Subject: [PATCH 4/5] Fix type signature Signed-off-by: Anuraag Agrawal --- docs/interceptors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/interceptors.md b/docs/interceptors.md index f8b7a0b..16808d9 100644 --- a/docs/interceptors.md +++ b/docs/interceptors.md @@ -212,7 +212,7 @@ An authentication interceptor checking bearer tokens and storing them to a conte def __init__(self, valid_tokens: list[str]): self._valid_tokens = valid_tokens - def on_start(self, ctx: RequestContext): + def on_start(self, ctx: RequestContext) -> Token["auth_token"]: authorization = ctx.request_headers().get("authorization") if not authorization or not authorization.startswith("Bearer "): raise ConnectError(Code.UNAUTHENTICATED) From d81bece8969df89fe247b40a9b6453c3dfd1cf30 Mon Sep 17 00:00:00 2001 From: "Anuraag (Rag) Agrawal" Date: Thu, 9 Oct 2025 10:08:10 +0900 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Yasushi Itoh Signed-off-by: Anuraag (Rag) Agrawal --- docs/interceptors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/interceptors.md b/docs/interceptors.md index 16808d9..8f4da83 100644 --- a/docs/interceptors.md +++ b/docs/interceptors.md @@ -193,7 +193,7 @@ An authentication interceptor checking bearer tokens and storing them to a conte if not authorization or not authorization.startswith("Bearer "): raise ConnectError(Code.UNAUTHENTICATED) token = authorization[len("Bearer "):] - if token not in valid_tokens: + if token not in self._valid_tokens: raise ConnectError(Code.PERMISSION_DENIED) return _auth_token.set(token) @@ -217,7 +217,7 @@ An authentication interceptor checking bearer tokens and storing them to a conte if not authorization or not authorization.startswith("Bearer "): raise ConnectError(Code.UNAUTHENTICATED) token = authorization[len("Bearer "):] - if token not in valid_tokens: + if token not in self._valid_tokens: raise ConnectError(Code.PERMISSION_DENIED) return _auth_token.set(token)