Skip to content

Commit 0b09d12

Browse files
committed
refactoring
1 parent 5af3430 commit 0b09d12

File tree

13 files changed

+257
-121
lines changed

13 files changed

+257
-121
lines changed

README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,92 @@ api = ApiClient("https://myapi.local/resource", session=session)
11491149
assert api.session == session
11501150
```
11511151

1152+
## Token and Authorization Requests serialization
1153+
1154+
If you implement a web application, you will most likely need to serialize access tokens inside the user session.
1155+
To make it easier, `requests_oauth2client` provides several classes that implement (de)serialization.
1156+
1157+
```python
1158+
from requests_oauth2client import BearerToken, TokenSerializer
1159+
1160+
token_serializer = TokenSerializer()
1161+
1162+
bearer_token = BearerToken("access_token", expires_in=60) # here is a sample token
1163+
serialized_value = token_serializer.dumps(bearer_token)
1164+
print(serialized_value)
1165+
# q1ZKTE5OLS6OL8nPTs1TskLl6iilVhRkFqUWxyeWKFkZmpsZWFiYmJqZ6iiB5eNLKgtSlayUnFITi1KLlGoB
1166+
# you can store that string value in session or anywhere needed
1167+
# beware, this is clear-text!
1168+
1169+
# loading back the token to a BearerToken instance
1170+
deserialized_token = token_serializer.loads(serialized_value)
1171+
assert isinstance(deserialized_token, BearerToken)
1172+
assert deserialized_token == bearer_token
1173+
```
1174+
1175+
Default `TokenSerializer` class supports both `BearerToken` and `DPoPToken` instances.
1176+
1177+
```python
1178+
from requests_oauth2client import AuthorizationRequest, AuthorizationRequestSerializer
1179+
1180+
ar_serializer = AuthorizationRequestSerializer()
1181+
1182+
auth_request = AuthorizationRequest(
1183+
authorization_endpoint="https://my.as.local/authorize",
1184+
client_id="my_client_id",
1185+
redirect_uri="http://localhost:8000/callback",
1186+
)
1187+
1188+
serialized_ar = ar_serializer.dumps(auth_request)
1189+
assert ar_serializer.loads(serialized_ar) == auth_request
1190+
```
1191+
1192+
### Customizing token (de)serialization
1193+
1194+
While provided serializers work well for standard tokens with default classes, you may need to override them for special
1195+
purposes or if you are using custom token classes.
1196+
To do that, you can pass custom methods as parameters when initializing your TokenSerializer instance:
1197+
1198+
```python
1199+
from __future__ import annotations
1200+
1201+
import base64
1202+
import json
1203+
from typing import Any, Mapping
1204+
1205+
from requests_oauth2client import BearerToken, TokenSerializer
1206+
1207+
1208+
class CustomToken(BearerToken):
1209+
TOKEN_TYPE = "CustomToken"
1210+
1211+
1212+
def custom_make_instance(args: Mapping[str, Any]) -> BearerToken:
1213+
"""This will add support for a custom token type."""
1214+
if args.get("token_type") == "CustomToken":
1215+
return CustomToken(**args)
1216+
return TokenSerializer.default_make_instance(args)
1217+
1218+
1219+
def custom_dumper(token: CustomToken) -> bytes:
1220+
"""This will serialize the token value to base64-encoded JSON"""
1221+
args = token.as_dict()
1222+
return base64.b64encode(json.dumps(args).encode())
1223+
1224+
1225+
def custom_loader(serialized: bytes) -> dict[str, Any]:
1226+
"""This will load from a base64-encoded JSON"""
1227+
return json.loads(base64.b64decode(serialized))
1228+
1229+
1230+
token_serializer = TokenSerializer(make_instance=custom_make_instance, dumper=custom_dumper, loader=custom_loader)
1231+
1232+
my_custom_token = CustomToken(**{"token_type": "CustomToken", "access_token": "..."})
1233+
serialized = token_serializer.dumps(my_custom_token)
1234+
assert serialized == b'eyJhY2Nlc3NfdG9rZW4iOiAiLi4uIiwgInRva2VuX3R5cGUiOiAiQ3VzdG9tVG9rZW4ifQ=='
1235+
assert token_serializer.loads(serialized) == my_custom_token
1236+
```
1237+
11521238
## Vendor-Specific clients
11531239

11541240
`requests_oauth2client` is flexible enough to handle most use cases, so you should be able to use any AS by any vendor

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ dev = [
4242
"virtualenv>=20.30.0",
4343
]
4444
doc = [
45+
"griffe-fieldz>=0.3.0",
4546
"mkdocs>=1.3.1",
4647
"mkdocs-autorefs>=0.3.0",
4748
"mkdocs-include-markdown-plugin>=6",
4849
"mkdocs-material>=9.6.11",
4950
"mkdocs-material-extensions>=1.0.1",
5051
"mkdocstrings[python]>=0.29.1",
51-
]
52+
]
5253
test = [
5354
"coverage>=7.8.0",
5455
"pytest>=7.0.1",

requests_oauth2client/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@
131131
from .polling import (
132132
BaseTokenEndpointPollingJob,
133133
)
134-
from .serializers import AuthorizationRequestSerializer, BearerTokenSerializer
134+
from .serializers import AuthorizationRequestSerializer, TokenSerializer
135135
from .tokens import (
136136
BearerToken,
137137
ExpiredAccessToken,
@@ -170,7 +170,6 @@
170170
"BaseTokenEndpointPollingJob",
171171
"BaseTokenEndpointPoolingJob",
172172
"BearerToken",
173-
"BearerTokenSerializer",
174173
"ClientSecretBasic",
175174
"ClientSecretJwt",
176175
"ClientSecretPost",
@@ -263,6 +262,7 @@
263262
"SignatureAlgs",
264263
"SlowDown",
265264
"TokenEndpointError",
265+
"TokenSerializer",
266266
"UnauthorizedClient",
267267
"UnknownActorTokenType",
268268
"UnknownIntrospectionError",

requests_oauth2client/deprecated.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .backchannel_authentication import BackChannelAuthenticationPollingJob
1212
from .device_authorization import DeviceAuthorizationPollingJob
1313
from .polling import BaseTokenEndpointPollingJob
14+
from .serializers import TokenSerializer
1415

1516

1617
class _DeprecatedClassMeta(type):
@@ -77,8 +78,13 @@ class DeviceAuthorizationPoolingJob(metaclass=_DeprecatedClassMeta):
7778
_DeprecatedClassMeta__alias = DeviceAuthorizationPollingJob
7879

7980

81+
class BearerTokenSerializer(metaclass=_DeprecatedClassMeta):
82+
_DeprecatedClassMeta__alias = TokenSerializer
83+
84+
8085
__all__ = [
8186
"BackChannelAuthenticationPoolingJob",
8287
"BaseTokenEndpointPoolingJob",
88+
"BearerTokenSerializer",
8389
"DeviceAuthorizationPoolingJob",
8490
]

requests_oauth2client/dpop.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from binapy import BinaPy
1414
from furl import furl # type: ignore[import-untyped]
1515
from requests import codes
16-
from typing_extensions import Self
16+
from typing_extensions import Self, override
1717

1818
from .enums import AccessTokenTypes
1919
from .tokens import BearerToken, IdToken, id_token_converter
@@ -150,13 +150,23 @@ def _response_hook(self, response: requests.Response, **kwargs: Any) -> requests
150150

151151
return response
152152

153+
@override
153154
def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
154155
"""Add a DPoP proof in each request."""
155156
request = super().__call__(request)
156157
add_dpop_proof(request, dpop_key=self.dpop_key, access_token=self.access_token, header_name=self.DPOP_HEADER)
157158
request.register_hook("response", self._response_hook) # type: ignore[no-untyped-call]
158159
return request
159160

161+
@override
162+
def as_dict(self, with_expires_in: bool = True) -> dict[str, Any]:
163+
d = super().as_dict(with_expires_in=with_expires_in)
164+
d["dpop_key"]["private_key"] = self.dpop_key.private_key.to_dict()
165+
d["dpop_key"].pop("jti_generator", None)
166+
d["dpop_key"].pop("iat_generator", None)
167+
d["dpop_key"].pop("dpop_token_class", None)
168+
return d
169+
160170

161171
def add_dpop_proof(
162172
request: requests.PreparedRequest,

requests_oauth2client/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,11 @@ class InvalidBackChannelAuthenticationResponse(OAuth2Error):
264264

265265
class InvalidPushedAuthorizationResponse(OAuth2Error):
266266
"""Raised when the Pushed Authorization Endpoint returns an error."""
267+
268+
269+
class UnsupportedTokenTypeError(ValueError):
270+
"""Raised when an unsupported token_type is provided."""
271+
272+
def __init__(self, token_type: str) -> None:
273+
super().__init__(f"Unsupported token_type: {token_type}")
274+
self.token_type = token_type

requests_oauth2client/flask/auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from flask import session
88

9-
from requests_oauth2client import BearerTokenSerializer
9+
from requests_oauth2client import TokenSerializer
1010
from requests_oauth2client.auth import OAuth2ClientCredentialsAuth
1111
from requests_oauth2client.tokens import BearerToken
1212

@@ -27,11 +27,11 @@ class FlaskSessionAuthMixin:
2727
def __init__(
2828
self,
2929
session_key: str,
30-
serializer: BearerTokenSerializer | None = None,
30+
serializer: TokenSerializer | None = None,
3131
*args: Any,
3232
**token_kwargs: Any,
3333
) -> None:
34-
self.serializer = serializer or BearerTokenSerializer()
34+
self.serializer = serializer or TokenSerializer()
3535
self.session_key = session_key
3636
super().__init__(*args, **token_kwargs)
3737

0 commit comments

Comments
 (0)