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
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
234 changes: 143 additions & 91 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ types-requests = ">=2.25.10"
types-cryptography = ">=3.3.15"
virtualenv = ">=20.30.0"
pytest-examples = ">=0.0.17"
griffe-fieldz = ">=0.3.0"


[tool.poetry.extras]
test = ["pytest", "pytest-cov"]
doc = [
"griffe-fieldz",
"mdformat",
"mkdocs",
"mkdocs-autorefs",
Expand Down
8 changes: 2 additions & 6 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, BearerTokenSerializer
from .tokens import (
BearerToken,
BearerTokenSerializer,
ExpiredAccessToken,
ExpiredIdToken,
IdToken,
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 @@ -908,92 +880,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)
37 changes: 2 additions & 35 deletions requests_oauth2client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import warnings
from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar

import requests
Expand All @@ -14,16 +13,15 @@
from .authorization_request import (
AuthorizationRequest,
AuthorizationResponse,
CodeChallengeMethods,
MissingIssuerParam,
RequestUriParameterAuthorizationRequest,
ResponseTypes,
)
from .backchannel_authentication import BackChannelAuthenticationResponse
from .client_authentication import ClientSecretPost, PrivateKeyJwt, client_auth_factory
from .device_authorization import DeviceAuthorizationResponse
from .discovery import oidc_discovery_document_url
from .dpop import DPoPKey, DPoPToken, InvalidDPoPAlg, MissingDPoPNonce, RepeatedDPoPNonce
from .enums import CodeChallengeMethods, Endpoints, GrantTypes, ResponseTypes, TokenType
from .exceptions import (
AccessDenied,
AuthorizationPending,
Expand All @@ -50,7 +48,7 @@
UnsupportedTokenType,
UseDPoPNonce,
)
from .tokens import BearerToken, IdToken, TokenResponse, TokenType
from .tokens import BearerToken, IdToken, TokenResponse
from .utils import InvalidUri, validate_endpoint_uri, validate_issuer_uri

if TYPE_CHECKING:
Expand Down Expand Up @@ -169,44 +167,13 @@ def __init__(self, message: str, discovery_document: dict[str, Any]) -> None:
self.discovery_document = discovery_document


class Endpoints(str, Enum):
"""All standardised OAuth 2.0 and extensions endpoints.

If an endpoint is not mentioned here, then its usage is not supported by OAuth2Client.

"""

TOKEN = "token_endpoint"
AUTHORIZATION = "authorization_endpoint"
BACKCHANNEL_AUTHENTICATION = "backchannel_authentication_endpoint"
DEVICE_AUTHORIZATION = "device_authorization_endpoint"
INTROSPECTION = "introspection_endpoint"
REVOCATION = "revocation_endpoint"
PUSHED_AUTHORIZATION_REQUEST = "pushed_authorization_request_endpoint"
JWKS = "jwks_uri"
USER_INFO = "userinfo_endpoint"


class MissingEndpointUri(AttributeError):
"""Raised when a required endpoint uri is not known."""

def __init__(self, endpoint: str) -> None:
super().__init__(f"No '{endpoint}' defined for this client.")


class GrantTypes(str, Enum):
"""An enum of standardized `grant_type` values."""

CLIENT_CREDENTIALS = "client_credentials"
AUTHORIZATION_CODE = "authorization_code"
REFRESH_TOKEN = "refresh_token"
RESOURCE_OWNER_PASSWORD = "password"
TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange"
JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"
CLIENT_INITIATED_BACKCHANNEL_AUTHENTICATION = "urn:openid:params:grant-type:ciba"
DEVICE_CODE = "urn:ietf:params:oauth:grant-type:device_code"


@frozen(init=False)
class OAuth2Client:
"""An OAuth 2.x Client that can send requests to an OAuth 2.x Authorization Server.
Expand Down
1 change: 1 addition & 0 deletions requests_oauth2client/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""Mark a class as deprecated.

https://stackoverflow.com/a/52087847

"""

from warnings import warn
Expand Down
7 changes: 4 additions & 3 deletions requests_oauth2client/dpop.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from requests import codes
from typing_extensions import Self

from .tokens import AccessTokenTypes, BearerToken, IdToken, id_token_converter
from .enums import AccessTokenTypes
from .tokens import BearerToken, IdToken, id_token_converter
from .utils import accepts_expires_in

if TYPE_CHECKING:
Expand Down Expand Up @@ -205,8 +206,8 @@ class DPoPKey:

alg: str = field(on_setattr=setters.frozen)
private_key: jwskate.Jwk = field(on_setattr=setters.frozen, repr=False)
jti_generator: Callable[[], str] = field(on_setattr=setters.frozen, repr=False)
iat_generator: Callable[[], int] = field(on_setattr=setters.frozen, repr=False)
jti_generator: Callable[[], str] = field(on_setattr=setters.frozen, repr=False, eq=False)
iat_generator: Callable[[], int] = field(on_setattr=setters.frozen, repr=False, eq=False)
jwt_typ: str = field(on_setattr=setters.frozen, repr=False)
dpop_token_class: type[DPoPToken] = field(on_setattr=setters.frozen, repr=False)
as_nonce: str | None
Expand Down
Loading