Skip to content
Open
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 authlib/integrations/httpx_client/assertion_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ class AssertionClient(_AssertionClient, httpx.Client):
}
DEFAULT_GRANT_TYPE = JWT_BEARER_GRANT_TYPE

def _apply_token_endpoint_kwargs(self, kwargs):
# httpx does not support per-request verify; it's set at client level.
pass

def __init__(
self,
token_endpoint,
Expand Down
8 changes: 8 additions & 0 deletions authlib/integrations/httpx_client/oauth2_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class AsyncOAuth2Client(_OAuth2Client, httpx.AsyncClient):
token_auth_class = OAuth2Auth
oauth_error_class = OAuthError

def _apply_token_endpoint_kwargs(self, session_kwargs):
# httpx does not support per-request verify; it's set at client level.
pass

def __init__(
self,
client_id=None,
Expand Down Expand Up @@ -215,6 +219,10 @@ class OAuth2Client(_OAuth2Client, httpx.Client):
token_auth_class = OAuth2Auth
oauth_error_class = OAuthError

def _apply_token_endpoint_kwargs(self, session_kwargs):
# httpx does not support per-request verify; it's set at client level.
pass

def __init__(
self,
client_id=None,
Expand Down
16 changes: 16 additions & 0 deletions authlib/oauth2/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ class OAuth2Client:
values: "header", "body", "uri".
:param update_token: A function for you to update token. It accept a
:class:`OAuth2Token` as parameter.
:param token_endpoint_verify: SSL verification for token endpoint requests.
Defaults to ``None``, which means the session's default verify setting
is used (same as resource requests). Set to a CA bundle path to use a
custom certificate, ``True`` to use the default CA bundle, or ``False``
to disable verification. When not ``None``, it applies only to
``fetch_token`` and ``refresh_token`` requests without affecting
resource requests. Only works with the requests integration (httpx
uses client-level verify).
:param leeway: Time window in seconds before the actual expiration of the
authentication token, that the token is considered expired and will
be refreshed.
Expand All @@ -64,6 +72,7 @@ def __init__(
token=None,
token_placement="header",
update_token=None,
token_endpoint_verify=None,
leeway=60,
**metadata,
):
Expand Down Expand Up @@ -101,6 +110,7 @@ def __init__(
"update token has been redesigned, checkout the documentation"
)

self.token_endpoint_verify = token_endpoint_verify
self.metadata = metadata

self.compliance_hook = {
Expand Down Expand Up @@ -216,6 +226,7 @@ def fetch_token(
return self.token_from_fragment(authorization_response, state)

session_kwargs = self._extract_session_request_params(kwargs)
self._apply_token_endpoint_kwargs(session_kwargs)

if authorization_response and "code=" in authorization_response:
grant_type = "authorization_code"
Expand Down Expand Up @@ -270,6 +281,7 @@ def refresh_token(
:return: A :class:`OAuth2Token` object (a dict too).
"""
session_kwargs = self._extract_session_request_params(kwargs)
self._apply_token_endpoint_kwargs(session_kwargs)
refresh_token = refresh_token or self.token.get("refresh_token")
if "scope" not in kwargs and self.scope:
kwargs["scope"] = self.scope
Expand Down Expand Up @@ -509,6 +521,10 @@ def _extract_session_request_params(self, kwargs):
rv[k] = kwargs.pop(k)
return rv

def _apply_token_endpoint_kwargs(self, session_kwargs):
"""Apply token-endpoint-specific kwargs (e.g., verify) to session kwargs."""
session_kwargs.setdefault("verify", self.token_endpoint_verify)

def _http_post(self, url, body=None, auth=None, headers=None, **kwargs):
return self.session.post(
url, data=dict(url_decode(body)), headers=headers, auth=auth, **kwargs
Expand Down
10 changes: 9 additions & 1 deletion authlib/oauth2/rfc7521/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(
claims=None,
token_placement="header",
scope=None,
token_endpoint_verify=None,
leeway=60,
**kwargs,
):
Expand All @@ -46,6 +47,7 @@ def __init__(
self.audience = audience
self.claims = claims
self.scope = scope
self.token_endpoint_verify = token_endpoint_verify
if self.token_auth_class is not None:
self.token_auth = self.token_auth_class(None, token_placement, self)
self._kwargs = kwargs
Expand Down Expand Up @@ -95,9 +97,15 @@ def parse_response_token(self, resp):
self.token = token
return self.token

def _apply_token_endpoint_kwargs(self, kwargs):
"""Apply token-endpoint-specific kwargs (e.g., verify) to request kwargs."""
kwargs.setdefault("verify", self.token_endpoint_verify)

def _refresh_token(self, data):
kwargs = {}
self._apply_token_endpoint_kwargs(kwargs)
resp = self.session.request(
"POST", self.token_endpoint, data=data, withhold_token=True
"POST", self.token_endpoint, data=data, withhold_token=True, **kwargs
)

return self.parse_response_token(resp)
Expand Down
16 changes: 16 additions & 0 deletions tests/clients/test_httpx/test_assertion_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,19 @@ def test_without_alg():
) as client:
with pytest.raises(ValueError):
client.get("https://provider.test")


def test_token_endpoint_verify_does_not_crash_httpx():
"""token_endpoint_verify is accepted but ignored for httpx (client-level verify only)."""
with AssertionClient(
"https://provider.test/token",
issuer="foo",
subject="foo",
audience="foo",
alg="HS256",
key="secret",
token_endpoint_verify="/path/to/ca.pem",
transport=WSGITransport(MockDispatch(default_token)),
) as client:
client.get("https://provider.test")
assert client.token["access_token"] == "a"
17 changes: 17 additions & 0 deletions tests/clients/test_httpx/test_async_assertion_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,20 @@ async def test_without_alg():
) as client:
with pytest.raises(ValueError):
await client.get("https://provider.test")


@pytest.mark.asyncio
async def test_token_endpoint_verify_does_not_crash_httpx():
"""token_endpoint_verify is accepted but ignored for httpx (client-level verify only)."""
async with AsyncAssertionClient(
"https://provider.test/token",
issuer="foo",
subject="foo",
audience="foo",
alg="HS256",
key="secret",
token_endpoint_verify="/path/to/ca.pem",
transport=ASGITransport(AsyncMockDispatch(default_token)),
) as client:
await client.get("https://provider.test")
assert client.token["access_token"] == "a"
15 changes: 15 additions & 0 deletions tests/clients/test_httpx/test_async_oauth2_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,3 +445,18 @@ async def test_request_without_token():
async with AsyncOAuth2Client("a", transport=transport) as client:
with pytest.raises(OAuthError):
await client.get("https://provider.test/token")


@pytest.mark.asyncio
async def test_token_endpoint_verify_does_not_crash_httpx():
"""token_endpoint_verify is accepted but ignored for httpx (client-level verify only)."""
transport = ASGITransport(AsyncMockDispatch(default_token))
async with AsyncOAuth2Client(
"foo",
token_endpoint="https://provider.test/token",
grant_type="client_credentials",
token_endpoint_verify="/path/to/ca.pem",
transport=transport,
) as client:
token = await client.fetch_token("https://provider.test/token")
assert token["access_token"] == "a"
14 changes: 14 additions & 0 deletions tests/clients/test_httpx/test_oauth2_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,17 @@ def test_request_without_token():
with OAuth2Client("a", transport=transport) as client:
with pytest.raises(OAuthError):
client.get("https://provider.test/token")


def test_token_endpoint_verify_does_not_crash_httpx():
"""token_endpoint_verify is accepted but ignored for httpx (client-level verify only)."""
transport = WSGITransport(MockDispatch(default_token))
with OAuth2Client(
"foo",
token_endpoint="https://provider.test/token",
grant_type="client_credentials",
token_endpoint_verify="/path/to/ca.pem",
transport=transport,
) as client:
token = client.fetch_token("https://provider.test/token")
assert token["access_token"] == "a"
67 changes: 67 additions & 0 deletions tests/clients/test_requests/test_assertion_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,70 @@ def test_without_alg():
)
with pytest.raises(ValueError):
sess.get("https://provider.test")


def test_token_endpoint_verify_passed_to_refresh_token():
ca_path = "/path/to/custom-ca.pem"
token = {
"token_type": "Bearer",
"access_token": "a",
"expires_in": "3600",
}
calls = []

def fake_send(r, **kwargs):
calls.append((r.url, kwargs.get("verify")))
resp = mock.MagicMock()
resp.status_code = 200
resp.json = lambda: token
return resp

now = int(time.time())
sess = AssertionSession(
"https://provider.test/token",
issuer="foo",
subject="foo",
audience="foo",
issued_at=now,
expires_at=now + 3600,
header={"alg": "HS256"},
key="secret",
token_endpoint_verify=ca_path,
)
sess.send = fake_send
sess.get("https://provider.test")
# First call is the token request — should use custom CA
assert calls[0] == ("https://provider.test/token", ca_path)


def test_token_endpoint_verify_false_disables_verification():
token = {
"token_type": "Bearer",
"access_token": "a",
"expires_in": "3600",
}
calls = []

def fake_send(r, **kwargs):
calls.append((r.url, kwargs.get("verify")))
resp = mock.MagicMock()
resp.status_code = 200
resp.json = lambda: token
return resp

now = int(time.time())
sess = AssertionSession(
"https://provider.test/token",
issuer="foo",
subject="foo",
audience="foo",
issued_at=now,
expires_at=now + 3600,
header={"alg": "HS256"},
key="secret",
token_endpoint_verify=False,
)
sess.send = fake_send
sess.get("https://provider.test")
# Token request should have verify=False
assert calls[0] == ("https://provider.test/token", False)
Loading
Loading