From 50e4db11ef8ffa5590af206e6204d24f7c1d9892 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 11 May 2026 23:14:04 +0200 Subject: [PATCH 1/3] Fix #673 --- CHANGELOG.md | 4 + blacksheep/__init__.py | 2 +- blacksheep/server/authentication/oidc.py | 17 ++-- tests/test_auth.py | 121 +++++++++++++++++++++++ 4 files changed, 136 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e14f08b..98721f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.6.3] - 2026-05-?? +- Fix [#673](https://github.com/Neoteroi/BlackSheep/issues/673): `JWTOpenIDTokensHandler.authenticate` + was discarding the `Identity` returned by the inner `auth_handler`, causing + `request.identity` to always be empty. Also fixes the refresh token not being + attached to the identity when a refresh token header is present. - Fix [#675](https://github.com/Neoteroi/BlackSheep/issues/675): fix `OverflowError` when serving large files inefficiently; `get_chunks` in `scribe.pyx` used a C `int` loop variable that overflows for responses larger than ~2 GB. Changed to `Py_ssize_t`. diff --git a/blacksheep/__init__.py b/blacksheep/__init__.py index ae2371d2..aa0d0fc3 100644 --- a/blacksheep/__init__.py +++ b/blacksheep/__init__.py @@ -4,7 +4,7 @@ """ __author__ = "Roberto Prevato " -__version__ = "2.6.2" +__version__ = "2.6.3" from .contents import Content as Content from .contents import FileBuffer as FileBuffer diff --git a/blacksheep/server/authentication/oidc.py b/blacksheep/server/authentication/oidc.py index 85a70c82..c24ad945 100644 --- a/blacksheep/server/authentication/oidc.py +++ b/blacksheep/server/authentication/oidc.py @@ -667,7 +667,9 @@ def _get_refresh_token_header_name(self) -> bytes: def protect_refresh_token(self, refresh_token: str) -> str: return self._serializer.dumps(refresh_token) # type: ignore - def restore_refresh_token(self, context: Request) -> None: + def restore_refresh_token( + self, context: Request, identity: Identity | None = None + ) -> Identity | None: refresh_token_header = context.get_first_header( self._get_refresh_token_header_name() ) @@ -681,14 +683,15 @@ def restore_refresh_token(self, context: Request) -> None: self.refresh_token_key, ) else: - if context.user is None: - context.user = Identity() - context.user.refresh_token = value + if identity is None: + identity = Identity() + identity.refresh_token = value - async def authenticate(self, context: Request) -> Identity | None: - await self.auth_handler.authenticate(context) + return identity - self.restore_refresh_token(context) + async def authenticate(self, context: Request) -> Identity | None: + identity = await self.auth_handler.authenticate(context) + return self.restore_refresh_token(context, identity) class TokenType(Enum): diff --git a/tests/test_auth.py b/tests/test_auth.py index fb8bc6a3..0c51cc10 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1017,3 +1017,124 @@ async def home(request): # endregion + + +# region JWTOpenIDTokensHandler + + +async def test_jwt_openid_tokens_handler_authenticate_returns_identity( + app, symmetric_secret +): + """ + Verifies that JWTOpenIDTokensHandler.authenticate returns the identity from the + inner auth_handler instead of discarding it (regression test for issue #673). + """ + from blacksheep.server.authentication.oidc import JWTOpenIDTokensHandler + + jwt_auth = JWTBearerAuthentication( + valid_audiences=["test-audience"], + valid_issuers=["test-issuer"], + secret_key=symmetric_secret, + ) + + app.use_authentication().add(JWTOpenIDTokensHandler(jwt_auth)) + + identity: Identity | None = None + + @app.router.get("/") + async def home(request): + nonlocal identity + identity = request.user + return None + + access_token = get_symmetric_token( + symmetric_secret.get_value(), + { + "aud": "test-audience", + "iss": "test-issuer", + "sub": "user123", + "name": "Test User", + "exp": 9999999999, + }, + ) + + await app( + get_example_scope( + "GET", + "/", + extra_headers=[(b"Authorization", b"Bearer " + access_token.encode())], + ), + MockReceive(), + MockSend(), + ) + + assert app.response is not None + assert app.response.status == 204 + assert identity is not None + assert identity.is_authenticated() is True + assert identity["sub"] == "user123" + + +async def test_jwt_openid_tokens_handler_authenticate_with_refresh_token( + app, symmetric_secret +): + """ + Verifies that JWTOpenIDTokensHandler.authenticate restores the refresh token + on the returned identity when the refresh token header is present. + """ + from blacksheep.server.authentication.oidc import ( + JWTOpenIDTokensHandler, + HTMLStorageType, + ) + from itsdangerous import URLSafeSerializer + + jwt_auth = JWTBearerAuthentication( + valid_audiences=["test-audience"], + valid_issuers=["test-issuer"], + secret_key=symmetric_secret, + ) + + handler = JWTOpenIDTokensHandler(jwt_auth) + app.use_authentication().add(handler) + + identity: Identity | None = None + + @app.router.get("/") + async def home(request): + nonlocal identity + identity = request.user + return None + + access_token = get_symmetric_token( + symmetric_secret.get_value(), + { + "aud": "test-audience", + "iss": "test-issuer", + "sub": "user456", + "exp": 9999999999, + }, + ) + protected_refresh_token = handler.protect_refresh_token("my-refresh-token") + + await app( + get_example_scope( + "GET", + "/", + extra_headers=[ + (b"Authorization", b"Bearer " + access_token.encode()), + (b"X-REFRESH-TOKEN", protected_refresh_token.encode()), + ], + ), + MockReceive(), + MockSend(), + ) + + assert app.response is not None + assert app.response.status == 204 + assert identity is not None + assert identity.is_authenticated() is True + assert identity["sub"] == "user456" + assert identity.refresh_token == "my-refresh-token" # type: ignore[attr-defined] + + +# endregion From b63e40886884728535942b1266f443fcf719e9ea Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 11 May 2026 23:32:42 +0200 Subject: [PATCH 2/3] Update oidc.py --- blacksheep/server/authentication/oidc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/blacksheep/server/authentication/oidc.py b/blacksheep/server/authentication/oidc.py index c24ad945..f40f3d45 100644 --- a/blacksheep/server/authentication/oidc.py +++ b/blacksheep/server/authentication/oidc.py @@ -686,6 +686,7 @@ def restore_refresh_token( if identity is None: identity = Identity() identity.refresh_token = value + context.user = identity return identity From dae64a20bcdf69bd1cea496c99c633622778c827 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Tue, 12 May 2026 07:21:00 +0200 Subject: [PATCH 3/3] Refine code --- blacksheep/server/authentication/jwt.py | 4 +++- blacksheep/server/authentication/oidc.py | 18 ++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/blacksheep/server/authentication/jwt.py b/blacksheep/server/authentication/jwt.py index 539bedd8..cbea8d75 100644 --- a/blacksheep/server/authentication/jwt.py +++ b/blacksheep/server/authentication/jwt.py @@ -210,7 +210,9 @@ async def authenticate(self, context: Request) -> Identity | None: # Raise a dedicated exception to keep track of the event raise InvalidCredentialsError(context.original_client_ip) else: - return Identity(decoded, self.scheme) + identity = Identity(decoded, self.scheme) + context.user = identity + return identity @property def scheme(self) -> str: diff --git a/blacksheep/server/authentication/oidc.py b/blacksheep/server/authentication/oidc.py index f40f3d45..047cd180 100644 --- a/blacksheep/server/authentication/oidc.py +++ b/blacksheep/server/authentication/oidc.py @@ -667,9 +667,7 @@ def _get_refresh_token_header_name(self) -> bytes: def protect_refresh_token(self, refresh_token: str) -> str: return self._serializer.dumps(refresh_token) # type: ignore - def restore_refresh_token( - self, context: Request, identity: Identity | None = None - ) -> Identity | None: + def restore_refresh_token(self, context: Request) -> Identity | None: refresh_token_header = context.get_first_header( self._get_refresh_token_header_name() ) @@ -683,16 +681,16 @@ def restore_refresh_token( self.refresh_token_key, ) else: - if identity is None: - identity = Identity() - identity.refresh_token = value - context.user = identity + if context.user is None: + context.user = Identity() + context.user.refresh_token = value - return identity + return context.user async def authenticate(self, context: Request) -> Identity | None: - identity = await self.auth_handler.authenticate(context) - return self.restore_refresh_token(context, identity) + await self.auth_handler.authenticate(context) + self.restore_refresh_token(context) + return context.user class TokenType(Enum):