1818from starlette .responses import JSONResponse
1919from starlette .types import ASGIApp , Receive , Scope , Send
2020
21+ from .north_context import (
22+ DEFAULT_CONNECTOR_TOKENS_HEADER ,
23+ DEFAULT_SERVER_SECRET_HEADER ,
24+ DEFAULT_USER_ID_TOKEN_HEADER ,
25+ NORTH_CONTEXT_SCOPE_KEY ,
26+ NorthRequestContext ,
27+ decode_connector_tokens ,
28+ reset_north_request_context ,
29+ set_north_request_context ,
30+ )
2131
2232class AuthHeaderTokens (BaseModel ):
2333 server_secret : str | None
@@ -30,9 +40,15 @@ def __init__(
3040 self ,
3141 connector_access_tokens : dict [str , str ],
3242 email : str | None = None ,
43+ user_id_token : str | None = None ,
3344 ):
3445 self .connector_access_tokens = connector_access_tokens
3546 self .email = email
47+ self .user_id_token = user_id_token
48+ self .north_context = NorthRequestContext (
49+ user_id_token = user_id_token ,
50+ connector_tokens = connector_access_tokens ,
51+ )
3652
3753
3854class NorthAuthenticationMiddleware (AuthenticationMiddleware ):
@@ -153,16 +169,32 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send):
153169 return await self .app (scope , receive , send )
154170
155171 user = scope .get ("user" )
172+ existing_context = scope .get (NORTH_CONTEXT_SCOPE_KEY )
173+
174+ def store_context (context : NorthRequestContext ) -> None :
175+ scope [NORTH_CONTEXT_SCOPE_KEY ] = context
176+ state = scope .get ("state" )
177+ if state is None :
178+ scope ["state" ] = {"north_context" : context }
179+ elif isinstance (state , dict ):
180+ state ["north_context" ] = context
181+ else :
182+ setattr (state , "north_context" , context )
156183
157184 # For custom routes that don't require auth, user will be None
158185 if user is None :
159186 self .logger .debug (
160187 "Custom route accessed without authentication (operational endpoint)"
161188 )
189+ context = existing_context or NorthRequestContext ()
190+ store_context (context )
191+
192+ context_token = set_north_request_context (context )
162193 token = auth_context_var .set (None )
163194 try :
164195 await self .app (scope , receive , send )
165196 finally :
197+ reset_north_request_context (context_token )
166198 auth_context_var .reset (token )
167199 return
168200
@@ -179,10 +211,15 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send):
179211 list (user .connector_access_tokens .keys ()),
180212 )
181213
214+ context = existing_context or user .north_context
215+ store_context (context )
216+
217+ context_token = set_north_request_context (context )
182218 token = auth_context_var .set (user )
183219 try :
184220 await self .app (scope , receive , send )
185221 finally :
222+ reset_north_request_context (context_token )
186223 auth_context_var .reset (token )
187224
188225
@@ -211,26 +248,12 @@ def _has_x_north_headers(self, conn: HTTPConnection) -> bool:
211248 return any (
212249 conn .headers .get (header )
213250 for header in [
214- "X-North-ID-Token" ,
215- "X-North-Connector-Tokens" ,
216- "X-North-Server-Secret" ,
251+ DEFAULT_USER_ID_TOKEN_HEADER ,
252+ DEFAULT_CONNECTOR_TOKENS_HEADER ,
253+ DEFAULT_SERVER_SECRET_HEADER ,
217254 ]
218255 )
219256
220- def _parse_connector_tokens (self , header_value : str ) -> dict [str , str ]:
221- """Parse Base64 URL-safe encoded JSON connector tokens."""
222- try :
223- # Add padding if needed for Base64 decoding
224- padded = header_value + "=" * (4 - len (header_value ) % 4 )
225- decoded_json = base64 .urlsafe_b64decode (padded ).decode ()
226- tokens = json .loads (decoded_json )
227- if not isinstance (tokens , dict ):
228- raise ValueError ("Connector tokens must be a JSON object" )
229- return tokens
230- except Exception as e :
231- self .logger .debug ("Failed to parse connector tokens: %s" , e )
232- raise AuthenticationError ("invalid connector tokens format" )
233-
234257 def _validate_server_secret (self , provided_secret : str | None ) -> None :
235258 """Validate server secret matches expected value."""
236259 if self ._server_secret and self ._server_secret != provided_secret :
@@ -271,11 +294,16 @@ def _process_user_id_token(self, user_id_token: str | None) -> str | None:
271294 raise AuthenticationError ("invalid user id token" )
272295
273296 def _create_authenticated_user (
274- self , email : str | None , connector_access_tokens : dict [str , str ]
297+ self ,
298+ email : str | None ,
299+ connector_access_tokens : dict [str , str ],
300+ user_id_token : str | None ,
275301 ) -> tuple [AuthCredentials , AuthenticatedNorthUser ]:
276302 """Create authenticated user from validated tokens."""
277303 return AuthCredentials (), AuthenticatedNorthUser (
278- connector_access_tokens = connector_access_tokens , email = email
304+ connector_access_tokens = connector_access_tokens ,
305+ email = email ,
306+ user_id_token = user_id_token ,
279307 )
280308
281309 async def _authenticate_x_north_headers (
@@ -285,16 +313,25 @@ async def _authenticate_x_north_headers(
285313 self .logger .debug ("Using X-North headers for authentication" )
286314
287315 # Extract headers
288- user_id_token = conn .headers .get ("X-North-ID-Token" )
289- connector_tokens_header = conn .headers .get ("X-North-Connector-Tokens" )
290- server_secret = conn .headers .get ("X-North-Server-Secret" )
316+ user_id_token = conn .headers .get (DEFAULT_USER_ID_TOKEN_HEADER )
317+ connector_tokens_header = conn .headers .get (
318+ DEFAULT_CONNECTOR_TOKENS_HEADER
319+ )
320+ server_secret = conn .headers .get (DEFAULT_SERVER_SECRET_HEADER )
291321
292322 # Parse connector tokens (Base64 URL-safe encoded JSON)
293323 connector_access_tokens = {}
294324 if connector_tokens_header :
295- connector_access_tokens = self ._parse_connector_tokens (
296- connector_tokens_header
297- )
325+ try :
326+ connector_access_tokens = decode_connector_tokens (
327+ connector_tokens_header ,
328+ logger = self .logger ,
329+ raise_on_error = True ,
330+ )
331+ except ValueError as exc :
332+ raise AuthenticationError (
333+ "invalid connector tokens format"
334+ ) from exc
298335
299336 self .logger .debug (
300337 "X-North headers parsed. Has server_secret: %s, Has user_id_token: %s, Connector count: %d" ,
@@ -309,8 +346,16 @@ async def _authenticate_x_north_headers(
309346 self ._validate_server_secret (server_secret )
310347 email = self ._process_user_id_token (user_id_token )
311348
349+ context = NorthRequestContext (
350+ user_id_token = user_id_token ,
351+ connector_tokens = connector_access_tokens ,
352+ )
353+ conn .scope [NORTH_CONTEXT_SCOPE_KEY ] = context
354+
312355 self .logger .debug ("X-North authentication successful" )
313- return self ._create_authenticated_user (email , connector_access_tokens )
356+ return self ._create_authenticated_user (
357+ email , connector_access_tokens , user_id_token
358+ )
314359
315360 async def _authenticate_legacy_bearer (
316361 self , conn : HTTPConnection
@@ -358,9 +403,15 @@ async def _authenticate_legacy_bearer(
358403 self ._validate_server_secret (tokens .server_secret )
359404 email = self ._process_user_id_token (tokens .user_id_token )
360405
406+ context = NorthRequestContext (
407+ user_id_token = tokens .user_id_token ,
408+ connector_tokens = tokens .connector_access_tokens ,
409+ )
410+ conn .scope [NORTH_CONTEXT_SCOPE_KEY ] = context
411+
361412 self .logger .debug ("Legacy authentication successful" )
362413 return self ._create_authenticated_user (
363- email , tokens .connector_access_tokens
414+ email , tokens .connector_access_tokens , tokens . user_id_token
364415 )
365416
366417 async def authenticate (
0 commit comments