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
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,93 @@ api = ApiClient("https://myapi.local/resource", session=session)
assert api.session == session
```

## Token and Authorization Requests serialization

If you implement a web application, you will most likely need to serialize access tokens inside the user session.
To make it easier, `requests_oauth2client` provides several classes that implement (de)serialization of `BearerToken`,
'DPoPToken', `AuthorizationRequest` (and derivates) and `DPoPKey` to `bytes`.

```python
from requests_oauth2client import BearerToken, TokenSerializer

token_serializer = TokenSerializer()

bearer_token = BearerToken("access_token", expires_in=60) # here is a sample token
serialized_value = token_serializer.dumps(bearer_token)
print(serialized_value)
# b'q1ZKTE5OLS6OL8nPTs1TskLl6iilVhRkFqUWxyeWKFkZmpsZWFiYmJqZ6iiB5eNLKgtSlayUnFITi1KLlGoB'
# you can store that value in session or anywhere needed
# beware, this is decodable clear-text!

# loading back the token to a BearerToken instance
deserialized_token = token_serializer.loads(serialized_value)
assert isinstance(deserialized_token, BearerToken)
assert deserialized_token == bearer_token
```

Default `TokenSerializer` class supports both `BearerToken` and `DPoPToken` instances.

```python
from requests_oauth2client import AuthorizationRequest, AuthorizationRequestSerializer

ar_serializer = AuthorizationRequestSerializer()

auth_request = AuthorizationRequest(
authorization_endpoint="https://my.as.local/authorize",
client_id="my_client_id",
redirect_uri="http://localhost:8000/callback",
)

serialized_ar = ar_serializer.dumps(auth_request)
assert ar_serializer.loads(serialized_ar) == auth_request
```

### Customizing token (de)serialization

While provided serializers work well for standard tokens with default classes, you may need to override them for special
purposes or if you are using custom token classes.
To do that, you can pass custom methods as parameters when initializing your TokenSerializer instance:

```python
from __future__ import annotations

import base64
import json
from typing import Any, Mapping

from requests_oauth2client import BearerToken, TokenSerializer


class CustomToken(BearerToken):
TOKEN_TYPE = "CustomToken"


def custom_make_instance(args: Mapping[str, Any]) -> BearerToken:
"""This will add support for a custom token type."""
if args.get("token_type") == "CustomToken":
return CustomToken(**args)
return TokenSerializer.default_make_instance(args)


def custom_dumper(token: CustomToken) -> bytes:
"""This will serialize the token value to base64-encoded JSON"""
args = token.as_dict()
return base64.b64encode(json.dumps(args).encode())


def custom_loader(serialized: bytes) -> dict[str, Any]:
"""This will load from a base64-encoded JSON"""
return json.loads(base64.b64decode(serialized))


token_serializer = TokenSerializer(make_instance=custom_make_instance, dumper=custom_dumper, loader=custom_loader)

my_custom_token = CustomToken(token_type="CustomToken", access_token="...")
serialized = token_serializer.dumps(my_custom_token)
assert serialized == b"eyJhY2Nlc3NfdG9rZW4iOiAiLi4uIiwgInRva2VuX3R5cGUiOiAiQ3VzdG9tVG9rZW4ifQ=="
assert token_serializer.loads(serialized) == my_custom_token
```

## Vendor-Specific clients

`requests_oauth2client` is flexible enough to handle most use cases, so you should be able to use any AS by any vendor
Expand Down
6 changes: 3 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@ plugins:
handlers:
python:
options:
#extensions:
#- griffe_fieldz: {include_inherited: true}
extensions:
- griffe_fieldz: {include_inherited: true}
filters:
- "!^_"
- "^__init__"
- "!^utils"
members_order: source
show_root_heading: true
show_submodules: true
import:
inventories:
- https://requests.readthedocs.io/en/master/objects.inv
- https://guillp.github.io/jwskate/objects.inv
extra:
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ dev = [
"virtualenv>=20.30.0",
]
doc = [
"griffe-fieldz>=0.3.0",
"mkdocs>=1.3.1",
"mkdocs-autorefs>=0.3.0",
"mkdocs-include-markdown-plugin>=6",
"mkdocs-material>=9.6.11",
"mkdocs-material-extensions>=1.0.1",
"mkdocstrings[python]>=0.29.1",
]
]
test = [
"coverage>=7.8.0",
"pytest>=7.0.1",
Expand Down
10 changes: 3 additions & 7 deletions requests_oauth2client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,13 @@
)
from .authorization_request import (
AuthorizationRequest,
AuthorizationRequestSerializer,
AuthorizationResponse,
CodeChallengeMethods,
InvalidCodeVerifierParam,
InvalidMaxAgeParam,
MissingIssuerParam,
PkceUtils,
RequestParameterAuthorizationRequest,
RequestUriParameterAuthorizationRequest,
ResponseTypes,
UnsupportedCodeChallengeMethod,
UnsupportedResponseTypeParam,
)
Expand All @@ -36,8 +33,6 @@
BackChannelAuthenticationResponse,
)
from .client import (
Endpoints,
GrantTypes,
InvalidAcrValuesParam,
InvalidBackchannelAuthenticationRequestHintParam,
InvalidDiscoveryDocument,
Expand Down Expand Up @@ -93,6 +88,7 @@
RepeatedDPoPNonce,
validate_dpop_proof,
)
from .enums import CodeChallengeMethods, Endpoints, GrantTypes, ResponseTypes
from .exceptions import (
AccessDenied,
AccountSelectionRequired,
Expand Down Expand Up @@ -135,9 +131,9 @@
from .polling import (
BaseTokenEndpointPollingJob,
)
from .serializers import AuthorizationRequestSerializer, TokenSerializer
from .tokens import (
BearerToken,
BearerTokenSerializer,
ExpiredAccessToken,
ExpiredIdToken,
IdToken,
Expand Down Expand Up @@ -174,7 +170,6 @@
"BaseTokenEndpointPollingJob",
"BaseTokenEndpointPoolingJob",
"BearerToken",
"BearerTokenSerializer",
"ClientSecretBasic",
"ClientSecretJwt",
"ClientSecretPost",
Expand Down Expand Up @@ -267,6 +262,7 @@
"SignatureAlgs",
"SlowDown",
"TokenEndpointError",
"TokenSerializer",
"UnauthorizedClient",
"UnknownActorTokenType",
"UnknownIntrospectionError",
Expand Down
121 changes: 2 additions & 119 deletions requests_oauth2client/authorization_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

import re
import secrets
from enum import Enum
from functools import cached_property
from typing import TYPE_CHECKING, Any, Callable, ClassVar
from typing import TYPE_CHECKING, Any, ClassVar

from attrs import asdict, field, fields, frozen
from binapy import BinaPy
from furl import furl # type: ignore[import-untyped]
from jwskate import JweCompact, Jwk, Jwt, SignatureAlgs, SignedJwt

from .dpop import DPoPKey
from .enums import CodeChallengeMethods, ResponseTypes
from .exceptions import (
AuthorizationResponseError,
ConsentRequired,
Expand All @@ -32,34 +32,6 @@
from datetime import datetime


class ResponseTypes(str, Enum):
"""All standardised `response_type` values.

Note that you should always use `code`. All other values are deprecated.

"""

CODE = "code"
NONE = "none"
TOKEN = "token"
IDTOKEN = "id_token"
CODE_IDTOKEN = "code id_token"
CODE_TOKEN = "code token"
CODE_IDTOKEN_TOKEN = "code id_token token"
IDTOKEN_TOKEN = "id_token token"


class CodeChallengeMethods(str, Enum):
"""All standardised `code_challenge_method` values.

You should always use `S256`.

"""

S256 = "S256"
plain = "plain"


class UnsupportedCodeChallengeMethod(ValueError):
"""Raised when an unsupported `code_challenge_method` is provided."""

Expand Down Expand Up @@ -910,92 +882,3 @@ def __getattr__(self, item: str) -> Any:
def __repr__(self) -> str:
"""Return the Authorization Request URI, as a `str`."""
return self.uri


class AuthorizationRequestSerializer:
"""(De)Serializer for `AuthorizationRequest` instances.

You might need to store pending authorization requests in session, either server-side or client- side. This class is
here to help you do that.

"""

def __init__(
self,
dumper: Callable[[AuthorizationRequest], str] | None = None,
loader: Callable[[str], AuthorizationRequest] | None = None,
) -> None:
self.dumper = dumper or self.default_dumper
self.loader = loader or self.default_loader

@staticmethod
def default_dumper(azr: AuthorizationRequest) -> str:
"""Provide a default dumper implementation.

Serialize an AuthorizationRequest as JSON, then compress with deflate, then encodes as
base64url.

Args:
azr: the `AuthorizationRequest` to serialize

Returns:
the serialized value

"""
d = asdict(azr)
if azr.dpop_key:
d["dpop_key"]["private_key"] = azr.dpop_key.private_key.to_dict()
d.update(**d.pop("kwargs", {}))
return BinaPy.serialize_to("json", d).to("deflate").to("b64u").ascii()

@staticmethod
def default_loader(
serialized: str,
azr_class: type[AuthorizationRequest] = AuthorizationRequest,
) -> AuthorizationRequest:
"""Provide a default deserializer implementation.

This does the opposite operations than `default_dumper`.

Args:
serialized: the serialized AuthorizationRequest
azr_class: the class to deserialize the Authorization Request to

Returns:
an AuthorizationRequest

"""
args = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json")

if dpop_key := args.get("dpop_key"):
dpop_key["private_key"] = Jwk(dpop_key["private_key"])
dpop_key.pop("jti_generator", None)
dpop_key.pop("iat_generator", None)
dpop_key.pop("dpop_token_class", None)
args["dpop_key"] = DPoPKey(**dpop_key)

return azr_class(**args)

def dumps(self, azr: AuthorizationRequest) -> str:
"""Serialize and compress a given AuthorizationRequest for easier storage.

Args:
azr: an AuthorizationRequest to serialize

Returns:
the serialized AuthorizationRequest, as a str

"""
return self.dumper(azr)

def loads(self, serialized: str) -> AuthorizationRequest:
"""Deserialize a serialized AuthorizationRequest.

Args:
serialized: the serialized AuthorizationRequest

Returns:
the deserialized AuthorizationRequest

"""
return self.loader(serialized)
Loading