Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion blacksheep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

__author__ = "Roberto Prevato <roberto.prevato@gmail.com>"
__version__ = "2.6.2"
__version__ = "2.6.3"

from .contents import Content as Content
from .contents import FileBuffer as FileBuffer
Expand Down
4 changes: 3 additions & 1 deletion blacksheep/server/authentication/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions blacksheep/server/authentication/oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
Expand All @@ -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):
Expand Down
121 changes: 121 additions & 0 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading