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/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 85a70c82..047cd180 100644 --- a/blacksheep/server/authentication/oidc.py +++ b/blacksheep/server/authentication/oidc.py @@ -667,7 +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) -> None: + def restore_refresh_token(self, context: Request) -> Identity | None: refresh_token_header = context.get_first_header( self._get_refresh_token_header_name() ) @@ -685,10 +685,12 @@ def restore_refresh_token(self, context: Request) -> None: context.user = Identity() context.user.refresh_token = value + return context.user + async def authenticate(self, context: Request) -> Identity | None: await self.auth_handler.authenticate(context) - self.restore_refresh_token(context) + return context.user 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