Skip to content

API

requests_oauth2client

Main module for requests_oauth2client.

You can import any class from any submodule directly from this main module.

ApiClient

A Wrapper around requests.Session with extra features for REST API calls.

Additional features compared to using a requests.Session directly:

  • You must set a root url at creation time, which then allows passing relative urls at request time.
  • It may also raise exceptions instead of returning error responses.
  • You can also pass additional kwargs at init time, which will be used to configure the Session, instead of setting them later.
  • for parameters passed as json, params or data, values that are None can be automatically discarded from the request
  • boolean values in data or params fields can be serialized to values that are suitable for the target API, like "true" or "false", or "1" / "0", instead of the default values "True" or "False",
  • you may pass cookies and headers, which will be added to the session cookie handler or request headers respectively.
  • you may use the user_agent parameter to change the User-Agent header easily. Set it to None to remove that header.

base_url will serve as root for relative urls passed to ApiClient.request(), ApiClient.get(), etc.

A requests.HTTPError will be raised everytime an API call returns an error code (>= 400), unless you set raise_for_status to False. Additional parameters passed at init time, including auth will be used to configure the Session.

Example
from requests_oauth2client import ApiClient

api = ApiClient("https://myapi.local/resource", timeout=10)
resp = api.get("/myid")  # this will send a GET request
# to https://myapi.local/resource/myid

# you can pass an underlying requests.Session at init time
session = requests.Session()
session.proxies = {"https": "https://localhost:3128"}
api = ApiClient("https://myapi.local/resource", session=session)

# or you can let ApiClient init its own session and provide additional configuration
# parameters:
api = ApiClient(
    "https://myapi.local/resource",
    proxies={"https": "https://localhost:3128"},
)

Parameters:

Name Type Description Default
base_url str

the base api url, that is the root for all the target API endpoints.

required
auth AuthBase | None

the requests.auth.AuthBase to use as authentication handler.

None
timeout int | None

the default timeout, in seconds, to use for each request from this ApiClient. Can be set to None to disable timeout.

60
raise_for_status bool

if True, exceptions will be raised everytime a request returns an error code (>= 400).

True
none_fields Literal['include', 'exclude', 'empty']

defines what to do with parameters with value None in data or json fields.

  • if "exclude" (default), fields whose values are None are not included in the request.
  • if "include", they are included with string value None. This is the default behavior of requests. Note that they will be serialized to null in JSON.
  • if "empty", they are included with an empty value (as an empty string).
'exclude'
bool_fields tuple[Any, Any] | None

a tuple of (true_value, false_value). Fields from data or params with a boolean value (True or False) will be serialized to the corresponding value. This can be useful since some APIs expect a 'true' or 'false' value as boolean, and requests serializes True to 'True' and False to 'False'. Set it to None to restore default requests behavior.

('true', 'false')
cookies Mapping[str, Any] | None

a mapping of cookies to set in the underlying requests.Session.

None
headers Mapping[str, Any] | None

a mapping of headers to set in the underlying requests.Session.

None
session Session | None

a preconfigured requests.Session to use with this ApiClient.

None
**session_kwargs Any

additional kwargs to configure the underlying requests.Session.

{}

Raises:

Type Description
InvalidBoolFieldsParam

if the provided bool_fields parameter is invalid.

Source code in requests_oauth2client/api_client.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
@frozen(init=False)
class ApiClient:
    """A Wrapper around [requests.Session][] with extra features for REST API calls.

    Additional features compared to using a [requests.Session][] directly:

    - You must set a root url at creation time, which then allows passing relative urls at request time.
    - It may also raise exceptions instead of returning error responses.
    - You can also pass additional kwargs at init time, which will be used to configure the
    [Session][requests.Session], instead of setting them later.
    - for parameters passed as `json`, `params` or `data`, values that are `None` can be
    automatically discarded from the request
    - boolean values in `data` or `params` fields can be serialized to values that are suitable
    for the target API, like `"true"`  or `"false"`, or `"1"` / `"0"`, instead of the default
    values `"True"` or `"False"`,
    - you may pass `cookies` and `headers`, which will be added to the session cookie handler or
    request headers respectively.
    - you may use the `user_agent` parameter to change the `User-Agent` header easily. Set it to
      `None` to remove that header.

    `base_url` will serve as root for relative urls passed to
    [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request],
    [ApiClient.get()][requests_oauth2client.api_client.ApiClient.get], etc.

    A [requests.HTTPError][] will be raised everytime an API call returns an error code (>= 400), unless
    you set `raise_for_status` to `False`. Additional parameters passed at init time, including
    `auth` will be used to configure the [Session][requests.Session].

    Example:
        ```python
        from requests_oauth2client import ApiClient

        api = ApiClient("https://myapi.local/resource", timeout=10)
        resp = api.get("/myid")  # this will send a GET request
        # to https://myapi.local/resource/myid

        # you can pass an underlying requests.Session at init time
        session = requests.Session()
        session.proxies = {"https": "https://localhost:3128"}
        api = ApiClient("https://myapi.local/resource", session=session)

        # or you can let ApiClient init its own session and provide additional configuration
        # parameters:
        api = ApiClient(
            "https://myapi.local/resource",
            proxies={"https": "https://localhost:3128"},
        )
        ```

    Args:
        base_url: the base api url, that is the root for all the target API endpoints.
        auth: the [requests.auth.AuthBase][] to use as authentication handler.
        timeout: the default timeout, in seconds, to use for each request from this `ApiClient`.
            Can be set to `None` to disable timeout.
        raise_for_status: if `True`, exceptions will be raised everytime a request returns an
            error code (>= 400).
        none_fields: defines what to do with parameters with value `None` in `data` or `json` fields.

            - if `"exclude"` (default), fields whose values are `None` are not included in the request.
            - if `"include"`, they are included with string value `None`. This is
            the default behavior of `requests`. Note that they will be serialized to `null` in JSON.
            - if `"empty"`, they are included with an empty value (as an empty string).
        bool_fields: a tuple of `(true_value, false_value)`. Fields from `data` or `params` with
            a boolean value (`True` or `False`) will be serialized to the corresponding value.
            This can be useful since some APIs expect a `'true'` or `'false'` value as boolean,
            and `requests` serializes `True` to `'True'` and `False` to `'False'`.
            Set it to `None` to restore default requests behavior.
        cookies: a mapping of cookies to set in the underlying `requests.Session`.
        headers: a mapping of headers to set in the underlying `requests.Session`.
        session: a preconfigured `requests.Session` to use with this `ApiClient`.
        **session_kwargs: additional kwargs to configure the underlying `requests.Session`.

    Raises:
        InvalidBoolFieldsParam: if the provided `bool_fields` parameter is invalid.

    """

    base_url: str
    auth: requests.auth.AuthBase | None
    timeout: int | None
    raise_for_status: bool
    none_fields: Literal["include", "exclude", "empty"]
    bool_fields: tuple[Any, Any] | None
    session: requests.Session

    def __init__(
        self,
        base_url: str,
        *,
        auth: requests.auth.AuthBase | None = None,
        timeout: int | None = 60,
        raise_for_status: bool = True,
        none_fields: Literal["include", "exclude", "empty"] = "exclude",
        bool_fields: tuple[Any, Any] | None = ("true", "false"),
        cookies: Mapping[str, Any] | None = None,
        headers: Mapping[str, Any] | None = None,
        user_agent: str | None = requests.utils.default_user_agent(),
        session: requests.Session | None = None,
        **session_kwargs: Any,
    ) -> None:
        session = session or requests.Session()

        if cookies:
            for key, val in cookies.items():
                session.cookies[key] = str(val)

        if headers:
            for key, val in headers.items():
                session.headers[key] = str(val)

        if user_agent is None:
            session.headers.pop("User-Agent", None)
        else:
            session.headers["User-Agent"] = str(user_agent)

        for key, val in session_kwargs.items():
            setattr(session, key, val)

        if bool_fields is None:
            bool_fields = ("True", "False")
        else:
            validate_bool_fields(bool_fields)

        self.__attrs_init__(
            base_url=base_url,
            auth=auth,
            raise_for_status=raise_for_status,
            none_fields=none_fields,
            bool_fields=bool_fields,
            timeout=timeout,
            session=session,
        )

    def request(  # noqa: C901, PLR0913, D417
        self,
        method: str,
        path: None | str | bytes | Iterable[str | bytes | int] = None,
        *,
        params: None | bytes | MutableMapping[str, str] = None,
        data: (
            Iterable[bytes]
            | str
            | bytes
            | list[tuple[Any, Any]]
            | tuple[tuple[Any, Any], ...]
            | Mapping[Any, Any]
            | None
        ) = None,
        headers: MutableMapping[str, str] | None = None,
        cookies: None | RequestsCookieJar | MutableMapping[str, str] = None,
        files: MutableMapping[str, IO[Any]] | None = None,
        auth: (
            None
            | tuple[str, str]
            | requests.auth.AuthBase
            | Callable[[requests.PreparedRequest], requests.PreparedRequest]
        ) = None,
        timeout: None | float | tuple[float, float] | tuple[float, None] = None,
        allow_redirects: bool = False,
        proxies: MutableMapping[str, str] | None = None,
        hooks: None
        | (
            MutableMapping[
                str,
                (Iterable[Callable[[requests.Response], Any]] | Callable[[requests.Response], Any]),
            ]
        ) = None,
        stream: bool | None = None,
        verify: str | bool | None = None,
        cert: str | tuple[str, str] | None = None,
        json: Mapping[str, Any] | None = None,
        raise_for_status: bool | None = None,
        none_fields: Literal["include", "exclude", "empty"] | None = None,
        bool_fields: tuple[Any, Any] | None = None,
    ) -> requests.Response:
        """A wrapper around [requests.Session.request][] method with extra features.

        Additional features are described in
        [ApiClient][requests_oauth2client.api_client.ApiClient] documentation.

        All parameters will be passed as-is to [requests.Session.request][], expected those
        described below which have a special behavior.

        Args:
          path: the url where the request will be sent to. Can be:

            - a path, as `str`: that path will be joined to the configured API url,
            - an iterable of path segments: that will be joined to the root url.
          raise_for_status: like the parameter of the same name from
            [ApiClient][requests_oauth2client.api_client.ApiClient],
            but this will be applied for this request only.
          none_fields: like the parameter of the same name from
            [ApiClient][requests_oauth2client.api_client.ApiClient],
            but this will be applied for this request only.
          bool_fields: like the parameter of the same name from
            [ApiClient][requests_oauth2client.api_client.ApiClient],
            but this will be applied for this request only.

        Returns:
          a Response as returned by requests

        Raises:
            InvalidBoolFieldsParam: if the provided `bool_fields` parameter is invalid.

        """
        path = self.to_absolute_url(path)

        if none_fields is None:
            none_fields = self.none_fields

        if none_fields == "exclude":
            if isinstance(data, Mapping):
                data = {key: val for key, val in data.items() if val is not None}
            if isinstance(json, Mapping):
                json = {key: val for key, val in json.items() if val is not None}
        elif none_fields == "empty":
            if isinstance(data, Mapping):
                data = {key: val if val is not None else "" for key, val in data.items()}
            if isinstance(json, Mapping):
                json = {key: val if val is not None else "" for key, val in json.items()}

        if bool_fields is None:
            bool_fields = self.bool_fields

        if bool_fields:
            true_value, false_value = validate_bool_fields(bool_fields)
            if isinstance(data, MutableMapping):
                for key, val in data.items():
                    if val is True:
                        data[key] = true_value
                    elif val is False:
                        data[key] = false_value
            if isinstance(params, MutableMapping):
                for key, val in params.items():
                    if val is True:
                        params[key] = true_value
                    elif val is False:
                        params[key] = false_value

        timeout = timeout or self.timeout

        response = self.session.request(
            method,
            path,
            params=params,
            data=data,
            headers=headers,
            cookies=cookies,
            files=files,
            auth=auth or self.auth,
            timeout=timeout,
            allow_redirects=allow_redirects,
            proxies=proxies,
            hooks=hooks,
            stream=stream,
            verify=verify,
            cert=cert,
            json=json,
        )

        if raise_for_status is None:
            raise_for_status = self.raise_for_status
        if raise_for_status:
            response.raise_for_status()
        return response

    def to_absolute_url(self, path: None | str | bytes | Iterable[str | bytes | int] = None) -> str:
        """Convert a relative url to an absolute url.

        Given a `path`, return the matching absolute url, based on the `base_url` that is
        configured for this API.

        The result of this method is different from a standard `urljoin()`, because a relative_url
        that starts with a "/" will not override the path from the base url. You can also pass an
        iterable of path parts as relative url, which will be properly joined with "/". Those parts
        may be `str` (which will be urlencoded) or `bytes` (which will be decoded as UTF-8 first) or
        any other type (which will be converted to `str` first, using the `str() function`). See the
        table below for example results which would exhibit most cases:

        | base_url | relative_url | result_url |
        |---------------------------|-----------------------------|-------------------------------------------|
        | `"https://myhost.com/root"` | `"/path"` | `"https://myhost.com/root/path"` |
        | `"https://myhost.com/root"` | `"/path"` | `"https://myhost.com/root/path"` |
        | `"https://myhost.com/root"` | `b"/path"` | `"https://myhost.com/root/path"` |
        | `"https://myhost.com/root"` | `"path"` | `"https://myhost.com/root/path"` |
        | `"https://myhost.com/root"` | `None` | `"https://myhost.com/root"` |
        | `"https://myhost.com/root"` |  `("user", 1, "resource")` | `"https://myhost.com/root/user/1/resource"` |
        | `"https://myhost.com/root"` | `"https://otherhost.org/foo"` | `ValueError` |

        Args:
          path: a relative url

        Returns:
          the resulting absolute url

        Raises:
            InvalidPathParam: if the provided path does not allow constructing a valid url

        """
        url = path

        if url is None:
            url = self.base_url
        else:
            if not isinstance(url, (str, bytes)):
                try:
                    url = "/".join(
                        [urlencode(part.decode() if isinstance(part, bytes) else str(part)) for part in url if part],
                    )
                except Exception as exc:
                    raise InvalidPathParam(url) from exc

            if isinstance(url, bytes):
                url = url.decode()

            if "://" in url:
                raise InvalidPathParam(url)

            url = urljoin(self.base_url + "/", url.lstrip("/"))

        if url is None or not isinstance(url, str):
            raise InvalidPathParam(url)  # pragma: no cover

        return url

    def get(
        self,
        path: None | str | bytes | Iterable[str | bytes | int] = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a GET request and return a [Response][requests.Response] object.

        The passed `url` is relative to the `base_url` passed at initialization time.
        It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
            path: the path where the request will be sent.
            raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
            **kwargs: additional kwargs for `requests.request()`

        Returns:
            a response object.

        Raises:
            requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("GET", path, raise_for_status=raise_for_status, **kwargs)

    def post(
        self,
        path: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a POST request and return a [Response][requests.Response] object.

        The passed `url` is relative to the `base_url` passed at initialization time.
        It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
          path: the path where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: additional kwargs for `requests.request()`

        Returns:
          a response object.

        Raises:
          requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("POST", path, raise_for_status=raise_for_status, **kwargs)

    def patch(
        self,
        path: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a PATCH request. Return a [Response][requests.Response] object.

        The passed `url` is relative to the `base_url` passed at initialization time.
        It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
          path: the path where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: additional kwargs for `requests.request()`

        Returns:
          a [Response][requests.Response] object.

        Raises:
          requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("PATCH", path, raise_for_status=raise_for_status, **kwargs)

    def put(
        self,
        path: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a PUT request. Return a [Response][requests.Response] object.

        The passed `url` is relative to the `base_url` passed at initialization time.
        It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
          path: the path where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: additional kwargs for `requests.request()`

        Returns:
          a [Response][requests.Response] object.

        Raises:
          requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("PUT", path, raise_for_status=raise_for_status, **kwargs)

    def delete(
        self,
        path: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a DELETE request. Return a [Response][requests.Response] object.

        The passed `url` may be relative to the url passed at initialization time. It takes the same
        parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
          path: the path where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: additional kwargs for `requests.request()`.

        Returns:
          a response object.

        Raises:
          requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("DELETE", path, raise_for_status=raise_for_status, **kwargs)

    def __getattr__(self, item: str) -> ApiClient:
        """Allow access sub resources with an attribute-based syntax.

        Args:
            item: a subpath

        Returns:
            a new `ApiClient` initialized on the new base url

        Example:
            ```python
            from requests_oauth2client import ApiClient

            api = ApiClient("https://myapi.local")
            resource1 = api.resource1.get()  # GET https://myapi.local/resource1
            resource2 = api.resource2.get()  # GET https://myapi.local/resource2
            ```

        """
        return self[item]

    def __getitem__(self, item: str) -> ApiClient:
        """Allow access to sub resources with a subscription-based syntax.

        Args:
            item: a subpath

        Returns:
            a new `ApiClient` initialized on the new base url

        Example:
            ```python
            from requests_oauth2client import ApiClient

            api = ApiClient("https://myapi.local")
            resource1 = api["resource1"].get()  # GET https://myapi.local/resource1
            resource2 = api["resource2"].get()  # GET https://myapi.local/resource2
            ```

        """
        new_base_uri = self.to_absolute_url(item)
        return ApiClient(
            new_base_uri,
            session=self.session,
            none_fields=self.none_fields,
            bool_fields=self.bool_fields,
            timeout=self.timeout,
            raise_for_status=self.raise_for_status,
        )

    def __enter__(self) -> Self:
        """Allow `ApiClient` to act as a context manager.

        You can then use an `ApiClient` instance in a `with` clause, the same way as
        `requests.Session`. The underlying request.Session will be closed on exit.

        Example:
            ```python
            with ApiClient("https://myapi.com/path") as client:
                resp = client.get("resource")
            ```

        """
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        """Close the underlying requests.Session on exit."""
        self.session.close()

request(method, path=None, *, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=False, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, raise_for_status=None, none_fields=None, bool_fields=None)

A wrapper around requests.Session.request method with extra features.

Additional features are described in ApiClient documentation.

All parameters will be passed as-is to requests.Session.request, expected those described below which have a special behavior.

Parameters:

Name Type Description Default
path None | str | bytes | Iterable[str | bytes | int]

the url where the request will be sent to. Can be:

  • a path, as str: that path will be joined to the configured API url,
  • an iterable of path segments: that will be joined to the root url.
None
raise_for_status bool | None

like the parameter of the same name from ApiClient, but this will be applied for this request only.

None
none_fields Literal['include', 'exclude', 'empty'] | None

like the parameter of the same name from ApiClient, but this will be applied for this request only.

None
bool_fields tuple[Any, Any] | None

like the parameter of the same name from ApiClient, but this will be applied for this request only.

None

Returns:

Type Description
Response

a Response as returned by requests

Raises:

Type Description
InvalidBoolFieldsParam

if the provided bool_fields parameter is invalid.

Source code in requests_oauth2client/api_client.py
def request(  # noqa: C901, PLR0913, D417
    self,
    method: str,
    path: None | str | bytes | Iterable[str | bytes | int] = None,
    *,
    params: None | bytes | MutableMapping[str, str] = None,
    data: (
        Iterable[bytes]
        | str
        | bytes
        | list[tuple[Any, Any]]
        | tuple[tuple[Any, Any], ...]
        | Mapping[Any, Any]
        | None
    ) = None,
    headers: MutableMapping[str, str] | None = None,
    cookies: None | RequestsCookieJar | MutableMapping[str, str] = None,
    files: MutableMapping[str, IO[Any]] | None = None,
    auth: (
        None
        | tuple[str, str]
        | requests.auth.AuthBase
        | Callable[[requests.PreparedRequest], requests.PreparedRequest]
    ) = None,
    timeout: None | float | tuple[float, float] | tuple[float, None] = None,
    allow_redirects: bool = False,
    proxies: MutableMapping[str, str] | None = None,
    hooks: None
    | (
        MutableMapping[
            str,
            (Iterable[Callable[[requests.Response], Any]] | Callable[[requests.Response], Any]),
        ]
    ) = None,
    stream: bool | None = None,
    verify: str | bool | None = None,
    cert: str | tuple[str, str] | None = None,
    json: Mapping[str, Any] | None = None,
    raise_for_status: bool | None = None,
    none_fields: Literal["include", "exclude", "empty"] | None = None,
    bool_fields: tuple[Any, Any] | None = None,
) -> requests.Response:
    """A wrapper around [requests.Session.request][] method with extra features.

    Additional features are described in
    [ApiClient][requests_oauth2client.api_client.ApiClient] documentation.

    All parameters will be passed as-is to [requests.Session.request][], expected those
    described below which have a special behavior.

    Args:
      path: the url where the request will be sent to. Can be:

        - a path, as `str`: that path will be joined to the configured API url,
        - an iterable of path segments: that will be joined to the root url.
      raise_for_status: like the parameter of the same name from
        [ApiClient][requests_oauth2client.api_client.ApiClient],
        but this will be applied for this request only.
      none_fields: like the parameter of the same name from
        [ApiClient][requests_oauth2client.api_client.ApiClient],
        but this will be applied for this request only.
      bool_fields: like the parameter of the same name from
        [ApiClient][requests_oauth2client.api_client.ApiClient],
        but this will be applied for this request only.

    Returns:
      a Response as returned by requests

    Raises:
        InvalidBoolFieldsParam: if the provided `bool_fields` parameter is invalid.

    """
    path = self.to_absolute_url(path)

    if none_fields is None:
        none_fields = self.none_fields

    if none_fields == "exclude":
        if isinstance(data, Mapping):
            data = {key: val for key, val in data.items() if val is not None}
        if isinstance(json, Mapping):
            json = {key: val for key, val in json.items() if val is not None}
    elif none_fields == "empty":
        if isinstance(data, Mapping):
            data = {key: val if val is not None else "" for key, val in data.items()}
        if isinstance(json, Mapping):
            json = {key: val if val is not None else "" for key, val in json.items()}

    if bool_fields is None:
        bool_fields = self.bool_fields

    if bool_fields:
        true_value, false_value = validate_bool_fields(bool_fields)
        if isinstance(data, MutableMapping):
            for key, val in data.items():
                if val is True:
                    data[key] = true_value
                elif val is False:
                    data[key] = false_value
        if isinstance(params, MutableMapping):
            for key, val in params.items():
                if val is True:
                    params[key] = true_value
                elif val is False:
                    params[key] = false_value

    timeout = timeout or self.timeout

    response = self.session.request(
        method,
        path,
        params=params,
        data=data,
        headers=headers,
        cookies=cookies,
        files=files,
        auth=auth or self.auth,
        timeout=timeout,
        allow_redirects=allow_redirects,
        proxies=proxies,
        hooks=hooks,
        stream=stream,
        verify=verify,
        cert=cert,
        json=json,
    )

    if raise_for_status is None:
        raise_for_status = self.raise_for_status
    if raise_for_status:
        response.raise_for_status()
    return response

to_absolute_url(path=None)

Convert a relative url to an absolute url.

Given a path, return the matching absolute url, based on the base_url that is configured for this API.

The result of this method is different from a standard urljoin(), because a relative_url that starts with a "/" will not override the path from the base url. You can also pass an iterable of path parts as relative url, which will be properly joined with "/". Those parts may be str (which will be urlencoded) or bytes (which will be decoded as UTF-8 first) or any other type (which will be converted to str first, using the str() function). See the table below for example results which would exhibit most cases:

base_url relative_url result_url
"https://myhost.com/root" "/path" "https://myhost.com/root/path"
"https://myhost.com/root" "/path" "https://myhost.com/root/path"
"https://myhost.com/root" b"/path" "https://myhost.com/root/path"
"https://myhost.com/root" "path" "https://myhost.com/root/path"
"https://myhost.com/root" None "https://myhost.com/root"
"https://myhost.com/root" ("user", 1, "resource") "https://myhost.com/root/user/1/resource"
"https://myhost.com/root" "https://otherhost.org/foo" ValueError

Parameters:

Name Type Description Default
path None | str | bytes | Iterable[str | bytes | int]

a relative url

None

Returns:

Type Description
str

the resulting absolute url

Raises:

Type Description
InvalidPathParam

if the provided path does not allow constructing a valid url

Source code in requests_oauth2client/api_client.py
def to_absolute_url(self, path: None | str | bytes | Iterable[str | bytes | int] = None) -> str:
    """Convert a relative url to an absolute url.

    Given a `path`, return the matching absolute url, based on the `base_url` that is
    configured for this API.

    The result of this method is different from a standard `urljoin()`, because a relative_url
    that starts with a "/" will not override the path from the base url. You can also pass an
    iterable of path parts as relative url, which will be properly joined with "/". Those parts
    may be `str` (which will be urlencoded) or `bytes` (which will be decoded as UTF-8 first) or
    any other type (which will be converted to `str` first, using the `str() function`). See the
    table below for example results which would exhibit most cases:

    | base_url | relative_url | result_url |
    |---------------------------|-----------------------------|-------------------------------------------|
    | `"https://myhost.com/root"` | `"/path"` | `"https://myhost.com/root/path"` |
    | `"https://myhost.com/root"` | `"/path"` | `"https://myhost.com/root/path"` |
    | `"https://myhost.com/root"` | `b"/path"` | `"https://myhost.com/root/path"` |
    | `"https://myhost.com/root"` | `"path"` | `"https://myhost.com/root/path"` |
    | `"https://myhost.com/root"` | `None` | `"https://myhost.com/root"` |
    | `"https://myhost.com/root"` |  `("user", 1, "resource")` | `"https://myhost.com/root/user/1/resource"` |
    | `"https://myhost.com/root"` | `"https://otherhost.org/foo"` | `ValueError` |

    Args:
      path: a relative url

    Returns:
      the resulting absolute url

    Raises:
        InvalidPathParam: if the provided path does not allow constructing a valid url

    """
    url = path

    if url is None:
        url = self.base_url
    else:
        if not isinstance(url, (str, bytes)):
            try:
                url = "/".join(
                    [urlencode(part.decode() if isinstance(part, bytes) else str(part)) for part in url if part],
                )
            except Exception as exc:
                raise InvalidPathParam(url) from exc

        if isinstance(url, bytes):
            url = url.decode()

        if "://" in url:
            raise InvalidPathParam(url)

        url = urljoin(self.base_url + "/", url.lstrip("/"))

    if url is None or not isinstance(url, str):
        raise InvalidPathParam(url)  # pragma: no cover

    return url

get(path=None, raise_for_status=None, **kwargs)

Send a GET request and return a Response object.

The passed url is relative to the base_url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path None | str | bytes | Iterable[str | bytes | int]

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request()

{}

Returns:

Type Description
Response

a response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def get(
    self,
    path: None | str | bytes | Iterable[str | bytes | int] = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a GET request and return a [Response][requests.Response] object.

    The passed `url` is relative to the `base_url` passed at initialization time.
    It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
        path: the path where the request will be sent.
        raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
        **kwargs: additional kwargs for `requests.request()`

    Returns:
        a response object.

    Raises:
        requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("GET", path, raise_for_status=raise_for_status, **kwargs)

post(path=None, raise_for_status=None, **kwargs)

Send a POST request and return a Response object.

The passed url is relative to the base_url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path str | bytes | Iterable[str | bytes] | None

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request()

{}

Returns:

Type Description
Response

a response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def post(
    self,
    path: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a POST request and return a [Response][requests.Response] object.

    The passed `url` is relative to the `base_url` passed at initialization time.
    It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
      path: the path where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: additional kwargs for `requests.request()`

    Returns:
      a response object.

    Raises:
      requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("POST", path, raise_for_status=raise_for_status, **kwargs)

patch(path=None, raise_for_status=None, **kwargs)

Send a PATCH request. Return a Response object.

The passed url is relative to the base_url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path str | bytes | Iterable[str | bytes] | None

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request()

{}

Returns:

Type Description
Response

a Response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def patch(
    self,
    path: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a PATCH request. Return a [Response][requests.Response] object.

    The passed `url` is relative to the `base_url` passed at initialization time.
    It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
      path: the path where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: additional kwargs for `requests.request()`

    Returns:
      a [Response][requests.Response] object.

    Raises:
      requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("PATCH", path, raise_for_status=raise_for_status, **kwargs)

put(path=None, raise_for_status=None, **kwargs)

Send a PUT request. Return a Response object.

The passed url is relative to the base_url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path str | bytes | Iterable[str | bytes] | None

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request()

{}

Returns:

Type Description
Response

a Response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def put(
    self,
    path: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a PUT request. Return a [Response][requests.Response] object.

    The passed `url` is relative to the `base_url` passed at initialization time.
    It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
      path: the path where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: additional kwargs for `requests.request()`

    Returns:
      a [Response][requests.Response] object.

    Raises:
      requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("PUT", path, raise_for_status=raise_for_status, **kwargs)

delete(path=None, raise_for_status=None, **kwargs)

Send a DELETE request. Return a Response object.

The passed url may be relative to the url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path str | bytes | Iterable[str | bytes] | None

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request().

{}

Returns:

Type Description
Response

a response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def delete(
    self,
    path: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a DELETE request. Return a [Response][requests.Response] object.

    The passed `url` may be relative to the url passed at initialization time. It takes the same
    parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
      path: the path where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: additional kwargs for `requests.request()`.

    Returns:
      a response object.

    Raises:
      requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("DELETE", path, raise_for_status=raise_for_status, **kwargs)

InvalidBoolFieldsParam

Bases: ValueError

Raised when an invalid value is passed as 'bool_fields' parameter.

Source code in requests_oauth2client/api_client.py
class InvalidBoolFieldsParam(ValueError):
    """Raised when an invalid value is passed as 'bool_fields' parameter."""

    def __init__(self, bool_fields: object) -> None:
        super().__init__("""\
Invalid value for `bool_fields` parameter. It must be an iterable of 2 `str` values:
- first one for the `True` value,
- second one for the `False` value.
Boolean fields in `data` or `params` with a boolean value (`True` or `False`)
will be serialized to the corresponding value.
Default is `('true', 'false')`
Use this parameter when the target API expects some other values, e.g.:
- ('on', 'off')
- ('1', '0')
- ('yes', 'no')
""")
        self.value = bool_fields

InvalidPathParam

Bases: TypeError, ValueError

Raised when an unexpected path is passed as 'url' parameter.

Source code in requests_oauth2client/api_client.py
class InvalidPathParam(TypeError, ValueError):
    """Raised when an unexpected path is passed as 'url' parameter."""

    def __init__(self, path: None | str | bytes | Iterable[str | bytes | int]) -> None:
        super().__init__("""\
Unexpected path. Please provide a path that is relative to the configured `base_url`:
- `None` (default) to call the base_url
- a `str` or `bytes`, that will be joined to the base_url (with a / separator, if required)
- or an iterable of string-able objects, which will be joined to the base_url with / separators
""")
        self.url = path

NonRenewableTokenError

Bases: Exception

Raised when attempting to renew a token non-interactively when missing renewing material.

Source code in requests_oauth2client/auth.py
class NonRenewableTokenError(Exception):
    """Raised when attempting to renew a token non-interactively when missing renewing material."""

OAuth2AccessTokenAuth

Bases: AuthBase

Authentication Handler for OAuth 2.0 Access Tokens and (optional) Refresh Tokens.

This Requests Auth handler implementation uses an access token as Bearer or DPoP token, and can automatically refresh it when expired, if a refresh token is available.

Token can be a simple str containing a raw access token value, or a BearerToken that can contain a refresh_token.

In addition to adding a properly formatted Authorization header, this will obtain a new token once the current token is expired. Expiration is detected based on the expires_in hint returned by the AS. A configurable leeway, in number of seconds, will make sure that a new token is obtained some seconds before the actual expiration is reached. This may help in situations where the client, AS and RS have slightly offset clocks.

Parameters:

Name Type Description Default
client OAuth2Client

the client to use to refresh tokens.

required
token str | BearerToken

an initial Access Token, if you have one already. In most cases, leave None.

required
leeway int

expiration leeway, in number of seconds.

20
**token_kwargs Any

additional kwargs to pass to the token endpoint.

{}
Example
1
2
3
4
5
6
7
8
from requests_oauth2client import BearerToken, OAuth2Client, OAuth2AccessTokenAuth, requests

client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
# obtain a BearerToken any way you see fit, optionally including a refresh token
# for this example, the token value is hardcoded
token = BearerToken(access_token="access_token", expires_in=600, refresh_token="refresh_token")
auth = OAuth2AccessTokenAuth(client, token, scope="my_scope")
resp = requests.post("https://my.api.local/resource", auth=auth)
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2AccessTokenAuth(requests.auth.AuthBase):
    """Authentication Handler for OAuth 2.0 Access Tokens and (optional) Refresh Tokens.

    This [Requests Auth handler][requests.auth.AuthBase] implementation uses an access token as
    Bearer or DPoP token, and can automatically refresh it when expired, if a refresh token is available.

    Token can be a simple `str` containing a raw access token value, or a
    [BearerToken][requests_oauth2client.tokens.BearerToken] that can contain a `refresh_token`.

    In addition to adding a properly formatted `Authorization` header, this will obtain a new token
    once the current token is expired. Expiration is detected based on the `expires_in` hint
    returned by the AS. A configurable `leeway`, in number of seconds, will make sure that a new
    token is obtained some seconds before the actual expiration is reached. This may help in
    situations where the client, AS and RS have slightly offset clocks.

    Args:
        client: the client to use to refresh tokens.
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        leeway: expiration leeway, in number of seconds.
        **token_kwargs: additional kwargs to pass to the token endpoint.

    Example:
        ```python
        from requests_oauth2client import BearerToken, OAuth2Client, OAuth2AccessTokenAuth, requests

        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        # obtain a BearerToken any way you see fit, optionally including a refresh token
        # for this example, the token value is hardcoded
        token = BearerToken(access_token="access_token", expires_in=600, refresh_token="refresh_token")
        auth = OAuth2AccessTokenAuth(client, token, scope="my_scope")
        resp = requests.post("https://my.api.local/resource", auth=auth)
        ```

    """

    client: OAuth2Client = field(on_setattr=setters.frozen)
    token: BearerToken | None
    leeway: int = field(on_setattr=setters.frozen)
    token_kwargs: dict[str, Any] = field(on_setattr=setters.frozen)

    def __init__(
        self, client: OAuth2Client, token: str | BearerToken, *, leeway: int = 20, **token_kwargs: Any
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(client=client, token=token, leeway=leeway, token_kwargs=token_kwargs)

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the Access Token to the request.

        If Access Token is not specified or expired, obtain a new one first.

        Raises:
            NonRenewableTokenError: if the token is not renewable

        """
        if self.token is None or self.token.is_expired(self.leeway):
            self.renew_token()
        if self.token is None:
            raise NonRenewableTokenError  # pragma: no cover
        return self.token(request)

    def renew_token(self) -> None:
        """Obtain a new Bearer Token.

        This will try to use the `refresh_token`, if there is one.

        """
        if self.token is not None and self.token.refresh_token is not None:
            self.token = self.client.refresh_token(refresh_token=self.token, **self.token_kwargs)

    def forget_token(self) -> None:
        """Forget the current token, forcing a renewal on the next HTTP request."""
        self.token = None

renew_token()

Obtain a new Bearer Token.

This will try to use the refresh_token, if there is one.

Source code in requests_oauth2client/auth.py
def renew_token(self) -> None:
    """Obtain a new Bearer Token.

    This will try to use the `refresh_token`, if there is one.

    """
    if self.token is not None and self.token.refresh_token is not None:
        self.token = self.client.refresh_token(refresh_token=self.token, **self.token_kwargs)

forget_token()

Forget the current token, forcing a renewal on the next HTTP request.

Source code in requests_oauth2client/auth.py
def forget_token(self) -> None:
    """Forget the current token, forcing a renewal on the next HTTP request."""
    self.token = None

OAuth2AuthorizationCodeAuth

Bases: OAuth2AccessTokenAuth

Authentication handler for the Authorization Code grant.

This Requests Auth handler implementation exchanges an Authorization Code for an access token, then automatically refreshes it once it is expired.

Parameters:

Name Type Description Default
client OAuth2Client

the client to use to obtain Access Tokens.

required
code str | AuthorizationResponse | None

an Authorization Code that has been obtained from the AS.

required
token str | BearerToken | None

an initial Access Token, if you have one already. In most cases, leave None.

None
leeway int

expiration leeway, in number of seconds.

20
**token_kwargs Any

additional kwargs to pass to the token endpoint.

{}
Example
1
2
3
4
5
from requests_oauth2client import ApiClient, OAuth2Client, OAuth2AuthorizationCodeAuth

client = OAuth2Client(token_endpoint="https://myas.local/token", auth=("client_id", "client_secret"))
code = "my_code"  # you must obtain this code yourself
api = ApiClient("https://my.api.local/resource", auth=OAuth2AuthorizationCodeAuth(client, code))
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2AuthorizationCodeAuth(OAuth2AccessTokenAuth):  # type: ignore[override]
    """Authentication handler for the [Authorization Code grant](https://www.rfc-editor.org/rfc/rfc6749#section-4.1).

    This [Requests Auth handler][requests.auth.AuthBase] implementation exchanges an Authorization
    Code for an access token, then automatically refreshes it once it is expired.

    Args:
        client: the client to use to obtain Access Tokens.
        code: an Authorization Code that has been obtained from the AS.
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        leeway: expiration leeway, in number of seconds.
        **token_kwargs: additional kwargs to pass to the token endpoint.

    Example:
        ```python
        from requests_oauth2client import ApiClient, OAuth2Client, OAuth2AuthorizationCodeAuth

        client = OAuth2Client(token_endpoint="https://myas.local/token", auth=("client_id", "client_secret"))
        code = "my_code"  # you must obtain this code yourself
        api = ApiClient("https://my.api.local/resource", auth=OAuth2AuthorizationCodeAuth(client, code))
        ```

    """

    code: str | AuthorizationResponse | None

    def __init__(
        self,
        client: OAuth2Client,
        code: str | AuthorizationResponse | None,
        *,
        leeway: int = 20,
        token: str | BearerToken | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(
            client=client,
            token=token,
            code=code,
            leeway=leeway,
            token_kwargs=token_kwargs,
        )

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Implement the Authorization Code grant as an Authentication Handler.

        This exchanges an Authorization Code for an access token and adds it in the request.

        Args:
            request: the request

        Returns:
            the request, with an Access Token added in Authorization Header

        """
        if self.token is None or self.token.is_expired():
            self.exchange_code_for_token()
        return super().__call__(request)

    def exchange_code_for_token(self) -> None:
        """Exchange the authorization code for an access token."""
        if self.code:  # pragma: no branch
            self.token = self.client.authorization_code(code=self.code, **self.token_kwargs)
            self.code = None

exchange_code_for_token()

Exchange the authorization code for an access token.

Source code in requests_oauth2client/auth.py
def exchange_code_for_token(self) -> None:
    """Exchange the authorization code for an access token."""
    if self.code:  # pragma: no branch
        self.token = self.client.authorization_code(code=self.code, **self.token_kwargs)
        self.code = None

OAuth2ClientCredentialsAuth

Bases: OAuth2AccessTokenAuth

An Auth Handler for the Client Credentials grant.

This requests AuthBase automatically gets Access Tokens from an OAuth 2.0 Token Endpoint with the Client Credentials grant, and will get a new one once the current one is expired.

Parameters:

Name Type Description Default
client OAuth2Client

the OAuth2Client to use to obtain Access Tokens.

required
token str | BearerToken | None

an initial Access Token, if you have one already. In most cases, leave None.

None
leeway int

expiration leeway, in number of seconds

20
**token_kwargs Any

extra kw parameters to pass to the Token Endpoint. May include scope, resource, etc.

{}
Example
1
2
3
4
5
from requests_oauth2client import OAuth2Client, OAuth2ClientCredentialsAuth, requests

client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
oauth2cc = OAuth2ClientCredentialsAuth(client, scope="my_scope")
resp = requests.post("https://my.api.local/resource", auth=oauth2cc)
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2ClientCredentialsAuth(OAuth2AccessTokenAuth):
    """An Auth Handler for the [Client Credentials grant](https://www.rfc-editor.org/rfc/rfc6749#section-4.4).

    This [requests AuthBase][requests.auth.AuthBase] automatically gets Access Tokens from an OAuth
    2.0 Token Endpoint with the Client Credentials grant, and will get a new one once the current
    one is expired.

    Args:
        client: the [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to obtain Access Tokens.
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        leeway: expiration leeway, in number of seconds
        **token_kwargs: extra kw parameters to pass to the Token Endpoint. May include `scope`, `resource`, etc.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client, OAuth2ClientCredentialsAuth, requests

        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        oauth2cc = OAuth2ClientCredentialsAuth(client, scope="my_scope")
        resp = requests.post("https://my.api.local/resource", auth=oauth2cc)
        ```

    """

    def __init__(
        self, client: OAuth2Client, *, leeway: int = 20, token: str | BearerToken | None = None, **token_kwargs: Any
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(client=client, token=token, leeway=leeway, token_kwargs=token_kwargs)

    @override
    def renew_token(self) -> None:
        """Obtain a new token for use within this Auth Handler."""
        self.token = self.client.client_credentials(**self.token_kwargs)

renew_token()

Obtain a new token for use within this Auth Handler.

Source code in requests_oauth2client/auth.py
@override
def renew_token(self) -> None:
    """Obtain a new token for use within this Auth Handler."""
    self.token = self.client.client_credentials(**self.token_kwargs)

OAuth2DeviceCodeAuth

Bases: OAuth2AccessTokenAuth

Authentication Handler for the Device Code Flow.

This Requests Auth handler implementation exchanges a Device Code for an Access Token, then automatically refreshes it once it is expired.

It needs a Device Code and an OAuth2Client to be able to get a token from the AS Token Endpoint just before the first request using this Auth Handler is being sent.

Parameters:

Name Type Description Default
client OAuth2Client

the OAuth2Client to use to obtain Access Tokens.

required
device_code str | DeviceAuthorizationResponse

a Device Code obtained from the AS.

required
interval int

the interval to use to pool the Token Endpoint, in seconds.

5
expires_in int

the lifetime of the token, in seconds.

360
token str | BearerToken | None

an initial Access Token, if you have one already. In most cases, leave None.

None
leeway int

expiration leeway, in number of seconds.

20
**token_kwargs Any

additional kwargs to pass to the token endpoint.

{}
Example
1
2
3
4
5
6
from requests_oauth2client import OAuth2Client, OAuth2DeviceCodeAuth, requests

client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
device_code = client.device_authorization()
auth = OAuth2DeviceCodeAuth(client, device_code)
resp = requests.post("https://my.api.local/resource", auth=auth)
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2DeviceCodeAuth(OAuth2AccessTokenAuth):  # type: ignore[override]
    """Authentication Handler for the [Device Code Flow](https://www.rfc-editor.org/rfc/rfc8628).

    This [Requests Auth handler][requests.auth.AuthBase] implementation exchanges a Device Code for
    an Access Token, then automatically refreshes it once it is expired.

    It needs a Device Code and an [OAuth2Client][requests_oauth2client.client.OAuth2Client] to be
    able to get a token from the AS Token Endpoint just before the first request using this Auth
    Handler is being sent.

    Args:
        client: the [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to obtain Access Tokens.
        device_code: a Device Code obtained from the AS.
        interval: the interval to use to pool the Token Endpoint, in seconds.
        expires_in: the lifetime of the token, in seconds.
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        leeway: expiration leeway, in number of seconds.
        **token_kwargs: additional kwargs to pass to the token endpoint.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client, OAuth2DeviceCodeAuth, requests

        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        device_code = client.device_authorization()
        auth = OAuth2DeviceCodeAuth(client, device_code)
        resp = requests.post("https://my.api.local/resource", auth=auth)
        ```

    """

    device_code: str | DeviceAuthorizationResponse | None
    interval: int
    expires_in: int

    def __init__(
        self,
        client: OAuth2Client,
        *,
        device_code: str | DeviceAuthorizationResponse,
        leeway: int = 20,
        interval: int = 5,
        expires_in: int = 360,
        token: str | BearerToken | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(
            client=client,
            token=token,
            leeway=leeway,
            token_kwargs=token_kwargs,
            device_code=device_code,
            interval=interval,
            expires_in=expires_in,
        )

    @override
    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Implement the Device Code grant as a request Authentication Handler.

        This exchanges a Device Code for an access token and adds it in HTTP requests.

        Args:
            request: a [requests.PreparedRequest][]

        Returns:
            a [requests.PreparedRequest][] with an Access Token added in Authorization Header

        """
        if self.token is None:
            self.exchange_device_code_for_token()
        return super().__call__(request)

    def exchange_device_code_for_token(self) -> None:
        """Exchange the Device Code for an access token.

        This will poll the Token Endpoint until the user finishes the authorization process.

        """
        from .device_authorization import DeviceAuthorizationPoolingJob

        if self.device_code:  # pragma: no branch
            pooling_job = DeviceAuthorizationPoolingJob(
                client=self.client,
                device_code=self.device_code,
                interval=self.interval,
            )
            token = None
            while token is None:
                token = pooling_job()
            self.token = token
            self.device_code = None

exchange_device_code_for_token()

Exchange the Device Code for an access token.

This will poll the Token Endpoint until the user finishes the authorization process.

Source code in requests_oauth2client/auth.py
def exchange_device_code_for_token(self) -> None:
    """Exchange the Device Code for an access token.

    This will poll the Token Endpoint until the user finishes the authorization process.

    """
    from .device_authorization import DeviceAuthorizationPoolingJob

    if self.device_code:  # pragma: no branch
        pooling_job = DeviceAuthorizationPoolingJob(
            client=self.client,
            device_code=self.device_code,
            interval=self.interval,
        )
        token = None
        while token is None:
            token = pooling_job()
        self.token = token
        self.device_code = None

OAuth2ResourceOwnerPasswordAuth

Bases: OAuth2AccessTokenAuth

Authentication Handler for the Resource Owner Password Credentials Flow.

This Requests Auth handler implementation exchanges the user credentials for an Access Token, then automatically repeats the process to get a new one once the current one is expired.

Note that this flow is considered deprecated, and the Authorization Code flow should be used whenever possible. Among other bad things, ROPC:

  • does not support SSO between multiple apps,
  • does not support MFA or risk-based adaptative authentication,
  • depends on the user typing its credentials directly inside the application, instead of on a dedicated, centralized login page managed by the AS, which makes it totally insecure for 3rd party apps.

It needs the username and password and an OAuth2Client to be able to get a token from the AS Token Endpoint just before the first request using this Auth Handler is being sent.

Parameters:

Name Type Description Default
client OAuth2Client

the client to use to obtain Access Tokens

required
username str

the username

required
password str

the user password

required
leeway int

an amount of time, in seconds

20
token str | BearerToken | None

an initial Access Token, if you have one already. In most cases, leave None.

None
**token_kwargs Any

additional kwargs to pass to the token endpoint

{}
Example
from requests_oauth2client import ApiClient, OAuth2Client, OAuth2ResourceOwnerPasswordAuth

client = OAuth2Client(
    token_endpoint="https://myas.local/token",
    auth=("client_id", "client_secret"),
)
username = "my_username"
password = "my_password"  # you must obtain those credentials from the user
auth = OAuth2ResourceOwnerPasswordAuth(client, username=username, password=password)
api = ApiClient("https://myapi.local", auth=auth)
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2ResourceOwnerPasswordAuth(OAuth2AccessTokenAuth):  # type: ignore[override]
    """Authentication Handler for the [Resource Owner Password Credentials Flow](https://www.rfc-editor.org/rfc/rfc6749#section-4.3).

    This [Requests Auth handler][requests.auth.AuthBase] implementation exchanges the user
    credentials for an Access Token, then automatically repeats the process to get a new one
    once the current one is expired.

    Note that this flow is considered *deprecated*, and the Authorization Code flow should be
    used whenever possible.
    Among other bad things, ROPC:

    - does not support SSO between multiple apps,
    - does not support MFA or risk-based adaptative authentication,
    - depends on the user typing its credentials directly inside the application, instead of on a
    dedicated, centralized login page managed by the AS, which makes it totally insecure for 3rd party apps.

    It needs the username and password and an
    [OAuth2Client][requests_oauth2client.client.OAuth2Client] to be able to get a token from
    the AS Token Endpoint just before the first request using this Auth Handler is being sent.

    Args:
        client: the client to use to obtain Access Tokens
        username: the username
        password: the user password
        leeway: an amount of time, in seconds
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        **token_kwargs: additional kwargs to pass to the token endpoint

    Example:
        ```python
        from requests_oauth2client import ApiClient, OAuth2Client, OAuth2ResourceOwnerPasswordAuth

        client = OAuth2Client(
            token_endpoint="https://myas.local/token",
            auth=("client_id", "client_secret"),
        )
        username = "my_username"
        password = "my_password"  # you must obtain those credentials from the user
        auth = OAuth2ResourceOwnerPasswordAuth(client, username=username, password=password)
        api = ApiClient("https://myapi.local", auth=auth)
        ```
    """

    username: str
    password: str

    def __init__(
        self,
        client: OAuth2Client,
        *,
        username: str,
        password: str,
        leeway: int = 20,
        token: str | BearerToken | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(
            client=client,
            token=token,
            leeway=leeway,
            token_kwargs=token_kwargs,
            username=username,
            password=password,
        )

    @override
    def renew_token(self) -> None:
        """Exchange the user credentials for an Access Token."""
        self.token = self.client.resource_owner_password(
            username=self.username,
            password=self.password,
            **self.token_kwargs,
        )

renew_token()

Exchange the user credentials for an Access Token.

Source code in requests_oauth2client/auth.py
@override
def renew_token(self) -> None:
    """Exchange the user credentials for an Access Token."""
    self.token = self.client.resource_owner_password(
        username=self.username,
        password=self.password,
        **self.token_kwargs,
    )

AuthorizationRequest

Represent an Authorization Request.

This class makes it easy to generate valid Authorization Request URI (possibly including a state, nonce, PKCE, and custom args), to store all parameters, and to validate an Authorization Response.

All parameters passed at init time will be included in the request query parameters as-is, excepted for a few parameters which have a special behaviour:

  • state: if ... (default), a random state parameter will be generated for you. You may pass your own state as str, or set it to None so that the state parameter will not be included in the request. You may access that state in the state attribute from this request.
  • nonce: if ... (default) and scope includes 'openid', a random nonce will be generated and included in the request. You may access that nonce in the nonce attribute from this request.
  • code_verifier: if None, and code_challenge_method is 'S256' or 'plain', a valid code_challenge and code_verifier for PKCE will be automatically generated, and the code_challenge will be included in the request. You may pass your own code_verifier as a str parameter, in which case the appropriate code_challenge will be included in the request, according to the code_challenge_method.
  • authorization_response_iss_parameter_supported and issuer: those are used for Server Issuer Identification. By default:

    • If ìssuer is set and an issuer is included in the Authorization Response, then the consistency between those 2 values will be checked when using validate_callback().
    • If issuer is not included in the response, then no issuer check is performed.

    Set authorization_response_iss_parameter_supported to True to enforce server identification:

    • an issuer must also be provided as parameter, and the AS must return that same value for the response to be considered valid by validate_callback().
    • if no issuer is included in the Authorization Response, then an error will be raised.

Parameters:

Name Type Description Default
authorization_endpoint str

the uri for the authorization endpoint.

required
client_id str

the client_id to include in the request.

required
redirect_uri str | None

the redirect_uri to include in the request. This is required in OAuth 2.0 and optional in OAuth 2.1. Pass None if you don't need any redirect_uri in the Authorization Request.

None
scope None | str | Iterable[str]

the scope to include in the request, as an iterable of str, or a single space-separated str.

'openid'
response_type str

the response type to include in the request.

CODE
state str | ellipsis | None

the state to include in the request, or ... to autogenerate one (default).

...
nonce str | ellipsis | None

the nonce to include in the request, or ... to autogenerate one (default).

...
code_verifier str | None

the code verifier to include in the request. If left as None and code_challenge_method is set, a valid code_verifier will be generated.

None
code_challenge_method str | None

the method to use to derive the code_challenge from the code_verifier.

S256
acr_values str | Iterable[str] | None

requested Authentication Context Class Reference values.

None
issuer str | None

Issuer Identifier value from the OAuth/OIDC Server, if using Server Issuer Identification.

None
**kwargs Any

extra parameters to include in the request, as-is.

{}
Example
1
2
3
4
5
6
7
8
9
from requests_oauth2client import AuthorizationRequest

azr = AuthorizationRequest(
    authorization_endpoint="https://url.to.the/authorization_endpoint",
    client_id="my_client_id",
    redirect_uri="http://localhost/callback",
    scope="openid email profile",
)
print(azr)

Raises:

Type Description
InvalidMaxAgeParam

if the max_age parameter is invalid.

MissingIssuerParam

if authorization_response_iss_parameter_supported is set to True but the issuer parameter is not provided.

UnsupportedResponseTypeParam

if response_type is not supported.

Source code in requests_oauth2client/authorization_request.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
@frozen(init=False, repr=False)
class AuthorizationRequest:
    """Represent an Authorization Request.

    This class makes it easy to generate valid Authorization Request URI (possibly including a
    state, nonce, PKCE, and custom args), to store all parameters, and to validate an Authorization
    Response.

    All parameters passed at init time will be included in the request query parameters as-is,
    excepted for a few parameters which have a special behaviour:

    - `state`: if `...` (default), a random `state` parameter will be generated for you.
      You may pass your own `state` as `str`, or set it to `None` so that the `state` parameter
      will not be included in the request. You may access that state in the `state` attribute
      from this request.
    - `nonce`: if `...` (default) and `scope` includes 'openid', a random `nonce` will be
      generated and included in the request. You may access that `nonce` in the `nonce` attribute
      from this request.
    - `code_verifier`: if `None`, and `code_challenge_method` is `'S256'` or `'plain'`,
      a valid `code_challenge` and `code_verifier` for PKCE will be automatically generated,
      and the `code_challenge` will be included in the request.
      You may pass your own `code_verifier` as a `str` parameter, in which case the
      appropriate `code_challenge` will be included in the request, according to the
      `code_challenge_method`.
    - `authorization_response_iss_parameter_supported` and `issuer`:
       those are used for Server Issuer Identification. By default:

        - If `ìssuer` is set and an issuer is included in the Authorization Response,
        then the consistency between those 2 values will be checked when using `validate_callback()`.
        - If issuer is not included in the response, then no issuer check is performed.

        Set `authorization_response_iss_parameter_supported` to `True` to enforce server identification:

        - an `issuer` must also be provided as parameter, and the AS must return that same value
        for the response to be considered valid by `validate_callback()`.
        - if no issuer is included in the Authorization Response, then an error will be raised.

    Args:
        authorization_endpoint: the uri for the authorization endpoint.
        client_id: the client_id to include in the request.
        redirect_uri: the redirect_uri to include in the request. This is required in OAuth 2.0 and optional
            in OAuth 2.1. Pass `None` if you don't need any redirect_uri in the Authorization
            Request.
        scope: the scope to include in the request, as an iterable of `str`, or a single space-separated `str`.
        response_type: the response type to include in the request.
        state: the state to include in the request, or `...` to autogenerate one (default).
        nonce: the nonce to include in the request, or `...` to autogenerate one (default).
        code_verifier: the code verifier to include in the request.
            If left as `None` and `code_challenge_method` is set, a valid code_verifier
            will be generated.
        code_challenge_method: the method to use to derive the `code_challenge` from the `code_verifier`.
        acr_values: requested Authentication Context Class Reference values.
        issuer: Issuer Identifier value from the OAuth/OIDC Server, if using Server Issuer Identification.
        **kwargs: extra parameters to include in the request, as-is.

    Example:
        ```python
        from requests_oauth2client import AuthorizationRequest

        azr = AuthorizationRequest(
            authorization_endpoint="https://url.to.the/authorization_endpoint",
            client_id="my_client_id",
            redirect_uri="http://localhost/callback",
            scope="openid email profile",
        )
        print(azr)
        ```

    Raises:
        InvalidMaxAgeParam: if the `max_age` parameter is invalid.
        MissingIssuerParam: if `authorization_response_iss_parameter_supported` is set to `True`
            but the `issuer` parameter is not provided.
        UnsupportedResponseTypeParam: if `response_type` is not supported.

    """

    authorization_endpoint: str

    client_id: str = field(metadata={"query": True})
    redirect_uri: str | None = field(metadata={"query": True})
    scope: tuple[str, ...] | None = field(metadata={"query": True})
    response_type: str = field(metadata={"query": True})
    state: str | None = field(metadata={"query": True})
    nonce: str | None = field(metadata={"query": True})
    code_challenge_method: str | None = field(metadata={"query": True})
    acr_values: tuple[str, ...] | None = field(metadata={"query": True})
    max_age: int | None = field(metadata={"query": True})
    kwargs: dict[str, Any]

    code_verifier: str | None
    authorization_response_iss_parameter_supported: bool
    issuer: str | None

    dpop_key: DPoPKey | None = None

    exception_classes: ClassVar[dict[str, type[AuthorizationResponseError]]] = {
        "interaction_required": InteractionRequired,
        "login_required": LoginRequired,
        "session_selection_required": SessionSelectionRequired,
        "consent_required": ConsentRequired,
    }

    @classmethod
    def generate_state(cls) -> str:
        """Generate a random `state` parameter."""
        return secrets.token_urlsafe(32)

    @classmethod
    def generate_nonce(cls) -> str:
        """Generate a random `nonce`."""
        return secrets.token_urlsafe(32)

    def __init__(  # noqa: PLR0913, C901
        self,
        authorization_endpoint: str,
        *,
        client_id: str,
        redirect_uri: str | None = None,
        scope: None | str | Iterable[str] = "openid",
        response_type: str = ResponseTypes.CODE,
        state: str | ellipsis | None = ...,  # noqa: F821
        nonce: str | ellipsis | None = ...,  # noqa: F821
        code_verifier: str | None = None,
        code_challenge_method: str | None = CodeChallengeMethods.S256,
        acr_values: str | Iterable[str] | None = None,
        max_age: int | None = None,
        issuer: str | None = None,
        authorization_response_iss_parameter_supported: bool = False,
        dpop: bool = False,
        dpop_alg: str = SignatureAlgs.ES256,
        dpop_key: DPoPKey | None = None,
        **kwargs: Any,
    ) -> None:
        if response_type != ResponseTypes.CODE:
            raise UnsupportedResponseTypeParam(response_type)

        if authorization_response_iss_parameter_supported and not issuer:
            raise MissingIssuerParam

        if state is ...:
            state = self.generate_state()
        if state is not None and not isinstance(state, str):
            state = str(state)  # pragma: no cover

        if nonce is ...:
            nonce = self.generate_nonce() if scope is not None and "openid" in scope else None
        if nonce is not None and not isinstance(nonce, str):
            nonce = str(nonce)  # pragma: no cover

        if not scope:
            scope = None

        if scope is not None:
            scope = tuple(scope.split(" ")) if isinstance(scope, str) else tuple(scope)

        if acr_values is not None:
            acr_values = tuple(acr_values.split()) if isinstance(acr_values, str) else tuple(acr_values)

        if max_age is not None and max_age < 0:
            raise InvalidMaxAgeParam

        if "code_challenge" in kwargs:
            msg = (
                "A `code_challenge` must not be passed as parameter. Pass the `code_verifier`"
                " instead, and the appropriate `code_challenge` will automatically be derived"
                " from it and included in the request, based on `code_challenge_method`."
            )
            raise ValueError(msg)

        if code_challenge_method:
            if not code_verifier:
                code_verifier = PkceUtils.generate_code_verifier()
        else:
            code_verifier = None

        if dpop and not dpop_key:
            dpop_key = DPoPKey.generate(dpop_alg)

        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            redirect_uri=redirect_uri,
            issuer=issuer,
            response_type=response_type,
            scope=scope,
            state=state,
            nonce=nonce,
            code_verifier=code_verifier,
            code_challenge_method=code_challenge_method,
            acr_values=acr_values,
            max_age=max_age,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            dpop_key=dpop_key,
            kwargs=kwargs,
        )

    @cached_property
    def code_challenge(self) -> str | None:
        """The `code_challenge` that matches `code_verifier` and `code_challenge_method`."""
        if self.code_verifier and self.code_challenge_method:
            return PkceUtils.derive_challenge(self.code_verifier, self.code_challenge_method)
        return None

    @cached_property
    def dpop_jkt(self) -> str | None:
        """The DPoP JWK thumbprint that matches ``dpop_key`."""
        if self.dpop_key:
            return self.dpop_key.dpop_jkt
        return None

    def as_dict(self) -> dict[str, Any]:
        """Return the full argument dict.

        This can be used to serialize this request and/or to initialize a similar request.

        """
        d = asdict(self)
        d.update(**d.pop("kwargs", {}))
        return d

    @property
    def args(self) -> dict[str, Any]:
        """Return a dict with all the query parameters from this AuthorizationRequest.

        Returns:
            a dict of parameters

        """
        d = {field.name: getattr(self, field.name) for field in fields(type(self)) if field.metadata.get("query")}
        if d["scope"]:
            d["scope"] = " ".join(d["scope"])
        d["code_challenge"] = self.code_challenge
        d["dpop_jkt"] = self.dpop_jkt
        d.update(self.kwargs)

        return {key: val for key, val in d.items() if val is not None}

    def validate_callback(self, response: str) -> AuthorizationResponse:
        """Validate an Authorization Response against this Request.

        Validate a given Authorization Response URI against this Authorization Request, and return
        an [AuthorizationResponse][requests_oauth2client.authorization_request.AuthorizationResponse].

        This includes matching the `state` parameter, checking for returned errors, and extracting
        the returned `code` and other parameters.

        Args:
            response: the Authorization Response URI. This can be the full URL, or just the
                query parameters (still encoded as x-www-form-urlencoded).

        Returns:
            the extracted code, if all checks are successful

        Raises:
            MissingAuthCode: if the `code` is missing in the response
            MissingIssuer: if Server Issuer verification is active and the response does
                not contain an `iss`.
            MismatchingIssuer: if the 'iss' received from the response does not match the
                expected value.
            MismatchingState: if the response `state` does not match the expected value.
            OAuth2Error: if the response includes an error.
            MissingAuthCode: if the response does not contain a `code`.
            UnsupportedResponseTypeParam: if response_type anything else than 'code'.

        """
        try:
            response_url = furl(response)
        except ValueError:
            return self.on_response_error(response)

        # validate 'iss' according to RFC9207
        received_issuer = response_url.args.get("iss")
        if self.authorization_response_iss_parameter_supported or received_issuer:
            if received_issuer is None:
                raise MissingIssuer(self, response)
            if self.issuer and received_issuer != self.issuer:
                raise MismatchingIssuer(self.issuer, received_issuer, self, response)

        # validate state
        requested_state = self.state
        if requested_state:
            received_state = response_url.args.get("state")
            if requested_state != received_state:
                raise MismatchingState(requested_state, received_state, self, response)

        error = response_url.args.get("error")
        if error:
            return self.on_response_error(response)

        if self.response_type == ResponseTypes.CODE:
            code: str = response_url.args.get("code")
            if code is None:
                raise MissingAuthCode(self, response)
        else:
            raise UnsupportedResponseTypeParam(self.response_type)  # pragma: no cover

        return AuthorizationResponse(
            code_verifier=self.code_verifier,
            redirect_uri=self.redirect_uri,
            nonce=self.nonce,
            acr_values=self.acr_values,
            max_age=self.max_age,
            dpop_key=self.dpop_key,
            **response_url.args,
        )

    def sign_request_jwt(
        self,
        jwk: Jwk | dict[str, Any],
        alg: str | None = None,
        lifetime: int | None = None,
    ) -> SignedJwt:
        """Sign the `request` object that matches this Authorization Request parameters.

        Args:
            jwk: the JWK to use to sign the request
            alg: the alg to use to sign the request, if the provided `jwk` has no `alg` parameter.
            lifetime: an optional number of seconds of validity for the signed request.
                If present, `iat` an `exp` claims will be included in the signed JWT.

        Returns:
            a `Jwt` that contains the signed request object.

        """
        claims = self.args
        if lifetime:
            claims["iat"] = Jwt.timestamp()
            claims["exp"] = Jwt.timestamp(lifetime)
        return Jwt.sign(
            claims,
            key=jwk,
            alg=alg,
        )

    def sign(
        self,
        jwk: Jwk | dict[str, Any],
        alg: str | None = None,
        lifetime: int | None = None,
        **kwargs: Any,
    ) -> RequestParameterAuthorizationRequest:
        """Sign this Authorization Request and return a new one.

        This replaces all parameters with a signed `request` JWT.

        Args:
            jwk: the JWK to use to sign the request
            alg: the alg to use to sign the request, if the provided `jwk` has no `alg` parameter.
            lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
                By default, don't use an 'exp' claim.
            kwargs: additional query parameters to include in the signed authorization request

        Returns:
            the signed Authorization Request

        """
        request_jwt = self.sign_request_jwt(jwk, alg, lifetime)
        return RequestParameterAuthorizationRequest(
            authorization_endpoint=self.authorization_endpoint,
            client_id=self.client_id,
            request=str(request_jwt),
            expires_at=request_jwt.expires_at,
            **kwargs,
        )

    def sign_and_encrypt_request_jwt(
        self,
        sign_jwk: Jwk | dict[str, Any],
        enc_jwk: Jwk | dict[str, Any],
        sign_alg: str | None = None,
        enc_alg: str | None = None,
        enc: str = "A128CBC-HS256",
        lifetime: int | None = None,
    ) -> JweCompact:
        """Sign and encrypt a `request` object for this Authorization Request.

        The signed `request` will contain the same parameters as this AuthorizationRequest.

        Args:
            sign_jwk: the JWK to use to sign the request
            enc_jwk: the JWK to use to encrypt the request
            sign_alg: the alg to use to sign the request, if `sign_jwk` has no `alg` parameter.
            enc_alg: the alg to use to encrypt the request, if `enc_jwk` has no `alg` parameter.
            enc: the encoding to use to encrypt the request.
            lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
                By default, do not include an 'exp' claim.

        Returns:
            the signed and encrypted request object, as a `jwskate.Jwt`

        """
        claims = self.args
        if lifetime:
            claims["iat"] = Jwt.timestamp()
            claims["exp"] = Jwt.timestamp(lifetime)
        return Jwt.sign_and_encrypt(
            claims=claims,
            sign_key=sign_jwk,
            sign_alg=sign_alg,
            enc_key=enc_jwk,
            enc_alg=enc_alg,
            enc=enc,
        )

    def sign_and_encrypt(
        self,
        sign_jwk: Jwk | dict[str, Any],
        enc_jwk: Jwk | dict[str, Any],
        sign_alg: str | None = None,
        enc_alg: str | None = None,
        enc: str = "A128CBC-HS256",
        lifetime: int | None = None,
    ) -> RequestParameterAuthorizationRequest:
        """Sign and encrypt the current Authorization Request.

        This replaces all parameters with a matching `request` object.

        Args:
            sign_jwk: the JWK to use to sign the request
            enc_jwk: the JWK to use to encrypt the request
            sign_alg: the alg to use to sign the request, if `sign_jwk` has no `alg` parameter.
            enc_alg: the alg to use to encrypt the request, if `enc_jwk` has no `alg` parameter.
            enc: the encoding to use to encrypt the request.
            lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
                By default, do not include an 'exp' claim.

        Returns:
            a `RequestParameterAuthorizationRequest`, with a request object as parameter

        """
        request_jwt = self.sign_and_encrypt_request_jwt(
            sign_jwk=sign_jwk,
            enc_jwk=enc_jwk,
            sign_alg=sign_alg,
            enc_alg=enc_alg,
            enc=enc,
            lifetime=lifetime,
        )
        return RequestParameterAuthorizationRequest(
            authorization_endpoint=self.authorization_endpoint,
            client_id=self.client_id,
            request=str(request_jwt),
        )

    def on_response_error(self, response: str) -> AuthorizationResponse:
        """Error handler for Authorization Response errors.

        Triggered by
        [validate_callback()][requests_oauth2client.authorization_request.AuthorizationRequest.validate_callback]
        if the response uri contains an error.

        Args:
            response: the Authorization Response URI. This can be the full URL, or just the query parameters.

        Returns:
            may return a default code that will be returned by `validate_callback`. But this method
            will most likely raise exceptions instead.

        Raises:
            AuthorizationResponseError: if the response contains an `error`. The raised exception may be a subclass

        """
        response_url = furl(response)
        error = response_url.args.get("error")
        error_description = response_url.args.get("error_description")
        error_uri = response_url.args.get("error_uri")
        exception_class = self.exception_classes.get(error, AuthorizationResponseError)
        raise exception_class(
            request=self, response=response, error=error, description=error_description, uri=error_uri
        )

    @property
    def furl(self) -> furl:
        """Return the Authorization Request URI, as a `furl`."""
        return furl(
            self.authorization_endpoint,
            args=self.args,
        )

    @property
    def uri(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return str(self.furl.url)

    def __getattr__(self, item: str) -> Any:
        """Allow attribute access to extra parameters."""
        return self.kwargs[item]

    def __repr__(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return self.uri

code_challenge cached property

The code_challenge that matches code_verifier and code_challenge_method.

dpop_jkt cached property

The DPoP JWK thumbprint that matches `dpop_key.

args property

Return a dict with all the query parameters from this AuthorizationRequest.

Returns:

Type Description
dict[str, Any]

a dict of parameters

furl property

Return the Authorization Request URI, as a furl.

uri property

Return the Authorization Request URI, as a str.

generate_state() classmethod

Generate a random state parameter.

Source code in requests_oauth2client/authorization_request.py
@classmethod
def generate_state(cls) -> str:
    """Generate a random `state` parameter."""
    return secrets.token_urlsafe(32)

generate_nonce() classmethod

Generate a random nonce.

Source code in requests_oauth2client/authorization_request.py
@classmethod
def generate_nonce(cls) -> str:
    """Generate a random `nonce`."""
    return secrets.token_urlsafe(32)

as_dict()

Return the full argument dict.

This can be used to serialize this request and/or to initialize a similar request.

Source code in requests_oauth2client/authorization_request.py
def as_dict(self) -> dict[str, Any]:
    """Return the full argument dict.

    This can be used to serialize this request and/or to initialize a similar request.

    """
    d = asdict(self)
    d.update(**d.pop("kwargs", {}))
    return d

validate_callback(response)

Validate an Authorization Response against this Request.

Validate a given Authorization Response URI against this Authorization Request, and return an AuthorizationResponse.

This includes matching the state parameter, checking for returned errors, and extracting the returned code and other parameters.

Parameters:

Name Type Description Default
response str

the Authorization Response URI. This can be the full URL, or just the query parameters (still encoded as x-www-form-urlencoded).

required

Returns:

Type Description
AuthorizationResponse

the extracted code, if all checks are successful

Raises:

Type Description
MissingAuthCode

if the code is missing in the response

MissingIssuer

if Server Issuer verification is active and the response does not contain an iss.

MismatchingIssuer

if the 'iss' received from the response does not match the expected value.

MismatchingState

if the response state does not match the expected value.

OAuth2Error

if the response includes an error.

MissingAuthCode

if the response does not contain a code.

UnsupportedResponseTypeParam

if response_type anything else than 'code'.

Source code in requests_oauth2client/authorization_request.py
def validate_callback(self, response: str) -> AuthorizationResponse:
    """Validate an Authorization Response against this Request.

    Validate a given Authorization Response URI against this Authorization Request, and return
    an [AuthorizationResponse][requests_oauth2client.authorization_request.AuthorizationResponse].

    This includes matching the `state` parameter, checking for returned errors, and extracting
    the returned `code` and other parameters.

    Args:
        response: the Authorization Response URI. This can be the full URL, or just the
            query parameters (still encoded as x-www-form-urlencoded).

    Returns:
        the extracted code, if all checks are successful

    Raises:
        MissingAuthCode: if the `code` is missing in the response
        MissingIssuer: if Server Issuer verification is active and the response does
            not contain an `iss`.
        MismatchingIssuer: if the 'iss' received from the response does not match the
            expected value.
        MismatchingState: if the response `state` does not match the expected value.
        OAuth2Error: if the response includes an error.
        MissingAuthCode: if the response does not contain a `code`.
        UnsupportedResponseTypeParam: if response_type anything else than 'code'.

    """
    try:
        response_url = furl(response)
    except ValueError:
        return self.on_response_error(response)

    # validate 'iss' according to RFC9207
    received_issuer = response_url.args.get("iss")
    if self.authorization_response_iss_parameter_supported or received_issuer:
        if received_issuer is None:
            raise MissingIssuer(self, response)
        if self.issuer and received_issuer != self.issuer:
            raise MismatchingIssuer(self.issuer, received_issuer, self, response)

    # validate state
    requested_state = self.state
    if requested_state:
        received_state = response_url.args.get("state")
        if requested_state != received_state:
            raise MismatchingState(requested_state, received_state, self, response)

    error = response_url.args.get("error")
    if error:
        return self.on_response_error(response)

    if self.response_type == ResponseTypes.CODE:
        code: str = response_url.args.get("code")
        if code is None:
            raise MissingAuthCode(self, response)
    else:
        raise UnsupportedResponseTypeParam(self.response_type)  # pragma: no cover

    return AuthorizationResponse(
        code_verifier=self.code_verifier,
        redirect_uri=self.redirect_uri,
        nonce=self.nonce,
        acr_values=self.acr_values,
        max_age=self.max_age,
        dpop_key=self.dpop_key,
        **response_url.args,
    )

sign_request_jwt(jwk, alg=None, lifetime=None)

Sign the request object that matches this Authorization Request parameters.

Parameters:

Name Type Description Default
jwk Jwk | dict[str, Any]

the JWK to use to sign the request

required
alg str | None

the alg to use to sign the request, if the provided jwk has no alg parameter.

None
lifetime int | None

an optional number of seconds of validity for the signed request. If present, iat an exp claims will be included in the signed JWT.

None

Returns:

Type Description
SignedJwt

a Jwt that contains the signed request object.

Source code in requests_oauth2client/authorization_request.py
def sign_request_jwt(
    self,
    jwk: Jwk | dict[str, Any],
    alg: str | None = None,
    lifetime: int | None = None,
) -> SignedJwt:
    """Sign the `request` object that matches this Authorization Request parameters.

    Args:
        jwk: the JWK to use to sign the request
        alg: the alg to use to sign the request, if the provided `jwk` has no `alg` parameter.
        lifetime: an optional number of seconds of validity for the signed request.
            If present, `iat` an `exp` claims will be included in the signed JWT.

    Returns:
        a `Jwt` that contains the signed request object.

    """
    claims = self.args
    if lifetime:
        claims["iat"] = Jwt.timestamp()
        claims["exp"] = Jwt.timestamp(lifetime)
    return Jwt.sign(
        claims,
        key=jwk,
        alg=alg,
    )

sign(jwk, alg=None, lifetime=None, **kwargs)

Sign this Authorization Request and return a new one.

This replaces all parameters with a signed request JWT.

Parameters:

Name Type Description Default
jwk Jwk | dict[str, Any]

the JWK to use to sign the request

required
alg str | None

the alg to use to sign the request, if the provided jwk has no alg parameter.

None
lifetime int | None

lifetime of the resulting Jwt (used to calculate the 'exp' claim). By default, don't use an 'exp' claim.

None
kwargs Any

additional query parameters to include in the signed authorization request

{}

Returns:

Type Description
RequestParameterAuthorizationRequest

the signed Authorization Request

Source code in requests_oauth2client/authorization_request.py
def sign(
    self,
    jwk: Jwk | dict[str, Any],
    alg: str | None = None,
    lifetime: int | None = None,
    **kwargs: Any,
) -> RequestParameterAuthorizationRequest:
    """Sign this Authorization Request and return a new one.

    This replaces all parameters with a signed `request` JWT.

    Args:
        jwk: the JWK to use to sign the request
        alg: the alg to use to sign the request, if the provided `jwk` has no `alg` parameter.
        lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
            By default, don't use an 'exp' claim.
        kwargs: additional query parameters to include in the signed authorization request

    Returns:
        the signed Authorization Request

    """
    request_jwt = self.sign_request_jwt(jwk, alg, lifetime)
    return RequestParameterAuthorizationRequest(
        authorization_endpoint=self.authorization_endpoint,
        client_id=self.client_id,
        request=str(request_jwt),
        expires_at=request_jwt.expires_at,
        **kwargs,
    )

sign_and_encrypt_request_jwt(sign_jwk, enc_jwk, sign_alg=None, enc_alg=None, enc='A128CBC-HS256', lifetime=None)

Sign and encrypt a request object for this Authorization Request.

The signed request will contain the same parameters as this AuthorizationRequest.

Parameters:

Name Type Description Default
sign_jwk Jwk | dict[str, Any]

the JWK to use to sign the request

required
enc_jwk Jwk | dict[str, Any]

the JWK to use to encrypt the request

required
sign_alg str | None

the alg to use to sign the request, if sign_jwk has no alg parameter.

None
enc_alg str | None

the alg to use to encrypt the request, if enc_jwk has no alg parameter.

None
enc str

the encoding to use to encrypt the request.

'A128CBC-HS256'
lifetime int | None

lifetime of the resulting Jwt (used to calculate the 'exp' claim). By default, do not include an 'exp' claim.

None

Returns:

Type Description
JweCompact

the signed and encrypted request object, as a jwskate.Jwt

Source code in requests_oauth2client/authorization_request.py
def sign_and_encrypt_request_jwt(
    self,
    sign_jwk: Jwk | dict[str, Any],
    enc_jwk: Jwk | dict[str, Any],
    sign_alg: str | None = None,
    enc_alg: str | None = None,
    enc: str = "A128CBC-HS256",
    lifetime: int | None = None,
) -> JweCompact:
    """Sign and encrypt a `request` object for this Authorization Request.

    The signed `request` will contain the same parameters as this AuthorizationRequest.

    Args:
        sign_jwk: the JWK to use to sign the request
        enc_jwk: the JWK to use to encrypt the request
        sign_alg: the alg to use to sign the request, if `sign_jwk` has no `alg` parameter.
        enc_alg: the alg to use to encrypt the request, if `enc_jwk` has no `alg` parameter.
        enc: the encoding to use to encrypt the request.
        lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
            By default, do not include an 'exp' claim.

    Returns:
        the signed and encrypted request object, as a `jwskate.Jwt`

    """
    claims = self.args
    if lifetime:
        claims["iat"] = Jwt.timestamp()
        claims["exp"] = Jwt.timestamp(lifetime)
    return Jwt.sign_and_encrypt(
        claims=claims,
        sign_key=sign_jwk,
        sign_alg=sign_alg,
        enc_key=enc_jwk,
        enc_alg=enc_alg,
        enc=enc,
    )

sign_and_encrypt(sign_jwk, enc_jwk, sign_alg=None, enc_alg=None, enc='A128CBC-HS256', lifetime=None)

Sign and encrypt the current Authorization Request.

This replaces all parameters with a matching request object.

Parameters:

Name Type Description Default
sign_jwk Jwk | dict[str, Any]

the JWK to use to sign the request

required
enc_jwk Jwk | dict[str, Any]

the JWK to use to encrypt the request

required
sign_alg str | None

the alg to use to sign the request, if sign_jwk has no alg parameter.

None
enc_alg str | None

the alg to use to encrypt the request, if enc_jwk has no alg parameter.

None
enc str

the encoding to use to encrypt the request.

'A128CBC-HS256'
lifetime int | None

lifetime of the resulting Jwt (used to calculate the 'exp' claim). By default, do not include an 'exp' claim.

None

Returns:

Type Description
RequestParameterAuthorizationRequest

a RequestParameterAuthorizationRequest, with a request object as parameter

Source code in requests_oauth2client/authorization_request.py
def sign_and_encrypt(
    self,
    sign_jwk: Jwk | dict[str, Any],
    enc_jwk: Jwk | dict[str, Any],
    sign_alg: str | None = None,
    enc_alg: str | None = None,
    enc: str = "A128CBC-HS256",
    lifetime: int | None = None,
) -> RequestParameterAuthorizationRequest:
    """Sign and encrypt the current Authorization Request.

    This replaces all parameters with a matching `request` object.

    Args:
        sign_jwk: the JWK to use to sign the request
        enc_jwk: the JWK to use to encrypt the request
        sign_alg: the alg to use to sign the request, if `sign_jwk` has no `alg` parameter.
        enc_alg: the alg to use to encrypt the request, if `enc_jwk` has no `alg` parameter.
        enc: the encoding to use to encrypt the request.
        lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
            By default, do not include an 'exp' claim.

    Returns:
        a `RequestParameterAuthorizationRequest`, with a request object as parameter

    """
    request_jwt = self.sign_and_encrypt_request_jwt(
        sign_jwk=sign_jwk,
        enc_jwk=enc_jwk,
        sign_alg=sign_alg,
        enc_alg=enc_alg,
        enc=enc,
        lifetime=lifetime,
    )
    return RequestParameterAuthorizationRequest(
        authorization_endpoint=self.authorization_endpoint,
        client_id=self.client_id,
        request=str(request_jwt),
    )

on_response_error(response)

Error handler for Authorization Response errors.

Triggered by validate_callback() if the response uri contains an error.

Parameters:

Name Type Description Default
response str

the Authorization Response URI. This can be the full URL, or just the query parameters.

required

Returns:

Type Description
AuthorizationResponse

may return a default code that will be returned by validate_callback. But this method

AuthorizationResponse

will most likely raise exceptions instead.

Raises:

Type Description
AuthorizationResponseError

if the response contains an error. The raised exception may be a subclass

Source code in requests_oauth2client/authorization_request.py
def on_response_error(self, response: str) -> AuthorizationResponse:
    """Error handler for Authorization Response errors.

    Triggered by
    [validate_callback()][requests_oauth2client.authorization_request.AuthorizationRequest.validate_callback]
    if the response uri contains an error.

    Args:
        response: the Authorization Response URI. This can be the full URL, or just the query parameters.

    Returns:
        may return a default code that will be returned by `validate_callback`. But this method
        will most likely raise exceptions instead.

    Raises:
        AuthorizationResponseError: if the response contains an `error`. The raised exception may be a subclass

    """
    response_url = furl(response)
    error = response_url.args.get("error")
    error_description = response_url.args.get("error_description")
    error_uri = response_url.args.get("error_uri")
    exception_class = self.exception_classes.get(error, AuthorizationResponseError)
    raise exception_class(
        request=self, response=response, error=error, description=error_description, uri=error_uri
    )

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.

Source code in requests_oauth2client/authorization_request.py
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)

default_dumper(azr) staticmethod

Provide a default dumper implementation.

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

Parameters:

Name Type Description Default
azr AuthorizationRequest

the AuthorizationRequest to serialize

required

Returns:

Type Description
str

the serialized value

Source code in requests_oauth2client/authorization_request.py
@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()

default_loader(serialized, azr_class=AuthorizationRequest) staticmethod

Provide a default deserializer implementation.

This does the opposite operations than default_dumper.

Parameters:

Name Type Description Default
serialized str

the serialized AuthorizationRequest

required
azr_class type[AuthorizationRequest]

the class to deserialize the Authorization Request to

AuthorizationRequest

Returns:

Type Description
AuthorizationRequest

an AuthorizationRequest

Source code in requests_oauth2client/authorization_request.py
@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)

dumps(azr)

Serialize and compress a given AuthorizationRequest for easier storage.

Parameters:

Name Type Description Default
azr AuthorizationRequest

an AuthorizationRequest to serialize

required

Returns:

Type Description
str

the serialized AuthorizationRequest, as a str

Source code in requests_oauth2client/authorization_request.py
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)

loads(serialized)

Deserialize a serialized AuthorizationRequest.

Parameters:

Name Type Description Default
serialized str

the serialized AuthorizationRequest

required

Returns:

Type Description
AuthorizationRequest

the deserialized AuthorizationRequest

Source code in requests_oauth2client/authorization_request.py
def loads(self, serialized: str) -> AuthorizationRequest:
    """Deserialize a serialized AuthorizationRequest.

    Args:
        serialized: the serialized AuthorizationRequest

    Returns:
        the deserialized AuthorizationRequest

    """
    return self.loader(serialized)

AuthorizationResponse

Represent a successful Authorization Response.

An Authorization Response is the redirection initiated by the AS to the client's redirection endpoint (redirect_uri), after an Authorization Request. This Response is typically created with a call to AuthorizationRequest.validate_callback() once the call to the client Redirection Endpoint is made. AuthorizationResponse contains the following attributes:

  • all the parameters that have been returned by the AS, most notably the code, and optional parameters such as state.
  • the redirect_uri that was used for the Authorization Request
  • the code_verifier matching the code_challenge that was used for the Authorization Request

Parameters redirect_uri and code_verifier must be those from the matching AuthorizationRequest. All other parameters including code and state must be those extracted from the Authorization Response parameters.

Parameters:

Name Type Description Default
code str

The authorization code returned by the AS.

required
redirect_uri str | None

The redirect_uri that was passed as parameter in the Authorization Request.

None
code_verifier str | None

the code_verifier matching the code_challenge that was passed as parameter in the Authorization Request.

None
state str | None

the state that was was passed as parameter in the Authorization Request and returned by the AS.

None
nonce str | None

the nonce that was was passed as parameter in the Authorization Request.

None
acr_values str | Sequence[str] | None

the acr_values that was passed as parameter in the Authorization Request.

None
max_age int | None

the max_age that was passed as parameter in the Authorization Request.

None
issuer str | None

the expected issuer identifier.

None
dpop_key DPoPKey | None

the DPoPKey that was used for Authorization Code DPoP binding.

None
**kwargs str

other parameters as returned by the AS.

{}
Source code in requests_oauth2client/authorization_request.py
@frozen(init=False)
class AuthorizationResponse:
    """Represent a successful Authorization Response.

    An Authorization Response is the redirection initiated by the AS to the client's redirection
    endpoint (redirect_uri), after an Authorization Request.
    This Response is typically created with a call to `AuthorizationRequest.validate_callback()`
    once the call to the client Redirection Endpoint is made.
    `AuthorizationResponse` contains the following attributes:

     - all the parameters that have been returned by the AS, most notably the `code`, and optional
       parameters such as `state`.
     - the `redirect_uri` that was used for the Authorization Request
     - the `code_verifier` matching the `code_challenge` that was used for the Authorization Request

    Parameters `redirect_uri` and `code_verifier` must be those from the matching
    `AuthorizationRequest`. All other parameters including `code` and `state` must be those
    extracted from the Authorization Response parameters.

    Args:
        code: The authorization `code` returned by the AS.
        redirect_uri: The `redirect_uri` that was passed as parameter in the Authorization Request.
        code_verifier: the `code_verifier` matching the `code_challenge` that was passed as
            parameter in the Authorization Request.
        state: the `state` that was was passed as parameter in the Authorization Request and returned by the AS.
        nonce: the `nonce` that was was passed as parameter in the Authorization Request.
        acr_values: the `acr_values` that was passed as parameter in the Authorization Request.
        max_age: the `max_age` that was passed as parameter in the Authorization Request.
        issuer: the expected `issuer` identifier.
        dpop_key: the `DPoPKey` that was used for Authorization Code DPoP binding.
        **kwargs: other parameters as returned by the AS.

    """

    code: str
    redirect_uri: str | None
    code_verifier: str | None
    state: str | None
    nonce: str | None
    acr_values: tuple[str, ...] | None
    max_age: int | None
    issuer: str | None
    dpop_key: DPoPKey | None
    kwargs: dict[str, Any]

    def __init__(
        self,
        *,
        code: str,
        redirect_uri: str | None = None,
        code_verifier: str | None = None,
        state: str | None = None,
        nonce: str | None = None,
        acr_values: str | Sequence[str] | None = None,
        max_age: int | None = None,
        issuer: str | None = None,
        dpop_key: DPoPKey | None = None,
        **kwargs: str,
    ) -> None:
        if not acr_values:
            acr_values = None
        elif isinstance(acr_values, str):
            acr_values = tuple(acr_values.split(" "))
        else:
            acr_values = tuple(acr_values)

        self.__attrs_init__(
            code=code,
            redirect_uri=redirect_uri,
            code_verifier=code_verifier,
            state=state,
            nonce=nonce,
            acr_values=acr_values,
            max_age=max_age,
            issuer=issuer,
            dpop_key=dpop_key,
            kwargs=kwargs,
        )

    def __getattr__(self, item: str) -> str | None:
        """Make additional parameters available as attributes.

        Args:
            item: the attribute name

        Returns:
            the attribute value, or None if it isn't part of the returned attributes

        """
        return self.kwargs.get(item)

CodeChallengeMethods

Bases: str, Enum

All standardised code_challenge_method values.

You should always use S256.

Source code in requests_oauth2client/authorization_request.py
class CodeChallengeMethods(str, Enum):
    """All standardised `code_challenge_method` values.

    You should always use `S256`.

    """

    S256 = "S256"
    plain = "plain"

InvalidCodeVerifierParam

Bases: ValueError

Raised when an invalid code_verifier is supplied.

Source code in requests_oauth2client/authorization_request.py
class InvalidCodeVerifierParam(ValueError):
    """Raised when an invalid code_verifier is supplied."""

    def __init__(self, code_verifier: str) -> None:
        super().__init__("""\
Invalid 'code_verifier'. It must be a 43 to 128 characters long string, with:
- lowercase letters
- uppercase letters
- digits
- underscore, dash, tilde, or dot (_-~.)
""")
        self.code_verifier = code_verifier

InvalidMaxAgeParam

Bases: ValueError

Raised when an invalid 'max_age' parameter is provided.

Source code in requests_oauth2client/authorization_request.py
class InvalidMaxAgeParam(ValueError):
    """Raised when an invalid 'max_age' parameter is provided."""

    def __init__(self) -> None:
        super().__init__("""\
Invalid 'max_age' parameter. It must be a positive number of seconds.
This specifies the allowable elapsed time in seconds since the last time
the End-User was actively authenticated by the OP.
""")

MissingIssuerParam

Bases: ValueError

Raised when the 'issuer' parameter is required but not provided.

Source code in requests_oauth2client/authorization_request.py
class MissingIssuerParam(ValueError):
    """Raised when the 'issuer' parameter is required but not provided."""

    def __init__(self) -> None:
        super().__init__("""\
When 'authorization_response_iss_parameter_supported' is `True`, you must
provide the expected `issuer` as parameter.
""")

PkceUtils

Contains helper methods for PKCE, as described in RFC7636.

See RFC7636.

Source code in requests_oauth2client/authorization_request.py
class PkceUtils:
    """Contains helper methods for PKCE, as described in RFC7636.

    See [RFC7636](https://tools.ietf.org/html/rfc7636).

    """

    code_verifier_pattern = re.compile(r"^[a-zA-Z0-9_\-~.]{43,128}$")
    """A regex that matches valid code verifiers."""

    @classmethod
    def generate_code_verifier(cls) -> str:
        """Generate a valid `code_verifier`.

        Returns:
            a `code_verifier` ready to use for PKCE

        """
        return secrets.token_urlsafe(96)

    @classmethod
    def derive_challenge(cls, verifier: str | bytes, method: str = CodeChallengeMethods.S256) -> str:
        """Derive the `code_challenge` from a given `code_verifier`.

        Args:
            verifier: a code verifier
            method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

        Returns:
            a `code_challenge` derived from the given verifier

        Raises:
            InvalidCodeVerifierParam: if the `verifier` does not match `code_verifier_pattern`
            UnsupportedCodeChallengeMethod: if the method is not supported

        """
        if isinstance(verifier, bytes):
            verifier = verifier.decode()

        if not cls.code_verifier_pattern.match(verifier):
            raise InvalidCodeVerifierParam(verifier)

        if method == CodeChallengeMethods.S256:
            return BinaPy(verifier).to("sha256").to("b64u").ascii()
        if method == CodeChallengeMethods.plain:
            return verifier

        raise UnsupportedCodeChallengeMethod(method)

    @classmethod
    def generate_code_verifier_and_challenge(cls, method: str = CodeChallengeMethods.S256) -> tuple[str, str]:
        """Generate a valid `code_verifier` and its matching `code_challenge`.

        Args:
            method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

        Returns:
            a `(code_verifier, code_challenge)` tuple.

        """
        verifier = cls.generate_code_verifier()
        challenge = cls.derive_challenge(verifier, method)
        return verifier, challenge

    @classmethod
    def validate_code_verifier(cls, verifier: str, challenge: str, method: str = CodeChallengeMethods.S256) -> bool:
        """Validate a `code_verifier` against a `code_challenge`.

        Args:
            verifier: the `code_verifier`, exactly as submitted by the client on token request.
            challenge: the `code_challenge`, exactly as submitted by the client on authorization request.
            method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

        Returns:
            `True` if verifier is valid, or `False` otherwise

        """
        return (
            cls.code_verifier_pattern.match(verifier) is not None
            and cls.derive_challenge(verifier, method) == challenge
        )

code_verifier_pattern = re.compile('^[a-zA-Z0-9_\\-~.]{43,128}$') class-attribute instance-attribute

A regex that matches valid code verifiers.

generate_code_verifier() classmethod

Generate a valid code_verifier.

Returns:

Type Description
str

a code_verifier ready to use for PKCE

Source code in requests_oauth2client/authorization_request.py
@classmethod
def generate_code_verifier(cls) -> str:
    """Generate a valid `code_verifier`.

    Returns:
        a `code_verifier` ready to use for PKCE

    """
    return secrets.token_urlsafe(96)

derive_challenge(verifier, method=CodeChallengeMethods.S256) classmethod

Derive the code_challenge from a given code_verifier.

Parameters:

Name Type Description Default
verifier str | bytes

a code verifier

required
method str

the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

S256

Returns:

Type Description
str

a code_challenge derived from the given verifier

Raises:

Type Description
InvalidCodeVerifierParam

if the verifier does not match code_verifier_pattern

UnsupportedCodeChallengeMethod

if the method is not supported

Source code in requests_oauth2client/authorization_request.py
@classmethod
def derive_challenge(cls, verifier: str | bytes, method: str = CodeChallengeMethods.S256) -> str:
    """Derive the `code_challenge` from a given `code_verifier`.

    Args:
        verifier: a code verifier
        method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

    Returns:
        a `code_challenge` derived from the given verifier

    Raises:
        InvalidCodeVerifierParam: if the `verifier` does not match `code_verifier_pattern`
        UnsupportedCodeChallengeMethod: if the method is not supported

    """
    if isinstance(verifier, bytes):
        verifier = verifier.decode()

    if not cls.code_verifier_pattern.match(verifier):
        raise InvalidCodeVerifierParam(verifier)

    if method == CodeChallengeMethods.S256:
        return BinaPy(verifier).to("sha256").to("b64u").ascii()
    if method == CodeChallengeMethods.plain:
        return verifier

    raise UnsupportedCodeChallengeMethod(method)

generate_code_verifier_and_challenge(method=CodeChallengeMethods.S256) classmethod

Generate a valid code_verifier and its matching code_challenge.

Parameters:

Name Type Description Default
method str

the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

S256

Returns:

Type Description
tuple[str, str]

a (code_verifier, code_challenge) tuple.

Source code in requests_oauth2client/authorization_request.py
@classmethod
def generate_code_verifier_and_challenge(cls, method: str = CodeChallengeMethods.S256) -> tuple[str, str]:
    """Generate a valid `code_verifier` and its matching `code_challenge`.

    Args:
        method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

    Returns:
        a `(code_verifier, code_challenge)` tuple.

    """
    verifier = cls.generate_code_verifier()
    challenge = cls.derive_challenge(verifier, method)
    return verifier, challenge

validate_code_verifier(verifier, challenge, method=CodeChallengeMethods.S256) classmethod

Validate a code_verifier against a code_challenge.

Parameters:

Name Type Description Default
verifier str

the code_verifier, exactly as submitted by the client on token request.

required
challenge str

the code_challenge, exactly as submitted by the client on authorization request.

required
method str

the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

S256

Returns:

Type Description
bool

True if verifier is valid, or False otherwise

Source code in requests_oauth2client/authorization_request.py
@classmethod
def validate_code_verifier(cls, verifier: str, challenge: str, method: str = CodeChallengeMethods.S256) -> bool:
    """Validate a `code_verifier` against a `code_challenge`.

    Args:
        verifier: the `code_verifier`, exactly as submitted by the client on token request.
        challenge: the `code_challenge`, exactly as submitted by the client on authorization request.
        method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

    Returns:
        `True` if verifier is valid, or `False` otherwise

    """
    return (
        cls.code_verifier_pattern.match(verifier) is not None
        and cls.derive_challenge(verifier, method) == challenge
    )

RequestParameterAuthorizationRequest

Represent an Authorization Request that includes a request JWT.

To construct such a request yourself, the easiest way is to initialize an AuthorizationRequest then sign it with AuthorizationRequest.sign().

Parameters:

Name Type Description Default
authorization_endpoint str

the Authorization Endpoint uri

required
client_id str

the client_id

required
request Jwt | str

the request JWT

required
expires_at datetime | None

the expiration date for this request

None
kwargs Any

extra parameters to include in the request

{}
Source code in requests_oauth2client/authorization_request.py
@frozen(init=False, repr=False)
class RequestParameterAuthorizationRequest:
    """Represent an Authorization Request that includes a `request` JWT.

    To construct such a request yourself, the easiest way is to initialize
    an [`AuthorizationRequest`][requests_oauth2client.authorization_request.AuthorizationRequest]
    then sign it with
    [`AuthorizationRequest.sign()`][requests_oauth2client.authorization_request.AuthorizationRequest.sign].

    Args:
        authorization_endpoint: the Authorization Endpoint uri
        client_id: the client_id
        request: the request JWT
        expires_at: the expiration date for this request
        kwargs: extra parameters to include in the request

    """

    authorization_endpoint: str
    client_id: str
    request: Jwt
    expires_at: datetime | None
    dpop_key: DPoPKey | None
    kwargs: dict[str, Any]

    @accepts_expires_in
    def __init__(
        self,
        authorization_endpoint: str,
        client_id: str,
        request: Jwt | str,
        expires_at: datetime | None = None,
        dpop_key: DPoPKey | None = None,
        **kwargs: Any,
    ) -> None:
        if isinstance(request, str):
            request = Jwt(request)

        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            request=request,
            expires_at=expires_at,
            dpop_key=dpop_key,
            kwargs=kwargs,
        )

    @property
    def furl(self) -> furl:
        """Return the Authorization Request URI, as a `furl` instance."""
        return furl(
            self.authorization_endpoint,
            args={"client_id": self.client_id, "request": str(self.request), **self.kwargs},
        )

    @property
    def uri(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return str(self.furl.url)

    def __getattr__(self, item: str) -> Any:
        """Allow attribute access to extra parameters."""
        return self.kwargs[item]

    def __repr__(self) -> str:
        """Return the Authorization Request URI, as a `str`.

        Returns:
             the Authorization Request URI

        """
        return self.uri

furl property

Return the Authorization Request URI, as a furl instance.

uri property

Return the Authorization Request URI, as a str.

RequestUriParameterAuthorizationRequest

Represent an Authorization Request that includes a request_uri parameter.

Parameters:

Name Type Description Default
authorization_endpoint str

The Authorization Endpoint uri.

required
client_id str

The Client ID.

required
request_uri str

The request_uri.

required
expires_at datetime | None

The expiration date for this request.

None
kwargs Any

Extra query parameters to include in the request.

{}
Source code in requests_oauth2client/authorization_request.py
@frozen(init=False)
class RequestUriParameterAuthorizationRequest:
    """Represent an Authorization Request that includes a `request_uri` parameter.

    Args:
        authorization_endpoint: The Authorization Endpoint uri.
        client_id: The Client ID.
        request_uri: The `request_uri`.
        expires_at: The expiration date for this request.
        kwargs: Extra query parameters to include in the request.

    """

    authorization_endpoint: str
    client_id: str
    request_uri: str
    expires_at: datetime | None
    dpop_key: DPoPKey | None
    kwargs: dict[str, Any]

    @accepts_expires_in
    def __init__(
        self,
        authorization_endpoint: str,
        *,
        client_id: str,
        request_uri: str,
        expires_at: datetime | None = None,
        dpop_key: DPoPKey | None = None,
        **kwargs: Any,
    ) -> None:
        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            request_uri=request_uri,
            expires_at=expires_at,
            dpop_key=dpop_key,
            kwargs=kwargs,
        )

    @property
    def furl(self) -> furl:
        """Return the Authorization Request URI, as a `furl` instance."""
        return furl(
            self.authorization_endpoint,
            args={"client_id": self.client_id, "request_uri": self.request_uri, **self.kwargs},
        )

    @property
    def uri(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return str(self.furl.url)

    def __getattr__(self, item: str) -> Any:
        """Allow attribute access to extra parameters."""
        return self.kwargs[item]

    def __repr__(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return self.uri

furl property

Return the Authorization Request URI, as a furl instance.

uri property

Return the Authorization Request URI, as a str.

ResponseTypes

Bases: str, Enum

All standardised response_type values.

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

Source code in requests_oauth2client/authorization_request.py
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"

UnsupportedCodeChallengeMethod

Bases: ValueError

Raised when an unsupported code_challenge_method is provided.

Source code in requests_oauth2client/authorization_request.py
class UnsupportedCodeChallengeMethod(ValueError):
    """Raised when an unsupported `code_challenge_method` is provided."""

UnsupportedResponseTypeParam

Bases: ValueError

Raised when an unsupported response_type is passed as parameter.

Source code in requests_oauth2client/authorization_request.py
class UnsupportedResponseTypeParam(ValueError):
    """Raised when an unsupported response_type is passed as parameter."""

    def __init__(self, response_type: str) -> None:
        super().__init__("""The only supported response type is 'code'.""", response_type)

BackChannelAuthenticationPoolingJob

Bases: BaseTokenEndpointPoolingJob

A pooling job for the BackChannel Authentication flow.

This will poll the Token Endpoint until the user finishes with its authentication.

Parameters:

Name Type Description Default
client OAuth2Client

an OAuth2Client that will be used to pool the token endpoint.

required
auth_req_id str | BackChannelAuthenticationResponse

an auth_req_id as str or a BackChannelAuthenticationResponse.

required
interval int | None

The pooling interval, in seconds, to use. This overrides the one in auth_req_id if it is a BackChannelAuthenticationResponse. Defaults to 5 seconds.

None
slow_down_interval int

Number of seconds to add to the pooling interval when the AS returns a slow down request.

5
requests_kwargs dict[str, Any] | None

Additional parameters for the underlying calls to requests.request.

None
**token_kwargs Any

Additional parameters for the token request.

{}
Example
1
2
3
4
5
6
7
8
9
client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
pool_job = BackChannelAuthenticationPoolingJob(
    client=client,
    auth_req_id="my_auth_req_id",
)

token = None
while token is None:
    token = pool_job()
Source code in requests_oauth2client/backchannel_authentication.py
@define(init=False)
class BackChannelAuthenticationPoolingJob(BaseTokenEndpointPoolingJob):
    """A pooling job for the BackChannel Authentication flow.

    This will poll the Token Endpoint until the user finishes with its authentication.

    Args:
        client: an OAuth2Client that will be used to pool the token endpoint.
        auth_req_id: an `auth_req_id` as `str` or a `BackChannelAuthenticationResponse`.
        interval: The pooling interval, in seconds, to use. This overrides
            the one in `auth_req_id` if it is a `BackChannelAuthenticationResponse`.
            Defaults to 5 seconds.
        slow_down_interval: Number of seconds to add to the pooling interval when the AS returns
            a slow down request.
        requests_kwargs: Additional parameters for the underlying calls to [requests.request][].
        **token_kwargs: Additional parameters for the token request.

    Example:
        ```python
        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        pool_job = BackChannelAuthenticationPoolingJob(
            client=client,
            auth_req_id="my_auth_req_id",
        )

        token = None
        while token is None:
            token = pool_job()
        ```

    """

    auth_req_id: str

    def __init__(
        self,
        client: OAuth2Client,
        auth_req_id: str | BackChannelAuthenticationResponse,
        *,
        interval: int | None = None,
        slow_down_interval: int = 5,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(auth_req_id, BackChannelAuthenticationResponse):
            interval = interval or auth_req_id.interval
            auth_req_id = auth_req_id.auth_req_id

        self.__attrs_init__(
            client=client,
            auth_req_id=auth_req_id,
            interval=interval or 5,
            slow_down_interval=slow_down_interval,
            requests_kwargs=requests_kwargs or {},
            token_kwargs=token_kwargs,
        )

    def token_request(self) -> BearerToken:
        """Implement the CIBA token request.

        This actually calls [OAuth2Client.ciba(auth_req_id)] on `client`.

        Returns:
            a [BearerToken][requests_oauth2client.tokens.BearerToken]

        """
        return self.client.ciba(self.auth_req_id, requests_kwargs=self.requests_kwargs, **self.token_kwargs)

token_request()

Implement the CIBA token request.

This actually calls [OAuth2Client.ciba(auth_req_id)] on client.

Returns:

Type Description
BearerToken
Source code in requests_oauth2client/backchannel_authentication.py
def token_request(self) -> BearerToken:
    """Implement the CIBA token request.

    This actually calls [OAuth2Client.ciba(auth_req_id)] on `client`.

    Returns:
        a [BearerToken][requests_oauth2client.tokens.BearerToken]

    """
    return self.client.ciba(self.auth_req_id, requests_kwargs=self.requests_kwargs, **self.token_kwargs)

BackChannelAuthenticationResponse

Represent a BackChannel Authentication Response.

This contains all the parameters that are returned by the AS as a result of a BackChannel Authentication Request, such as auth_req_id (required), and the optional expires_at, interval, and/or any custom parameters.

Parameters:

Name Type Description Default
auth_req_id str

the auth_req_id as returned by the AS.

required
expires_at datetime | None

the date when the auth_req_id expires. Note that this request also accepts an expires_in parameter, in seconds.

None
interval int | None

the Token Endpoint pooling interval, in seconds, as returned by the AS.

20
**kwargs Any

any additional custom parameters as returned by the AS.

{}
Source code in requests_oauth2client/backchannel_authentication.py
class BackChannelAuthenticationResponse:
    """Represent a BackChannel Authentication Response.

    This contains all the parameters that are returned by the AS as a result of a BackChannel
    Authentication Request, such as `auth_req_id` (required), and the optional `expires_at`,
    `interval`, and/or any custom parameters.

    Args:
        auth_req_id: the `auth_req_id` as returned by the AS.
        expires_at: the date when the `auth_req_id` expires.
            Note that this request also accepts an `expires_in` parameter, in seconds.
        interval: the Token Endpoint pooling interval, in seconds, as returned by the AS.
        **kwargs: any additional custom parameters as returned by the AS.

    """

    @accepts_expires_in
    def __init__(
        self,
        auth_req_id: str,
        expires_at: datetime | None = None,
        interval: int | None = 20,
        **kwargs: Any,
    ) -> None:
        self.auth_req_id = auth_req_id
        self.expires_at = expires_at
        self.interval = interval
        self.other = kwargs

    def is_expired(self, leeway: int = 0) -> bool | None:
        """Return `True` if the `auth_req_id` within this response is expired.

        Expiration is evaluated at the time of the call. If there is no "expires_at" hint (which is
        derived from the `expires_in` hint returned by the AS BackChannel Authentication endpoint),
        this will return `None`.

        Returns:
            `True` if the auth_req_id is expired, `False` if it is still valid, `None` if there is
            no `expires_in` hint.

        """
        if self.expires_at:
            return datetime.now(tz=timezone.utc) - timedelta(seconds=leeway) > self.expires_at
        return None

    @property
    def expires_in(self) -> int | None:
        """Number of seconds until expiration."""
        if self.expires_at:
            return ceil((self.expires_at - datetime.now(tz=timezone.utc)).total_seconds())
        return None

    def __getattr__(self, key: str) -> Any:
        """Return attributes from this `BackChannelAuthenticationResponse`.

        Allows accessing response parameters with `token_response.expires_in` or
        `token_response.any_custom_attribute`.

        Args:
            key: a key

        Returns:
            the associated value in this token response

        Raises:
            AttributeError: if the attribute is not present in the response

        """
        return self.other.get(key) or super().__getattribute__(key)

expires_in property

Number of seconds until expiration.

is_expired(leeway=0)

Return True if the auth_req_id within this response is expired.

Expiration is evaluated at the time of the call. If there is no "expires_at" hint (which is derived from the expires_in hint returned by the AS BackChannel Authentication endpoint), this will return None.

Returns:

Type Description
bool | None

True if the auth_req_id is expired, False if it is still valid, None if there is

bool | None

no expires_in hint.

Source code in requests_oauth2client/backchannel_authentication.py
def is_expired(self, leeway: int = 0) -> bool | None:
    """Return `True` if the `auth_req_id` within this response is expired.

    Expiration is evaluated at the time of the call. If there is no "expires_at" hint (which is
    derived from the `expires_in` hint returned by the AS BackChannel Authentication endpoint),
    this will return `None`.

    Returns:
        `True` if the auth_req_id is expired, `False` if it is still valid, `None` if there is
        no `expires_in` hint.

    """
    if self.expires_at:
        return datetime.now(tz=timezone.utc) - timedelta(seconds=leeway) > self.expires_at
    return None

Endpoints

Bases: 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.

Source code in requests_oauth2client/client.py
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"

GrantTypes

Bases: str, Enum

An enum of standardized grant_type values.

Source code in requests_oauth2client/client.py
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"

InvalidAcrValuesParam

Bases: InvalidParam

Raised when an invalid 'acr_values' parameter is provided.

Source code in requests_oauth2client/client.py
class InvalidAcrValuesParam(InvalidParam):
    """Raised when an invalid 'acr_values' parameter is provided."""

    def __init__(self, acr_values: object) -> None:
        super().__init__(f"Invalid 'acr_values' parameter: {acr_values}")
        self.acr_values = acr_values

InvalidBackchannelAuthenticationRequestHintParam

Bases: InvalidParam

Raised when an invalid hint is provided in a backchannel authentication request.

Source code in requests_oauth2client/client.py
class InvalidBackchannelAuthenticationRequestHintParam(InvalidParam):
    """Raised when an invalid hint is provided in a backchannel authentication request."""

InvalidDiscoveryDocument

Bases: ValueError

Raised when handling an invalid Discovery Document.

Source code in requests_oauth2client/client.py
class InvalidDiscoveryDocument(ValueError):
    """Raised when handling an invalid Discovery Document."""

    def __init__(self, message: str, discovery_document: dict[str, Any]) -> None:
        super().__init__(f"Invalid discovery document: {message}")
        self.discovery_document = discovery_document

InvalidEndpointUri

Bases: InvalidParam

Raised when an invalid endpoint uri is provided.

Source code in requests_oauth2client/client.py
class InvalidEndpointUri(InvalidParam):
    """Raised when an invalid endpoint uri is provided."""

    def __init__(self, endpoint: str, uri: str, exc: InvalidUri) -> None:
        super().__init__(f"Invalid endpoint uri '{uri}' for '{endpoint}': {exc}")
        self.endpoint = endpoint
        self.uri = uri

InvalidIssuer

Bases: InvalidEndpointUri

Raised when an invalid issuer parameter is provided.

Source code in requests_oauth2client/client.py
class InvalidIssuer(InvalidEndpointUri):
    """Raised when an invalid issuer parameter is provided."""

InvalidParam

Bases: ValueError

Base class for invalid parameters errors.

Source code in requests_oauth2client/client.py
class InvalidParam(ValueError):
    """Base class for invalid parameters errors."""

InvalidScopeParam

Bases: InvalidParam

Raised when an invalid scope parameter is provided.

Source code in requests_oauth2client/client.py
class InvalidScopeParam(InvalidParam):
    """Raised when an invalid scope parameter is provided."""

    def __init__(self, scope: object) -> None:
        super().__init__("""\
Unsupported scope value. It must be one of:
- a space separated `str` of scopes names
- an iterable of scope names as `str`
""")
        self.scope = scope

MissingAuthRequestId

Bases: ValueError

Raised when an 'auth_req_id' is missing in a BackChannelAuthenticationResponse.

Source code in requests_oauth2client/client.py
class MissingAuthRequestId(ValueError):
    """Raised when an 'auth_req_id' is missing in a BackChannelAuthenticationResponse."""

    def __init__(self, bcar: BackChannelAuthenticationResponse) -> None:
        super().__init__("An 'auth_req_id' is required but is missing from this BackChannelAuthenticationResponse.")
        self.backchannel_authentication_response = bcar

MissingDeviceCode

Bases: ValueError

Raised when a device_code is required but not provided.

Source code in requests_oauth2client/client.py
class MissingDeviceCode(ValueError):
    """Raised when a device_code is required but not provided."""

    def __init__(self, dar: DeviceAuthorizationResponse) -> None:
        super().__init__("A device_code is missing in this DeviceAuthorizationResponse")
        self.device_authorization_response = dar

MissingEndpointUri

Bases: AttributeError

Raised when a required endpoint uri is not known.

Source code in requests_oauth2client/client.py
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.")

MissingIdTokenEncryptedResponseAlgParam

Bases: InvalidParam

Raised when an ID Token encryption is required but not provided.

Source code in requests_oauth2client/client.py
class MissingIdTokenEncryptedResponseAlgParam(InvalidParam):
    """Raised when an ID Token encryption is required but not provided."""

    def __init__(self) -> None:
        super().__init__("""\
An ID Token decryption key has been provided but no decryption algorithm is defined.
You can either pass an `id_token_encrypted_response_alg` parameter with the alg identifier,
or include an `alg` attribute in the decryption key, if it is in Jwk format.
""")

MissingRefreshToken

Bases: ValueError

Raised when a refresh token is required but not present.

Source code in requests_oauth2client/client.py
class MissingRefreshToken(ValueError):
    """Raised when a refresh token is required but not present."""

    def __init__(self, token: TokenResponse) -> None:
        super().__init__("A refresh_token is required but is not present in this Access Token.")
        self.token = token

OAuth2Client

An OAuth 2.x Client that can send requests to an OAuth 2.x Authorization Server.

OAuth2Client is able to obtain tokens from the Token Endpoint using any of the standardised Grant Types, and to communicate with the various backend endpoints like the Revocation, Introspection, and UserInfo Endpoint.

To init an OAuth2Client, you only need the url to the Token Endpoint and the Credentials (a client_id and one of a secret or private_key) that will be used to authenticate to that endpoint. Other endpoint urls, such as the Authorization Endpoint, Revocation Endpoint, etc. can be passed as parameter as well if you intend to use them.

This class is not intended to help with the end-user authentication or any request that goes in a browser. For authentication requests, see AuthorizationRequest. You may use the method authorization_request() to generate AuthorizationRequests with the preconfigured authorization_endpoint, client_id and `redirect_uri' from this client.

Parameters:

Name Type Description Default
token_endpoint str

the Token Endpoint URI where this client will get access tokens

required
auth AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None

the authentication handler to use for client authentication on the token endpoint. Can be:

None
client_id str | None

client ID (use either this or auth)

None
client_secret str | None

client secret (use either this or auth)

None
private_key Jwk | dict[str, Any] | None

private_key to use for client authentication (use either this or auth)

None
revocation_endpoint str | None

the Revocation Endpoint URI to use for revoking tokens

None
introspection_endpoint str | None

the Introspection Endpoint URI to use to get info about tokens

None
userinfo_endpoint str | None

the Userinfo Endpoint URI to use to get information about the user

None
authorization_endpoint str | None

the Authorization Endpoint URI, used for initializing Authorization Requests

None
redirect_uri str | None

the redirect_uri for this client

None
backchannel_authentication_endpoint str | None

the BackChannel Authentication URI

None
device_authorization_endpoint str | None

the Device Authorization Endpoint URI to use to authorize devices

None
jwks_uri str | None

the JWKS URI to use to obtain the AS public keys

None
code_challenge_method str

challenge method to use for PKCE (should always be 'S256')

S256
session Session | None

a requests Session to use when sending HTTP requests. Useful if some extra parameters such as proxy or client certificate must be used to connect to the AS.

None
token_class type[BearerToken]

a custom BearerToken class, if required

BearerToken
dpop_bound_access_tokens bool

if True, DPoP will be used by default for every token request. otherwise, you can enable DPoP by passing dpop=True when doing a token request.

False
dpop_key_generator Callable[[str], DPoPKey]

a callable that generates a DPoPKey, for whill be called when doing a token request with DPoP enabled.

generate
testing bool

if True, don't verify the validity of the endpoint urls that are passed as parameter.

False
**extra_metadata Any

additional metadata for this client, unused by this class, but may be used by subclasses. Those will be accessible with the extra_metadata attribute.

{}
Example
client = OAuth2Client(
    token_endpoint="https://my.as.local/token",
    revocation_endpoint="https://my.as.local/revoke",
    client_id="client_id",
    client_secret="client_secret",
)

# once initialized, a client can send requests to its configured endpoints
cc_token = client.client_credentials(scope="my_scope")
ac_token = client.authorization_code(code="my_code")
client.revoke_access_token(cc_token)

Raises:

Type Description
MissingIDTokenEncryptedResponseAlgParam

if an id_token_decryption_key is provided but no decryption alg is provided, either:

  • using id_token_encrypted_response_alg,
  • or in the alg parameter of the Jwk key
MissingIssuerParam

if authorization_response_iss_parameter_supported is set to True but the issuer is not provided.

InvalidEndpointUri

if a provided endpoint uri is not considered valid. For the rare cases where those checks must be disabled, you can use testing=True.

InvalidIssuer

if the issuer value is not considered valid.

Source code in requests_oauth2client/client.py
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
@frozen(init=False)
class OAuth2Client:
    """An OAuth 2.x Client that can send requests to an OAuth 2.x Authorization Server.

    `OAuth2Client` is able to obtain tokens from the Token Endpoint using any of the standardised
    Grant Types, and to communicate with the various backend endpoints like the Revocation,
    Introspection, and UserInfo Endpoint.

    To init an OAuth2Client, you only need the url to the Token Endpoint and the Credentials
    (a client_id and one of a secret or private_key) that will be used to authenticate to that endpoint.
    Other endpoint urls, such as the Authorization Endpoint, Revocation Endpoint, etc. can be passed as
    parameter as well if you intend to use them.


    This class is not intended to help with the end-user authentication or any request that goes in
    a browser. For authentication requests, see
    [AuthorizationRequest][requests_oauth2client.authorization_request.AuthorizationRequest]. You
    may use the method `authorization_request()` to generate `AuthorizationRequest`s with the
    preconfigured `authorization_endpoint`, `client_id` and `redirect_uri' from this client.

    Args:
        token_endpoint: the Token Endpoint URI where this client will get access tokens
        auth: the authentication handler to use for client authentication on the token endpoint.
            Can be:

            - a [requests.auth.AuthBase][] instance (which will be used as-is)
            - a tuple of `(client_id, client_secret)` which will initialize an instance
            of [ClientSecretPost][requests_oauth2client.client_authentication.ClientSecretPost]
            - a `(client_id, jwk)` to initialize
            a [PrivateKeyJwt][requests_oauth2client.client_authentication.PrivateKeyJwt],
            - or a `client_id` which will
            use [PublicApp][requests_oauth2client.client_authentication.PublicApp] authentication.

        client_id: client ID (use either this or `auth`)
        client_secret: client secret (use either this or `auth`)
        private_key: private_key to use for client authentication (use either this or `auth`)
        revocation_endpoint: the Revocation Endpoint URI to use for revoking tokens
        introspection_endpoint: the Introspection Endpoint URI to use to get info about tokens
        userinfo_endpoint: the Userinfo Endpoint URI to use to get information about the user
        authorization_endpoint: the Authorization Endpoint URI, used for initializing Authorization Requests
        redirect_uri: the redirect_uri for this client
        backchannel_authentication_endpoint: the BackChannel Authentication URI
        device_authorization_endpoint: the Device Authorization Endpoint URI to use to authorize devices
        jwks_uri: the JWKS URI to use to obtain the AS public keys
        code_challenge_method: challenge method to use for PKCE (should always be 'S256')
        session: a requests Session to use when sending HTTP requests.
            Useful if some extra parameters such as proxy or client certificate must be used
            to connect to the AS.
        token_class: a custom BearerToken class, if required
        dpop_bound_access_tokens: if `True`, DPoP will be used by default for every token request.
            otherwise, you can enable DPoP by passing `dpop=True` when doing a token request.
        dpop_key_generator: a callable that generates a DPoPKey, for whill be called when doing a token request
            with DPoP enabled.
        testing: if `True`, don't verify the validity of the endpoint urls that are passed as parameter.
        **extra_metadata: additional metadata for this client, unused by this class, but may be
            used by subclasses. Those will be accessible with the `extra_metadata` attribute.

    Example:
        ```python
        client = OAuth2Client(
            token_endpoint="https://my.as.local/token",
            revocation_endpoint="https://my.as.local/revoke",
            client_id="client_id",
            client_secret="client_secret",
        )

        # once initialized, a client can send requests to its configured endpoints
        cc_token = client.client_credentials(scope="my_scope")
        ac_token = client.authorization_code(code="my_code")
        client.revoke_access_token(cc_token)
        ```

    Raises:
        MissingIDTokenEncryptedResponseAlgParam: if an `id_token_decryption_key` is provided
            but no decryption alg is provided, either:

            - using `id_token_encrypted_response_alg`,
            - or in the `alg` parameter of the `Jwk` key
        MissingIssuerParam: if `authorization_response_iss_parameter_supported` is set to `True`
            but the `issuer` is not provided.
        InvalidEndpointUri: if a provided endpoint uri is not considered valid. For the rare cases
            where those checks must be disabled, you can use `testing=True`.
        InvalidIssuer: if the `issuer` value is not considered valid.

    """

    auth: requests.auth.AuthBase
    token_endpoint: str = field()
    revocation_endpoint: str | None = field()
    introspection_endpoint: str | None = field()
    userinfo_endpoint: str | None = field()
    authorization_endpoint: str | None = field()
    redirect_uri: str | None = field()
    backchannel_authentication_endpoint: str | None = field()
    device_authorization_endpoint: str | None = field()
    pushed_authorization_request_endpoint: str | None = field()
    jwks_uri: str | None = field()
    authorization_server_jwks: JwkSet
    issuer: str | None = field()
    id_token_signed_response_alg: str | None
    id_token_encrypted_response_alg: str | None
    id_token_decryption_key: Jwk | None
    code_challenge_method: str | None
    authorization_response_iss_parameter_supported: bool
    session: requests.Session
    extra_metadata: dict[str, Any]
    testing: bool

    dpop_bound_access_tokens: bool
    dpop_key_generator: Callable[[str], DPoPKey]
    dpop_alg: str

    token_class: type[BearerToken]

    exception_classes: ClassVar[dict[str, type[EndpointError]]] = {
        "server_error": ServerError,
        "invalid_request": InvalidRequest,
        "invalid_client": InvalidClient,
        "invalid_scope": InvalidScope,
        "invalid_target": InvalidTarget,
        "invalid_grant": InvalidGrant,
        "access_denied": AccessDenied,
        "unauthorized_client": UnauthorizedClient,
        "authorization_pending": AuthorizationPending,
        "slow_down": SlowDown,
        "expired_token": ExpiredToken,
        "use_dpop_nonce": UseDPoPNonce,
        "unsupported_token_type": UnsupportedTokenType,
    }

    def __init__(  # noqa: PLR0913
        self,
        token_endpoint: str,
        auth: (
            requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
        ) = None,
        *,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_key: Jwk | dict[str, Any] | None = None,
        revocation_endpoint: str | None = None,
        introspection_endpoint: str | None = None,
        userinfo_endpoint: str | None = None,
        authorization_endpoint: str | None = None,
        redirect_uri: str | None = None,
        backchannel_authentication_endpoint: str | None = None,
        device_authorization_endpoint: str | None = None,
        pushed_authorization_request_endpoint: str | None = None,
        jwks_uri: str | None = None,
        authorization_server_jwks: JwkSet | dict[str, Any] | None = None,
        issuer: str | None = None,
        id_token_signed_response_alg: str | None = SignatureAlgs.RS256,
        id_token_encrypted_response_alg: str | None = None,
        id_token_decryption_key: Jwk | dict[str, Any] | None = None,
        code_challenge_method: str = CodeChallengeMethods.S256,
        authorization_response_iss_parameter_supported: bool = False,
        token_class: type[BearerToken] = BearerToken,
        session: requests.Session | None = None,
        dpop_bound_access_tokens: bool = False,
        dpop_key_generator: Callable[[str], DPoPKey] = DPoPKey.generate,
        dpop_alg: str = SignatureAlgs.ES256,
        testing: bool = False,
        **extra_metadata: Any,
    ) -> None:
        if authorization_response_iss_parameter_supported and not issuer:
            raise MissingIssuerParam

        auth = client_auth_factory(
            auth,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            default_auth_handler=ClientSecretPost,
        )

        if authorization_server_jwks is None:
            authorization_server_jwks = JwkSet()
        elif not isinstance(authorization_server_jwks, JwkSet):
            authorization_server_jwks = JwkSet(authorization_server_jwks)

        if id_token_decryption_key is not None and not isinstance(id_token_decryption_key, Jwk):
            id_token_decryption_key = Jwk(id_token_decryption_key)

        if id_token_decryption_key is not None and id_token_encrypted_response_alg is None:
            if id_token_decryption_key.alg:
                id_token_encrypted_response_alg = id_token_decryption_key.alg
            else:
                raise MissingIdTokenEncryptedResponseAlgParam

        if dpop_alg not in SignatureAlgs.ALL_ASYMMETRIC:
            raise InvalidDPoPAlg(dpop_alg)

        if session is None:
            session = requests.Session()

        self.__attrs_init__(
            testing=testing,
            token_endpoint=token_endpoint,
            revocation_endpoint=revocation_endpoint,
            introspection_endpoint=introspection_endpoint,
            userinfo_endpoint=userinfo_endpoint,
            authorization_endpoint=authorization_endpoint,
            redirect_uri=redirect_uri,
            backchannel_authentication_endpoint=backchannel_authentication_endpoint,
            device_authorization_endpoint=device_authorization_endpoint,
            pushed_authorization_request_endpoint=pushed_authorization_request_endpoint,
            jwks_uri=jwks_uri,
            authorization_server_jwks=authorization_server_jwks,
            issuer=issuer,
            session=session,
            auth=auth,
            id_token_signed_response_alg=id_token_signed_response_alg,
            id_token_encrypted_response_alg=id_token_encrypted_response_alg,
            id_token_decryption_key=id_token_decryption_key,
            code_challenge_method=code_challenge_method,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            extra_metadata=extra_metadata,
            token_class=token_class,
            dpop_key_generator=dpop_key_generator,
            dpop_bound_access_tokens=dpop_bound_access_tokens,
            dpop_alg=dpop_alg,
        )

    @token_endpoint.validator
    @revocation_endpoint.validator
    @introspection_endpoint.validator
    @userinfo_endpoint.validator
    @authorization_endpoint.validator
    @backchannel_authentication_endpoint.validator
    @device_authorization_endpoint.validator
    @pushed_authorization_request_endpoint.validator
    @jwks_uri.validator
    def validate_endpoint_uri(self, attribute: Attribute[str | None], uri: str | None) -> str | None:
        """Validate that an endpoint URI is suitable for use.

        If you need to disable some checks (for AS testing purposes only!), provide a different method here.

        """
        if self.testing or uri is None:
            return uri
        try:
            return validate_endpoint_uri(uri)
        except InvalidUri as exc:
            raise InvalidEndpointUri(endpoint=attribute.name, uri=uri, exc=exc) from exc

    @issuer.validator
    def validate_issuer_uri(self, attribute: Attribute[str | None], uri: str | None) -> str | None:
        """Validate that an Issuer identifier is suitable for use.

        This is the same check as an endpoint URI, but the path may be (and usually is) empty.

        """
        if self.testing or uri is None:
            return uri
        try:
            return validate_issuer_uri(uri)
        except InvalidUri as exc:
            raise InvalidIssuer(attribute.name, uri, exc) from exc

    @property
    def client_id(self) -> str:
        """Client ID."""
        if hasattr(self.auth, "client_id"):
            return self.auth.client_id  # type: ignore[no-any-return]
        msg = "This client uses a custom authentication method without client_id."
        raise AttributeError(msg)  # pragma: no cover

    @property
    def client_secret(self) -> str | None:
        """Client Secret."""
        if hasattr(self.auth, "client_secret"):
            return self.auth.client_secret  # type: ignore[no-any-return]
        return None

    @property
    def client_jwks(self) -> JwkSet:
        """A `JwkSet` containing the public keys for this client.

        Keys are:

        - the public key for client assertion signature verification (if using private_key_jwt)
        - the ID Token encryption key

        """
        jwks = JwkSet()
        if isinstance(self.auth, PrivateKeyJwt):
            jwks.add_jwk(self.auth.private_jwk.public_jwk().with_usage_parameters())
        if self.id_token_decryption_key:
            jwks.add_jwk(self.id_token_decryption_key.public_jwk().with_usage_parameters())
        return jwks

    def _request(
        self,
        endpoint: str,
        *,
        on_success: Callable[[requests.Response, DefaultNamedArg(DPoPKey | None, "dpop_key")], T],
        on_failure: Callable[[requests.Response, DefaultNamedArg(DPoPKey | None, "dpop_key")], T],
        dpop_key: DPoPKey | None = None,
        accept: str = "application/json",
        method: str = "POST",
        **requests_kwargs: Any,
    ) -> T:
        """Send a request to one of the endpoints.

        This is a helper method that takes care of the following tasks:

        - make sure the endpoint as been configured
        - set `Accept: application/json` header
        - send the HTTP POST request, then
            - apply `on_success` to a successful response
            - or apply `on_failure` otherwise
        - return the result

        Args:
            endpoint: name of the endpoint to use
            on_success: a callable to apply to successful responses
            on_failure: a callable to apply to error responses
            dpop_key: a `DPoPKey` to proof the request. If `None` (default), no DPoP proofing is done.
            accept: the Accept header to include in the request
            method: the HTTP method to use
            **requests_kwargs: keyword arguments for the request

        Raises:
            InvalidTokenResponse: if the AS response contains a `use_dpop_nonce` error but:
              - the response comes in reply to a non-DPoP request
              - the DPoPKey.handle_as_provided_dpop_nonce() method raises an exception. This should happen:
                    - if the response does not include a DPoP-Nonce HTTP header with the requested nonce value
                    - or if the requested nonce is the same value that was sent in the request DPoP proof
              - a new nonce value is requested again for the 3rd time in a row

        """
        endpoint_uri = self._require_endpoint(endpoint)
        requests_kwargs.setdefault("headers", {})
        requests_kwargs["headers"]["Accept"] = accept

        for _ in range(3):
            if dpop_key:
                dpop_proof = dpop_key.proof(htm="POST", htu=endpoint_uri, nonce=dpop_key.as_nonce)
                requests_kwargs.setdefault("headers", {})
                requests_kwargs["headers"]["DPoP"] = str(dpop_proof)

            response = self.session.request(
                method,
                endpoint_uri,
                **requests_kwargs,
            )
            if response.ok:
                return on_success(response, dpop_key=dpop_key)

            try:
                return on_failure(response, dpop_key=dpop_key)
            except UseDPoPNonce as exc:
                if dpop_key is None:
                    raise InvalidTokenResponse(
                        response,
                        self,
                        """\
Authorization Server requested client to include a DPoP `nonce` in its DPoP proof,
but the initial request did not include a DPoP proof.
""",
                    ) from exc
                try:
                    dpop_key.handle_as_provided_dpop_nonce(response)
                except (MissingDPoPNonce, RepeatedDPoPNonce) as exc:
                    raise InvalidTokenResponse(response, self, str(exc)) from exc

        raise InvalidTokenResponse(
            response,
            self,
            """\
Authorization Server requested client to use a different DPoP `nonce` for the third time in row.
This should never happen. This exception is raised to avoid a potential endless loop where the client
keeps trying to obey the new DPoP `nonce` values as provided by the Authorization Server after each token request.
""",
        )

    def token_request(
        self,
        data: dict[str, Any],
        *,
        timeout: int = 10,
        dpop: bool | None = None,
        dpop_key: DPoPKey | None = None,
        **requests_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint.

        Authentication will be added automatically based on the defined `auth` for this client.

        Args:
          data: parameters to send to the token endpoint. Items with a `None`
               or empty value will not be sent in the request.
          timeout: a timeout value for the call
          dpop: toggles DPoP-proofing for the token request:

                - if `False`, disable it,
                - if `True`, enable it,
                - if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
          dpop_key: a chosen `DPoPKey` for this request. If `None`, a new key will be generated automatically
                with a call to this client `dpop_key_generator`.
          **requests_kwargs: additional parameters for requests.post()

        Returns:
            the token endpoint response

        """
        if dpop is None:
            dpop = self.dpop_bound_access_tokens
        if dpop and not dpop_key:
            dpop_key = self.dpop_key_generator(self.dpop_alg)

        return self._request(
            Endpoints.TOKEN,
            auth=self.auth,
            data=data,
            timeout=timeout,
            dpop_key=dpop_key,
            on_success=self.parse_token_response,
            on_failure=self.on_token_error,
            **requests_kwargs,
        )

    def parse_token_response(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> BearerToken:
        """Parse a Response returned by the Token Endpoint.

        Invoked by [token_request][requests_oauth2client.client.OAuth2Client.token_request] to parse
        responses returned by the Token Endpoint. Those responses contain an `access_token` and
        additional attributes.

        Args:
            response: the `Response` returned by the Token Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            a `BearerToken` based on the response contents.

        """
        token_class = dpop_key.dpop_token_class if dpop_key is not None else self.token_class
        try:
            token_response = token_class(**response.json(), _dpop_key=dpop_key)
        except Exception:  # noqa: BLE001
            return self.on_token_error(response, dpop_key=dpop_key)
        else:
            return token_response

    def on_token_error(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> BearerToken:
        """Error handler for `token_request()`.

        Invoked by [token_request][requests_oauth2client.client.OAuth2Client.token_request] when the
        Token Endpoint returns an error.

        Args:
            response: the `Response` returned by the Token Endpoint.
            dpop_key: the DPoPKey that was used to proof the token request, if any.

        Returns:
            nothing, and raises an exception instead. But a subclass may return a
            `BearerToken` to implement a default behaviour if needed.

        Raises:
            InvalidTokenResponse: if the error response does not contain an OAuth 2.0 standard
                error response.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, UnknownTokenEndpointError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise InvalidTokenResponse(
                response=response,
                client=self,
                description=f"An error happened while processing the error response: {exc}",
            ) from exc
        raise exception

    def client_credentials(
        self,
        scope: str | Iterable[str] | None = None,
        *,
        dpop: bool | None = None,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint using the `client_credentials` grant.

        Args:
            scope: the scope to send with the request. Can be a str, or an iterable of str.
                to pass that way include `scope`, `audience`, `resource`, etc.
            dpop: toggles DPoP-proofing for the token request:

                - if `False`, disable it,
                - if `True`, enable it,
                - if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
            dpop_key: a chosen `DPoPKey` for this request. If `None`, a new key will be generated automatically
                with a call to `dpop_key_generator`.
            requests_kwargs: additional parameters for the call to requests
            **token_kwargs: additional parameters that will be added in the form data for the token endpoint,
                 alongside `grant_type`.

        Returns:
            a `BearerToken` or `DPoPToken`, depending on the AS response.

        Raises:
            InvalidScopeParam: if the `scope` parameter is not suitable

        """
        requests_kwargs = requests_kwargs or {}

        if scope and not isinstance(scope, str):
            try:
                scope = " ".join(scope)
            except Exception as exc:
                raise InvalidScopeParam(scope) from exc

        data = dict(grant_type=GrantTypes.CLIENT_CREDENTIALS, scope=scope, **token_kwargs)
        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def authorization_code(
        self,
        code: str | AuthorizationResponse,
        *,
        validate: bool = True,
        dpop: bool = False,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint with the `authorization_code` grant.

        You can either pass an authorization code, as a `str`, or pass an `AuthorizationResponse` instance as
        returned by `AuthorizationRequest.validate_callback()` (recommended). If you do the latter, this will
        automatically:

        - add the appropriate `redirect_uri` value that was initially passed in the Authorization Request parameters.
        This is no longer mandatory in OAuth 2.1, but a lot of Authorization Servers are still expecting it since it was
        part of the OAuth 2.0 specifications.
        - add the appropriate `code_verifier` for PKCE that was generated before sending the AuthorizationRequest.
        - handle DPoP binding based on the same `DPoPKey` that was used to initialize the `AuthenticationRequest` and
        whose JWK thumbprint was passed as `dpop_jkt` parameter in the Auth Request.

        Args:
            code: An authorization code or an `AuthorizationResponse` to exchange for tokens.
            validate: If `True`, validate the ID Token (this works only if `code` is an `AuthorizationResponse`).
            dpop: Toggles DPoP binding for the Access Token,
                 even if Authorization Code DPoP binding was not initially done.
            dpop_key: A chosen DPoP key. Leave `None` to automatically generate a key, if `dpop` is `True`.
            requests_kwargs: Additional parameters for the call to the underlying HTTP `requests` call.
            **token_kwargs: Additional parameters that will be added in the form data for the token endpoint,
                alongside `grant_type`, `code`, etc.

        Returns:
            The Token Endpoint Response.

        """
        azr: AuthorizationResponse | None = None
        if isinstance(code, AuthorizationResponse):
            token_kwargs.setdefault("code_verifier", code.code_verifier)
            token_kwargs.setdefault("redirect_uri", code.redirect_uri)
            azr = code
            dpop_key = code.dpop_key
            code = code.code

        requests_kwargs = requests_kwargs or {}

        data = dict(grant_type=GrantTypes.AUTHORIZATION_CODE, code=code, **token_kwargs)
        token = self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
        if validate and token.id_token and isinstance(azr, AuthorizationResponse):
            return token.validate_id_token(self, azr)
        return token

    def refresh_token(
        self,
        refresh_token: str | BearerToken,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint with the `refresh_token` grant.

        If `refresh_token` is a `DPoPToken` instance, (which means that DPoP was used to obtain the initial
        Access/Refresh Tokens), then the same DPoP key will be used to DPoP proof the refresh token request,
        as defined in RFC9449.

        Args:
            refresh_token: A refresh_token, as a string, or as a `BearerToken`.
                That `BearerToken` must have a `refresh_token`.
            requests_kwargs: Additional parameters for the call to `requests`.
            **token_kwargs: Additional parameters for the token endpoint,
                alongside `grant_type`, `refresh_token`, etc.

        Returns:
            The token endpoint response.

        Raises:
            MissingRefreshToken: If `refresh_token` is a `BearerToken` instance but does not
                contain a `refresh_token`.

        """
        dpop_key: DPoPKey | None = None
        if isinstance(refresh_token, BearerToken):
            if refresh_token.refresh_token is None or not isinstance(refresh_token.refresh_token, str):
                raise MissingRefreshToken(refresh_token)
            if isinstance(refresh_token, DPoPToken):
                dpop_key = refresh_token.dpop_key
            refresh_token = refresh_token.refresh_token

        requests_kwargs = requests_kwargs or {}
        data = dict(grant_type=GrantTypes.REFRESH_TOKEN, refresh_token=refresh_token, **token_kwargs)
        return self.token_request(data, dpop_key=dpop_key, **requests_kwargs)

    def device_code(
        self,
        device_code: str | DeviceAuthorizationResponse,
        *,
        dpop: bool = False,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint using the Device Code grant.

        The grant_type is `urn:ietf:params:oauth:grant-type:device_code`. This needs a Device Code,
        or a `DeviceAuthorizationResponse` as parameter.

        Args:
            device_code: A device code, or a `DeviceAuthorizationResponse`.
            dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
            requests_kwargs: Additional parameters for the call to requests.
            **token_kwargs: Additional parameters for the token endpoint, alongside `grant_type`, `device_code`, etc.

        Returns:
            The Token Endpoint response.

        Raises:
            MissingDeviceCode: if `device_code` is a DeviceAuthorizationResponse but does not
                contain a `device_code`.

        """
        if isinstance(device_code, DeviceAuthorizationResponse):
            if device_code.device_code is None or not isinstance(device_code.device_code, str):
                raise MissingDeviceCode(device_code)
            device_code = device_code.device_code

        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantTypes.DEVICE_CODE,
            device_code=device_code,
            **token_kwargs,
        )
        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def ciba(
        self,
        auth_req_id: str | BackChannelAuthenticationResponse,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a CIBA request to the Token Endpoint.

        A CIBA request is a Token Request using the `urn:openid:params:grant-type:ciba` grant.

        Args:
            auth_req_id: an authentication request ID, as returned by the AS
            requests_kwargs: additional parameters for the call to requests
            **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `auth_req_id`, etc.

        Returns:
            The Token Endpoint response.

        Raises:
            MissingAuthRequestId: if `auth_req_id` is a BackChannelAuthenticationResponse but does not contain
                an `auth_req_id`.

        """
        if isinstance(auth_req_id, BackChannelAuthenticationResponse):
            if auth_req_id.auth_req_id is None or not isinstance(auth_req_id.auth_req_id, str):
                raise MissingAuthRequestId(auth_req_id)
            auth_req_id = auth_req_id.auth_req_id

        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantTypes.CLIENT_INITIATED_BACKCHANNEL_AUTHENTICATION,
            auth_req_id=auth_req_id,
            **token_kwargs,
        )
        return self.token_request(data, **requests_kwargs)

    def token_exchange(
        self,
        *,
        subject_token: str | BearerToken | IdToken,
        subject_token_type: str | None = None,
        actor_token: None | str | BearerToken | IdToken = None,
        actor_token_type: str | None = None,
        requested_token_type: str | None = None,
        dpop: bool = False,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a Token Exchange request.

        A Token Exchange request is actually a request to the Token Endpoint with a grant_type
        `urn:ietf:params:oauth:grant-type:token-exchange`.

        Args:
            subject_token: The subject token to exchange for a new token.
            subject_token_type: A token type identifier for the subject_token, mandatory if it cannot be guessed based
                on `type(subject_token)`.
            actor_token: The actor token to include in the request, if any.
            actor_token_type: A token type identifier for the actor_token, mandatory if it cannot be guessed based
                on `type(actor_token)`.
            requested_token_type: A token type identifier for the requested token.
            dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
            requests_kwargs: Additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: Additional parameters to include in the request body.

        Returns:
            The Token Endpoint response.

        Raises:
            UnknownSubjectTokenType: If the type of `subject_token` cannot be determined automatically.
            UnknownActorTokenType: If the type of `actor_token` cannot be determined automatically.

        """
        requests_kwargs = requests_kwargs or {}

        try:
            subject_token_type = self.get_token_type(subject_token_type, subject_token)
        except ValueError as exc:
            raise UnknownSubjectTokenType(subject_token, subject_token_type) from exc
        if actor_token:  # pragma: no branch
            try:
                actor_token_type = self.get_token_type(actor_token_type, actor_token)
            except ValueError as exc:
                raise UnknownActorTokenType(actor_token, actor_token_type) from exc

        data = dict(
            grant_type=GrantTypes.TOKEN_EXCHANGE,
            subject_token=subject_token,
            subject_token_type=subject_token_type,
            actor_token=actor_token,
            actor_token_type=actor_token_type,
            requested_token_type=requested_token_type,
            **token_kwargs,
        )
        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def jwt_bearer(
        self,
        assertion: Jwt | str,
        *,
        dpop: bool = False,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request using a JWT as authorization grant.

        This is defined in (RFC7523 $2.1)[https://www.rfc-editor.org/rfc/rfc7523.html#section-2.1).

        Args:
            assertion: A JWT (as an instance of `jwskate.Jwt` or as a `str`) to use as authorization grant.
            dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
            requests_kwargs: Additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: Additional parameters to include in the request body.

        Returns:
            The Token Endpoint response.

        """
        requests_kwargs = requests_kwargs or {}

        if not isinstance(assertion, Jwt):
            assertion = Jwt(assertion)

        data = dict(
            grant_type=GrantTypes.JWT_BEARER,
            assertion=assertion,
            **token_kwargs,
        )

        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def resource_owner_password(
        self,
        username: str,
        password: str,
        *,
        dpop: bool | None = None,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request using the Resource Owner Password Grant.

        This Grant Type is deprecated and should only be used when there is no other choice.

        Args:
            username: the resource owner user name
            password: the resource owner password
            dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
            requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: additional parameters to include in the request body.

        Returns:
            The Token Endpoint response.

        """
        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantTypes.RESOURCE_OWNER_PASSWORD,
            username=username,
            password=password,
            **token_kwargs,
        )

        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def authorization_request(
        self,
        *,
        scope: None | str | Iterable[str] = "openid",
        response_type: str = ResponseTypes.CODE,
        redirect_uri: str | None = None,
        state: str | ellipsis | None = ...,  # noqa: F821
        nonce: str | ellipsis | None = ...,  # noqa: F821
        code_verifier: str | None = None,
        dpop: bool | None = None,
        dpop_key: DPoPKey | None = None,
        dpop_alg: str | None = None,
        **kwargs: Any,
    ) -> AuthorizationRequest:
        """Generate an Authorization Request for this client.

        Args:
            scope: The `scope` to use.
            response_type: The `response_type` to use.
            redirect_uri: The `redirect_uri` to include in the request. By default,
                the `redirect_uri` defined at init time is used.
            state: The `state` parameter to use. Leave default to generate a random value.
            nonce: A `nonce`. Leave default to generate a random value.
            dpop: Toggles DPoP binding.
                - if `True`, DPoP binding is used
                - if `False`, DPoP is not used
                - if `None`, defaults to `self.dpop_bound_access_tokens`
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for you.
            dpop_alg: A signature alg to sign the DPoP proof. If `None`, this defaults to `self.dpop_alg`.
                If DPoP is not used, or a chosen `dpop_key` is provided, this is ignored.
                This affects the key type if a DPoP key must be generated.
            code_verifier: The PKCE `code_verifier` to use. Leave default to generate a random value.
            **kwargs: Additional query parameters to include in the auth request.

        Returns:
            The Token Endpoint response.

        """
        authorization_endpoint = self._require_endpoint("authorization_endpoint")

        redirect_uri = redirect_uri or self.redirect_uri

        if dpop is None:
            dpop = self.dpop_bound_access_tokens
        if dpop_alg is None:
            dpop_alg = self.dpop_alg

        return AuthorizationRequest(
            authorization_endpoint=authorization_endpoint,
            client_id=self.client_id,
            redirect_uri=redirect_uri,
            issuer=self.issuer,
            response_type=response_type,
            scope=scope,
            state=state,
            nonce=nonce,
            code_verifier=code_verifier,
            code_challenge_method=self.code_challenge_method,
            dpop=dpop,
            dpop_key=dpop_key,
            dpop_alg=dpop_alg,
            **kwargs,
        )

    def pushed_authorization_request(
        self,
        authorization_request: AuthorizationRequest,
        requests_kwargs: dict[str, Any] | None = None,
    ) -> RequestUriParameterAuthorizationRequest:
        """Send a Pushed Authorization Request.

        This sends a request to the Pushed Authorization Request Endpoint, and returns a
        `RequestUriParameterAuthorizationRequest` initialized with the AS response.

        Args:
            authorization_request: The authorization request to send.
            requests_kwargs: Additional parameters for `requests.request()`.

        Returns:
            The `RequestUriParameterAuthorizationRequest` initialized based on the AS response.

        """
        requests_kwargs = requests_kwargs or {}
        return self._request(
            Endpoints.PUSHED_AUTHORIZATION_REQUEST,
            data=authorization_request.args,
            auth=self.auth,
            on_success=self.parse_pushed_authorization_response,
            on_failure=self.on_pushed_authorization_request_error,
            dpop_key=authorization_request.dpop_key,
            **requests_kwargs,
        )

    def parse_pushed_authorization_response(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,
    ) -> RequestUriParameterAuthorizationRequest:
        """Parse the response obtained by `pushed_authorization_request()`.

        Args:
            response: The `requests.Response` returned by the PAR endpoint.
            dpop_key: The `DPoPKey` that was used to proof the token request, if any.

        Returns:
            A `RequestUriParameterAuthorizationRequest` instance initialized based on the PAR endpoint response.

        """
        response_json = response.json()
        request_uri = response_json.get("request_uri")
        expires_in = response_json.get("expires_in")

        return RequestUriParameterAuthorizationRequest(
            authorization_endpoint=self.authorization_endpoint,
            client_id=self.client_id,
            request_uri=request_uri,
            expires_in=expires_in,
            dpop_key=dpop_key,
        )

    def on_pushed_authorization_request_error(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> RequestUriParameterAuthorizationRequest:
        """Error Handler for Pushed Authorization Endpoint errors.

        Args:
            response: The HTTP response as returned by the AS PAR endpoint.
            dpop_key: The `DPoPKey` that was used to proof the token request, if any.

        Returns:
            Should not return anything, but raise an Exception instead. A `RequestUriParameterAuthorizationRequest`
            may be returned by subclasses for testing purposes.

        Raises:
            EndpointError: A subclass of this error depending on the error returned by the AS.
            InvalidPushedAuthorizationResponse: If the returned response is not following the specifications.
            UnknownTokenEndpointError: For unknown/unhandled errors.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, UnknownTokenEndpointError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise InvalidPushedAuthorizationResponse(response=response, client=self) from exc
        raise exception

    def userinfo(self, access_token: BearerToken | str) -> Any:
        """Call the UserInfo endpoint.

        This sends a request to the UserInfo endpoint, with the specified access_token, and returns
        the parsed result.

        Args:
            access_token: the access token to use

        Returns:
            the [Response][requests.Response] returned by the userinfo endpoint.

        """
        if isinstance(access_token, str):
            access_token = BearerToken(access_token)
        return self._request(
            Endpoints.USER_INFO,
            auth=access_token,
            on_success=self.parse_userinfo_response,
            on_failure=self.on_userinfo_error,
        )

    def parse_userinfo_response(self, resp: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
        """Parse the response obtained by `userinfo()`.

        Invoked by [userinfo()][requests_oauth2client.client.OAuth2Client.userinfo] to parse the
        response from the UserInfo endpoint, this will extract and return its JSON content.

        Args:
            resp: a [Response][requests.Response] returned from the UserInfo endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            the parsed JSON content from this response.

        """
        return resp.json()

    def on_userinfo_error(self, resp: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
        """Parse UserInfo error response.

        Args:
            resp: a [Response][requests.Response] returned from the UserInfo endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            nothing, raises exception instead.

        """
        resp.raise_for_status()

    @classmethod
    def get_token_type(  # noqa: C901
        cls,
        token_type: str | None = None,
        token: None | str | BearerToken | IdToken = None,
    ) -> str:
        """Get standardized token type identifiers.

        Return a standardized token type identifier, based on a short `token_type` hint and/or a
        token value.

        Args:
            token_type: a token_type hint, as `str`. May be "access_token", "refresh_token"
                or "id_token"
            token: a token value, as an instance of `BearerToken` or IdToken, or as a `str`.

        Returns:
            the token_type as defined in the Token Exchange RFC8693.

        Raises:
            UnknownTokenType: if the type of token cannot be determined

        """
        if not (token_type or token):
            msg = "Cannot determine type of an empty token without a token_type hint"
            raise UnknownTokenType(msg, token, token_type)

        if token_type is None:
            if isinstance(token, str):
                msg = """\
Cannot determine the type of provided token when it is a bare `str`. Please specify a 'token_type'.
"""
                raise UnknownTokenType(msg, token, token_type)
            if isinstance(token, BearerToken):
                return "urn:ietf:params:oauth:token-type:access_token"
            if isinstance(token, IdToken):
                return "urn:ietf:params:oauth:token-type:id_token"
            msg = f"Unknown token type {type(token)}"
            raise UnknownTokenType(msg, token, token_type)
        if token_type == TokenType.ACCESS_TOKEN:
            if token is not None and not isinstance(token, (str, BearerToken)):
                msg = f"""\
The supplied token is of type '{type(token)}' which is inconsistent with token_type '{token_type}'.
A BearerToken or an access_token as a `str` is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:access_token"
        if token_type == TokenType.REFRESH_TOKEN:
            if token is not None and isinstance(token, BearerToken) and not token.refresh_token:
                msg = f"""\
The supplied BearerToken does not contain a refresh_token, which is inconsistent with token_type '{token_type}'.
A BearerToken containing a refresh_token is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:refresh_token"
        if token_type == TokenType.ID_TOKEN:
            if token is not None and not isinstance(token, (str, IdToken)):
                msg = f"""\
The supplied token is of type '{type(token)}' which is inconsistent with token_type '{token_type}'.
An IdToken or a string representation of it is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:id_token"

        return {
            "saml1": "urn:ietf:params:oauth:token-type:saml1",
            "saml2": "urn:ietf:params:oauth:token-type:saml2",
            "jwt": "urn:ietf:params:oauth:token-type:jwt",
        }.get(token_type, token_type)

    def revoke_access_token(
        self,
        access_token: BearerToken | str,
        requests_kwargs: dict[str, Any] | None = None,
        **revoke_kwargs: Any,
    ) -> bool:
        """Send a request to the Revocation Endpoint to revoke an access token.

        Args:
            access_token: the access token to revoke
            requests_kwargs: additional parameters for the underlying requests.post() call
            **revoke_kwargs: additional parameters to pass to the revocation endpoint

        """
        return self.revoke_token(
            access_token,
            token_type_hint=TokenType.ACCESS_TOKEN,
            requests_kwargs=requests_kwargs,
            **revoke_kwargs,
        )

    def revoke_refresh_token(
        self,
        refresh_token: str | BearerToken,
        requests_kwargs: dict[str, Any] | None = None,
        **revoke_kwargs: Any,
    ) -> bool:
        """Send a request to the Revocation Endpoint to revoke a refresh token.

        Args:
            refresh_token: the refresh token to revoke.
            requests_kwargs: additional parameters to pass to the revocation endpoint.
            **revoke_kwargs: additional parameters to pass to the revocation endpoint.

        Returns:
            `True` if the revocation request is successful, `False` if this client has no configured
            revocation endpoint.

        Raises:
            MissingRefreshToken: when `refresh_token` is a [BearerToken][requests_oauth2client.tokens.BearerToken]
                but does not contain a `refresh_token`.

        """
        if isinstance(refresh_token, BearerToken):
            if refresh_token.refresh_token is None:
                raise MissingRefreshToken(refresh_token)
            refresh_token = refresh_token.refresh_token

        return self.revoke_token(
            refresh_token,
            token_type_hint=TokenType.REFRESH_TOKEN,
            requests_kwargs=requests_kwargs,
            **revoke_kwargs,
        )

    def revoke_token(
        self,
        token: str | BearerToken,
        token_type_hint: str | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **revoke_kwargs: Any,
    ) -> bool:
        """Send a Token Revocation request.

        By default, authentication will be the same than the one used for the Token Endpoint.

        Args:
            token: the token to revoke.
            token_type_hint: a token_type_hint to send to the revocation endpoint.
            requests_kwargs: additional parameters to the underling call to requests.post()
            **revoke_kwargs: additional parameters to send to the revocation endpoint.

        Returns:
            the result from `parse_revocation_response` on the returned AS response.

        Raises:
            MissingEndpointUri: if the Revocation Endpoint URI is not configured.
            MissingRefreshToken: if `token_type_hint` is `"refresh_token"` and `token` is a BearerToken
                but does not contain a `refresh_token`.

        """
        requests_kwargs = requests_kwargs or {}

        if token_type_hint == TokenType.REFRESH_TOKEN and isinstance(token, BearerToken):
            if token.refresh_token is None:
                raise MissingRefreshToken(token)
            token = token.refresh_token

        data = dict(revoke_kwargs, token=str(token))
        if token_type_hint:
            data["token_type_hint"] = token_type_hint

        return self._request(
            Endpoints.REVOCATION,
            data=data,
            auth=self.auth,
            on_success=self.parse_revocation_response,
            on_failure=self.on_revocation_error,
            **requests_kwargs,
        )

    def parse_revocation_response(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> bool:  # noqa: ARG002
        """Parse reponses from the Revocation Endpoint.

        Since those do not return any meaningful information in a standardised fashion, this just returns `True`.

        Args:
            response: the `requests.Response` as returned by the Revocation Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            `True` if the revocation succeeds, `False` if no revocation endpoint is present or a
            non-standardised error is returned.

        """
        return True

    def on_revocation_error(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> bool:  # noqa: ARG002
        """Error handler for `revoke_token()`.

        Invoked by [revoke_token()][requests_oauth2client.client.OAuth2Client.revoke_token] when the
        revocation endpoint returns an error.

        Args:
            response: the `requests.Response` as returned by the Revocation Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            `False` to signal that an error occurred. May raise exceptions instead depending on the
            revocation response.

        Raises:
            EndpointError: if the response contains a standardised OAuth 2.0 error.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, RevocationError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception:  # noqa: BLE001
            return False
        raise exception

    def introspect_token(
        self,
        token: str | BearerToken,
        token_type_hint: str | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **introspect_kwargs: Any,
    ) -> Any:
        """Send a request to the Introspection Endpoint.

        Parameter `token` can be:

        - a `str`
        - a `BearerToken` instance

        You may pass any arbitrary `token` and `token_type_hint` values as `str`. Those will
        be included in the request, as-is.
        If `token` is a `BearerToken`, then `token_type_hint` must be either:

        - `None`: the access_token will be instrospected and no token_type_hint will be included
        in the request
        - `access_token`: same as `None`, but the token_type_hint will be included
        - or `refresh_token`: only available if a Refresh Token is present in the BearerToken.

        Args:
            token: the token to instrospect
            token_type_hint: the `token_type_hint` to include in the request.
            requests_kwargs: additional parameters to the underling call to requests.post()
            **introspect_kwargs: additional parameters to send to the introspection endpoint.

        Returns:
            the response as returned by the Introspection Endpoint.

        Raises:
            MissingRefreshToken: if `token_type_hint` is `"refresh_token"` and `token` is a BearerToken
                but does not contain a `refresh_token`.
            UnknownTokenType: if `token_type_hint` is neither `None`, `"access_token"` or `"refresh_token"`.

        """
        requests_kwargs = requests_kwargs or {}

        if isinstance(token, BearerToken):
            if token_type_hint is None or token_type_hint == TokenType.ACCESS_TOKEN:
                token = token.access_token
            elif token_type_hint == TokenType.REFRESH_TOKEN:
                if token.refresh_token is None:
                    raise MissingRefreshToken(token)

                token = token.refresh_token
            else:
                msg = """\
Invalid `token_type_hint`. To test arbitrary `token_type_hint` values, you must provide `token` as a `str`."""
                raise UnknownTokenType(msg, token, token_type_hint)

        data = dict(introspect_kwargs, token=str(token))
        if token_type_hint:
            data["token_type_hint"] = token_type_hint

        return self._request(
            Endpoints.INTROSPECTION,
            data=data,
            auth=self.auth,
            on_success=self.parse_introspection_response,
            on_failure=self.on_introspection_error,
            **requests_kwargs,
        )

    def parse_introspection_response(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> Any:
        """Parse Token Introspection Responses received by `introspect_token()`.

        Invoked by [introspect_token()][requests_oauth2client.client.OAuth2Client.introspect_token]
        to parse the returned response. This decodes the JSON content if possible, otherwise it
        returns the response as a string.

        Args:
            response: the [Response][requests.Response] as returned by the Introspection Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            the decoded JSON content, or a `str` with the content.

        """
        try:
            return response.json()
        except ValueError:
            return response.text

    def on_introspection_error(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
        """Error handler for `introspect_token()`.

        Invoked by [introspect_token()][requests_oauth2client.client.OAuth2Client.introspect_token]
        to parse the returned response in the case an error is returned.

        Args:
            response: the response as returned by the Introspection Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            usually raises exceptions. A subclass can return a default response instead.

        Raises:
            EndpointError: (or one of its subclasses) if the response contains a standard OAuth 2.0 error.
            UnknownIntrospectionError: if the response is not a standard error response.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, IntrospectionError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise UnknownIntrospectionError(response=response, client=self) from exc
        raise exception

    def backchannel_authentication_request(  # noqa: PLR0913
        self,
        scope: None | str | Iterable[str] = "openid",
        *,
        client_notification_token: str | None = None,
        acr_values: None | str | Iterable[str] = None,
        login_hint_token: str | None = None,
        id_token_hint: str | None = None,
        login_hint: str | None = None,
        binding_message: str | None = None,
        user_code: str | None = None,
        requested_expiry: int | None = None,
        private_jwk: Jwk | dict[str, Any] | None = None,
        alg: str | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **ciba_kwargs: Any,
    ) -> BackChannelAuthenticationResponse:
        """Send a CIBA Authentication Request.

        Args:
             scope: the scope to include in the request.
             client_notification_token: the Client Notification Token to include in the request.
             acr_values: the acr values to include in the request.
             login_hint_token: the Login Hint Token to include in the request.
             id_token_hint: the ID Token Hint to include in the request.
             login_hint: the Login Hint to include in the request.
             binding_message: the Binding Message to include in the request.
             user_code: the User Code to include in the request
             requested_expiry: the Requested Expiry, in seconds, to include in the request.
             private_jwk: the JWK to use to sign the request (optional)
             alg: the alg to use to sign the request, if the provided JWK does not include an "alg" parameter.
             requests_kwargs: additional parameters for
             **ciba_kwargs: additional parameters to include in the request.

        Returns:
            a BackChannelAuthenticationResponse as returned by AS

        Raises:
            InvalidBackchannelAuthenticationRequestHintParam: if none of `login_hint`, `login_hint_token`
                or `id_token_hint` is provided, or more than one of them is provided.
            InvalidScopeParam: if the `scope` parameter is invalid.
            InvalidAcrValuesParam: if the `acr_values` parameter is invalid.

        """
        if not (login_hint or login_hint_token or id_token_hint):
            msg = "One of `login_hint`, `login_hint_token` or `ìd_token_hint` must be provided"
            raise InvalidBackchannelAuthenticationRequestHintParam(msg)

        if (login_hint_token and id_token_hint) or (login_hint and id_token_hint) or (login_hint_token and login_hint):
            msg = "Only one of `login_hint`, `login_hint_token` or `ìd_token_hint` must be provided"
            raise InvalidBackchannelAuthenticationRequestHintParam(msg)

        requests_kwargs = requests_kwargs or {}

        if scope is not None and not isinstance(scope, str):
            try:
                scope = " ".join(scope)
            except Exception as exc:
                raise InvalidScopeParam(scope) from exc

        if acr_values is not None and not isinstance(acr_values, str):
            try:
                acr_values = " ".join(acr_values)
            except Exception as exc:
                raise InvalidAcrValuesParam(acr_values) from exc

        data = dict(
            ciba_kwargs,
            scope=scope,
            client_notification_token=client_notification_token,
            acr_values=acr_values,
            login_hint_token=login_hint_token,
            id_token_hint=id_token_hint,
            login_hint=login_hint,
            binding_message=binding_message,
            user_code=user_code,
            requested_expiry=requested_expiry,
        )

        if private_jwk is not None:
            data = {"request": str(Jwt.sign(data, key=private_jwk, alg=alg))}

        return self._request(
            Endpoints.BACKCHANNEL_AUTHENTICATION,
            data=data,
            auth=self.auth,
            on_success=self.parse_backchannel_authentication_response,
            on_failure=self.on_backchannel_authentication_error,
            **requests_kwargs,
        )

    def parse_backchannel_authentication_response(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> BackChannelAuthenticationResponse:
        """Parse a response received by `backchannel_authentication_request()`.

        Invoked by
        [backchannel_authentication_request()][requests_oauth2client.client.OAuth2Client.backchannel_authentication_request]
        to parse the response returned by the BackChannel Authentication Endpoint.

        Args:
            response: the response returned by the BackChannel Authentication Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            a `BackChannelAuthenticationResponse`

        Raises:
            InvalidBackChannelAuthenticationResponse: if the response does not contain a standard
                BackChannel Authentication response.

        """
        try:
            return BackChannelAuthenticationResponse(**response.json())
        except TypeError as exc:
            raise InvalidBackChannelAuthenticationResponse(response=response, client=self) from exc

    def on_backchannel_authentication_error(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> BackChannelAuthenticationResponse:
        """Error handler for `backchannel_authentication_request()`.

        Invoked by
        [backchannel_authentication_request()][requests_oauth2client.client.OAuth2Client.backchannel_authentication_request]
        to parse the response returned by the BackChannel Authentication Endpoint, when it is an
        error.

        Args:
            response: the response returned by the BackChannel Authentication Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            usually raises an exception. But a subclass can return a default response instead.

        Raises:
            EndpointError: (or one of its subclasses) if the response contains a standard OAuth 2.0 error.
            InvalidBackChannelAuthenticationResponse: for non-standard error responses.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, BackChannelAuthenticationError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise InvalidBackChannelAuthenticationResponse(response=response, client=self) from exc
        raise exception

    def authorize_device(
        self,
        requests_kwargs: dict[str, Any] | None = None,
        **data: Any,
    ) -> DeviceAuthorizationResponse:
        """Send a Device Authorization Request.

        Args:
            **data: additional data to send to the Device Authorization Endpoint
            requests_kwargs: additional parameters for `requests.request()`

        Returns:
            a Device Authorization Response

        Raises:
            MissingEndpointUri: if the Device Authorization URI is not configured

        """
        requests_kwargs = requests_kwargs or {}

        return self._request(
            Endpoints.DEVICE_AUTHORIZATION,
            data=data,
            auth=self.auth,
            on_success=self.parse_device_authorization_response,
            on_failure=self.on_device_authorization_error,
            **requests_kwargs,
        )

    def parse_device_authorization_response(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> DeviceAuthorizationResponse:
        """Parse a Device Authorization Response received by `authorize_device()`.

        Invoked by [authorize_device()][requests_oauth2client.client.OAuth2Client.authorize_device]
        to parse the response returned by the Device Authorization Endpoint.

        Args:
            response: the response returned by the Device Authorization Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            a `DeviceAuthorizationResponse` as returned by AS

        """
        return DeviceAuthorizationResponse(**response.json())

    def on_device_authorization_error(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> DeviceAuthorizationResponse:
        """Error handler for `authorize_device()`.

        Invoked by [authorize_device()][requests_oauth2client.client.OAuth2Client.authorize_device]
        to parse the response returned by the Device Authorization Endpoint, when that response is
        an error.

        Args:
            response: the response returned by the Device Authorization Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            usually raises an Exception. But a subclass may return a default response instead.

        Raises:
            EndpointError: for standard OAuth 2.0 errors
            InvalidDeviceAuthorizationResponse: for non-standard error responses.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, DeviceAuthorizationError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise InvalidDeviceAuthorizationResponse(response=response, client=self) from exc
        raise exception

    def update_authorization_server_public_keys(self, requests_kwargs: dict[str, Any] | None = None) -> JwkSet:
        """Update the cached AS public keys by retrieving them from its `jwks_uri`.

        Public keys are returned by this method, as a `jwskate.JwkSet`. They are also
        available in attribute `authorization_server_jwks`.

        Returns:
            the retrieved public keys

        Raises:
            ValueError: if no `jwks_uri` is configured

        """
        requests_kwargs = requests_kwargs or {}
        requests_kwargs.setdefault("auth", None)

        jwks_uri = self._require_endpoint(Endpoints.JWKS)
        resp = self.session.get(jwks_uri, **requests_kwargs)
        resp.raise_for_status()
        jwks = resp.json()
        self.authorization_server_jwks.update(jwks)
        return self.authorization_server_jwks

    @classmethod
    def from_discovery_endpoint(
        cls,
        url: str | None = None,
        issuer: str | None = None,
        *,
        auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_key: Jwk | dict[str, Any] | None = None,
        session: requests.Session | None = None,
        testing: bool = False,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialize an `OAuth2Client` using an AS Discovery Document endpoint.

        If an `url` is provided, an HTTPS request will be done to that URL to obtain the Authorization Server Metadata.

        If an `issuer` is provided, the OpenID Connect Discovery document url will be automatically
        derived from it, as specified in [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest).

        Once the standardized metadata document is obtained, this will extract
        all Endpoint Uris from that document, will fetch the current public keys from its
        `jwks_uri`, then will initialize an OAuth2Client based on those endpoints.

        Args:
          url: The url where the server metadata will be retrieved.
          issuer: The issuer value that is expected in the discovery document.
            If not `url` is given, the OpenID Connect Discovery url for this issuer will be retrieved.
          auth: The authentication handler to use for client authentication.
          client_id: Client ID.
          client_secret: Client secret to use to authenticate the client.
          private_key: Private key to sign client assertions.
          session: A `requests.Session` to use to retrieve the document and initialise the client with.
          testing: If `True`, do not try to validate the issuer uri nor the endpoint urls
            that are part of the document.
          **kwargs: Additional keyword parameters to pass to `OAuth2Client`.

        Returns:
          An `OAuth2Client` with endpoints initialized based on the obtained metadata.

        Raises:
          InvalidIssuer: If `issuer` is not using https, or contains credentials or fragment.
          InvalidParam: If neither `url` nor `issuer` are suitable urls.
          requests.HTTPError: If an error happens while fetching the documents.

        Example:
            ```python
            from requests_oauth2client import OAuth2Client

            client = OAuth2Client.from_discovery_endpoint(
                issuer="https://myserver.net",
                client_id="my_client_id,
                client_secret="my_client_secret",
            )
            ```

        """
        if issuer is not None and not testing:
            try:
                validate_issuer_uri(issuer)
            except InvalidUri as exc:
                raise InvalidIssuer("issuer", issuer, exc) from exc  # noqa: EM101
        if url is None and issuer is not None:
            url = oidc_discovery_document_url(issuer)
        if url is None:
            msg = "Please specify at least one of `issuer` or `url`"
            raise InvalidParam(msg)

        if not testing:
            validate_endpoint_uri(url, path=False)

        session = session or requests.Session()
        discovery = session.get(url).json()

        jwks_uri = discovery.get("jwks_uri")
        jwks = JwkSet(session.get(jwks_uri).json()) if jwks_uri else None

        return cls.from_discovery_document(
            discovery,
            issuer=issuer,
            auth=auth,
            session=session,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            authorization_server_jwks=jwks,
            testing=testing,
            **kwargs,
        )

    @classmethod
    def from_discovery_document(
        cls,
        discovery: dict[str, Any],
        issuer: str | None = None,
        *,
        auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_key: Jwk | dict[str, Any] | None = None,
        authorization_server_jwks: JwkSet | dict[str, Any] | None = None,
        https: bool = True,
        testing: bool = False,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialize an `OAuth2Client`, based on an AS Discovery Document.

        Args:
          discovery: A `dict` of server metadata, in the same format as retrieved from a discovery endpoint.
          issuer: If an issuer is given, check that it matches the one mentioned in the document.
          auth: The authentication handler to use for client authentication.
          client_id: Client ID.
          client_secret: Client secret to use to authenticate the client.
          private_key: Private key to sign client assertions.
          authorization_server_jwks: The current authorization server JWKS keys.
          https: (deprecated) If `True`, validates that urls in the discovery document use the https scheme.
          testing: If `True`, don't try to validate the endpoint urls that are part of the document.
          **kwargs: Additional args that will be passed to `OAuth2Client`.

        Returns:
            An `OAuth2Client` initialized with the endpoints from the discovery document.

        Raises:
            InvalidDiscoveryDocument: If the document does not contain at least a `"token_endpoint"`.

        Examples:
            ```python
            from requests_oauth2client import OAuth2Client

            client = OAuth2Client.from_discovery_document(
                {
                    "issuer": "https://myas.local",
                    "token_endpoint": "https://myas.local/token",
                },
                client_id="client_id",
                client_secret="client_secret",
            )
            ```

        """
        if not https:
            warnings.warn(
                """\
The `https` parameter is deprecated.
To disable endpoint uri validation, set `testing=True` when initializing your `OAuth2Client`.""",
                stacklevel=1,
            )
            testing = True
        if issuer and discovery.get("issuer") != issuer:
            msg = f"""\
Mismatching `issuer` value in discovery document (received '{discovery.get("issuer")}', expected '{issuer}')."""
            raise InvalidParam(
                msg,
                issuer,
                discovery.get("issuer"),
            )
        if issuer is None:
            issuer = discovery.get("issuer")

        token_endpoint = discovery.get(Endpoints.TOKEN)
        if token_endpoint is None:
            msg = "token_endpoint not found in that discovery document"
            raise InvalidDiscoveryDocument(msg, discovery)
        authorization_endpoint = discovery.get(Endpoints.AUTHORIZATION)
        revocation_endpoint = discovery.get(Endpoints.REVOCATION)
        introspection_endpoint = discovery.get(Endpoints.INTROSPECTION)
        userinfo_endpoint = discovery.get(Endpoints.USER_INFO)
        pushed_authorization_request_endpoint = discovery.get(Endpoints.PUSHED_AUTHORIZATION_REQUEST)
        jwks_uri = discovery.get(Endpoints.JWKS)
        if jwks_uri is not None and not testing:
            validate_endpoint_uri(jwks_uri)
        authorization_response_iss_parameter_supported = discovery.get(
            "authorization_response_iss_parameter_supported",
            False,
        )

        return cls(
            token_endpoint=token_endpoint,
            authorization_endpoint=authorization_endpoint,
            revocation_endpoint=revocation_endpoint,
            introspection_endpoint=introspection_endpoint,
            userinfo_endpoint=userinfo_endpoint,
            pushed_authorization_request_endpoint=pushed_authorization_request_endpoint,
            jwks_uri=jwks_uri,
            authorization_server_jwks=authorization_server_jwks,
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            issuer=issuer,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            testing=testing,
            **kwargs,
        )

    def __enter__(self) -> Self:
        """Allow using `OAuth2Client` as a context-manager.

        The Authorization Server public keys are retrieved on `__enter__`.

        """
        self.update_authorization_server_public_keys()
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> bool:
        return True

    def _require_endpoint(self, endpoint: str) -> str:
        """Check that a required endpoint url is set."""
        url = getattr(self, endpoint, None)
        if not url:
            raise MissingEndpointUri(endpoint)

        return str(url)

client_id property

Client ID.

client_secret property

Client Secret.

client_jwks property

A JwkSet containing the public keys for this client.

Keys are:

  • the public key for client assertion signature verification (if using private_key_jwt)
  • the ID Token encryption key

validate_endpoint_uri(attribute, uri)

Validate that an endpoint URI is suitable for use.

If you need to disable some checks (for AS testing purposes only!), provide a different method here.

Source code in requests_oauth2client/client.py
@token_endpoint.validator
@revocation_endpoint.validator
@introspection_endpoint.validator
@userinfo_endpoint.validator
@authorization_endpoint.validator
@backchannel_authentication_endpoint.validator
@device_authorization_endpoint.validator
@pushed_authorization_request_endpoint.validator
@jwks_uri.validator
def validate_endpoint_uri(self, attribute: Attribute[str | None], uri: str | None) -> str | None:
    """Validate that an endpoint URI is suitable for use.

    If you need to disable some checks (for AS testing purposes only!), provide a different method here.

    """
    if self.testing or uri is None:
        return uri
    try:
        return validate_endpoint_uri(uri)
    except InvalidUri as exc:
        raise InvalidEndpointUri(endpoint=attribute.name, uri=uri, exc=exc) from exc

validate_issuer_uri(attribute, uri)

Validate that an Issuer identifier is suitable for use.

This is the same check as an endpoint URI, but the path may be (and usually is) empty.

Source code in requests_oauth2client/client.py
@issuer.validator
def validate_issuer_uri(self, attribute: Attribute[str | None], uri: str | None) -> str | None:
    """Validate that an Issuer identifier is suitable for use.

    This is the same check as an endpoint URI, but the path may be (and usually is) empty.

    """
    if self.testing or uri is None:
        return uri
    try:
        return validate_issuer_uri(uri)
    except InvalidUri as exc:
        raise InvalidIssuer(attribute.name, uri, exc) from exc

token_request(data, *, timeout=10, dpop=None, dpop_key=None, **requests_kwargs)

Send a request to the token endpoint.

Authentication will be added automatically based on the defined auth for this client.

Parameters:

Name Type Description Default
data dict[str, Any]

parameters to send to the token endpoint. Items with a None or empty value will not be sent in the request.

required
timeout int

a timeout value for the call

10
dpop bool | None

toggles DPoP-proofing for the token request:

1
2
3
- if `False`, disable it,
- if `True`, enable it,
- if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
None
dpop_key DPoPKey | None

a chosen DPoPKey for this request. If None, a new key will be generated automatically with a call to this client dpop_key_generator.

None
**requests_kwargs Any

additional parameters for requests.post()

{}

Returns:

Type Description
BearerToken

the token endpoint response

Source code in requests_oauth2client/client.py
def token_request(
    self,
    data: dict[str, Any],
    *,
    timeout: int = 10,
    dpop: bool | None = None,
    dpop_key: DPoPKey | None = None,
    **requests_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint.

    Authentication will be added automatically based on the defined `auth` for this client.

    Args:
      data: parameters to send to the token endpoint. Items with a `None`
           or empty value will not be sent in the request.
      timeout: a timeout value for the call
      dpop: toggles DPoP-proofing for the token request:

            - if `False`, disable it,
            - if `True`, enable it,
            - if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
      dpop_key: a chosen `DPoPKey` for this request. If `None`, a new key will be generated automatically
            with a call to this client `dpop_key_generator`.
      **requests_kwargs: additional parameters for requests.post()

    Returns:
        the token endpoint response

    """
    if dpop is None:
        dpop = self.dpop_bound_access_tokens
    if dpop and not dpop_key:
        dpop_key = self.dpop_key_generator(self.dpop_alg)

    return self._request(
        Endpoints.TOKEN,
        auth=self.auth,
        data=data,
        timeout=timeout,
        dpop_key=dpop_key,
        on_success=self.parse_token_response,
        on_failure=self.on_token_error,
        **requests_kwargs,
    )

parse_token_response(response, *, dpop_key=None)

Parse a Response returned by the Token Endpoint.

Invoked by token_request to parse responses returned by the Token Endpoint. Those responses contain an access_token and additional attributes.

Parameters:

Name Type Description Default
response Response

the Response returned by the Token Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
BearerToken

a BearerToken based on the response contents.

Source code in requests_oauth2client/client.py
def parse_token_response(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> BearerToken:
    """Parse a Response returned by the Token Endpoint.

    Invoked by [token_request][requests_oauth2client.client.OAuth2Client.token_request] to parse
    responses returned by the Token Endpoint. Those responses contain an `access_token` and
    additional attributes.

    Args:
        response: the `Response` returned by the Token Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        a `BearerToken` based on the response contents.

    """
    token_class = dpop_key.dpop_token_class if dpop_key is not None else self.token_class
    try:
        token_response = token_class(**response.json(), _dpop_key=dpop_key)
    except Exception:  # noqa: BLE001
        return self.on_token_error(response, dpop_key=dpop_key)
    else:
        return token_response

on_token_error(response, *, dpop_key=None)

Error handler for token_request().

Invoked by token_request when the Token Endpoint returns an error.

Parameters:

Name Type Description Default
response Response

the Response returned by the Token Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
BearerToken

nothing, and raises an exception instead. But a subclass may return a

BearerToken

BearerToken to implement a default behaviour if needed.

Raises:

Type Description
InvalidTokenResponse

if the error response does not contain an OAuth 2.0 standard error response.

Source code in requests_oauth2client/client.py
def on_token_error(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> BearerToken:
    """Error handler for `token_request()`.

    Invoked by [token_request][requests_oauth2client.client.OAuth2Client.token_request] when the
    Token Endpoint returns an error.

    Args:
        response: the `Response` returned by the Token Endpoint.
        dpop_key: the DPoPKey that was used to proof the token request, if any.

    Returns:
        nothing, and raises an exception instead. But a subclass may return a
        `BearerToken` to implement a default behaviour if needed.

    Raises:
        InvalidTokenResponse: if the error response does not contain an OAuth 2.0 standard
            error response.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, UnknownTokenEndpointError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise InvalidTokenResponse(
            response=response,
            client=self,
            description=f"An error happened while processing the error response: {exc}",
        ) from exc
    raise exception

client_credentials(scope=None, *, dpop=None, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint using the client_credentials grant.

Parameters:

Name Type Description Default
scope str | Iterable[str] | None

the scope to send with the request. Can be a str, or an iterable of str. to pass that way include scope, audience, resource, etc.

None
dpop bool | None

toggles DPoP-proofing for the token request:

  • if False, disable it,
  • if True, enable it,
  • if None, defaults to dpop_bound_access_tokens configuration parameter for the client.
None
dpop_key DPoPKey | None

a chosen DPoPKey for this request. If None, a new key will be generated automatically with a call to dpop_key_generator.

None
requests_kwargs dict[str, Any] | None

additional parameters for the call to requests

None
**token_kwargs Any

additional parameters that will be added in the form data for the token endpoint, alongside grant_type.

{}

Returns:

Type Description
BearerToken

a BearerToken or DPoPToken, depending on the AS response.

Raises:

Type Description
InvalidScopeParam

if the scope parameter is not suitable

Source code in requests_oauth2client/client.py
def client_credentials(
    self,
    scope: str | Iterable[str] | None = None,
    *,
    dpop: bool | None = None,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint using the `client_credentials` grant.

    Args:
        scope: the scope to send with the request. Can be a str, or an iterable of str.
            to pass that way include `scope`, `audience`, `resource`, etc.
        dpop: toggles DPoP-proofing for the token request:

            - if `False`, disable it,
            - if `True`, enable it,
            - if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
        dpop_key: a chosen `DPoPKey` for this request. If `None`, a new key will be generated automatically
            with a call to `dpop_key_generator`.
        requests_kwargs: additional parameters for the call to requests
        **token_kwargs: additional parameters that will be added in the form data for the token endpoint,
             alongside `grant_type`.

    Returns:
        a `BearerToken` or `DPoPToken`, depending on the AS response.

    Raises:
        InvalidScopeParam: if the `scope` parameter is not suitable

    """
    requests_kwargs = requests_kwargs or {}

    if scope and not isinstance(scope, str):
        try:
            scope = " ".join(scope)
        except Exception as exc:
            raise InvalidScopeParam(scope) from exc

    data = dict(grant_type=GrantTypes.CLIENT_CREDENTIALS, scope=scope, **token_kwargs)
    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

authorization_code(code, *, validate=True, dpop=False, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint with the authorization_code grant.

You can either pass an authorization code, as a str, or pass an AuthorizationResponse instance as returned by AuthorizationRequest.validate_callback() (recommended). If you do the latter, this will automatically:

  • add the appropriate redirect_uri value that was initially passed in the Authorization Request parameters. This is no longer mandatory in OAuth 2.1, but a lot of Authorization Servers are still expecting it since it was part of the OAuth 2.0 specifications.
  • add the appropriate code_verifier for PKCE that was generated before sending the AuthorizationRequest.
  • handle DPoP binding based on the same DPoPKey that was used to initialize the AuthenticationRequest and whose JWK thumbprint was passed as dpop_jkt parameter in the Auth Request.

Parameters:

Name Type Description Default
code str | AuthorizationResponse

An authorization code or an AuthorizationResponse to exchange for tokens.

required
validate bool

If True, validate the ID Token (this works only if code is an AuthorizationResponse).

True
dpop bool

Toggles DPoP binding for the Access Token, even if Authorization Code DPoP binding was not initially done.

False
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to automatically generate a key, if dpop is True.

None
requests_kwargs dict[str, Any] | None

Additional parameters for the call to the underlying HTTP requests call.

None
**token_kwargs Any

Additional parameters that will be added in the form data for the token endpoint, alongside grant_type, code, etc.

{}

Returns:

Type Description
BearerToken

The Token Endpoint Response.

Source code in requests_oauth2client/client.py
def authorization_code(
    self,
    code: str | AuthorizationResponse,
    *,
    validate: bool = True,
    dpop: bool = False,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint with the `authorization_code` grant.

    You can either pass an authorization code, as a `str`, or pass an `AuthorizationResponse` instance as
    returned by `AuthorizationRequest.validate_callback()` (recommended). If you do the latter, this will
    automatically:

    - add the appropriate `redirect_uri` value that was initially passed in the Authorization Request parameters.
    This is no longer mandatory in OAuth 2.1, but a lot of Authorization Servers are still expecting it since it was
    part of the OAuth 2.0 specifications.
    - add the appropriate `code_verifier` for PKCE that was generated before sending the AuthorizationRequest.
    - handle DPoP binding based on the same `DPoPKey` that was used to initialize the `AuthenticationRequest` and
    whose JWK thumbprint was passed as `dpop_jkt` parameter in the Auth Request.

    Args:
        code: An authorization code or an `AuthorizationResponse` to exchange for tokens.
        validate: If `True`, validate the ID Token (this works only if `code` is an `AuthorizationResponse`).
        dpop: Toggles DPoP binding for the Access Token,
             even if Authorization Code DPoP binding was not initially done.
        dpop_key: A chosen DPoP key. Leave `None` to automatically generate a key, if `dpop` is `True`.
        requests_kwargs: Additional parameters for the call to the underlying HTTP `requests` call.
        **token_kwargs: Additional parameters that will be added in the form data for the token endpoint,
            alongside `grant_type`, `code`, etc.

    Returns:
        The Token Endpoint Response.

    """
    azr: AuthorizationResponse | None = None
    if isinstance(code, AuthorizationResponse):
        token_kwargs.setdefault("code_verifier", code.code_verifier)
        token_kwargs.setdefault("redirect_uri", code.redirect_uri)
        azr = code
        dpop_key = code.dpop_key
        code = code.code

    requests_kwargs = requests_kwargs or {}

    data = dict(grant_type=GrantTypes.AUTHORIZATION_CODE, code=code, **token_kwargs)
    token = self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
    if validate and token.id_token and isinstance(azr, AuthorizationResponse):
        return token.validate_id_token(self, azr)
    return token

refresh_token(refresh_token, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint with the refresh_token grant.

If refresh_token is a DPoPToken instance, (which means that DPoP was used to obtain the initial Access/Refresh Tokens), then the same DPoP key will be used to DPoP proof the refresh token request, as defined in RFC9449.

Parameters:

Name Type Description Default
refresh_token str | BearerToken

A refresh_token, as a string, or as a BearerToken. That BearerToken must have a refresh_token.

required
requests_kwargs dict[str, Any] | None

Additional parameters for the call to requests.

None
**token_kwargs Any

Additional parameters for the token endpoint, alongside grant_type, refresh_token, etc.

{}

Returns:

Type Description
BearerToken

The token endpoint response.

Raises:

Type Description
MissingRefreshToken

If refresh_token is a BearerToken instance but does not contain a refresh_token.

Source code in requests_oauth2client/client.py
def refresh_token(
    self,
    refresh_token: str | BearerToken,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint with the `refresh_token` grant.

    If `refresh_token` is a `DPoPToken` instance, (which means that DPoP was used to obtain the initial
    Access/Refresh Tokens), then the same DPoP key will be used to DPoP proof the refresh token request,
    as defined in RFC9449.

    Args:
        refresh_token: A refresh_token, as a string, or as a `BearerToken`.
            That `BearerToken` must have a `refresh_token`.
        requests_kwargs: Additional parameters for the call to `requests`.
        **token_kwargs: Additional parameters for the token endpoint,
            alongside `grant_type`, `refresh_token`, etc.

    Returns:
        The token endpoint response.

    Raises:
        MissingRefreshToken: If `refresh_token` is a `BearerToken` instance but does not
            contain a `refresh_token`.

    """
    dpop_key: DPoPKey | None = None
    if isinstance(refresh_token, BearerToken):
        if refresh_token.refresh_token is None or not isinstance(refresh_token.refresh_token, str):
            raise MissingRefreshToken(refresh_token)
        if isinstance(refresh_token, DPoPToken):
            dpop_key = refresh_token.dpop_key
        refresh_token = refresh_token.refresh_token

    requests_kwargs = requests_kwargs or {}
    data = dict(grant_type=GrantTypes.REFRESH_TOKEN, refresh_token=refresh_token, **token_kwargs)
    return self.token_request(data, dpop_key=dpop_key, **requests_kwargs)

device_code(device_code, *, dpop=False, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint using the Device Code grant.

The grant_type is urn:ietf:params:oauth:grant-type:device_code. This needs a Device Code, or a DeviceAuthorizationResponse as parameter.

Parameters:

Name Type Description Default
device_code str | DeviceAuthorizationResponse

A device code, or a DeviceAuthorizationResponse.

required
dpop bool

Toggles DPoP Binding. If None, defaults to self.dpop_bound_access_tokens.

False
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for this request.

None
requests_kwargs dict[str, Any] | None

Additional parameters for the call to requests.

None
**token_kwargs Any

Additional parameters for the token endpoint, alongside grant_type, device_code, etc.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Raises:

Type Description
MissingDeviceCode

if device_code is a DeviceAuthorizationResponse but does not contain a device_code.

Source code in requests_oauth2client/client.py
def device_code(
    self,
    device_code: str | DeviceAuthorizationResponse,
    *,
    dpop: bool = False,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint using the Device Code grant.

    The grant_type is `urn:ietf:params:oauth:grant-type:device_code`. This needs a Device Code,
    or a `DeviceAuthorizationResponse` as parameter.

    Args:
        device_code: A device code, or a `DeviceAuthorizationResponse`.
        dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
        requests_kwargs: Additional parameters for the call to requests.
        **token_kwargs: Additional parameters for the token endpoint, alongside `grant_type`, `device_code`, etc.

    Returns:
        The Token Endpoint response.

    Raises:
        MissingDeviceCode: if `device_code` is a DeviceAuthorizationResponse but does not
            contain a `device_code`.

    """
    if isinstance(device_code, DeviceAuthorizationResponse):
        if device_code.device_code is None or not isinstance(device_code.device_code, str):
            raise MissingDeviceCode(device_code)
        device_code = device_code.device_code

    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantTypes.DEVICE_CODE,
        device_code=device_code,
        **token_kwargs,
    )
    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

ciba(auth_req_id, requests_kwargs=None, **token_kwargs)

Send a CIBA request to the Token Endpoint.

A CIBA request is a Token Request using the urn:openid:params:grant-type:ciba grant.

Parameters:

Name Type Description Default
auth_req_id str | BackChannelAuthenticationResponse

an authentication request ID, as returned by the AS

required
requests_kwargs dict[str, Any] | None

additional parameters for the call to requests

None
**token_kwargs Any

additional parameters for the token endpoint, alongside grant_type, auth_req_id, etc.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Raises:

Type Description
MissingAuthRequestId

if auth_req_id is a BackChannelAuthenticationResponse but does not contain an auth_req_id.

Source code in requests_oauth2client/client.py
def ciba(
    self,
    auth_req_id: str | BackChannelAuthenticationResponse,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a CIBA request to the Token Endpoint.

    A CIBA request is a Token Request using the `urn:openid:params:grant-type:ciba` grant.

    Args:
        auth_req_id: an authentication request ID, as returned by the AS
        requests_kwargs: additional parameters for the call to requests
        **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `auth_req_id`, etc.

    Returns:
        The Token Endpoint response.

    Raises:
        MissingAuthRequestId: if `auth_req_id` is a BackChannelAuthenticationResponse but does not contain
            an `auth_req_id`.

    """
    if isinstance(auth_req_id, BackChannelAuthenticationResponse):
        if auth_req_id.auth_req_id is None or not isinstance(auth_req_id.auth_req_id, str):
            raise MissingAuthRequestId(auth_req_id)
        auth_req_id = auth_req_id.auth_req_id

    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantTypes.CLIENT_INITIATED_BACKCHANNEL_AUTHENTICATION,
        auth_req_id=auth_req_id,
        **token_kwargs,
    )
    return self.token_request(data, **requests_kwargs)

token_exchange(*, subject_token, subject_token_type=None, actor_token=None, actor_token_type=None, requested_token_type=None, dpop=False, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a Token Exchange request.

A Token Exchange request is actually a request to the Token Endpoint with a grant_type urn:ietf:params:oauth:grant-type:token-exchange.

Parameters:

Name Type Description Default
subject_token str | BearerToken | IdToken

The subject token to exchange for a new token.

required
subject_token_type str | None

A token type identifier for the subject_token, mandatory if it cannot be guessed based on type(subject_token).

None
actor_token None | str | BearerToken | IdToken

The actor token to include in the request, if any.

None
actor_token_type str | None

A token type identifier for the actor_token, mandatory if it cannot be guessed based on type(actor_token).

None
requested_token_type str | None

A token type identifier for the requested token.

None
dpop bool

Toggles DPoP Binding. If None, defaults to self.dpop_bound_access_tokens.

False
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for this request.

None
requests_kwargs dict[str, Any] | None

Additional parameters to pass to the underlying requests.post() call.

None
**token_kwargs Any

Additional parameters to include in the request body.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Raises:

Type Description
UnknownSubjectTokenType

If the type of subject_token cannot be determined automatically.

UnknownActorTokenType

If the type of actor_token cannot be determined automatically.

Source code in requests_oauth2client/client.py
def token_exchange(
    self,
    *,
    subject_token: str | BearerToken | IdToken,
    subject_token_type: str | None = None,
    actor_token: None | str | BearerToken | IdToken = None,
    actor_token_type: str | None = None,
    requested_token_type: str | None = None,
    dpop: bool = False,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a Token Exchange request.

    A Token Exchange request is actually a request to the Token Endpoint with a grant_type
    `urn:ietf:params:oauth:grant-type:token-exchange`.

    Args:
        subject_token: The subject token to exchange for a new token.
        subject_token_type: A token type identifier for the subject_token, mandatory if it cannot be guessed based
            on `type(subject_token)`.
        actor_token: The actor token to include in the request, if any.
        actor_token_type: A token type identifier for the actor_token, mandatory if it cannot be guessed based
            on `type(actor_token)`.
        requested_token_type: A token type identifier for the requested token.
        dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
        requests_kwargs: Additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: Additional parameters to include in the request body.

    Returns:
        The Token Endpoint response.

    Raises:
        UnknownSubjectTokenType: If the type of `subject_token` cannot be determined automatically.
        UnknownActorTokenType: If the type of `actor_token` cannot be determined automatically.

    """
    requests_kwargs = requests_kwargs or {}

    try:
        subject_token_type = self.get_token_type(subject_token_type, subject_token)
    except ValueError as exc:
        raise UnknownSubjectTokenType(subject_token, subject_token_type) from exc
    if actor_token:  # pragma: no branch
        try:
            actor_token_type = self.get_token_type(actor_token_type, actor_token)
        except ValueError as exc:
            raise UnknownActorTokenType(actor_token, actor_token_type) from exc

    data = dict(
        grant_type=GrantTypes.TOKEN_EXCHANGE,
        subject_token=subject_token,
        subject_token_type=subject_token_type,
        actor_token=actor_token,
        actor_token_type=actor_token_type,
        requested_token_type=requested_token_type,
        **token_kwargs,
    )
    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

jwt_bearer(assertion, *, dpop=False, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request using a JWT as authorization grant.

This is defined in (RFC7523 $2.1)[https://www.rfc-editor.org/rfc/rfc7523.html#section-2.1).

Parameters:

Name Type Description Default
assertion Jwt | str

A JWT (as an instance of jwskate.Jwt or as a str) to use as authorization grant.

required
dpop bool

Toggles DPoP Binding. If None, defaults to self.dpop_bound_access_tokens.

False
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for this request.

None
requests_kwargs dict[str, Any] | None

Additional parameters to pass to the underlying requests.post() call.

None
**token_kwargs Any

Additional parameters to include in the request body.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Source code in requests_oauth2client/client.py
def jwt_bearer(
    self,
    assertion: Jwt | str,
    *,
    dpop: bool = False,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request using a JWT as authorization grant.

    This is defined in (RFC7523 $2.1)[https://www.rfc-editor.org/rfc/rfc7523.html#section-2.1).

    Args:
        assertion: A JWT (as an instance of `jwskate.Jwt` or as a `str`) to use as authorization grant.
        dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
        requests_kwargs: Additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: Additional parameters to include in the request body.

    Returns:
        The Token Endpoint response.

    """
    requests_kwargs = requests_kwargs or {}

    if not isinstance(assertion, Jwt):
        assertion = Jwt(assertion)

    data = dict(
        grant_type=GrantTypes.JWT_BEARER,
        assertion=assertion,
        **token_kwargs,
    )

    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

resource_owner_password(username, password, *, dpop=None, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request using the Resource Owner Password Grant.

This Grant Type is deprecated and should only be used when there is no other choice.

Parameters:

Name Type Description Default
username str

the resource owner user name

required
password str

the resource owner password

required
dpop bool | None

Toggles DPoP Binding. If None, defaults to self.dpop_bound_access_tokens.

None
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for this request.

None
requests_kwargs dict[str, Any] | None

additional parameters to pass to the underlying requests.post() call.

None
**token_kwargs Any

additional parameters to include in the request body.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Source code in requests_oauth2client/client.py
def resource_owner_password(
    self,
    username: str,
    password: str,
    *,
    dpop: bool | None = None,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request using the Resource Owner Password Grant.

    This Grant Type is deprecated and should only be used when there is no other choice.

    Args:
        username: the resource owner user name
        password: the resource owner password
        dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
        requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: additional parameters to include in the request body.

    Returns:
        The Token Endpoint response.

    """
    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantTypes.RESOURCE_OWNER_PASSWORD,
        username=username,
        password=password,
        **token_kwargs,
    )

    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

authorization_request(*, scope='openid', response_type=ResponseTypes.CODE, redirect_uri=None, state=..., nonce=..., code_verifier=None, dpop=None, dpop_key=None, dpop_alg=None, **kwargs)

Generate an Authorization Request for this client.

Parameters:

Name Type Description Default
scope None | str | Iterable[str]

The scope to use.

'openid'
response_type str

The response_type to use.

CODE
redirect_uri str | None

The redirect_uri to include in the request. By default, the redirect_uri defined at init time is used.

None
state str | ellipsis | None

The state parameter to use. Leave default to generate a random value.

...
nonce str | ellipsis | None

A nonce. Leave default to generate a random value.

...
dpop bool | None

Toggles DPoP binding. - if True, DPoP binding is used - if False, DPoP is not used - if None, defaults to self.dpop_bound_access_tokens

None
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for you.

None
dpop_alg str | None

A signature alg to sign the DPoP proof. If None, this defaults to self.dpop_alg. If DPoP is not used, or a chosen dpop_key is provided, this is ignored. This affects the key type if a DPoP key must be generated.

None
code_verifier str | None

The PKCE code_verifier to use. Leave default to generate a random value.

None
**kwargs Any

Additional query parameters to include in the auth request.

{}

Returns:

Type Description
AuthorizationRequest

The Token Endpoint response.

Source code in requests_oauth2client/client.py
def authorization_request(
    self,
    *,
    scope: None | str | Iterable[str] = "openid",
    response_type: str = ResponseTypes.CODE,
    redirect_uri: str | None = None,
    state: str | ellipsis | None = ...,  # noqa: F821
    nonce: str | ellipsis | None = ...,  # noqa: F821
    code_verifier: str | None = None,
    dpop: bool | None = None,
    dpop_key: DPoPKey | None = None,
    dpop_alg: str | None = None,
    **kwargs: Any,
) -> AuthorizationRequest:
    """Generate an Authorization Request for this client.

    Args:
        scope: The `scope` to use.
        response_type: The `response_type` to use.
        redirect_uri: The `redirect_uri` to include in the request. By default,
            the `redirect_uri` defined at init time is used.
        state: The `state` parameter to use. Leave default to generate a random value.
        nonce: A `nonce`. Leave default to generate a random value.
        dpop: Toggles DPoP binding.
            - if `True`, DPoP binding is used
            - if `False`, DPoP is not used
            - if `None`, defaults to `self.dpop_bound_access_tokens`
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for you.
        dpop_alg: A signature alg to sign the DPoP proof. If `None`, this defaults to `self.dpop_alg`.
            If DPoP is not used, or a chosen `dpop_key` is provided, this is ignored.
            This affects the key type if a DPoP key must be generated.
        code_verifier: The PKCE `code_verifier` to use. Leave default to generate a random value.
        **kwargs: Additional query parameters to include in the auth request.

    Returns:
        The Token Endpoint response.

    """
    authorization_endpoint = self._require_endpoint("authorization_endpoint")

    redirect_uri = redirect_uri or self.redirect_uri

    if dpop is None:
        dpop = self.dpop_bound_access_tokens
    if dpop_alg is None:
        dpop_alg = self.dpop_alg

    return AuthorizationRequest(
        authorization_endpoint=authorization_endpoint,
        client_id=self.client_id,
        redirect_uri=redirect_uri,
        issuer=self.issuer,
        response_type=response_type,
        scope=scope,
        state=state,
        nonce=nonce,
        code_verifier=code_verifier,
        code_challenge_method=self.code_challenge_method,
        dpop=dpop,
        dpop_key=dpop_key,
        dpop_alg=dpop_alg,
        **kwargs,
    )

pushed_authorization_request(authorization_request, requests_kwargs=None)

Send a Pushed Authorization Request.

This sends a request to the Pushed Authorization Request Endpoint, and returns a RequestUriParameterAuthorizationRequest initialized with the AS response.

Parameters:

Name Type Description Default
authorization_request AuthorizationRequest

The authorization request to send.

required
requests_kwargs dict[str, Any] | None

Additional parameters for requests.request().

None

Returns:

Type Description
RequestUriParameterAuthorizationRequest

The RequestUriParameterAuthorizationRequest initialized based on the AS response.

Source code in requests_oauth2client/client.py
def pushed_authorization_request(
    self,
    authorization_request: AuthorizationRequest,
    requests_kwargs: dict[str, Any] | None = None,
) -> RequestUriParameterAuthorizationRequest:
    """Send a Pushed Authorization Request.

    This sends a request to the Pushed Authorization Request Endpoint, and returns a
    `RequestUriParameterAuthorizationRequest` initialized with the AS response.

    Args:
        authorization_request: The authorization request to send.
        requests_kwargs: Additional parameters for `requests.request()`.

    Returns:
        The `RequestUriParameterAuthorizationRequest` initialized based on the AS response.

    """
    requests_kwargs = requests_kwargs or {}
    return self._request(
        Endpoints.PUSHED_AUTHORIZATION_REQUEST,
        data=authorization_request.args,
        auth=self.auth,
        on_success=self.parse_pushed_authorization_response,
        on_failure=self.on_pushed_authorization_request_error,
        dpop_key=authorization_request.dpop_key,
        **requests_kwargs,
    )

parse_pushed_authorization_response(response, *, dpop_key=None)

Parse the response obtained by pushed_authorization_request().

Parameters:

Name Type Description Default
response Response

The requests.Response returned by the PAR endpoint.

required
dpop_key DPoPKey | None

The DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
RequestUriParameterAuthorizationRequest

A RequestUriParameterAuthorizationRequest instance initialized based on the PAR endpoint response.

Source code in requests_oauth2client/client.py
def parse_pushed_authorization_response(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,
) -> RequestUriParameterAuthorizationRequest:
    """Parse the response obtained by `pushed_authorization_request()`.

    Args:
        response: The `requests.Response` returned by the PAR endpoint.
        dpop_key: The `DPoPKey` that was used to proof the token request, if any.

    Returns:
        A `RequestUriParameterAuthorizationRequest` instance initialized based on the PAR endpoint response.

    """
    response_json = response.json()
    request_uri = response_json.get("request_uri")
    expires_in = response_json.get("expires_in")

    return RequestUriParameterAuthorizationRequest(
        authorization_endpoint=self.authorization_endpoint,
        client_id=self.client_id,
        request_uri=request_uri,
        expires_in=expires_in,
        dpop_key=dpop_key,
    )

on_pushed_authorization_request_error(response, *, dpop_key=None)

Error Handler for Pushed Authorization Endpoint errors.

Parameters:

Name Type Description Default
response Response

The HTTP response as returned by the AS PAR endpoint.

required
dpop_key DPoPKey | None

The DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
RequestUriParameterAuthorizationRequest

Should not return anything, but raise an Exception instead. A RequestUriParameterAuthorizationRequest

RequestUriParameterAuthorizationRequest

may be returned by subclasses for testing purposes.

Raises:

Type Description
EndpointError

A subclass of this error depending on the error returned by the AS.

InvalidPushedAuthorizationResponse

If the returned response is not following the specifications.

UnknownTokenEndpointError

For unknown/unhandled errors.

Source code in requests_oauth2client/client.py
def on_pushed_authorization_request_error(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> RequestUriParameterAuthorizationRequest:
    """Error Handler for Pushed Authorization Endpoint errors.

    Args:
        response: The HTTP response as returned by the AS PAR endpoint.
        dpop_key: The `DPoPKey` that was used to proof the token request, if any.

    Returns:
        Should not return anything, but raise an Exception instead. A `RequestUriParameterAuthorizationRequest`
        may be returned by subclasses for testing purposes.

    Raises:
        EndpointError: A subclass of this error depending on the error returned by the AS.
        InvalidPushedAuthorizationResponse: If the returned response is not following the specifications.
        UnknownTokenEndpointError: For unknown/unhandled errors.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, UnknownTokenEndpointError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise InvalidPushedAuthorizationResponse(response=response, client=self) from exc
    raise exception

userinfo(access_token)

Call the UserInfo endpoint.

This sends a request to the UserInfo endpoint, with the specified access_token, and returns the parsed result.

Parameters:

Name Type Description Default
access_token BearerToken | str

the access token to use

required

Returns:

Type Description
Any

the Response returned by the userinfo endpoint.

Source code in requests_oauth2client/client.py
def userinfo(self, access_token: BearerToken | str) -> Any:
    """Call the UserInfo endpoint.

    This sends a request to the UserInfo endpoint, with the specified access_token, and returns
    the parsed result.

    Args:
        access_token: the access token to use

    Returns:
        the [Response][requests.Response] returned by the userinfo endpoint.

    """
    if isinstance(access_token, str):
        access_token = BearerToken(access_token)
    return self._request(
        Endpoints.USER_INFO,
        auth=access_token,
        on_success=self.parse_userinfo_response,
        on_failure=self.on_userinfo_error,
    )

parse_userinfo_response(resp, *, dpop_key=None)

Parse the response obtained by userinfo().

Invoked by userinfo() to parse the response from the UserInfo endpoint, this will extract and return its JSON content.

Parameters:

Name Type Description Default
resp Response

a Response returned from the UserInfo endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
Any

the parsed JSON content from this response.

Source code in requests_oauth2client/client.py
def parse_userinfo_response(self, resp: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
    """Parse the response obtained by `userinfo()`.

    Invoked by [userinfo()][requests_oauth2client.client.OAuth2Client.userinfo] to parse the
    response from the UserInfo endpoint, this will extract and return its JSON content.

    Args:
        resp: a [Response][requests.Response] returned from the UserInfo endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        the parsed JSON content from this response.

    """
    return resp.json()

on_userinfo_error(resp, *, dpop_key=None)

Parse UserInfo error response.

Parameters:

Name Type Description Default
resp Response

a Response returned from the UserInfo endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
Any

nothing, raises exception instead.

Source code in requests_oauth2client/client.py
def on_userinfo_error(self, resp: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
    """Parse UserInfo error response.

    Args:
        resp: a [Response][requests.Response] returned from the UserInfo endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        nothing, raises exception instead.

    """
    resp.raise_for_status()

get_token_type(token_type=None, token=None) classmethod

Get standardized token type identifiers.

Return a standardized token type identifier, based on a short token_type hint and/or a token value.

Parameters:

Name Type Description Default
token_type str | None

a token_type hint, as str. May be "access_token", "refresh_token" or "id_token"

None
token None | str | BearerToken | IdToken

a token value, as an instance of BearerToken or IdToken, or as a str.

None

Returns:

Type Description
str

the token_type as defined in the Token Exchange RFC8693.

Raises:

Type Description
UnknownTokenType

if the type of token cannot be determined

Source code in requests_oauth2client/client.py
    @classmethod
    def get_token_type(  # noqa: C901
        cls,
        token_type: str | None = None,
        token: None | str | BearerToken | IdToken = None,
    ) -> str:
        """Get standardized token type identifiers.

        Return a standardized token type identifier, based on a short `token_type` hint and/or a
        token value.

        Args:
            token_type: a token_type hint, as `str`. May be "access_token", "refresh_token"
                or "id_token"
            token: a token value, as an instance of `BearerToken` or IdToken, or as a `str`.

        Returns:
            the token_type as defined in the Token Exchange RFC8693.

        Raises:
            UnknownTokenType: if the type of token cannot be determined

        """
        if not (token_type or token):
            msg = "Cannot determine type of an empty token without a token_type hint"
            raise UnknownTokenType(msg, token, token_type)

        if token_type is None:
            if isinstance(token, str):
                msg = """\
Cannot determine the type of provided token when it is a bare `str`. Please specify a 'token_type'.
"""
                raise UnknownTokenType(msg, token, token_type)
            if isinstance(token, BearerToken):
                return "urn:ietf:params:oauth:token-type:access_token"
            if isinstance(token, IdToken):
                return "urn:ietf:params:oauth:token-type:id_token"
            msg = f"Unknown token type {type(token)}"
            raise UnknownTokenType(msg, token, token_type)
        if token_type == TokenType.ACCESS_TOKEN:
            if token is not None and not isinstance(token, (str, BearerToken)):
                msg = f"""\
The supplied token is of type '{type(token)}' which is inconsistent with token_type '{token_type}'.
A BearerToken or an access_token as a `str` is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:access_token"
        if token_type == TokenType.REFRESH_TOKEN:
            if token is not None and isinstance(token, BearerToken) and not token.refresh_token:
                msg = f"""\
The supplied BearerToken does not contain a refresh_token, which is inconsistent with token_type '{token_type}'.
A BearerToken containing a refresh_token is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:refresh_token"
        if token_type == TokenType.ID_TOKEN:
            if token is not None and not isinstance(token, (str, IdToken)):
                msg = f"""\
The supplied token is of type '{type(token)}' which is inconsistent with token_type '{token_type}'.
An IdToken or a string representation of it is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:id_token"

        return {
            "saml1": "urn:ietf:params:oauth:token-type:saml1",
            "saml2": "urn:ietf:params:oauth:token-type:saml2",
            "jwt": "urn:ietf:params:oauth:token-type:jwt",
        }.get(token_type, token_type)

revoke_access_token(access_token, requests_kwargs=None, **revoke_kwargs)

Send a request to the Revocation Endpoint to revoke an access token.

Parameters:

Name Type Description Default
access_token BearerToken | str

the access token to revoke

required
requests_kwargs dict[str, Any] | None

additional parameters for the underlying requests.post() call

None
**revoke_kwargs Any

additional parameters to pass to the revocation endpoint

{}
Source code in requests_oauth2client/client.py
def revoke_access_token(
    self,
    access_token: BearerToken | str,
    requests_kwargs: dict[str, Any] | None = None,
    **revoke_kwargs: Any,
) -> bool:
    """Send a request to the Revocation Endpoint to revoke an access token.

    Args:
        access_token: the access token to revoke
        requests_kwargs: additional parameters for the underlying requests.post() call
        **revoke_kwargs: additional parameters to pass to the revocation endpoint

    """
    return self.revoke_token(
        access_token,
        token_type_hint=TokenType.ACCESS_TOKEN,
        requests_kwargs=requests_kwargs,
        **revoke_kwargs,
    )

revoke_refresh_token(refresh_token, requests_kwargs=None, **revoke_kwargs)

Send a request to the Revocation Endpoint to revoke a refresh token.

Parameters:

Name Type Description Default
refresh_token str | BearerToken

the refresh token to revoke.

required
requests_kwargs dict[str, Any] | None

additional parameters to pass to the revocation endpoint.

None
**revoke_kwargs Any

additional parameters to pass to the revocation endpoint.

{}

Returns:

Type Description
bool

True if the revocation request is successful, False if this client has no configured

bool

revocation endpoint.

Raises:

Type Description
MissingRefreshToken

when refresh_token is a BearerToken but does not contain a refresh_token.

Source code in requests_oauth2client/client.py
def revoke_refresh_token(
    self,
    refresh_token: str | BearerToken,
    requests_kwargs: dict[str, Any] | None = None,
    **revoke_kwargs: Any,
) -> bool:
    """Send a request to the Revocation Endpoint to revoke a refresh token.

    Args:
        refresh_token: the refresh token to revoke.
        requests_kwargs: additional parameters to pass to the revocation endpoint.
        **revoke_kwargs: additional parameters to pass to the revocation endpoint.

    Returns:
        `True` if the revocation request is successful, `False` if this client has no configured
        revocation endpoint.

    Raises:
        MissingRefreshToken: when `refresh_token` is a [BearerToken][requests_oauth2client.tokens.BearerToken]
            but does not contain a `refresh_token`.

    """
    if isinstance(refresh_token, BearerToken):
        if refresh_token.refresh_token is None:
            raise MissingRefreshToken(refresh_token)
        refresh_token = refresh_token.refresh_token

    return self.revoke_token(
        refresh_token,
        token_type_hint=TokenType.REFRESH_TOKEN,
        requests_kwargs=requests_kwargs,
        **revoke_kwargs,
    )

revoke_token(token, token_type_hint=None, requests_kwargs=None, **revoke_kwargs)

Send a Token Revocation request.

By default, authentication will be the same than the one used for the Token Endpoint.

Parameters:

Name Type Description Default
token str | BearerToken

the token to revoke.

required
token_type_hint str | None

a token_type_hint to send to the revocation endpoint.

None
requests_kwargs dict[str, Any] | None

additional parameters to the underling call to requests.post()

None
**revoke_kwargs Any

additional parameters to send to the revocation endpoint.

{}

Returns:

Type Description
bool

the result from parse_revocation_response on the returned AS response.

Raises:

Type Description
MissingEndpointUri

if the Revocation Endpoint URI is not configured.

MissingRefreshToken

if token_type_hint is "refresh_token" and token is a BearerToken but does not contain a refresh_token.

Source code in requests_oauth2client/client.py
def revoke_token(
    self,
    token: str | BearerToken,
    token_type_hint: str | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **revoke_kwargs: Any,
) -> bool:
    """Send a Token Revocation request.

    By default, authentication will be the same than the one used for the Token Endpoint.

    Args:
        token: the token to revoke.
        token_type_hint: a token_type_hint to send to the revocation endpoint.
        requests_kwargs: additional parameters to the underling call to requests.post()
        **revoke_kwargs: additional parameters to send to the revocation endpoint.

    Returns:
        the result from `parse_revocation_response` on the returned AS response.

    Raises:
        MissingEndpointUri: if the Revocation Endpoint URI is not configured.
        MissingRefreshToken: if `token_type_hint` is `"refresh_token"` and `token` is a BearerToken
            but does not contain a `refresh_token`.

    """
    requests_kwargs = requests_kwargs or {}

    if token_type_hint == TokenType.REFRESH_TOKEN and isinstance(token, BearerToken):
        if token.refresh_token is None:
            raise MissingRefreshToken(token)
        token = token.refresh_token

    data = dict(revoke_kwargs, token=str(token))
    if token_type_hint:
        data["token_type_hint"] = token_type_hint

    return self._request(
        Endpoints.REVOCATION,
        data=data,
        auth=self.auth,
        on_success=self.parse_revocation_response,
        on_failure=self.on_revocation_error,
        **requests_kwargs,
    )

parse_revocation_response(response, *, dpop_key=None)

Parse reponses from the Revocation Endpoint.

Since those do not return any meaningful information in a standardised fashion, this just returns True.

Parameters:

Name Type Description Default
response Response

the requests.Response as returned by the Revocation Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
bool

True if the revocation succeeds, False if no revocation endpoint is present or a

bool

non-standardised error is returned.

Source code in requests_oauth2client/client.py
def parse_revocation_response(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> bool:  # noqa: ARG002
    """Parse reponses from the Revocation Endpoint.

    Since those do not return any meaningful information in a standardised fashion, this just returns `True`.

    Args:
        response: the `requests.Response` as returned by the Revocation Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        `True` if the revocation succeeds, `False` if no revocation endpoint is present or a
        non-standardised error is returned.

    """
    return True

on_revocation_error(response, *, dpop_key=None)

Error handler for revoke_token().

Invoked by revoke_token() when the revocation endpoint returns an error.

Parameters:

Name Type Description Default
response Response

the requests.Response as returned by the Revocation Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
bool

False to signal that an error occurred. May raise exceptions instead depending on the

bool

revocation response.

Raises:

Type Description
EndpointError

if the response contains a standardised OAuth 2.0 error.

Source code in requests_oauth2client/client.py
def on_revocation_error(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> bool:  # noqa: ARG002
    """Error handler for `revoke_token()`.

    Invoked by [revoke_token()][requests_oauth2client.client.OAuth2Client.revoke_token] when the
    revocation endpoint returns an error.

    Args:
        response: the `requests.Response` as returned by the Revocation Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        `False` to signal that an error occurred. May raise exceptions instead depending on the
        revocation response.

    Raises:
        EndpointError: if the response contains a standardised OAuth 2.0 error.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, RevocationError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception:  # noqa: BLE001
        return False
    raise exception

introspect_token(token, token_type_hint=None, requests_kwargs=None, **introspect_kwargs)

Send a request to the Introspection Endpoint.

Parameter token can be:

  • a str
  • a BearerToken instance

You may pass any arbitrary token and token_type_hint values as str. Those will be included in the request, as-is. If token is a BearerToken, then token_type_hint must be either:

  • None: the access_token will be instrospected and no token_type_hint will be included in the request
  • access_token: same as None, but the token_type_hint will be included
  • or refresh_token: only available if a Refresh Token is present in the BearerToken.

Parameters:

Name Type Description Default
token str | BearerToken

the token to instrospect

required
token_type_hint str | None

the token_type_hint to include in the request.

None
requests_kwargs dict[str, Any] | None

additional parameters to the underling call to requests.post()

None
**introspect_kwargs Any

additional parameters to send to the introspection endpoint.

{}

Returns:

Type Description
Any

the response as returned by the Introspection Endpoint.

Raises:

Type Description
MissingRefreshToken

if token_type_hint is "refresh_token" and token is a BearerToken but does not contain a refresh_token.

UnknownTokenType

if token_type_hint is neither None, "access_token" or "refresh_token".

Source code in requests_oauth2client/client.py
    def introspect_token(
        self,
        token: str | BearerToken,
        token_type_hint: str | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **introspect_kwargs: Any,
    ) -> Any:
        """Send a request to the Introspection Endpoint.

        Parameter `token` can be:

        - a `str`
        - a `BearerToken` instance

        You may pass any arbitrary `token` and `token_type_hint` values as `str`. Those will
        be included in the request, as-is.
        If `token` is a `BearerToken`, then `token_type_hint` must be either:

        - `None`: the access_token will be instrospected and no token_type_hint will be included
        in the request
        - `access_token`: same as `None`, but the token_type_hint will be included
        - or `refresh_token`: only available if a Refresh Token is present in the BearerToken.

        Args:
            token: the token to instrospect
            token_type_hint: the `token_type_hint` to include in the request.
            requests_kwargs: additional parameters to the underling call to requests.post()
            **introspect_kwargs: additional parameters to send to the introspection endpoint.

        Returns:
            the response as returned by the Introspection Endpoint.

        Raises:
            MissingRefreshToken: if `token_type_hint` is `"refresh_token"` and `token` is a BearerToken
                but does not contain a `refresh_token`.
            UnknownTokenType: if `token_type_hint` is neither `None`, `"access_token"` or `"refresh_token"`.

        """
        requests_kwargs = requests_kwargs or {}

        if isinstance(token, BearerToken):
            if token_type_hint is None or token_type_hint == TokenType.ACCESS_TOKEN:
                token = token.access_token
            elif token_type_hint == TokenType.REFRESH_TOKEN:
                if token.refresh_token is None:
                    raise MissingRefreshToken(token)

                token = token.refresh_token
            else:
                msg = """\
Invalid `token_type_hint`. To test arbitrary `token_type_hint` values, you must provide `token` as a `str`."""
                raise UnknownTokenType(msg, token, token_type_hint)

        data = dict(introspect_kwargs, token=str(token))
        if token_type_hint:
            data["token_type_hint"] = token_type_hint

        return self._request(
            Endpoints.INTROSPECTION,
            data=data,
            auth=self.auth,
            on_success=self.parse_introspection_response,
            on_failure=self.on_introspection_error,
            **requests_kwargs,
        )

parse_introspection_response(response, *, dpop_key=None)

Parse Token Introspection Responses received by introspect_token().

Invoked by introspect_token() to parse the returned response. This decodes the JSON content if possible, otherwise it returns the response as a string.

Parameters:

Name Type Description Default
response Response

the Response as returned by the Introspection Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
Any

the decoded JSON content, or a str with the content.

Source code in requests_oauth2client/client.py
def parse_introspection_response(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> Any:
    """Parse Token Introspection Responses received by `introspect_token()`.

    Invoked by [introspect_token()][requests_oauth2client.client.OAuth2Client.introspect_token]
    to parse the returned response. This decodes the JSON content if possible, otherwise it
    returns the response as a string.

    Args:
        response: the [Response][requests.Response] as returned by the Introspection Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        the decoded JSON content, or a `str` with the content.

    """
    try:
        return response.json()
    except ValueError:
        return response.text

on_introspection_error(response, *, dpop_key=None)

Error handler for introspect_token().

Invoked by introspect_token() to parse the returned response in the case an error is returned.

Parameters:

Name Type Description Default
response Response

the response as returned by the Introspection Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
Any

usually raises exceptions. A subclass can return a default response instead.

Raises:

Type Description
EndpointError

(or one of its subclasses) if the response contains a standard OAuth 2.0 error.

UnknownIntrospectionError

if the response is not a standard error response.

Source code in requests_oauth2client/client.py
def on_introspection_error(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
    """Error handler for `introspect_token()`.

    Invoked by [introspect_token()][requests_oauth2client.client.OAuth2Client.introspect_token]
    to parse the returned response in the case an error is returned.

    Args:
        response: the response as returned by the Introspection Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        usually raises exceptions. A subclass can return a default response instead.

    Raises:
        EndpointError: (or one of its subclasses) if the response contains a standard OAuth 2.0 error.
        UnknownIntrospectionError: if the response is not a standard error response.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, IntrospectionError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise UnknownIntrospectionError(response=response, client=self) from exc
    raise exception

backchannel_authentication_request(scope='openid', *, client_notification_token=None, acr_values=None, login_hint_token=None, id_token_hint=None, login_hint=None, binding_message=None, user_code=None, requested_expiry=None, private_jwk=None, alg=None, requests_kwargs=None, **ciba_kwargs)

Send a CIBA Authentication Request.

Parameters:

Name Type Description Default
scope None | str | Iterable[str]

the scope to include in the request.

'openid'
client_notification_token str | None

the Client Notification Token to include in the request.

None
acr_values None | str | Iterable[str]

the acr values to include in the request.

None
login_hint_token str | None

the Login Hint Token to include in the request.

None
id_token_hint str | None

the ID Token Hint to include in the request.

None
login_hint str | None

the Login Hint to include in the request.

None
binding_message str | None

the Binding Message to include in the request.

None
user_code str | None

the User Code to include in the request

None
requested_expiry int | None

the Requested Expiry, in seconds, to include in the request.

None
private_jwk Jwk | dict[str, Any] | None

the JWK to use to sign the request (optional)

None
alg str | None

the alg to use to sign the request, if the provided JWK does not include an "alg" parameter.

None
requests_kwargs dict[str, Any] | None

additional parameters for

None
**ciba_kwargs Any

additional parameters to include in the request.

{}

Returns:

Type Description
BackChannelAuthenticationResponse

a BackChannelAuthenticationResponse as returned by AS

Raises:

Type Description
InvalidBackchannelAuthenticationRequestHintParam

if none of login_hint, login_hint_token or id_token_hint is provided, or more than one of them is provided.

InvalidScopeParam

if the scope parameter is invalid.

InvalidAcrValuesParam

if the acr_values parameter is invalid.

Source code in requests_oauth2client/client.py
def backchannel_authentication_request(  # noqa: PLR0913
    self,
    scope: None | str | Iterable[str] = "openid",
    *,
    client_notification_token: str | None = None,
    acr_values: None | str | Iterable[str] = None,
    login_hint_token: str | None = None,
    id_token_hint: str | None = None,
    login_hint: str | None = None,
    binding_message: str | None = None,
    user_code: str | None = None,
    requested_expiry: int | None = None,
    private_jwk: Jwk | dict[str, Any] | None = None,
    alg: str | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **ciba_kwargs: Any,
) -> BackChannelAuthenticationResponse:
    """Send a CIBA Authentication Request.

    Args:
         scope: the scope to include in the request.
         client_notification_token: the Client Notification Token to include in the request.
         acr_values: the acr values to include in the request.
         login_hint_token: the Login Hint Token to include in the request.
         id_token_hint: the ID Token Hint to include in the request.
         login_hint: the Login Hint to include in the request.
         binding_message: the Binding Message to include in the request.
         user_code: the User Code to include in the request
         requested_expiry: the Requested Expiry, in seconds, to include in the request.
         private_jwk: the JWK to use to sign the request (optional)
         alg: the alg to use to sign the request, if the provided JWK does not include an "alg" parameter.
         requests_kwargs: additional parameters for
         **ciba_kwargs: additional parameters to include in the request.

    Returns:
        a BackChannelAuthenticationResponse as returned by AS

    Raises:
        InvalidBackchannelAuthenticationRequestHintParam: if none of `login_hint`, `login_hint_token`
            or `id_token_hint` is provided, or more than one of them is provided.
        InvalidScopeParam: if the `scope` parameter is invalid.
        InvalidAcrValuesParam: if the `acr_values` parameter is invalid.

    """
    if not (login_hint or login_hint_token or id_token_hint):
        msg = "One of `login_hint`, `login_hint_token` or `ìd_token_hint` must be provided"
        raise InvalidBackchannelAuthenticationRequestHintParam(msg)

    if (login_hint_token and id_token_hint) or (login_hint and id_token_hint) or (login_hint_token and login_hint):
        msg = "Only one of `login_hint`, `login_hint_token` or `ìd_token_hint` must be provided"
        raise InvalidBackchannelAuthenticationRequestHintParam(msg)

    requests_kwargs = requests_kwargs or {}

    if scope is not None and not isinstance(scope, str):
        try:
            scope = " ".join(scope)
        except Exception as exc:
            raise InvalidScopeParam(scope) from exc

    if acr_values is not None and not isinstance(acr_values, str):
        try:
            acr_values = " ".join(acr_values)
        except Exception as exc:
            raise InvalidAcrValuesParam(acr_values) from exc

    data = dict(
        ciba_kwargs,
        scope=scope,
        client_notification_token=client_notification_token,
        acr_values=acr_values,
        login_hint_token=login_hint_token,
        id_token_hint=id_token_hint,
        login_hint=login_hint,
        binding_message=binding_message,
        user_code=user_code,
        requested_expiry=requested_expiry,
    )

    if private_jwk is not None:
        data = {"request": str(Jwt.sign(data, key=private_jwk, alg=alg))}

    return self._request(
        Endpoints.BACKCHANNEL_AUTHENTICATION,
        data=data,
        auth=self.auth,
        on_success=self.parse_backchannel_authentication_response,
        on_failure=self.on_backchannel_authentication_error,
        **requests_kwargs,
    )

parse_backchannel_authentication_response(response, *, dpop_key=None)

Parse a response received by backchannel_authentication_request().

Invoked by backchannel_authentication_request() to parse the response returned by the BackChannel Authentication Endpoint.

Parameters:

Name Type Description Default
response Response

the response returned by the BackChannel Authentication Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
BackChannelAuthenticationResponse

a BackChannelAuthenticationResponse

Raises:

Type Description
InvalidBackChannelAuthenticationResponse

if the response does not contain a standard BackChannel Authentication response.

Source code in requests_oauth2client/client.py
def parse_backchannel_authentication_response(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> BackChannelAuthenticationResponse:
    """Parse a response received by `backchannel_authentication_request()`.

    Invoked by
    [backchannel_authentication_request()][requests_oauth2client.client.OAuth2Client.backchannel_authentication_request]
    to parse the response returned by the BackChannel Authentication Endpoint.

    Args:
        response: the response returned by the BackChannel Authentication Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        a `BackChannelAuthenticationResponse`

    Raises:
        InvalidBackChannelAuthenticationResponse: if the response does not contain a standard
            BackChannel Authentication response.

    """
    try:
        return BackChannelAuthenticationResponse(**response.json())
    except TypeError as exc:
        raise InvalidBackChannelAuthenticationResponse(response=response, client=self) from exc

on_backchannel_authentication_error(response, *, dpop_key=None)

Error handler for backchannel_authentication_request().

Invoked by backchannel_authentication_request() to parse the response returned by the BackChannel Authentication Endpoint, when it is an error.

Parameters:

Name Type Description Default
response Response

the response returned by the BackChannel Authentication Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
BackChannelAuthenticationResponse

usually raises an exception. But a subclass can return a default response instead.

Raises:

Type Description
EndpointError

(or one of its subclasses) if the response contains a standard OAuth 2.0 error.

InvalidBackChannelAuthenticationResponse

for non-standard error responses.

Source code in requests_oauth2client/client.py
def on_backchannel_authentication_error(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> BackChannelAuthenticationResponse:
    """Error handler for `backchannel_authentication_request()`.

    Invoked by
    [backchannel_authentication_request()][requests_oauth2client.client.OAuth2Client.backchannel_authentication_request]
    to parse the response returned by the BackChannel Authentication Endpoint, when it is an
    error.

    Args:
        response: the response returned by the BackChannel Authentication Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        usually raises an exception. But a subclass can return a default response instead.

    Raises:
        EndpointError: (or one of its subclasses) if the response contains a standard OAuth 2.0 error.
        InvalidBackChannelAuthenticationResponse: for non-standard error responses.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, BackChannelAuthenticationError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise InvalidBackChannelAuthenticationResponse(response=response, client=self) from exc
    raise exception

authorize_device(requests_kwargs=None, **data)

Send a Device Authorization Request.

Parameters:

Name Type Description Default
**data Any

additional data to send to the Device Authorization Endpoint

{}
requests_kwargs dict[str, Any] | None

additional parameters for requests.request()

None

Returns:

Type Description
DeviceAuthorizationResponse

a Device Authorization Response

Raises:

Type Description
MissingEndpointUri

if the Device Authorization URI is not configured

Source code in requests_oauth2client/client.py
def authorize_device(
    self,
    requests_kwargs: dict[str, Any] | None = None,
    **data: Any,
) -> DeviceAuthorizationResponse:
    """Send a Device Authorization Request.

    Args:
        **data: additional data to send to the Device Authorization Endpoint
        requests_kwargs: additional parameters for `requests.request()`

    Returns:
        a Device Authorization Response

    Raises:
        MissingEndpointUri: if the Device Authorization URI is not configured

    """
    requests_kwargs = requests_kwargs or {}

    return self._request(
        Endpoints.DEVICE_AUTHORIZATION,
        data=data,
        auth=self.auth,
        on_success=self.parse_device_authorization_response,
        on_failure=self.on_device_authorization_error,
        **requests_kwargs,
    )

parse_device_authorization_response(response, *, dpop_key=None)

Parse a Device Authorization Response received by authorize_device().

Invoked by authorize_device() to parse the response returned by the Device Authorization Endpoint.

Parameters:

Name Type Description Default
response Response

the response returned by the Device Authorization Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
DeviceAuthorizationResponse

a DeviceAuthorizationResponse as returned by AS

Source code in requests_oauth2client/client.py
def parse_device_authorization_response(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> DeviceAuthorizationResponse:
    """Parse a Device Authorization Response received by `authorize_device()`.

    Invoked by [authorize_device()][requests_oauth2client.client.OAuth2Client.authorize_device]
    to parse the response returned by the Device Authorization Endpoint.

    Args:
        response: the response returned by the Device Authorization Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        a `DeviceAuthorizationResponse` as returned by AS

    """
    return DeviceAuthorizationResponse(**response.json())

on_device_authorization_error(response, *, dpop_key=None)

Error handler for authorize_device().

Invoked by authorize_device() to parse the response returned by the Device Authorization Endpoint, when that response is an error.

Parameters:

Name Type Description Default
response Response

the response returned by the Device Authorization Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
DeviceAuthorizationResponse

usually raises an Exception. But a subclass may return a default response instead.

Raises:

Type Description
EndpointError

for standard OAuth 2.0 errors

InvalidDeviceAuthorizationResponse

for non-standard error responses.

Source code in requests_oauth2client/client.py
def on_device_authorization_error(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> DeviceAuthorizationResponse:
    """Error handler for `authorize_device()`.

    Invoked by [authorize_device()][requests_oauth2client.client.OAuth2Client.authorize_device]
    to parse the response returned by the Device Authorization Endpoint, when that response is
    an error.

    Args:
        response: the response returned by the Device Authorization Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        usually raises an Exception. But a subclass may return a default response instead.

    Raises:
        EndpointError: for standard OAuth 2.0 errors
        InvalidDeviceAuthorizationResponse: for non-standard error responses.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, DeviceAuthorizationError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise InvalidDeviceAuthorizationResponse(response=response, client=self) from exc
    raise exception

update_authorization_server_public_keys(requests_kwargs=None)

Update the cached AS public keys by retrieving them from its jwks_uri.

Public keys are returned by this method, as a jwskate.JwkSet. They are also available in attribute authorization_server_jwks.

Returns:

Type Description
JwkSet

the retrieved public keys

Raises:

Type Description
ValueError

if no jwks_uri is configured

Source code in requests_oauth2client/client.py
def update_authorization_server_public_keys(self, requests_kwargs: dict[str, Any] | None = None) -> JwkSet:
    """Update the cached AS public keys by retrieving them from its `jwks_uri`.

    Public keys are returned by this method, as a `jwskate.JwkSet`. They are also
    available in attribute `authorization_server_jwks`.

    Returns:
        the retrieved public keys

    Raises:
        ValueError: if no `jwks_uri` is configured

    """
    requests_kwargs = requests_kwargs or {}
    requests_kwargs.setdefault("auth", None)

    jwks_uri = self._require_endpoint(Endpoints.JWKS)
    resp = self.session.get(jwks_uri, **requests_kwargs)
    resp.raise_for_status()
    jwks = resp.json()
    self.authorization_server_jwks.update(jwks)
    return self.authorization_server_jwks

from_discovery_endpoint(url=None, issuer=None, *, auth=None, client_id=None, client_secret=None, private_key=None, session=None, testing=False, **kwargs) classmethod

Initialize an OAuth2Client using an AS Discovery Document endpoint.

If an url is provided, an HTTPS request will be done to that URL to obtain the Authorization Server Metadata.

If an issuer is provided, the OpenID Connect Discovery document url will be automatically derived from it, as specified in OpenID Connect Discovery.

Once the standardized metadata document is obtained, this will extract all Endpoint Uris from that document, will fetch the current public keys from its jwks_uri, then will initialize an OAuth2Client based on those endpoints.

Parameters:

Name Type Description Default
url str | None

The url where the server metadata will be retrieved.

None
issuer str | None

The issuer value that is expected in the discovery document. If not url is given, the OpenID Connect Discovery url for this issuer will be retrieved.

None
auth AuthBase | tuple[str, str] | str | None

The authentication handler to use for client authentication.

None
client_id str | None

Client ID.

None
client_secret str | None

Client secret to use to authenticate the client.

None
private_key Jwk | dict[str, Any] | None

Private key to sign client assertions.

None
session Session | None

A requests.Session to use to retrieve the document and initialise the client with.

None
testing bool

If True, do not try to validate the issuer uri nor the endpoint urls that are part of the document.

False
**kwargs Any

Additional keyword parameters to pass to OAuth2Client.

{}

Returns:

Type Description
OAuth2Client

An OAuth2Client with endpoints initialized based on the obtained metadata.

Raises:

Type Description
InvalidIssuer

If issuer is not using https, or contains credentials or fragment.

InvalidParam

If neither url nor issuer are suitable urls.

HTTPError

If an error happens while fetching the documents.

Example
1
2
3
4
5
6
7
from requests_oauth2client import OAuth2Client

client = OAuth2Client.from_discovery_endpoint(
    issuer="https://myserver.net",
    client_id="my_client_id,
    client_secret="my_client_secret",
)
Source code in requests_oauth2client/client.py
@classmethod
def from_discovery_endpoint(
    cls,
    url: str | None = None,
    issuer: str | None = None,
    *,
    auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_key: Jwk | dict[str, Any] | None = None,
    session: requests.Session | None = None,
    testing: bool = False,
    **kwargs: Any,
) -> OAuth2Client:
    """Initialize an `OAuth2Client` using an AS Discovery Document endpoint.

    If an `url` is provided, an HTTPS request will be done to that URL to obtain the Authorization Server Metadata.

    If an `issuer` is provided, the OpenID Connect Discovery document url will be automatically
    derived from it, as specified in [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest).

    Once the standardized metadata document is obtained, this will extract
    all Endpoint Uris from that document, will fetch the current public keys from its
    `jwks_uri`, then will initialize an OAuth2Client based on those endpoints.

    Args:
      url: The url where the server metadata will be retrieved.
      issuer: The issuer value that is expected in the discovery document.
        If not `url` is given, the OpenID Connect Discovery url for this issuer will be retrieved.
      auth: The authentication handler to use for client authentication.
      client_id: Client ID.
      client_secret: Client secret to use to authenticate the client.
      private_key: Private key to sign client assertions.
      session: A `requests.Session` to use to retrieve the document and initialise the client with.
      testing: If `True`, do not try to validate the issuer uri nor the endpoint urls
        that are part of the document.
      **kwargs: Additional keyword parameters to pass to `OAuth2Client`.

    Returns:
      An `OAuth2Client` with endpoints initialized based on the obtained metadata.

    Raises:
      InvalidIssuer: If `issuer` is not using https, or contains credentials or fragment.
      InvalidParam: If neither `url` nor `issuer` are suitable urls.
      requests.HTTPError: If an error happens while fetching the documents.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client

        client = OAuth2Client.from_discovery_endpoint(
            issuer="https://myserver.net",
            client_id="my_client_id,
            client_secret="my_client_secret",
        )
        ```

    """
    if issuer is not None and not testing:
        try:
            validate_issuer_uri(issuer)
        except InvalidUri as exc:
            raise InvalidIssuer("issuer", issuer, exc) from exc  # noqa: EM101
    if url is None and issuer is not None:
        url = oidc_discovery_document_url(issuer)
    if url is None:
        msg = "Please specify at least one of `issuer` or `url`"
        raise InvalidParam(msg)

    if not testing:
        validate_endpoint_uri(url, path=False)

    session = session or requests.Session()
    discovery = session.get(url).json()

    jwks_uri = discovery.get("jwks_uri")
    jwks = JwkSet(session.get(jwks_uri).json()) if jwks_uri else None

    return cls.from_discovery_document(
        discovery,
        issuer=issuer,
        auth=auth,
        session=session,
        client_id=client_id,
        client_secret=client_secret,
        private_key=private_key,
        authorization_server_jwks=jwks,
        testing=testing,
        **kwargs,
    )

from_discovery_document(discovery, issuer=None, *, auth=None, client_id=None, client_secret=None, private_key=None, authorization_server_jwks=None, https=True, testing=False, **kwargs) classmethod

Initialize an OAuth2Client, based on an AS Discovery Document.

Parameters:

Name Type Description Default
discovery dict[str, Any]

A dict of server metadata, in the same format as retrieved from a discovery endpoint.

required
issuer str | None

If an issuer is given, check that it matches the one mentioned in the document.

None
auth AuthBase | tuple[str, str] | str | None

The authentication handler to use for client authentication.

None
client_id str | None

Client ID.

None
client_secret str | None

Client secret to use to authenticate the client.

None
private_key Jwk | dict[str, Any] | None

Private key to sign client assertions.

None
authorization_server_jwks JwkSet | dict[str, Any] | None

The current authorization server JWKS keys.

None
https bool

(deprecated) If True, validates that urls in the discovery document use the https scheme.

True
testing bool

If True, don't try to validate the endpoint urls that are part of the document.

False
**kwargs Any

Additional args that will be passed to OAuth2Client.

{}

Returns:

Type Description
OAuth2Client

An OAuth2Client initialized with the endpoints from the discovery document.

Raises:

Type Description
InvalidDiscoveryDocument

If the document does not contain at least a "token_endpoint".

Examples:

from requests_oauth2client import OAuth2Client

client = OAuth2Client.from_discovery_document(
    {
        "issuer": "https://myas.local",
        "token_endpoint": "https://myas.local/token",
    },
    client_id="client_id",
    client_secret="client_secret",
)
Source code in requests_oauth2client/client.py
    @classmethod
    def from_discovery_document(
        cls,
        discovery: dict[str, Any],
        issuer: str | None = None,
        *,
        auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_key: Jwk | dict[str, Any] | None = None,
        authorization_server_jwks: JwkSet | dict[str, Any] | None = None,
        https: bool = True,
        testing: bool = False,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialize an `OAuth2Client`, based on an AS Discovery Document.

        Args:
          discovery: A `dict` of server metadata, in the same format as retrieved from a discovery endpoint.
          issuer: If an issuer is given, check that it matches the one mentioned in the document.
          auth: The authentication handler to use for client authentication.
          client_id: Client ID.
          client_secret: Client secret to use to authenticate the client.
          private_key: Private key to sign client assertions.
          authorization_server_jwks: The current authorization server JWKS keys.
          https: (deprecated) If `True`, validates that urls in the discovery document use the https scheme.
          testing: If `True`, don't try to validate the endpoint urls that are part of the document.
          **kwargs: Additional args that will be passed to `OAuth2Client`.

        Returns:
            An `OAuth2Client` initialized with the endpoints from the discovery document.

        Raises:
            InvalidDiscoveryDocument: If the document does not contain at least a `"token_endpoint"`.

        Examples:
            ```python
            from requests_oauth2client import OAuth2Client

            client = OAuth2Client.from_discovery_document(
                {
                    "issuer": "https://myas.local",
                    "token_endpoint": "https://myas.local/token",
                },
                client_id="client_id",
                client_secret="client_secret",
            )
            ```

        """
        if not https:
            warnings.warn(
                """\
The `https` parameter is deprecated.
To disable endpoint uri validation, set `testing=True` when initializing your `OAuth2Client`.""",
                stacklevel=1,
            )
            testing = True
        if issuer and discovery.get("issuer") != issuer:
            msg = f"""\
Mismatching `issuer` value in discovery document (received '{discovery.get("issuer")}', expected '{issuer}')."""
            raise InvalidParam(
                msg,
                issuer,
                discovery.get("issuer"),
            )
        if issuer is None:
            issuer = discovery.get("issuer")

        token_endpoint = discovery.get(Endpoints.TOKEN)
        if token_endpoint is None:
            msg = "token_endpoint not found in that discovery document"
            raise InvalidDiscoveryDocument(msg, discovery)
        authorization_endpoint = discovery.get(Endpoints.AUTHORIZATION)
        revocation_endpoint = discovery.get(Endpoints.REVOCATION)
        introspection_endpoint = discovery.get(Endpoints.INTROSPECTION)
        userinfo_endpoint = discovery.get(Endpoints.USER_INFO)
        pushed_authorization_request_endpoint = discovery.get(Endpoints.PUSHED_AUTHORIZATION_REQUEST)
        jwks_uri = discovery.get(Endpoints.JWKS)
        if jwks_uri is not None and not testing:
            validate_endpoint_uri(jwks_uri)
        authorization_response_iss_parameter_supported = discovery.get(
            "authorization_response_iss_parameter_supported",
            False,
        )

        return cls(
            token_endpoint=token_endpoint,
            authorization_endpoint=authorization_endpoint,
            revocation_endpoint=revocation_endpoint,
            introspection_endpoint=introspection_endpoint,
            userinfo_endpoint=userinfo_endpoint,
            pushed_authorization_request_endpoint=pushed_authorization_request_endpoint,
            jwks_uri=jwks_uri,
            authorization_server_jwks=authorization_server_jwks,
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            issuer=issuer,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            testing=testing,
            **kwargs,
        )

UnknownActorTokenType

Bases: UnknownTokenType

Raised when the type of actor_token cannot be determined automatically.

Source code in requests_oauth2client/client.py
class UnknownActorTokenType(UnknownTokenType):
    """Raised when the type of actor_token cannot be determined automatically."""

    def __init__(self, actor_token: object, actor_token_type: str | None) -> None:
        super().__init__("actor_token", token=actor_token, token_type=actor_token_type)

UnknownSubjectTokenType

Bases: UnknownTokenType

Raised when the type of subject_token cannot be determined automatically.

Source code in requests_oauth2client/client.py
class UnknownSubjectTokenType(UnknownTokenType):
    """Raised when the type of subject_token cannot be determined automatically."""

    def __init__(self, subject_token: object, subject_token_type: str | None) -> None:
        super().__init__("subject_token", subject_token, subject_token_type)

UnknownTokenType

Bases: InvalidParam, TypeError

Raised when the type of a token cannot be determined automatically.

Source code in requests_oauth2client/client.py
class UnknownTokenType(InvalidParam, TypeError):
    """Raised when the type of a token cannot be determined automatically."""

    def __init__(self, message: str, token: object, token_type: str | None) -> None:
        super().__init__(f"Unable to determine the type of token provided: {message}")
        self.token = token
        self.token_type = token_type

BaseClientAssertionAuthenticationMethod

Bases: BaseClientAuthenticationMethod

Base class for assertion-based client authentication methods.

Source code in requests_oauth2client/client_authentication.py
@frozen
class BaseClientAssertionAuthenticationMethod(BaseClientAuthenticationMethod):
    """Base class for assertion-based client authentication methods."""

    lifetime: int
    jti_gen: Callable[[], str]
    aud: str | None
    alg: str | None

    def client_assertion(self, audience: str) -> str:
        """Generate a Client Assertion for a specific audience.

        Args:
            audience: the audience to use for the `aud` claim of the generated Client Assertion.

        Returns:
            a Client Assertion, as `str`.

        """
        raise NotImplementedError

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add a `client_assertion` field in the request body.

        Args:
            request: a [requests.PreparedRequest][].

        Returns:
            a [requests.PreparedRequest][] with the added `client_assertion` field.

        """
        request = super().__call__(request)
        audience = self.aud or request.url
        if audience is None:
            raise InvalidRequestForClientAuthentication(request)  # pragma: no cover
        params = (
            parse_qs(request.body, strict_parsing=True, keep_blank_values=True)  # type: ignore[type-var]
            if request.body
            else {}
        )
        client_assertion = self.client_assertion(audience)
        params[b"client_id"] = [self.client_id.encode()]
        params[b"client_assertion"] = [client_assertion.encode()]
        params[b"client_assertion_type"] = [b"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]
        request.prepare_body(params, files=None)
        return request

client_assertion(audience)

Generate a Client Assertion for a specific audience.

Parameters:

Name Type Description Default
audience str

the audience to use for the aud claim of the generated Client Assertion.

required

Returns:

Type Description
str

a Client Assertion, as str.

Source code in requests_oauth2client/client_authentication.py
def client_assertion(self, audience: str) -> str:
    """Generate a Client Assertion for a specific audience.

    Args:
        audience: the audience to use for the `aud` claim of the generated Client Assertion.

    Returns:
        a Client Assertion, as `str`.

    """
    raise NotImplementedError

BaseClientAuthenticationMethod

Bases: AuthBase

Base class for all Client Authentication methods. This extends requests.auth.AuthBase.

This base class checks that requests are suitable to add Client Authentication parameters to, and does not modify the request.

Source code in requests_oauth2client/client_authentication.py
@frozen
class BaseClientAuthenticationMethod(requests.auth.AuthBase):
    """Base class for all Client Authentication methods. This extends [requests.auth.AuthBase][].

    This base class checks that requests are suitable to add Client Authentication parameters to, and does not modify
    the request.

    """

    client_id: str

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Check that the request is suitable for Client Authentication.

        It checks:

        * that the method is `POST`
        * that the Content-Type is "application/x-www-form-urlencoded" or None

        Args:
            request: a [requests.PreparedRequest][]

        Returns:
            a [requests.PreparedRequest][], unmodified

        Raises:
            RuntimeError: if the request is not suitable for OAuth 2.0 Client Authentication

        """
        if request.method != "POST" or request.headers.get("Content-Type") not in (
            "application/x-www-form-urlencoded",
            None,
        ):
            raise InvalidRequestForClientAuthentication(request)
        return request

ClientSecretBasic

Bases: BaseClientAuthenticationMethod

Implement client_secret_basic authentication.

With this method, the client sends its Client ID and Secret, in the HTTP Authorization header, with the Basic scheme, in each authenticated request to the Authorization Server.

Parameters:

Name Type Description Default
client_id str

Client ID

required
client_secret str

Client Secret

required
Example
1
2
3
4
from requests_oauth2client import ClientSecretBasic, OAuth2Client

auth = ClientSecretBasic("my_client_id", "my_client_secret")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen(init=False)
class ClientSecretBasic(BaseClientAuthenticationMethod):
    """Implement `client_secret_basic` authentication.

    With this method, the client sends its Client ID and Secret, in the HTTP `Authorization` header, with
    the `Basic` scheme, in each authenticated request to the Authorization Server.

    Args:
        client_id: Client ID
        client_secret: Client Secret

    Example:
        ```python
        from requests_oauth2client import ClientSecretBasic, OAuth2Client

        auth = ClientSecretBasic("my_client_id", "my_client_secret")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    client_secret: str

    def __init__(self, client_id: str, client_secret: str) -> None:
        self.__attrs_init__(
            client_id=client_id,
            client_secret=client_secret,
        )

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the appropriate `Authorization` header in each request.

        The Authorization header is formatted as such:
        `Authorization: Basic BASE64('<client_id:client_secret>')`

        Args:
            request: the request

        Returns:
            a [requests.PreparedRequest][] with the added Authorization header.

        """
        request = super().__call__(request)
        b64encoded_credentials = BinaPy(f"{self.client_id}:{self.client_secret}").to("b64").ascii()
        request.headers["Authorization"] = f"Basic {b64encoded_credentials}"
        return request

ClientSecretJwt

Bases: BaseClientAssertionAuthenticationMethod

Implement client_secret_jwt client authentication method.

With this method, the client generates a client assertion, then symmetrically signs it with its Client Secret. The assertion is then sent to the AS in a client_assertion field with each authenticated request.

Parameters:

Name Type Description Default
client_id str

the client_id to use.

required
client_secret str

the client_secret to use to sign generated Client Assertions.

required
alg str

the alg to use to sign generated Client Assertions.

HS256
lifetime int

the lifetime to use for generated Client Assertions.

60
jti_gen Callable[[], str]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

lambda: str(uuid4())
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.

None
Example
1
2
3
4
from requests_oauth2client import OAuth2Client, ClientSecretJwt

auth = ClientSecretJwt("my_client_id", "my_client_secret")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen(init=False)
class ClientSecretJwt(BaseClientAssertionAuthenticationMethod):
    """Implement `client_secret_jwt` client authentication method.

    With this method, the client generates a client assertion, then symmetrically signs it with its Client Secret.
    The assertion is then sent to the AS in a `client_assertion` field with each authenticated request.

    Args:
        client_id: the `client_id` to use.
        client_secret: the `client_secret` to use to sign generated Client Assertions.
        alg: the alg to use to sign generated Client Assertions.
        lifetime: the lifetime to use for generated Client Assertions.
        jti_gen: a function to generate JWT Token Ids (`jti`) for generated Client Assertions.
        aud: the audience value to use. If `None` (default), the endpoint URL will be used.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client, ClientSecretJwt

        auth = ClientSecretJwt("my_client_id", "my_client_secret")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    client_secret: str

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        lifetime: int = 60,
        alg: str = SignatureAlgs.HS256,
        jti_gen: Callable[[], str] = lambda: str(uuid4()),
        aud: str | None = None,
    ) -> None:
        self.__attrs_init__(
            client_id=client_id,
            client_secret=client_secret,
            lifetime=lifetime,
            alg=alg,
            jti_gen=jti_gen,
            aud=aud,
        )

    def client_assertion(self, audience: str) -> str:
        """Generate a symmetrically signed Client Assertion.

        Assertion is signed with the `client_secret` as key and the `alg` passed at init time.

        Args:
            audience: the audience to use for the generated Client Assertion.

        Returns:
            a Client Assertion, as `str`.

        """
        iat = int(datetime.now(tz=timezone.utc).timestamp())
        exp = iat + self.lifetime
        jti = str(self.jti_gen())

        jwk = SymmetricJwk.from_bytes(self.client_secret.encode())

        jwt = Jwt.sign(
            claims={
                "iss": self.client_id,
                "sub": self.client_id,
                "aud": audience,
                "iat": iat,
                "exp": exp,
                "jti": jti,
            },
            key=jwk,
            alg=self.alg,
        )
        return str(jwt)

client_assertion(audience)

Generate a symmetrically signed Client Assertion.

Assertion is signed with the client_secret as key and the alg passed at init time.

Parameters:

Name Type Description Default
audience str

the audience to use for the generated Client Assertion.

required

Returns:

Type Description
str

a Client Assertion, as str.

Source code in requests_oauth2client/client_authentication.py
def client_assertion(self, audience: str) -> str:
    """Generate a symmetrically signed Client Assertion.

    Assertion is signed with the `client_secret` as key and the `alg` passed at init time.

    Args:
        audience: the audience to use for the generated Client Assertion.

    Returns:
        a Client Assertion, as `str`.

    """
    iat = int(datetime.now(tz=timezone.utc).timestamp())
    exp = iat + self.lifetime
    jti = str(self.jti_gen())

    jwk = SymmetricJwk.from_bytes(self.client_secret.encode())

    jwt = Jwt.sign(
        claims={
            "iss": self.client_id,
            "sub": self.client_id,
            "aud": audience,
            "iat": iat,
            "exp": exp,
            "jti": jti,
        },
        key=jwk,
        alg=self.alg,
    )
    return str(jwt)

ClientSecretPost

Bases: BaseClientAuthenticationMethod

Implement client_secret_post client authentication method.

With this method, the client inserts its client_id and client_secret in each authenticated request to the AS.

Parameters:

Name Type Description Default
client_id str

Client ID

required
client_secret str

Client Secret

required
Example
1
2
3
4
from requests_oauth2client import ClientSecretPost, OAuth2Client

auth = ClientSecretPost("my_client_id", "my_client_secret")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen(init=False)
class ClientSecretPost(BaseClientAuthenticationMethod):
    """Implement `client_secret_post` client authentication method.

    With this method, the client inserts its client_id and client_secret in each authenticated
    request to the AS.

    Args:
        client_id: Client ID
        client_secret: Client Secret

    Example:
        ```python
        from requests_oauth2client import ClientSecretPost, OAuth2Client

        auth = ClientSecretPost("my_client_id", "my_client_secret")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    client_secret: str

    def __init__(self, client_id: str, client_secret: str) -> None:
        self.__attrs_init__(
            client_id=client_id,
            client_secret=client_secret,
        )

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the `client_id` and `client_secret` parameters in the request body.

        Args:
            request: a [requests.PreparedRequest][].

        Returns:
            a [requests.PreparedRequest][] with the added client credentials fields.

        """
        request = super().__call__(request)
        params = (
            parse_qs(request.body, strict_parsing=True, keep_blank_values=True)  # type: ignore[type-var]
            if isinstance(request.body, (str, bytes))
            else {}
        )
        params[b"client_id"] = [self.client_id.encode()]
        params[b"client_secret"] = [self.client_secret.encode()]
        request.prepare_body(params, files=None)
        return request

InvalidClientAssertionSigningKeyOrAlg

Bases: ValueError

Raised when the client assertion signing alg is not specified or invalid.

Source code in requests_oauth2client/client_authentication.py
class InvalidClientAssertionSigningKeyOrAlg(ValueError):
    """Raised when the client assertion signing alg is not specified or invalid."""

    def __init__(self, alg: str | None) -> None:
        super().__init__("""\
An asymmetric private signing key, and an alg that is supported by the signing key is required.
It can be provided either:
- as part of the private `Jwk`, in the parameter 'alg'
- or passed as parameter `alg` when initializing a `PrivateKeyJwt`.
Examples of valid `alg` values and matching key type:
- 'RS256', 'RS512' (with a key of type RSA)
- 'ES256', 'ES512' (with a key of type EC)
The private key must include a Key ID (in its 'kid' parameter).
""")
        self.alg = alg

InvalidRequestForClientAuthentication

Bases: RuntimeError

Raised when a request is not suitable for OAuth 2.0 client authentication.

Source code in requests_oauth2client/client_authentication.py
class InvalidRequestForClientAuthentication(RuntimeError):
    """Raised when a request is not suitable for OAuth 2.0 client authentication."""

    def __init__(self, request: requests.PreparedRequest) -> None:
        super().__init__("This request is not suitabe for OAuth 2.0 client authentication.")
        self.request = request

PrivateKeyJwt

Bases: BaseClientAssertionAuthenticationMethod

Implement private_key_jwt client authentication method.

With this method, the client generates and sends a client_assertion, that is asymmetrically signed with a private key, on each direct request to the Authorization Server.

The private key must be supplied as a jwskate.Jwk instance, or any key material that can be used to initialize one.

Parameters:

Name Type Description Default
client_id str

the client_id to use.

required
private_jwk Jwk | dict[str, Any] | Any

the private key to use to sign generated Client Assertions.

required
alg str | None

the alg to use to sign generated Client Assertions.

None
lifetime int

the lifetime to use for generated Client Assertions.

60
jti_gen Callable[[], str]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

lambda: str(uuid4())
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.k

None
Example
1
2
3
4
5
6
7
8
9
from jwskate import Jwk
from requests_oauth2client import OAuth2Client, PrivateKeyJwt

# load your private key from wherever it is stored:
with open("my_private_key.pem") as f:
    my_private_key = Jwk.from_pem(f.read(), password="my_private_key_password")

auth = PrivateKeyJwt("my_client_id", my_private_key, alg="RS256")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen(init=False)
class PrivateKeyJwt(BaseClientAssertionAuthenticationMethod):
    """Implement `private_key_jwt` client authentication method.

    With this method, the client generates and sends a client_assertion, that is asymmetrically
    signed with a private key, on each direct request to the Authorization Server.

    The private key must be supplied as a [`jwskate.Jwk`][jwskate.jwk.Jwk] instance,
    or any key material that can be used to initialize one.

    Args:
        client_id: the `client_id` to use.
        private_jwk: the private key to use to sign generated Client Assertions.
        alg: the alg to use to sign generated Client Assertions.
        lifetime: the lifetime to use for generated Client Assertions.
        jti_gen: a function to generate JWT Token Ids (`jti`) for generated Client Assertions.
        aud: the audience value to use. If `None` (default), the endpoint URL will be used.k

    Example:
        ```python
        from jwskate import Jwk
        from requests_oauth2client import OAuth2Client, PrivateKeyJwt

        # load your private key from wherever it is stored:
        with open("my_private_key.pem") as f:
            my_private_key = Jwk.from_pem(f.read(), password="my_private_key_password")

        auth = PrivateKeyJwt("my_client_id", my_private_key, alg="RS256")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    private_jwk: Jwk

    def __init__(
        self,
        client_id: str,
        private_jwk: Jwk | dict[str, Any] | Any,
        *,
        alg: str | None = None,
        lifetime: int = 60,
        jti_gen: Callable[[], str] = lambda: str(uuid4()),
        aud: str | None = None,
    ) -> None:
        private_jwk = to_jwk(private_jwk)

        alg = private_jwk.alg or alg
        if not alg:
            raise InvalidClientAssertionSigningKeyOrAlg(alg)

        if alg not in private_jwk.supported_signing_algorithms():
            raise InvalidClientAssertionSigningKeyOrAlg(alg)

        if not private_jwk.is_private or private_jwk.is_symmetric:
            raise InvalidClientAssertionSigningKeyOrAlg(alg)

        kid = private_jwk.get("kid")
        if not kid:
            raise InvalidClientAssertionSigningKeyOrAlg(alg)

        self.__attrs_init__(
            client_id=client_id,
            private_jwk=private_jwk,
            alg=alg,
            lifetime=lifetime,
            jti_gen=jti_gen,
            aud=aud,
        )

    def client_assertion(self, audience: str) -> str:
        """Generate a Client Assertion, asymmetrically signed with `private_jwk` as key.

        Args:
            audience: the audience to use for the generated Client Assertion.

        Returns:
            a Client Assertion.

        """
        iat = int(datetime.now(tz=timezone.utc).timestamp())
        exp = iat + self.lifetime
        jti = str(self.jti_gen())

        jwt = Jwt.sign(
            claims={
                "iss": self.client_id,
                "sub": self.client_id,
                "aud": audience,
                "iat": iat,
                "exp": exp,
                "jti": jti,
            },
            key=self.private_jwk,
            alg=self.alg,
        )
        return str(jwt)

client_assertion(audience)

Generate a Client Assertion, asymmetrically signed with private_jwk as key.

Parameters:

Name Type Description Default
audience str

the audience to use for the generated Client Assertion.

required

Returns:

Type Description
str

a Client Assertion.

Source code in requests_oauth2client/client_authentication.py
def client_assertion(self, audience: str) -> str:
    """Generate a Client Assertion, asymmetrically signed with `private_jwk` as key.

    Args:
        audience: the audience to use for the generated Client Assertion.

    Returns:
        a Client Assertion.

    """
    iat = int(datetime.now(tz=timezone.utc).timestamp())
    exp = iat + self.lifetime
    jti = str(self.jti_gen())

    jwt = Jwt.sign(
        claims={
            "iss": self.client_id,
            "sub": self.client_id,
            "aud": audience,
            "iat": iat,
            "exp": exp,
            "jti": jti,
        },
        key=self.private_jwk,
        alg=self.alg,
    )
    return str(jwt)

PublicApp

Bases: BaseClientAuthenticationMethod

Implement the none authentication method for public apps.

This scheme is used for Public Clients, which do not have any secret credentials. Those only send their client_id to the Authorization Server.

Example
1
2
3
4
from requests_oauth2client import OAuth2Client, PublicApp

auth = PublicApp("my_client_id")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen
class PublicApp(BaseClientAuthenticationMethod):
    """Implement the `none` authentication method for public apps.

    This scheme is used for Public Clients, which do not have any secret credentials. Those only
    send their client_id to the Authorization Server.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client, PublicApp

        auth = PublicApp("my_client_id")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the `client_id` field in the request body.

        Args:
            request: a request.

        Returns:
            the request with the added `client_id` form field.

        """
        request = super().__call__(request)
        params = (
            parse_qs(request.body, strict_parsing=True, keep_blank_values=True)  # type: ignore[type-var]
            if request.body
            else {}
        )
        params[b"client_id"] = [self.client_id.encode()]
        request.prepare_body(params, files=None)
        return request

UnsupportedClientCredentials

Bases: TypeError, ValueError

Raised when unsupported client credentials are provided.

Source code in requests_oauth2client/client_authentication.py
class UnsupportedClientCredentials(TypeError, ValueError):
    """Raised when unsupported client credentials are provided."""

DeviceAuthorizationPoolingJob

Bases: BaseTokenEndpointPoolingJob

A Token Endpoint pooling job for the Device Authorization Flow.

This periodically checks if the user has finished with his authorization in a Device Authorization flow.

Parameters:

Name Type Description Default
client OAuth2Client

an OAuth2Client that will be used to pool the token endpoint.

required
device_code str | DeviceAuthorizationResponse

a device_code as str or a DeviceAuthorizationResponse.

required
interval int | None

The pooling interval to use. This overrides the one in auth_req_id if it is a BackChannelAuthenticationResponse.

None
slow_down_interval int

Number of seconds to add to the pooling interval when the AS returns a slow-down request.

5
requests_kwargs dict[str, Any] | None

Additional parameters for the underlying calls to requests.request.

None
**token_kwargs Any

Additional parameters for the token request.

{}
Example
1
2
3
4
5
6
7
8
from requests_oauth2client import DeviceAuthorizationPoolingJob, OAuth2Client

client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
pooler = DeviceAuthorizationPoolingJob(client=client, device_code="my_device_code")

token = None
while token is None:
    token = pooler()
Source code in requests_oauth2client/device_authorization.py
@define(init=False)
class DeviceAuthorizationPoolingJob(BaseTokenEndpointPoolingJob):
    """A Token Endpoint pooling job for the Device Authorization Flow.

    This periodically checks if the user has finished with his authorization in a Device
    Authorization flow.

    Args:
        client: an OAuth2Client that will be used to pool the token endpoint.
        device_code: a `device_code` as `str` or a `DeviceAuthorizationResponse`.
        interval: The pooling interval to use. This overrides the one in `auth_req_id` if it is
            a `BackChannelAuthenticationResponse`.
        slow_down_interval: Number of seconds to add to the pooling interval when the AS returns
            a slow-down request.
        requests_kwargs: Additional parameters for the underlying calls to [requests.request][].
        **token_kwargs: Additional parameters for the token request.

    Example:
        ```python
        from requests_oauth2client import DeviceAuthorizationPoolingJob, OAuth2Client

        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        pooler = DeviceAuthorizationPoolingJob(client=client, device_code="my_device_code")

        token = None
        while token is None:
            token = pooler()
        ```

    """

    device_code: str

    def __init__(
        self,
        client: OAuth2Client,
        device_code: str | DeviceAuthorizationResponse,
        interval: int | None = None,
        slow_down_interval: int = 5,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(device_code, DeviceAuthorizationResponse):
            interval = interval or device_code.interval
            device_code = device_code.device_code

        self.__attrs_init__(
            client=client,
            device_code=device_code,
            interval=interval or 5,
            slow_down_interval=slow_down_interval,
            requests_kwargs=requests_kwargs or {},
            token_kwargs=token_kwargs,
        )

    def token_request(self) -> BearerToken:
        """Implement the Device Code token request.

        This actually calls [OAuth2Client.device_code(device_code)][requests_oauth2client.OAuth2Client.device_code]
        on `self.client`.

        Returns:
            a [BearerToken][requests_oauth2client.tokens.BearerToken]

        """
        return self.client.device_code(self.device_code, requests_kwargs=self.requests_kwargs, **self.token_kwargs)

token_request()

Implement the Device Code token request.

This actually calls OAuth2Client.device_code(device_code) on self.client.

Returns:

Type Description
BearerToken
Source code in requests_oauth2client/device_authorization.py
def token_request(self) -> BearerToken:
    """Implement the Device Code token request.

    This actually calls [OAuth2Client.device_code(device_code)][requests_oauth2client.OAuth2Client.device_code]
    on `self.client`.

    Returns:
        a [BearerToken][requests_oauth2client.tokens.BearerToken]

    """
    return self.client.device_code(self.device_code, requests_kwargs=self.requests_kwargs, **self.token_kwargs)

DeviceAuthorizationResponse

Represent a response returned by the device Authorization Endpoint.

All parameters are those returned by the AS as response to a Device Authorization Request.

Parameters:

Name Type Description Default
device_code str

the device_code as returned by the AS.

required
user_code str

the device_code as returned by the AS.

required
verification_uri str

the device_code as returned by the AS.

required
verification_uri_complete str | None

the device_code as returned by the AS.

None
expires_at datetime | None

the expiration date for the device_code. Also accepts an expires_in parameter, as a number of seconds in the future.

None
interval int | None

the pooling interval as returned by the AS.

None
**kwargs Any

additional parameters as returned by the AS.

{}
Source code in requests_oauth2client/device_authorization.py
class DeviceAuthorizationResponse:
    """Represent a response returned by the device Authorization Endpoint.

    All parameters are those returned by the AS as response to a Device Authorization Request.

    Args:
        device_code: the `device_code` as returned by the AS.
        user_code: the `device_code` as returned by the AS.
        verification_uri: the `device_code` as returned by the AS.
        verification_uri_complete: the `device_code` as returned by the AS.
        expires_at: the expiration date for the device_code.
            Also accepts an `expires_in` parameter, as a number of seconds in the future.
        interval: the pooling `interval` as returned by the AS.
        **kwargs: additional parameters as returned by the AS.

    """

    @accepts_expires_in
    def __init__(
        self,
        device_code: str,
        user_code: str,
        verification_uri: str,
        verification_uri_complete: str | None = None,
        expires_at: datetime | None = None,
        interval: int | None = None,
        **kwargs: Any,
    ) -> None:
        self.device_code = device_code
        self.user_code = user_code
        self.verification_uri = verification_uri
        self.verification_uri_complete = verification_uri_complete
        self.expires_at = expires_at
        self.interval = interval
        self.other = kwargs

    def is_expired(self, leeway: int = 0) -> bool | None:
        """Check if the `device_code` within this response is expired.

        Returns:
            `True` if the device_code is expired, `False` if it is still valid, `None` if there is
            no `expires_in` hint.

        """
        if self.expires_at:
            return datetime.now(tz=timezone.utc) - timedelta(seconds=leeway) > self.expires_at
        return None

is_expired(leeway=0)

Check if the device_code within this response is expired.

Returns:

Type Description
bool | None

True if the device_code is expired, False if it is still valid, None if there is

bool | None

no expires_in hint.

Source code in requests_oauth2client/device_authorization.py
def is_expired(self, leeway: int = 0) -> bool | None:
    """Check if the `device_code` within this response is expired.

    Returns:
        `True` if the device_code is expired, `False` if it is still valid, `None` if there is
        no `expires_in` hint.

    """
    if self.expires_at:
        return datetime.now(tz=timezone.utc) - timedelta(seconds=leeway) > self.expires_at
    return None

DPoPKey

Wrapper around a DPoP proof signature key.

This handles DPoP proof generation. It also keeps track of a nonce, if provided by the Resource Server. Its behavior follows the standard DPoP specifications. You may subclass or otherwise customize this class to implement custom behavior, like adding or modifying claims to the proofs.

Parameters:

Name Type Description Default
private_key Any

the private key to use for DPoP proof signatures.

required
alg str | None

the alg to use for signatures, if not specified of the private_key.

None
jti_generator Callable[[], str]

a callable that generates unique JWT Token ID (jti) values to include in proofs.

lambda: str(uuid4())
iat_generator Callable[[], int]

a callable that generates the Issuer Date (iat) to include in proofs.

lambda: timestamp()
jwt_typ str

the token type (typ) header to include in the generated proofs.

'dpop+jwt'
dpop_token_class type[DPoPToken]

the class to use to represent DPoP tokens.

DPoPToken
rs_nonce str | None

an initial DPoP nonce to include in requests, for testing purposes. You should leave None.

None
Source code in requests_oauth2client/dpop.py
@define(init=False)
class DPoPKey:
    """Wrapper around a DPoP proof signature key.

    This handles DPoP proof generation. It also keeps track of a nonce, if provided
    by the Resource Server.
    Its behavior follows the standard DPoP specifications.
    You may subclass or otherwise customize this class to implement custom behavior,
    like adding or modifying claims to the proofs.

    Args:
        private_key: the private key to use for DPoP proof signatures.
        alg: the alg to use for signatures, if not specified of the `private_key`.
        jti_generator: a callable that generates unique JWT Token ID (jti) values to include in proofs.
        iat_generator: a callable that generates the Issuer Date (iat) to include in proofs.
        jwt_typ: the token type (`typ`) header to include in the generated proofs.
        dpop_token_class: the class to use to represent DPoP tokens.
        rs_nonce: an initial DPoP `nonce` to include in requests, for testing purposes. You should leave `None`.

    """

    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)
    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
    rs_nonce: str | None

    def __init__(
        self,
        private_key: Any,
        alg: str | None = None,
        jti_generator: Callable[[], str] = lambda: str(uuid4()),
        iat_generator: Callable[[], int] = lambda: jwskate.Jwt.timestamp(),
        jwt_typ: str = "dpop+jwt",
        dpop_token_class: type[DPoPToken] = DPoPToken,
        as_nonce: str | None = None,
        rs_nonce: str | None = None,
    ) -> None:
        try:
            private_key = jwskate.to_jwk(private_key).check(is_private=True, is_symmetric=False)
        except ValueError as exc:
            raise InvalidDPoPKey(private_key) from exc

        alg_name = jwskate.select_alg_class(private_key.SIGNATURE_ALGORITHMS, jwk_alg=private_key.alg, alg=alg).name

        self.__attrs_init__(
            alg=alg_name,
            private_key=private_key,
            jti_generator=jti_generator,
            iat_generator=iat_generator,
            jwt_typ=jwt_typ,
            dpop_token_class=dpop_token_class,
            as_nonce=as_nonce,
            rs_nonce=rs_nonce,
        )

    @classmethod
    def generate(
        cls,
        alg: str = jwskate.SignatureAlgs.ES256,
        jwt_typ: str = "dpop+jwt",
        jti_generator: Callable[[], str] = lambda: str(uuid4()),
        iat_generator: Callable[[], int] = lambda: jwskate.Jwt.timestamp(),
        dpop_token_class: type[DPoPToken] = DPoPToken,
        as_nonce: str | None = None,
        rs_nonce: str | None = None,
    ) -> Self:
        """Generate a new DPoPKey with a new private key that is suitable for the given `alg`."""
        if alg not in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
            raise InvalidDPoPAlg(alg)
        key = jwskate.Jwk.generate(alg=alg)
        return cls(
            private_key=key,
            jti_generator=jti_generator,
            iat_generator=iat_generator,
            jwt_typ=jwt_typ,
            dpop_token_class=dpop_token_class,
            as_nonce=as_nonce,
            rs_nonce=rs_nonce,
        )

    @cached_property
    def public_jwk(self) -> jwskate.Jwk:
        """The public JWK key that matches the private key."""
        return self.private_key.public_jwk()

    @cached_property
    def dpop_jkt(self) -> str:
        """The key thumbprint, used for Authorization Code DPoP binding."""
        return self.private_key.thumbprint()

    def proof(self, htm: str, htu: str, ath: str | None = None, nonce: str | None = None) -> jwskate.SignedJwt:
        """Generate a DPoP proof.

        Proof will contain the following claims:

            - The HTTP method (`htm`), target URI (`htu`), and Access Token hash (`ath`) that are passed as parameters.
            - The `iat` claim will be generated by the configured `iat_generator`, which defaults to current datetime.
            - The `jti` claim will be generated by the configured `jti_generator`, which defaults to a random UUID4.
            - The `nonce` claim will be the value stored in the `nonce` attribute. This attribute is updated
              automatically when using a `DPoPToken` or one of the provided Authentication handlers as a `requests`
              auth handler.

        The proof will be signed with the private key of this DPoPKey, using the configured `alg` signature algorithm.

        Args:
            htm: The HTTP method value of the request to which the proof is attached.
            htu: The HTTP target URI of the request to which the proof is attached. Query and Fragment parts will
                be automatically removed before being used as `htu` value in the generated proof.
            ath: The Access Token hash value.
            nonce: A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS.  If `None`, the
                value stored in `rs_nonce` will be used instead.
                In typical cases, you should never have to use this parameter. It is only used internally when
                requesting the AS token endpoint.

        Returns:
            the proof value (as a signed JWT)

        """
        htu = furl(htu).remove(query=True, fragment=True).url
        proof_claims = {"jti": self.jti_generator(), "htm": htm, "htu": htu, "iat": self.iat_generator()}
        if nonce:
            proof_claims["nonce"] = nonce
        elif self.rs_nonce:
            proof_claims["nonce"] = self.rs_nonce
        if ath:
            proof_claims["ath"] = ath
        return jwskate.SignedJwt.sign(
            proof_claims,
            key=self.private_key,
            alg=self.alg,
            typ=self.jwt_typ,
            extra_headers={"jwk": self.public_jwk},
        )

    def handle_as_provided_dpop_nonce(self, response: requests.Response) -> None:
        """Handle an Authorization Server response containing a `use_dpop_nonce` error.

        Args:
            response: the response from the AS.

        """
        nonce = response.headers.get("DPoP-Nonce")
        if not nonce:
            raise MissingDPoPNonce(response)
        if self.as_nonce == nonce:
            raise RepeatedDPoPNonce(response)
        self.as_nonce = nonce

    def handle_rs_provided_dpop_nonce(self, response: requests.Response) -> None:
        """Handle a Resource Server response containing a `use_dpop_nonce` error.

        Args:
            response: the response from the AS.

        """
        nonce = response.headers.get("DPoP-Nonce")
        if not nonce:
            raise MissingDPoPNonce(response)
        if self.rs_nonce == nonce:
            raise RepeatedDPoPNonce(response)
        self.rs_nonce = nonce

public_jwk cached property

The public JWK key that matches the private key.

dpop_jkt cached property

The key thumbprint, used for Authorization Code DPoP binding.

generate(alg=jwskate.SignatureAlgs.ES256, jwt_typ='dpop+jwt', jti_generator=lambda: str(uuid4()), iat_generator=lambda: jwskate.Jwt.timestamp(), dpop_token_class=DPoPToken, as_nonce=None, rs_nonce=None) classmethod

Generate a new DPoPKey with a new private key that is suitable for the given alg.

Source code in requests_oauth2client/dpop.py
@classmethod
def generate(
    cls,
    alg: str = jwskate.SignatureAlgs.ES256,
    jwt_typ: str = "dpop+jwt",
    jti_generator: Callable[[], str] = lambda: str(uuid4()),
    iat_generator: Callable[[], int] = lambda: jwskate.Jwt.timestamp(),
    dpop_token_class: type[DPoPToken] = DPoPToken,
    as_nonce: str | None = None,
    rs_nonce: str | None = None,
) -> Self:
    """Generate a new DPoPKey with a new private key that is suitable for the given `alg`."""
    if alg not in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
        raise InvalidDPoPAlg(alg)
    key = jwskate.Jwk.generate(alg=alg)
    return cls(
        private_key=key,
        jti_generator=jti_generator,
        iat_generator=iat_generator,
        jwt_typ=jwt_typ,
        dpop_token_class=dpop_token_class,
        as_nonce=as_nonce,
        rs_nonce=rs_nonce,
    )

proof(htm, htu, ath=None, nonce=None)

Generate a DPoP proof.

Proof will contain the following claims:

1
2
3
4
5
6
- The HTTP method (`htm`), target URI (`htu`), and Access Token hash (`ath`) that are passed as parameters.
- The `iat` claim will be generated by the configured `iat_generator`, which defaults to current datetime.
- The `jti` claim will be generated by the configured `jti_generator`, which defaults to a random UUID4.
- The `nonce` claim will be the value stored in the `nonce` attribute. This attribute is updated
  automatically when using a `DPoPToken` or one of the provided Authentication handlers as a `requests`
  auth handler.

The proof will be signed with the private key of this DPoPKey, using the configured alg signature algorithm.

Parameters:

Name Type Description Default
htm str

The HTTP method value of the request to which the proof is attached.

required
htu str

The HTTP target URI of the request to which the proof is attached. Query and Fragment parts will be automatically removed before being used as htu value in the generated proof.

required
ath str | None

The Access Token hash value.

None
nonce str | None

A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS. If None, the value stored in rs_nonce will be used instead. In typical cases, you should never have to use this parameter. It is only used internally when requesting the AS token endpoint.

None

Returns:

Type Description
SignedJwt

the proof value (as a signed JWT)

Source code in requests_oauth2client/dpop.py
def proof(self, htm: str, htu: str, ath: str | None = None, nonce: str | None = None) -> jwskate.SignedJwt:
    """Generate a DPoP proof.

    Proof will contain the following claims:

        - The HTTP method (`htm`), target URI (`htu`), and Access Token hash (`ath`) that are passed as parameters.
        - The `iat` claim will be generated by the configured `iat_generator`, which defaults to current datetime.
        - The `jti` claim will be generated by the configured `jti_generator`, which defaults to a random UUID4.
        - The `nonce` claim will be the value stored in the `nonce` attribute. This attribute is updated
          automatically when using a `DPoPToken` or one of the provided Authentication handlers as a `requests`
          auth handler.

    The proof will be signed with the private key of this DPoPKey, using the configured `alg` signature algorithm.

    Args:
        htm: The HTTP method value of the request to which the proof is attached.
        htu: The HTTP target URI of the request to which the proof is attached. Query and Fragment parts will
            be automatically removed before being used as `htu` value in the generated proof.
        ath: The Access Token hash value.
        nonce: A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS.  If `None`, the
            value stored in `rs_nonce` will be used instead.
            In typical cases, you should never have to use this parameter. It is only used internally when
            requesting the AS token endpoint.

    Returns:
        the proof value (as a signed JWT)

    """
    htu = furl(htu).remove(query=True, fragment=True).url
    proof_claims = {"jti": self.jti_generator(), "htm": htm, "htu": htu, "iat": self.iat_generator()}
    if nonce:
        proof_claims["nonce"] = nonce
    elif self.rs_nonce:
        proof_claims["nonce"] = self.rs_nonce
    if ath:
        proof_claims["ath"] = ath
    return jwskate.SignedJwt.sign(
        proof_claims,
        key=self.private_key,
        alg=self.alg,
        typ=self.jwt_typ,
        extra_headers={"jwk": self.public_jwk},
    )

handle_as_provided_dpop_nonce(response)

Handle an Authorization Server response containing a use_dpop_nonce error.

Parameters:

Name Type Description Default
response Response

the response from the AS.

required
Source code in requests_oauth2client/dpop.py
def handle_as_provided_dpop_nonce(self, response: requests.Response) -> None:
    """Handle an Authorization Server response containing a `use_dpop_nonce` error.

    Args:
        response: the response from the AS.

    """
    nonce = response.headers.get("DPoP-Nonce")
    if not nonce:
        raise MissingDPoPNonce(response)
    if self.as_nonce == nonce:
        raise RepeatedDPoPNonce(response)
    self.as_nonce = nonce

handle_rs_provided_dpop_nonce(response)

Handle a Resource Server response containing a use_dpop_nonce error.

Parameters:

Name Type Description Default
response Response

the response from the AS.

required
Source code in requests_oauth2client/dpop.py
def handle_rs_provided_dpop_nonce(self, response: requests.Response) -> None:
    """Handle a Resource Server response containing a `use_dpop_nonce` error.

    Args:
        response: the response from the AS.

    """
    nonce = response.headers.get("DPoP-Nonce")
    if not nonce:
        raise MissingDPoPNonce(response)
    if self.rs_nonce == nonce:
        raise RepeatedDPoPNonce(response)
    self.rs_nonce = nonce

DPoPToken

Bases: BearerToken

Represent a DPoP token (RFC9449).

A DPoP is very much like a BearerToken, with an additional private key bound to it.

Source code in requests_oauth2client/dpop.py
@frozen(init=False)
class DPoPToken(BearerToken):  # type: ignore[override]
    """Represent a DPoP token (RFC9449).

    A DPoP is very much like a BearerToken, with an additional private key bound to it.

    """

    TOKEN_TYPE = AccessTokenTypes.DPOP.value
    AUTHORIZATION_SCHEME = AccessTokenTypes.DPOP.value
    DPOP_HEADER: ClassVar[str] = "DPoP"

    dpop_key: DPoPKey = field(kw_only=True)

    @accepts_expires_in
    def __init__(
        self,
        access_token: str,
        *,
        _dpop_key: DPoPKey,
        expires_at: datetime | None = None,
        scope: str | None = None,
        refresh_token: str | None = None,
        token_type: str = TOKEN_TYPE,
        id_token: str | bytes | IdToken | jwskate.JweCompact | None = None,
        **kwargs: Any,
    ) -> None:
        if not token68_pattern.match(access_token):
            raise InvalidDPoPAccessToken(access_token)

        id_token = id_token_converter(id_token)

        self.__attrs_init__(
            access_token=access_token,
            expires_at=expires_at,
            scope=scope,
            refresh_token=refresh_token,
            token_type=token_type,
            id_token=id_token,
            dpop_key=_dpop_key,
            kwargs=kwargs,
        )

    def _response_hook(self, response: requests.Response, **kwargs: Any) -> requests.Response:
        """Handles a Resource Server provided DPoP nonce."""
        if response.status_code == codes.unauthorized and response.headers.get("WWW-Authenticate", "").startswith(
            "DPoP"
        ):
            self.dpop_key.handle_rs_provided_dpop_nonce(response)
            new_request = response.request.copy()
            # remove the previously registered hook to avoid registering it multiple times
            new_request.deregister_hook("response", self._response_hook)  # type: ignore[no-untyped-call]
            new_request = self(new_request)  # another hook will be re-registered here in the __call__() method

            return response.connection.send(new_request, **kwargs)

        return response

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add a DPoP proof in each request."""
        request = super().__call__(request)
        add_dpop_proof(request, dpop_key=self.dpop_key, access_token=self.access_token, header_name=self.DPOP_HEADER)
        request.register_hook("response", self._response_hook)  # type: ignore[no-untyped-call]
        return request

InvalidDPoPAccessToken

Bases: ValueError

Raised when an access token contains invalid characters.

Source code in requests_oauth2client/dpop.py
class InvalidDPoPAccessToken(ValueError):
    """Raised when an access token contains invalid characters."""

    def __init__(self, access_token: str) -> None:
        super().__init__("""\
This DPoP token contains invalid characters. DPoP tokens are limited to a set of 68 characters,
to avoid encoding inconsistencies when doing the token value hashing for the DPoP proof.""")
        self.access_token = access_token

InvalidDPoPAlg

Bases: ValueError

Raised when an invalid or unsupported DPoP alg is given.

Source code in requests_oauth2client/dpop.py
class InvalidDPoPAlg(ValueError):
    """Raised when an invalid or unsupported DPoP alg is given."""

    def __init__(self, alg: str) -> None:
        super().__init__("DPoP proofing require an asymmetric signing alg.")
        self.alg = alg

InvalidDPoPKey

Bases: ValueError

Raised when a DPoPToken is initialized with a non-suitable key.

Source code in requests_oauth2client/dpop.py
class InvalidDPoPKey(ValueError):
    """Raised when a DPoPToken is initialized with a non-suitable key."""

    def __init__(self, key: Any) -> None:
        super().__init__("The key you are trying to use with DPoP is not an asymmetric private key.")
        self.key = key

InvalidDPoPProof

Bases: ValueError

Raised when a DPoP proof does not verify.

Source code in requests_oauth2client/dpop.py
class InvalidDPoPProof(ValueError):
    """Raised when a DPoP proof does not verify."""

    def __init__(self, proof: bytes, message: str) -> None:
        super().__init__(f"Invalid DPoP proof: {message}")
        self.proof = proof

InvalidUseDPoPNonceResponse

Bases: Exception

Base class for invalid Responses with a use_dpop_nonce error.

Source code in requests_oauth2client/dpop.py
class InvalidUseDPoPNonceResponse(Exception):
    """Base class for invalid Responses with a `use_dpop_nonce` error."""

    def __init__(self, response: requests.Response, message: str) -> None:
        super().__init__(message)
        self.response = response

MissingDPoPNonce

Bases: InvalidUseDPoPNonceResponse

Raised when a server requests a DPoP nonce but none is provided in its response.

Source code in requests_oauth2client/dpop.py
class MissingDPoPNonce(InvalidUseDPoPNonceResponse):
    """Raised when a server requests a DPoP nonce but none is provided in its response."""

    def __init__(self, response: requests.Response) -> None:
        super().__init__(
            response,
            "Server requested client to use a DPoP `nonce`, but the `DPoP-Nonce` HTTP header is missing.",
        )

RepeatedDPoPNonce

Bases: InvalidUseDPoPNonceResponse

Raised when the server requests a DPoP nonce value that is the same as already included in the request.

Source code in requests_oauth2client/dpop.py
class RepeatedDPoPNonce(InvalidUseDPoPNonceResponse):
    """Raised when the server requests a DPoP nonce value that is the same as already included in the request."""

    def __init__(self, response: requests.Response) -> None:
        super().__init__(
            response,
            """\
Server requested client to use a DPoP `nonce`,
but provided the same value for that nonce that was already included in the DPoP proof.""",
        )

AccessDenied

Bases: EndpointError

Raised when the Authorization Server returns error = access_denied.

Source code in requests_oauth2client/exceptions.py
class AccessDenied(EndpointError):
    """Raised when the Authorization Server returns `error = access_denied`."""

AccountSelectionRequired

Bases: InteractionRequired

Raised when the Authorization Endpoint returns error = account_selection_required.

Source code in requests_oauth2client/exceptions.py
class AccountSelectionRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = account_selection_required`."""

AuthorizationPending

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = authorization_pending.

Source code in requests_oauth2client/exceptions.py
class AuthorizationPending(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = authorization_pending`."""

AuthorizationResponseError

Bases: Exception

Base class for error responses returned by the Authorization endpoint.

An AuthorizationResponseError contains the error message, description and uri that are returned by the AS.

Parameters:

Name Type Description Default
error str

the error identifier as returned by the AS

required
description str | None

the error_description as returned by the AS

None
uri str | None

the error_uri as returned by the AS

None
Source code in requests_oauth2client/exceptions.py
class AuthorizationResponseError(Exception):
    """Base class for error responses returned by the Authorization endpoint.

    An `AuthorizationResponseError` contains the error message, description and uri that are
    returned by the AS.

    Args:
        error: the `error` identifier as returned by the AS
        description: the `error_description` as returned by the AS
        uri: the `error_uri` as returned by the AS

    """

    def __init__(
        self,
        request: AuthorizationRequest,
        response: str,
        error: str,
        description: str | None = None,
        uri: str | None = None,
    ) -> None:
        self.error = error
        self.description = description
        self.uri = uri
        self.request = request
        self.response = response

BackChannelAuthenticationError

Bases: EndpointError

Base class for errors returned by the BackChannel Authentication endpoint.

Source code in requests_oauth2client/exceptions.py
class BackChannelAuthenticationError(EndpointError):
    """Base class for errors returned by the BackChannel Authentication endpoint."""

ConsentRequired

Bases: InteractionRequired

Raised when the Authorization Endpoint returns error = consent_required.

Source code in requests_oauth2client/exceptions.py
class ConsentRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = consent_required`."""

DeviceAuthorizationError

Bases: EndpointError

Base class for Device Authorization Endpoint errors.

Source code in requests_oauth2client/exceptions.py
class DeviceAuthorizationError(EndpointError):
    """Base class for Device Authorization Endpoint errors."""

EndpointError

Bases: OAuth2Error

Base class for exceptions raised from backend endpoint errors.

This contains the error message, description and uri that are returned by the AS in the OAuth 2.0 standardised way.

Parameters:

Name Type Description Default
response Response

the raw response containing the error.

required
error str

the error identifier as returned by the AS.

required
description str | None

the error_description as returned by the AS.

None
uri str | None

the error_uri as returned by the AS.

None
Source code in requests_oauth2client/exceptions.py
class EndpointError(OAuth2Error):
    """Base class for exceptions raised from backend endpoint errors.

    This contains the error message, description and uri that are returned
    by the AS in the OAuth 2.0 standardised way.

    Args:
        response: the raw response containing the error.
        error: the `error` identifier as returned by the AS.
        description: the `error_description` as returned by the AS.
        uri: the `error_uri` as returned by the AS.

    """

    def __init__(
        self,
        response: requests.Response,
        client: OAuth2Client,
        error: str,
        description: str | None = None,
        uri: str | None = None,
    ) -> None:
        super().__init__(response=response, client=client, description=description)
        self.error = error
        self.uri = uri

ExpiredToken

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = expired_token.

Source code in requests_oauth2client/exceptions.py
class ExpiredToken(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = expired_token`."""

InteractionRequired

Bases: AuthorizationResponseError

Raised when the Authorization Endpoint returns error = interaction_required.

Source code in requests_oauth2client/exceptions.py
class InteractionRequired(AuthorizationResponseError):
    """Raised when the Authorization Endpoint returns `error = interaction_required`."""

IntrospectionError

Bases: EndpointError

Base class for Introspection Endpoint errors.

Source code in requests_oauth2client/exceptions.py
class IntrospectionError(EndpointError):
    """Base class for Introspection Endpoint errors."""

InvalidAuthResponse

Bases: ValueError

Raised when the Authorization Endpoint returns an invalid response.

Source code in requests_oauth2client/exceptions.py
class InvalidAuthResponse(ValueError):
    """Raised when the Authorization Endpoint returns an invalid response."""

    def __init__(self, message: str, request: AuthorizationRequest, response: str) -> None:
        super().__init__(f"The Authorization Response is invalid: {message}")
        self.request = request
        self.response = response

InvalidBackChannelAuthenticationResponse

Bases: OAuth2Error

Raised when the BackChannel Authentication endpoint returns a non-standard response.

Source code in requests_oauth2client/exceptions.py
class InvalidBackChannelAuthenticationResponse(OAuth2Error):
    """Raised when the BackChannel Authentication endpoint returns a non-standard response."""

InvalidClient

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_client.

Source code in requests_oauth2client/exceptions.py
class InvalidClient(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_client`."""

InvalidDeviceAuthorizationResponse

Bases: OAuth2Error

Raised when the Device Authorization Endpoint returns a non-standard error response.

Source code in requests_oauth2client/exceptions.py
class InvalidDeviceAuthorizationResponse(OAuth2Error):
    """Raised when the Device Authorization Endpoint returns a non-standard error response."""

InvalidGrant

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_grant.

Source code in requests_oauth2client/exceptions.py
class InvalidGrant(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_grant`."""

InvalidPushedAuthorizationResponse

Bases: OAuth2Error

Raised when the Pushed Authorization Endpoint returns an error.

Source code in requests_oauth2client/exceptions.py
class InvalidPushedAuthorizationResponse(OAuth2Error):
    """Raised when the Pushed Authorization Endpoint returns an error."""

InvalidRequest

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_request.

Source code in requests_oauth2client/exceptions.py
class InvalidRequest(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_request`."""

InvalidScope

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_scope.

Source code in requests_oauth2client/exceptions.py
class InvalidScope(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_scope`."""

InvalidTarget

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_target.

Source code in requests_oauth2client/exceptions.py
class InvalidTarget(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_target`."""

InvalidTokenResponse

Bases: OAuth2Error

Raised when the Token Endpoint returns a non-standard response.

Source code in requests_oauth2client/exceptions.py
class InvalidTokenResponse(OAuth2Error):
    """Raised when the Token Endpoint returns a non-standard response."""

LoginRequired

Bases: InteractionRequired

Raised when the Authorization Endpoint returns error = login_required.

Source code in requests_oauth2client/exceptions.py
class LoginRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = login_required`."""

MismatchingIssuer

Bases: InvalidAuthResponse

Raised on mismatching iss value.

This happens when the Authorization Endpoints returns an 'iss' that doesn't match the expected value.

Source code in requests_oauth2client/exceptions.py
class MismatchingIssuer(InvalidAuthResponse):
    """Raised on mismatching `iss` value.

    This happens when the Authorization Endpoints returns an 'iss' that doesn't match the expected value.

    """

    def __init__(self, received: str, expected: str, request: AuthorizationRequest, response: str) -> None:
        super().__init__(f"mismatching `iss` (received '{received}', expected '{expected}')", request, response)
        self.received = received
        self.expected = expected

MismatchingState

Bases: InvalidAuthResponse

Raised on mismatching state value.

This happens when the Authorization Endpoints returns a 'state' parameter that doesn't match the value passed in the Authorization Request.

Source code in requests_oauth2client/exceptions.py
class MismatchingState(InvalidAuthResponse):
    """Raised on mismatching `state` value.

    This happens when the Authorization Endpoints returns a 'state' parameter that doesn't match the value passed in the
    Authorization Request.

    """

    def __init__(self, received: str, expected: str, request: AuthorizationRequest, response: str) -> None:
        super().__init__(f"mismatching `state` (received '{received}', expected '{expected}')", request, response)
        self.received = received
        self.expected = expected

MissingAuthCode

Bases: InvalidAuthResponse

Raised when the Authorization Endpoint does not return the mandatory code.

This happens when the Authorization Endpoint does not return an error, but does not return an authorization code either.

Source code in requests_oauth2client/exceptions.py
class MissingAuthCode(InvalidAuthResponse):
    """Raised when the Authorization Endpoint does not return the mandatory `code`.

    This happens when the Authorization Endpoint does not return an error, but does not return an
    authorization `code` either.

    """

    def __init__(self, request: AuthorizationRequest, response: str) -> None:
        super().__init__("missing `code` query parameter in response", request, response)

MissingIssuer

Bases: InvalidAuthResponse

Raised when the Authorization Endpoint does not return an iss parameter as expected.

The Authorization Server advertises its support with a flag authorization_response_iss_parameter_supported in its discovery document. If it is set to true, it must include an iss parameter in its authorization responses, containing its issuer identifier.

Source code in requests_oauth2client/exceptions.py
class MissingIssuer(InvalidAuthResponse):
    """Raised when the Authorization Endpoint does not return an `iss` parameter as expected.

    The Authorization Server advertises its support with a flag
    `authorization_response_iss_parameter_supported` in its discovery document. If it is set to
    `true`, it must include an `iss` parameter in its authorization responses, containing its issuer
    identifier.

    """

    def __init__(self, request: AuthorizationRequest, response: str) -> None:
        super().__init__("missing `iss` query parameter in response", request, response)

OAuth2Error

Bases: Exception

Base class for Exceptions raised when a backend endpoint returns an error.

Parameters:

Name Type Description Default
response Response

the HTTP response containing the error

required
client

the OAuth2Client used to send the request

required
description str | None

description of the error

None
Source code in requests_oauth2client/exceptions.py
class OAuth2Error(Exception):
    """Base class for Exceptions raised when a backend endpoint returns an error.

    Args:
        response: the HTTP response containing the error
        client : the OAuth2Client used to send the request
        description: description of the error

    """

    def __init__(self, response: requests.Response, client: OAuth2Client, description: str | None = None) -> None:
        super().__init__(f"The remote endpoint returned an error: {description or 'no description provided'}")
        self.response = response
        self.client = client
        self.description = description

    @property
    def request(self) -> requests.PreparedRequest:
        """The request leading to the error."""
        return self.response.request

request property

The request leading to the error.

RevocationError

Bases: EndpointError

Base class for Revocation Endpoint errors.

Source code in requests_oauth2client/exceptions.py
class RevocationError(EndpointError):
    """Base class for Revocation Endpoint errors."""

ServerError

Bases: EndpointError

Raised when the token endpoint returns error = server_error.

Source code in requests_oauth2client/exceptions.py
class ServerError(EndpointError):
    """Raised when the token endpoint returns `error = server_error`."""

SessionSelectionRequired

Bases: InteractionRequired

Raised when the Authorization Endpoint returns error = session_selection_required.

Source code in requests_oauth2client/exceptions.py
class SessionSelectionRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = session_selection_required`."""

SlowDown

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = slow_down.

Source code in requests_oauth2client/exceptions.py
class SlowDown(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = slow_down`."""

TokenEndpointError

Bases: EndpointError

Base class for errors that are specific to the token endpoint.

Source code in requests_oauth2client/exceptions.py
class TokenEndpointError(EndpointError):
    """Base class for errors that are specific to the token endpoint."""

UnauthorizedClient

Bases: EndpointError

Raised when the Authorization Server returns error = unauthorized_client.

Source code in requests_oauth2client/exceptions.py
class UnauthorizedClient(EndpointError):
    """Raised when the Authorization Server returns `error = unauthorized_client`."""

UnknownIntrospectionError

Bases: OAuth2Error

Raised when the Introspection Endpoint returns a non-standard error.

Source code in requests_oauth2client/exceptions.py
class UnknownIntrospectionError(OAuth2Error):
    """Raised when the Introspection Endpoint returns a non-standard error."""

UnknownTokenEndpointError

Bases: EndpointError

Raised when the token endpoint returns an otherwise unknown error.

Source code in requests_oauth2client/exceptions.py
class UnknownTokenEndpointError(EndpointError):
    """Raised when the token endpoint returns an otherwise unknown error."""

UnsupportedTokenType

Bases: RevocationError

Raised when the Revocation endpoint returns error = unsupported_token_type.

Source code in requests_oauth2client/exceptions.py
class UnsupportedTokenType(RevocationError):
    """Raised when the Revocation endpoint returns `error = unsupported_token_type`."""

UseDPoPNonce

Bases: TokenEndpointError

Raised when the Token Endpoint raises error = use_dpop_nonce`.

Source code in requests_oauth2client/exceptions.py
class UseDPoPNonce(TokenEndpointError):
    """Raised when the Token Endpoint raises error = use_dpop_nonce`."""

BaseTokenEndpointPoolingJob

Base class for Token Endpoint pooling jobs.

This is used for decoupled flows like CIBA or Device Authorization.

This class must be subclassed to implement actual BackChannel flows. This needs an OAuth2Client that will be used to pool the token endpoint. The initial pooling interval is configurable.

Source code in requests_oauth2client/pooling.py
@define
class BaseTokenEndpointPoolingJob:
    """Base class for Token Endpoint pooling jobs.

    This is used for decoupled flows like CIBA or Device Authorization.

    This class must be subclassed to implement actual BackChannel flows. This needs an
    [OAuth2Client][requests_oauth2client.client.OAuth2Client] that will be used to pool the token
    endpoint. The initial pooling `interval` is configurable.

    """

    client: OAuth2Client = field(on_setattr=setters.frozen)
    requests_kwargs: dict[str, Any] = field(on_setattr=setters.frozen)
    token_kwargs: dict[str, Any] = field(on_setattr=setters.frozen)
    slow_down_interval: int = field(on_setattr=setters.frozen)
    interval: int

    def __call__(self) -> BearerToken | None:
        """Wrap the actual Token Endpoint call with a pooling interval.

        Everytime this method is called, it will wait for the entire duration of the pooling
        interval before calling
        [token_request()][requests_oauth2client.pooling.TokenEndpointPoolingJob.token_request]. So
        you can call it immediately after initiating the BackChannel flow, and it will wait before
        initiating the first call.

        This implements the logic to handle
        [AuthorizationPending][requests_oauth2client.exceptions.AuthorizationPending] or
        [SlowDown][requests_oauth2client.exceptions.SlowDown] requests by the AS.

        Returns:
            a `BearerToken` if the AS returns one, or `None` if the Authorization is still pending.

        """
        self.sleep()
        try:
            return self.token_request()
        except SlowDown:
            self.slow_down()
        except AuthorizationPending:
            self.authorization_pending()
        return None

    def sleep(self) -> None:
        """Implement the wait between two requests of the token endpoint.

        By default, relies on time.sleep().

        """
        time.sleep(self.interval)

    def slow_down(self) -> None:
        """Implement the behavior when receiving a 'slow_down' response from the AS.

        By default, it increases the pooling interval by the slow down interval.

        """
        self.interval += self.slow_down_interval

    def authorization_pending(self) -> None:
        """Implement the behavior when receiving an 'authorization_pending' response from the AS.

        By default, it does nothing.

        """

    def token_request(self) -> BearerToken:
        """Abstract method for the token endpoint call.

        Subclasses must implement this. This method must raise
        [AuthorizationPending][requests_oauth2client.exceptions.AuthorizationPending] to retry after
        the pooling interval, or [SlowDown][requests_oauth2client.exceptions.SlowDown] to increase
        the pooling interval by `slow_down_interval` seconds.

        Returns:
            a [BearerToken][requests_oauth2client.tokens.BearerToken]

        """
        raise NotImplementedError

sleep()

Implement the wait between two requests of the token endpoint.

By default, relies on time.sleep().

Source code in requests_oauth2client/pooling.py
def sleep(self) -> None:
    """Implement the wait between two requests of the token endpoint.

    By default, relies on time.sleep().

    """
    time.sleep(self.interval)

slow_down()

Implement the behavior when receiving a 'slow_down' response from the AS.

By default, it increases the pooling interval by the slow down interval.

Source code in requests_oauth2client/pooling.py
def slow_down(self) -> None:
    """Implement the behavior when receiving a 'slow_down' response from the AS.

    By default, it increases the pooling interval by the slow down interval.

    """
    self.interval += self.slow_down_interval

authorization_pending()

Implement the behavior when receiving an 'authorization_pending' response from the AS.

By default, it does nothing.

Source code in requests_oauth2client/pooling.py
def authorization_pending(self) -> None:
    """Implement the behavior when receiving an 'authorization_pending' response from the AS.

    By default, it does nothing.

    """

token_request()

Abstract method for the token endpoint call.

Subclasses must implement this. This method must raise AuthorizationPending to retry after the pooling interval, or SlowDown to increase the pooling interval by slow_down_interval seconds.

Returns:

Type Description
BearerToken
Source code in requests_oauth2client/pooling.py
def token_request(self) -> BearerToken:
    """Abstract method for the token endpoint call.

    Subclasses must implement this. This method must raise
    [AuthorizationPending][requests_oauth2client.exceptions.AuthorizationPending] to retry after
    the pooling interval, or [SlowDown][requests_oauth2client.exceptions.SlowDown] to increase
    the pooling interval by `slow_down_interval` seconds.

    Returns:
        a [BearerToken][requests_oauth2client.tokens.BearerToken]

    """
    raise NotImplementedError

BearerToken

Bases: TokenResponse, AuthBase

Represents a Bearer Token as returned by a Token Endpoint.

This is a wrapper around a Bearer Token and associated parameters, such as expiration date and refresh token, as returned by an OAuth 2.x or OIDC 1.0 Token Endpoint.

All parameters are as returned by a Token Endpoint. The token expiration date can be passed as datetime in the expires_at parameter, or an expires_in parameter, as number of seconds in the future, can be passed instead.

Parameters:

Name Type Description Default
access_token str

an access_token, as returned by the AS.

required
expires_at datetime | None

an expiration date. This method also accepts an expires_in hint as returned by the AS, if any.

None
scope str | None

a scope, as returned by the AS, if any.

None
refresh_token str | None

a refresh_token, as returned by the AS, if any.

None
token_type str

a token_type, as returned by the AS.

TOKEN_TYPE
id_token str | bytes | IdToken | JweCompact | None

an id_token, as returned by the AS, if any.

None
**kwargs Any

additional parameters as returned by the AS, if any.

{}
Source code in requests_oauth2client/tokens.py
@frozen(init=False)
class BearerToken(TokenResponse, requests.auth.AuthBase):
    """Represents a Bearer Token as returned by a Token Endpoint.

    This is a wrapper around a Bearer Token and associated parameters, such as expiration date and
    refresh token, as returned by an OAuth 2.x or OIDC 1.0 Token Endpoint.

    All parameters are as returned by a Token Endpoint. The token expiration date can be passed as
    datetime in the `expires_at` parameter, or an `expires_in` parameter, as number of seconds in
    the future, can be passed instead.

    Args:
        access_token: an `access_token`, as returned by the AS.
        expires_at: an expiration date. This method also accepts an `expires_in` hint as
            returned by the AS, if any.
        scope: a `scope`, as returned by the AS, if any.
        refresh_token: a `refresh_token`, as returned by the AS, if any.
        token_type: a `token_type`, as returned by the AS.
        id_token: an `id_token`, as returned by the AS, if any.
        **kwargs: additional parameters as returned by the AS, if any.

    """

    TOKEN_TYPE: ClassVar[str] = AccessTokenTypes.BEARER.value
    AUTHORIZATION_HEADER: ClassVar[str] = "Authorization"
    AUTHORIZATION_SCHEME: ClassVar[str] = AccessTokenTypes.BEARER.value

    access_token: str
    expires_at: datetime | None
    scope: str | None
    refresh_token: str | None
    token_type: str
    id_token: IdToken | jwskate.JweCompact | None
    kwargs: dict[str, Any]

    @accepts_expires_in
    def __init__(
        self,
        access_token: str,
        *,
        expires_at: datetime | None = None,
        scope: str | None = None,
        refresh_token: str | None = None,
        token_type: str = TOKEN_TYPE,
        id_token: str | bytes | IdToken | jwskate.JweCompact | None = None,
        **kwargs: Any,
    ) -> None:
        if token_type.title() != self.TOKEN_TYPE.title():
            raise UnsupportedTokenType(token_type)

        id_token = id_token_converter(id_token)

        self.__attrs_init__(
            access_token=access_token,
            expires_at=expires_at,
            scope=scope,
            refresh_token=refresh_token,
            token_type=token_type,
            id_token=id_token,
            kwargs=kwargs,
        )

    def is_expired(self, leeway: int = 0) -> bool | None:
        """Check if the access token is expired.

        Args:
            leeway: If the token expires in the next given number of seconds,
                then consider it expired already.

        Returns:
            One of:

            - `True` if the access token is expired
            - `False` if it is still valid
            - `None` if there is no expires_in hint.

        """
        if self.expires_at:
            return datetime.now(tz=timezone.utc) + timedelta(seconds=leeway) > self.expires_at
        return None

    def authorization_header(self) -> str:
        """Return the appropriate Authorization Header value for this token.

        The value is formatted correctly according to RFC6750.

        Returns:
            the value to use in an HTTP Authorization Header

        """
        return f"{self.AUTHORIZATION_SCHEME} {self.access_token}"

    def validate_id_token(  # noqa: PLR0915, C901
        self, client: OAuth2Client, azr: AuthorizationResponse, exp_leeway: int = 0, auth_time_leeway: int = 10
    ) -> Self:
        """Validate the ID Token, and return a new instance with the decrypted ID Token.

        If the ID Token was not encrypted, the returned instance will contain the same ID Token.

        This will validate the id_token as described in [OIDC 1.0
        $3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).

        Args:
            client: the `OAuth2Client` that was used to obtain this token
            azr: the `AuthorizationResponse`, as obtained by a call to `AuthorizationRequest.validate()`
            exp_leeway: a leeway, in seconds, applied to the ID Token expiration date
            auth_time_leeway: a leeway, in seconds, applied to the `auth_time` validation

        Raises:
            MissingIdToken: if the ID Token is missing
            InvalidIdToken: this is a base exception class, which is raised:

                - if the ID Token is not a JWT
                - or is encrypted while a clear-text token is expected
                - or is clear-text while an encrypted token is expected
                - if token is encrypted but client does not have a decryption key
                - if the token does not contain an `alg` header
            MismatchingIdTokenAlg: if the `alg` header from the ID Token does not match
                the expected `client.id_token_signed_response_alg`.
            MismatchingIdTokenIssuer: if the `iss` claim from the ID Token does not match
                the expected `azr.issuer`.
            MismatchingIdTokenAcr: if the `acr` claim from the ID Token does not match
                on of the expected `azr.acr_values`.
            MismatchingIdTokenAudience: if the `aud` claim from the ID Token does not match
                the expected `client.client_id`.
            MismatchingIdTokenAzp: if the `azp` claim from the ID Token does not match
                the expected `client.client_id`.
            MismatchingIdTokenNonce: if the `nonce` claim from the ID Token does not match
                the expected `azr.nonce`.
            ExpiredIdToken: if the ID Token is expired at the time of the check.
            UnsupportedIdTokenAlg: if the signature alg for the ID Token is not supported.

        """
        if not self.id_token:
            raise MissingIdToken(self)

        raw_id_token = self.id_token

        if isinstance(raw_id_token, jwskate.JweCompact) and client.id_token_encrypted_response_alg is None:
            msg = "token is encrypted while it should be clear-text"
            raise InvalidIdToken(msg, raw_id_token, self)
        if isinstance(raw_id_token, IdToken) and client.id_token_encrypted_response_alg is not None:
            msg = "token is clear-text while it should be encrypted"
            raise InvalidIdToken(msg, raw_id_token, self)

        if isinstance(raw_id_token, jwskate.JweCompact):
            enc_jwk = client.id_token_decryption_key
            if enc_jwk is None:
                msg = "token is encrypted but client does not have a decryption key"
                raise InvalidIdToken(msg, raw_id_token, self)
            nested_id_token = raw_id_token.decrypt(enc_jwk)
            id_token = IdToken(nested_id_token)
        else:
            id_token = raw_id_token

        id_token_alg = id_token.get_header("alg")
        if id_token_alg is None:
            id_token_alg = client.id_token_signed_response_alg
        if id_token_alg is None:
            msg = """
token does not contain an `alg` parameter to specify the signature algorithm,
and no algorithm has been configured for the client (using param `id_token_signed_response_alg`).
"""
            raise InvalidIdToken(msg, id_token, self)
        if client.id_token_signed_response_alg is not None and id_token_alg != client.id_token_signed_response_alg:
            raise MismatchingIdTokenAlg(id_token.alg, client.id_token_signed_response_alg, self, id_token)

        verification_jwk: jwskate.Jwk

        if id_token_alg in jwskate.SignatureAlgs.ALL_SYMMETRIC:
            if not client.client_secret:
                msg = "token is symmetrically signed but this client does not have a Client Secret."
                raise InvalidIdToken(msg, id_token, self)
            verification_jwk = jwskate.SymmetricJwk.from_bytes(client.client_secret, alg=id_token_alg)
            id_token.verify_signature(verification_jwk, alg=id_token_alg)
        elif id_token_alg in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
            if not client.authorization_server_jwks:
                msg = "token is asymmetrically signed but the Authorization Server JWKS is not available."
                raise InvalidIdToken(msg, id_token, self)

            if id_token.get_header("kid") is None:
                msg = """
token does not contain a Key ID (kid) to specify the asymmetric key
to use for signature verification."""
                raise InvalidIdToken(msg, id_token, self)
            try:
                verification_jwk = client.authorization_server_jwks.get_jwk_by_kid(id_token.kid)
            except KeyError:
                msg = f"""\
token is asymmetrically signed but there is no key
with kid='{id_token.kid}' in the Authorization Server JWKS."""
                raise InvalidIdToken(msg, id_token, self) from None

            if id_token_alg not in verification_jwk.supported_signing_algorithms():
                msg = "token is asymmetrically signed but its algorithm is not supported by the verification key."
                raise InvalidIdToken(msg, id_token, self)
        else:
            raise UnsupportedIdTokenAlg(self, id_token, id_token_alg)

        id_token.verify(verification_jwk, alg=id_token_alg)

        if azr.issuer and id_token.issuer != azr.issuer:
            raise MismatchingIdTokenIssuer(id_token.issuer, azr.issuer, self, id_token)

        if id_token.audiences and client.client_id not in id_token.audiences:
            raise MismatchingIdTokenAudience(id_token.audiences, client.client_id, self, id_token)

        if id_token.authorized_party is not None and id_token.authorized_party != client.client_id:
            raise MismatchingIdTokenAzp(id_token.azp, client.client_id, self, id_token)

        if id_token.is_expired(leeway=exp_leeway):
            raise ExpiredIdToken(self, id_token)

        if azr.nonce and id_token.nonce != azr.nonce:
            raise MismatchingIdTokenNonce(id_token.nonce, azr.nonce, self, id_token)

        if azr.acr_values and id_token.acr not in azr.acr_values:
            raise MismatchingIdTokenAcr(id_token.acr, azr.acr_values, self, id_token)

        hash_function = IdToken.hash_method(verification_jwk, id_token_alg)

        at_hash = id_token.get_claim("at_hash")
        if at_hash is not None:
            expected_at_hash = hash_function(self.access_token)
            if expected_at_hash != at_hash:
                msg = f"mismatching 'at_hash' value (expected '{expected_at_hash}', got '{at_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        c_hash = id_token.get_claim("c_hash")
        if c_hash is not None:
            expected_c_hash = hash_function(azr.code)
            if expected_c_hash != c_hash:
                msg = f"mismatching 'c_hash' value (expected '{expected_c_hash}', got '{c_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        s_hash = id_token.get_claim("s_hash")
        if s_hash is not None:
            if azr.state is None:
                msg = "token has a 's_hash' claim but no state was included in the request."
                raise InvalidIdToken(msg, id_token, self)
            expected_s_hash = hash_function(azr.state)
            if expected_s_hash != s_hash:
                msg = f"mismatching 's_hash' value (expected '{expected_s_hash}', got '{s_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        if azr.max_age is not None:
            auth_time = id_token.auth_datetime
            if auth_time is None:
                msg = """
a `max_age` parameter was included in the authorization request,
but the ID Token does not contain an `auth_time` claim.
"""
                raise InvalidIdToken(msg, id_token, self) from None
            auth_age = datetime.now(tz=timezone.utc) - auth_time
            if auth_age.total_seconds() > azr.max_age + auth_time_leeway:
                msg = f"""
user authentication happened too far in the past.
The `auth_time` parameter from the ID Token indicate that
the last Authentication Time was at {auth_time} ({auth_age.total_seconds()} sec ago),
but the authorization request `max_age` parameter specified that it must
be a maximum of {azr.max_age} sec ago.
"""
                raise InvalidIdToken(msg, id_token, self)

        return self.__class__(
            access_token=self.access_token,
            expires_at=self.expires_at,
            scope=self.scope,
            refresh_token=self.refresh_token,
            token_type=self.token_type,
            id_token=id_token,
            **self.kwargs,
        )

    def __str__(self) -> str:
        """Return the access token value, as a string.

        Returns:
            the access token string

        """
        return self.access_token

    def as_dict(self) -> dict[str, Any]:
        """Return a dict of parameters.

        That is suitable for serialization or to init another BearerToken.

        """
        d = asdict(self)
        d.pop("expires_at")
        d["expires_in"] = self.expires_in
        d.update(**d.pop("kwargs", {}))
        return {key: val for key, val in d.items() if val is not None}

    @property
    def expires_in(self) -> int | None:
        """Number of seconds until expiration."""
        if self.expires_at:
            return ceil((self.expires_at - datetime.now(tz=timezone.utc)).total_seconds())
        return None

    def __getattr__(self, key: str) -> Any:
        """Return custom attributes from this BearerToken.

        Args:
            key: a key

        Returns:
            the associated value in this token response

        Raises:
            AttributeError: if the attribute is not found in this response.

        """
        return self.kwargs.get(key) or super().__getattribute__(key)

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Implement the usage of Bearer Tokens in requests.

        This will add a properly formatted `Authorization: Bearer <token>` header in the request.

        If the configured token is an instance of BearerToken with an expires_at attribute, raises
        [ExpiredAccessToken][requests_oauth2client.exceptions.ExpiredAccessToken] once the access
        token is expired.

        Args:
            request: the request

        Returns:
            the same request with an Access Token added in `Authorization` Header

        Raises:
            ExpiredAccessToken: if the token is expired

        """
        if self.access_token is None:
            return request  # pragma: no cover
        if self.is_expired():
            raise ExpiredAccessToken(self)
        request.headers[self.AUTHORIZATION_HEADER] = self.authorization_header()
        return request

    @cached_property
    def access_token_jwt(self) -> jwskate.SignedJwt:
        """If the access token is a JWT, return it as an instance of `jwskate.SignedJwt`.

        This method is just a helper for AS testing purposes. Note that, as an OAuth 2.0 Client, you should never have
        to decode or analyze an access token, since it is simply an abstract string value. It is not even mandatory that
        Access Tokens are JWTs, just an implementation choice. Only Resource Servers (APIs) should check for the
        contents of Access Tokens they receive.

        """
        return jwskate.SignedJwt(self.access_token)

expires_in property

Number of seconds until expiration.

access_token_jwt cached property

If the access token is a JWT, return it as an instance of jwskate.SignedJwt.

This method is just a helper for AS testing purposes. Note that, as an OAuth 2.0 Client, you should never have to decode or analyze an access token, since it is simply an abstract string value. It is not even mandatory that Access Tokens are JWTs, just an implementation choice. Only Resource Servers (APIs) should check for the contents of Access Tokens they receive.

is_expired(leeway=0)

Check if the access token is expired.

Parameters:

Name Type Description Default
leeway int

If the token expires in the next given number of seconds, then consider it expired already.

0

Returns:

Type Description
bool | None

One of:

bool | None
  • True if the access token is expired
bool | None
  • False if it is still valid
bool | None
  • None if there is no expires_in hint.
Source code in requests_oauth2client/tokens.py
def is_expired(self, leeway: int = 0) -> bool | None:
    """Check if the access token is expired.

    Args:
        leeway: If the token expires in the next given number of seconds,
            then consider it expired already.

    Returns:
        One of:

        - `True` if the access token is expired
        - `False` if it is still valid
        - `None` if there is no expires_in hint.

    """
    if self.expires_at:
        return datetime.now(tz=timezone.utc) + timedelta(seconds=leeway) > self.expires_at
    return None

authorization_header()

Return the appropriate Authorization Header value for this token.

The value is formatted correctly according to RFC6750.

Returns:

Type Description
str

the value to use in an HTTP Authorization Header

Source code in requests_oauth2client/tokens.py
def authorization_header(self) -> str:
    """Return the appropriate Authorization Header value for this token.

    The value is formatted correctly according to RFC6750.

    Returns:
        the value to use in an HTTP Authorization Header

    """
    return f"{self.AUTHORIZATION_SCHEME} {self.access_token}"

validate_id_token(client, azr, exp_leeway=0, auth_time_leeway=10)

Validate the ID Token, and return a new instance with the decrypted ID Token.

If the ID Token was not encrypted, the returned instance will contain the same ID Token.

This will validate the id_token as described in OIDC 1.0 $3.1.3.7.

Parameters:

Name Type Description Default
client OAuth2Client

the OAuth2Client that was used to obtain this token

required
azr AuthorizationResponse

the AuthorizationResponse, as obtained by a call to AuthorizationRequest.validate()

required
exp_leeway int

a leeway, in seconds, applied to the ID Token expiration date

0
auth_time_leeway int

a leeway, in seconds, applied to the auth_time validation

10

Raises:

Type Description
MissingIdToken

if the ID Token is missing

InvalidIdToken

this is a base exception class, which is raised:

  • if the ID Token is not a JWT
  • or is encrypted while a clear-text token is expected
  • or is clear-text while an encrypted token is expected
  • if token is encrypted but client does not have a decryption key
  • if the token does not contain an alg header
MismatchingIdTokenAlg

if the alg header from the ID Token does not match the expected client.id_token_signed_response_alg.

MismatchingIdTokenIssuer

if the iss claim from the ID Token does not match the expected azr.issuer.

MismatchingIdTokenAcr

if the acr claim from the ID Token does not match on of the expected azr.acr_values.

MismatchingIdTokenAudience

if the aud claim from the ID Token does not match the expected client.client_id.

MismatchingIdTokenAzp

if the azp claim from the ID Token does not match the expected client.client_id.

MismatchingIdTokenNonce

if the nonce claim from the ID Token does not match the expected azr.nonce.

ExpiredIdToken

if the ID Token is expired at the time of the check.

UnsupportedIdTokenAlg

if the signature alg for the ID Token is not supported.

Source code in requests_oauth2client/tokens.py
    def validate_id_token(  # noqa: PLR0915, C901
        self, client: OAuth2Client, azr: AuthorizationResponse, exp_leeway: int = 0, auth_time_leeway: int = 10
    ) -> Self:
        """Validate the ID Token, and return a new instance with the decrypted ID Token.

        If the ID Token was not encrypted, the returned instance will contain the same ID Token.

        This will validate the id_token as described in [OIDC 1.0
        $3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).

        Args:
            client: the `OAuth2Client` that was used to obtain this token
            azr: the `AuthorizationResponse`, as obtained by a call to `AuthorizationRequest.validate()`
            exp_leeway: a leeway, in seconds, applied to the ID Token expiration date
            auth_time_leeway: a leeway, in seconds, applied to the `auth_time` validation

        Raises:
            MissingIdToken: if the ID Token is missing
            InvalidIdToken: this is a base exception class, which is raised:

                - if the ID Token is not a JWT
                - or is encrypted while a clear-text token is expected
                - or is clear-text while an encrypted token is expected
                - if token is encrypted but client does not have a decryption key
                - if the token does not contain an `alg` header
            MismatchingIdTokenAlg: if the `alg` header from the ID Token does not match
                the expected `client.id_token_signed_response_alg`.
            MismatchingIdTokenIssuer: if the `iss` claim from the ID Token does not match
                the expected `azr.issuer`.
            MismatchingIdTokenAcr: if the `acr` claim from the ID Token does not match
                on of the expected `azr.acr_values`.
            MismatchingIdTokenAudience: if the `aud` claim from the ID Token does not match
                the expected `client.client_id`.
            MismatchingIdTokenAzp: if the `azp` claim from the ID Token does not match
                the expected `client.client_id`.
            MismatchingIdTokenNonce: if the `nonce` claim from the ID Token does not match
                the expected `azr.nonce`.
            ExpiredIdToken: if the ID Token is expired at the time of the check.
            UnsupportedIdTokenAlg: if the signature alg for the ID Token is not supported.

        """
        if not self.id_token:
            raise MissingIdToken(self)

        raw_id_token = self.id_token

        if isinstance(raw_id_token, jwskate.JweCompact) and client.id_token_encrypted_response_alg is None:
            msg = "token is encrypted while it should be clear-text"
            raise InvalidIdToken(msg, raw_id_token, self)
        if isinstance(raw_id_token, IdToken) and client.id_token_encrypted_response_alg is not None:
            msg = "token is clear-text while it should be encrypted"
            raise InvalidIdToken(msg, raw_id_token, self)

        if isinstance(raw_id_token, jwskate.JweCompact):
            enc_jwk = client.id_token_decryption_key
            if enc_jwk is None:
                msg = "token is encrypted but client does not have a decryption key"
                raise InvalidIdToken(msg, raw_id_token, self)
            nested_id_token = raw_id_token.decrypt(enc_jwk)
            id_token = IdToken(nested_id_token)
        else:
            id_token = raw_id_token

        id_token_alg = id_token.get_header("alg")
        if id_token_alg is None:
            id_token_alg = client.id_token_signed_response_alg
        if id_token_alg is None:
            msg = """
token does not contain an `alg` parameter to specify the signature algorithm,
and no algorithm has been configured for the client (using param `id_token_signed_response_alg`).
"""
            raise InvalidIdToken(msg, id_token, self)
        if client.id_token_signed_response_alg is not None and id_token_alg != client.id_token_signed_response_alg:
            raise MismatchingIdTokenAlg(id_token.alg, client.id_token_signed_response_alg, self, id_token)

        verification_jwk: jwskate.Jwk

        if id_token_alg in jwskate.SignatureAlgs.ALL_SYMMETRIC:
            if not client.client_secret:
                msg = "token is symmetrically signed but this client does not have a Client Secret."
                raise InvalidIdToken(msg, id_token, self)
            verification_jwk = jwskate.SymmetricJwk.from_bytes(client.client_secret, alg=id_token_alg)
            id_token.verify_signature(verification_jwk, alg=id_token_alg)
        elif id_token_alg in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
            if not client.authorization_server_jwks:
                msg = "token is asymmetrically signed but the Authorization Server JWKS is not available."
                raise InvalidIdToken(msg, id_token, self)

            if id_token.get_header("kid") is None:
                msg = """
token does not contain a Key ID (kid) to specify the asymmetric key
to use for signature verification."""
                raise InvalidIdToken(msg, id_token, self)
            try:
                verification_jwk = client.authorization_server_jwks.get_jwk_by_kid(id_token.kid)
            except KeyError:
                msg = f"""\
token is asymmetrically signed but there is no key
with kid='{id_token.kid}' in the Authorization Server JWKS."""
                raise InvalidIdToken(msg, id_token, self) from None

            if id_token_alg not in verification_jwk.supported_signing_algorithms():
                msg = "token is asymmetrically signed but its algorithm is not supported by the verification key."
                raise InvalidIdToken(msg, id_token, self)
        else:
            raise UnsupportedIdTokenAlg(self, id_token, id_token_alg)

        id_token.verify(verification_jwk, alg=id_token_alg)

        if azr.issuer and id_token.issuer != azr.issuer:
            raise MismatchingIdTokenIssuer(id_token.issuer, azr.issuer, self, id_token)

        if id_token.audiences and client.client_id not in id_token.audiences:
            raise MismatchingIdTokenAudience(id_token.audiences, client.client_id, self, id_token)

        if id_token.authorized_party is not None and id_token.authorized_party != client.client_id:
            raise MismatchingIdTokenAzp(id_token.azp, client.client_id, self, id_token)

        if id_token.is_expired(leeway=exp_leeway):
            raise ExpiredIdToken(self, id_token)

        if azr.nonce and id_token.nonce != azr.nonce:
            raise MismatchingIdTokenNonce(id_token.nonce, azr.nonce, self, id_token)

        if azr.acr_values and id_token.acr not in azr.acr_values:
            raise MismatchingIdTokenAcr(id_token.acr, azr.acr_values, self, id_token)

        hash_function = IdToken.hash_method(verification_jwk, id_token_alg)

        at_hash = id_token.get_claim("at_hash")
        if at_hash is not None:
            expected_at_hash = hash_function(self.access_token)
            if expected_at_hash != at_hash:
                msg = f"mismatching 'at_hash' value (expected '{expected_at_hash}', got '{at_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        c_hash = id_token.get_claim("c_hash")
        if c_hash is not None:
            expected_c_hash = hash_function(azr.code)
            if expected_c_hash != c_hash:
                msg = f"mismatching 'c_hash' value (expected '{expected_c_hash}', got '{c_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        s_hash = id_token.get_claim("s_hash")
        if s_hash is not None:
            if azr.state is None:
                msg = "token has a 's_hash' claim but no state was included in the request."
                raise InvalidIdToken(msg, id_token, self)
            expected_s_hash = hash_function(azr.state)
            if expected_s_hash != s_hash:
                msg = f"mismatching 's_hash' value (expected '{expected_s_hash}', got '{s_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        if azr.max_age is not None:
            auth_time = id_token.auth_datetime
            if auth_time is None:
                msg = """
a `max_age` parameter was included in the authorization request,
but the ID Token does not contain an `auth_time` claim.
"""
                raise InvalidIdToken(msg, id_token, self) from None
            auth_age = datetime.now(tz=timezone.utc) - auth_time
            if auth_age.total_seconds() > azr.max_age + auth_time_leeway:
                msg = f"""
user authentication happened too far in the past.
The `auth_time` parameter from the ID Token indicate that
the last Authentication Time was at {auth_time} ({auth_age.total_seconds()} sec ago),
but the authorization request `max_age` parameter specified that it must
be a maximum of {azr.max_age} sec ago.
"""
                raise InvalidIdToken(msg, id_token, self)

        return self.__class__(
            access_token=self.access_token,
            expires_at=self.expires_at,
            scope=self.scope,
            refresh_token=self.refresh_token,
            token_type=self.token_type,
            id_token=id_token,
            **self.kwargs,
        )

as_dict()

Return a dict of parameters.

That is suitable for serialization or to init another BearerToken.

Source code in requests_oauth2client/tokens.py
def as_dict(self) -> dict[str, Any]:
    """Return a dict of parameters.

    That is suitable for serialization or to init another BearerToken.

    """
    d = asdict(self)
    d.pop("expires_at")
    d["expires_in"] = self.expires_in
    d.update(**d.pop("kwargs", {}))
    return {key: val for key, val in d.items() if val is not None}

BearerTokenSerializer

A helper class to serialize Token Response returned by an AS.

This may be used to store BearerTokens in session or cookies.

It needs a dumper and a loader functions that will respectively serialize and deserialize BearerTokens. Default implementations are provided with use gzip and base64url on the serialized JSON representation.

Parameters:

Name Type Description Default
dumper Callable[[BearerToken], str] | None

a function to serialize a token into a str.

None
loader Callable[[str], BearerToken] | None

a function to deserialize a serialized token representation.

None
Source code in requests_oauth2client/tokens.py
class BearerTokenSerializer:
    """A helper class to serialize Token Response returned by an AS.

    This may be used to store BearerTokens in session or cookies.

    It needs a `dumper` and a `loader` functions that will respectively serialize and deserialize
    BearerTokens. Default implementations are provided with use gzip and base64url on the serialized
    JSON representation.

    Args:
        dumper: a function to serialize a token into a `str`.
        loader: a function to deserialize a serialized token representation.

    """

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

    @staticmethod
    def default_dumper(token: BearerToken) -> str:
        """Serialize a token as JSON, then compress with deflate, then encodes as base64url.

        Args:
            token: the `BearerToken` to serialize

        Returns:
            the serialized value

        """
        d = asdict(token)
        d.update(**d.pop("kwargs", {}))
        return (
            BinaPy.serialize_to("json", {k: w for k, w in d.items() if w is not None}).to("deflate").to("b64u").ascii()
        )

    def default_loader(self, serialized: str, token_class: type[BearerToken] = BearerToken) -> BearerToken:
        """Deserialize a BearerToken.

        This does the opposite operations than `default_dumper`.

        Args:
            serialized: the serialized token
            token_class: class to use to deserialize the Token

        Returns:
            a BearerToken

        """
        attrs = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json")
        expires_at = attrs.get("expires_at")
        if expires_at:
            attrs["expires_at"] = datetime.fromtimestamp(expires_at, tz=timezone.utc)
        return token_class(**attrs)

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

        Args:
            token: a BearerToken to serialize

        Returns:
            the serialized token, as a str

        """
        return self.dumper(token)

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

        Args:
            serialized: the serialized token

        Returns:
            the deserialized token

        """
        return self.loader(serialized)

default_dumper(token) staticmethod

Serialize a token as JSON, then compress with deflate, then encodes as base64url.

Parameters:

Name Type Description Default
token BearerToken

the BearerToken to serialize

required

Returns:

Type Description
str

the serialized value

Source code in requests_oauth2client/tokens.py
@staticmethod
def default_dumper(token: BearerToken) -> str:
    """Serialize a token as JSON, then compress with deflate, then encodes as base64url.

    Args:
        token: the `BearerToken` to serialize

    Returns:
        the serialized value

    """
    d = asdict(token)
    d.update(**d.pop("kwargs", {}))
    return (
        BinaPy.serialize_to("json", {k: w for k, w in d.items() if w is not None}).to("deflate").to("b64u").ascii()
    )

default_loader(serialized, token_class=BearerToken)

Deserialize a BearerToken.

This does the opposite operations than default_dumper.

Parameters:

Name Type Description Default
serialized str

the serialized token

required
token_class type[BearerToken]

class to use to deserialize the Token

BearerToken

Returns:

Type Description
BearerToken

a BearerToken

Source code in requests_oauth2client/tokens.py
def default_loader(self, serialized: str, token_class: type[BearerToken] = BearerToken) -> BearerToken:
    """Deserialize a BearerToken.

    This does the opposite operations than `default_dumper`.

    Args:
        serialized: the serialized token
        token_class: class to use to deserialize the Token

    Returns:
        a BearerToken

    """
    attrs = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json")
    expires_at = attrs.get("expires_at")
    if expires_at:
        attrs["expires_at"] = datetime.fromtimestamp(expires_at, tz=timezone.utc)
    return token_class(**attrs)

dumps(token)

Serialize and compress a given token for easier storage.

Parameters:

Name Type Description Default
token BearerToken

a BearerToken to serialize

required

Returns:

Type Description
str

the serialized token, as a str

Source code in requests_oauth2client/tokens.py
def dumps(self, token: BearerToken) -> str:
    """Serialize and compress a given token for easier storage.

    Args:
        token: a BearerToken to serialize

    Returns:
        the serialized token, as a str

    """
    return self.dumper(token)

loads(serialized)

Deserialize a serialized token.

Parameters:

Name Type Description Default
serialized str

the serialized token

required

Returns:

Type Description
BearerToken

the deserialized token

Source code in requests_oauth2client/tokens.py
def loads(self, serialized: str) -> BearerToken:
    """Deserialize a serialized token.

    Args:
        serialized: the serialized token

    Returns:
        the deserialized token

    """
    return self.loader(serialized)

ExpiredAccessToken

Bases: RuntimeError

Raised when an expired access token is used.

Source code in requests_oauth2client/tokens.py
class ExpiredAccessToken(RuntimeError):
    """Raised when an expired access token is used."""

ExpiredIdToken

Bases: InvalidIdToken

Raised when the returned ID Token is expired.

Source code in requests_oauth2client/tokens.py
class ExpiredIdToken(InvalidIdToken):
    """Raised when the returned ID Token is expired."""

    def __init__(self, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__("token is expired", id_token, token)
        self.received = id_token.expires_at
        self.expected = datetime.now(tz=timezone.utc)

IdToken

Bases: SignedJwt

Represent an ID Token.

An ID Token is actually a Signed JWT. If the ID Token is encrypted, it must be decoded beforehand.

Source code in requests_oauth2client/tokens.py
class IdToken(jwskate.SignedJwt):
    """Represent an ID Token.

    An ID Token is actually a Signed JWT. If the ID Token is encrypted, it must be decoded beforehand.

    """

    @property
    def authorized_party(self) -> str | None:
        """The Authorized Party (azp)."""
        azp = self.claims.get("azp")
        if azp is None or isinstance(azp, str):
            return azp
        msg = "`azp` attribute must be a string."
        raise AttributeError(msg)

    @property
    def auth_datetime(self) -> datetime | None:
        """The last user authentication time (auth_time)."""
        auth_time = self.claims.get("auth_time")
        if auth_time is None:
            return None
        if isinstance(auth_time, int) and auth_time > 0:
            return self.timestamp_to_datetime(auth_time)
        msg = "`auth_time` must be a positive integer"
        raise AttributeError(msg)

    @classmethod
    def hash_method(cls, key: jwskate.Jwk, alg: str | None = None) -> Callable[[str], str]:
        """Returns a callable that generates valid OIDC hashes, such as `at_hash`, `c_hash`, etc.

        Args:
            key: the ID token signature verification public key
            alg: the ID token signature algorithm

        Returns:
            a callable that takes a string as input and produces a valid hash as a str output

        """
        alg_class = jwskate.select_alg_class(key.SIGNATURE_ALGORITHMS, jwk_alg=key.alg, alg=alg)
        if alg_class == jwskate.EdDsa:
            if key.crv == "Ed25519":

                def hash_method(token: str) -> str:
                    return BinaPy(token).to("sha512")[:32].to("b64u").decode()

            elif key.crv == "Ed448":

                def hash_method(token: str) -> str:
                    return BinaPy(token).to("shake256", 456).to("b64u").decode()

        else:
            hash_alg = alg_class.hashing_alg.name
            hash_size = alg_class.hashing_alg.digest_size

            def hash_method(token: str) -> str:
                return BinaPy(token).to(hash_alg)[: hash_size // 2].to("b64u").decode()

        return hash_method

authorized_party property

The Authorized Party (azp).

auth_datetime property

The last user authentication time (auth_time).

hash_method(key, alg=None) classmethod

Returns a callable that generates valid OIDC hashes, such as at_hash, c_hash, etc.

Parameters:

Name Type Description Default
key Jwk

the ID token signature verification public key

required
alg str | None

the ID token signature algorithm

None

Returns:

Type Description
Callable[[str], str]

a callable that takes a string as input and produces a valid hash as a str output

Source code in requests_oauth2client/tokens.py
@classmethod
def hash_method(cls, key: jwskate.Jwk, alg: str | None = None) -> Callable[[str], str]:
    """Returns a callable that generates valid OIDC hashes, such as `at_hash`, `c_hash`, etc.

    Args:
        key: the ID token signature verification public key
        alg: the ID token signature algorithm

    Returns:
        a callable that takes a string as input and produces a valid hash as a str output

    """
    alg_class = jwskate.select_alg_class(key.SIGNATURE_ALGORITHMS, jwk_alg=key.alg, alg=alg)
    if alg_class == jwskate.EdDsa:
        if key.crv == "Ed25519":

            def hash_method(token: str) -> str:
                return BinaPy(token).to("sha512")[:32].to("b64u").decode()

        elif key.crv == "Ed448":

            def hash_method(token: str) -> str:
                return BinaPy(token).to("shake256", 456).to("b64u").decode()

    else:
        hash_alg = alg_class.hashing_alg.name
        hash_size = alg_class.hashing_alg.digest_size

        def hash_method(token: str) -> str:
            return BinaPy(token).to(hash_alg)[: hash_size // 2].to("b64u").decode()

    return hash_method

InvalidIdToken

Bases: ValueError

Raised when trying to validate an invalid ID Token value.

Source code in requests_oauth2client/tokens.py
class InvalidIdToken(ValueError):
    """Raised when trying to validate an invalid ID Token value."""

    def __init__(
        self,
        message: str,
        id_token: IdToken | jwskate.JweCompact | object | None = None,
        token_resp: TokenResponse | None = None,
    ) -> None:
        super().__init__(f"Invalid ID Token: {message}")
        self.id_token = id_token
        self.token_resp = token_resp

MismatchingIdTokenAcr

Bases: InvalidIdToken

Raised when the returned ID Token doesn't contain one of the requested ACR Values.

This happens when the authorization request includes an acr_values parameter but the returned ID Token includes a different value.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenAcr(InvalidIdToken):
    """Raised when the returned ID Token doesn't contain one of the requested ACR Values.

    This happens when the authorization request includes an `acr_values` parameter but the returned
    ID Token includes a different value.

    """

    def __init__(self, acr: str, expected: Sequence[str], token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(f"token contains acr '{acr}' while client expects one of '{expected}'", id_token, token)
        self.received = acr
        self.expected = expected

MismatchingIdTokenAlg

Bases: InvalidIdToken

Raised when the returned ID Token is signed with an unexpected alg.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenAlg(InvalidIdToken):
    """Raised when the returned ID Token is signed with an unexpected alg."""

    def __init__(self, token_alg: str, client_alg: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(f"token is signed with alg {token_alg}, client expects {client_alg}", id_token, token)
        self.received = token_alg
        self.expected = client_alg

MismatchingIdTokenAudience

Bases: InvalidIdToken

Raised when the ID Token audience does not include the requesting Client ID.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenAudience(InvalidIdToken):
    """Raised when the ID Token audience does not include the requesting Client ID."""

    def __init__(self, audiences: Sequence[str], client_id: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(
            f"token audience (`aud`) '{audiences}' does not match client_id '{client_id}'",
            id_token,
            token,
        )
        self.received = audiences
        self.expected = client_id

MismatchingIdTokenAzp

Bases: InvalidIdToken

Raised when the ID Token Authorized Presenter (azp) claim is not the Client ID.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenAzp(InvalidIdToken):
    """Raised when the ID Token Authorized Presenter (azp) claim is not the Client ID."""

    def __init__(self, azp: str, client_id: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(
            f"token Authorized Presenter (`azp`) claim '{azp}' does not match client_id '{client_id}'", id_token, token
        )
        self.received = azp
        self.expected = client_id

MismatchingIdTokenIssuer

Bases: InvalidIdToken

Raised on mismatching iss value in an ID Token.

This happens when the expected issuer value is different from the iss value in an obtained ID Token.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenIssuer(InvalidIdToken):
    """Raised on mismatching `iss` value in an ID Token.

    This happens when the expected `issuer` value is different from the `iss` value in an obtained ID Token.

    """

    def __init__(self, iss: str | None, expected: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(f"`iss` from token '{iss}' does not match expected value '{expected}'", id_token, token)
        self.received = iss
        self.expected = expected

MismatchingIdTokenNonce

Bases: InvalidIdToken

Raised on mismatching nonce value in an ID Token.

This happens when the authorization request includes a nonce but the returned ID Token include a different value.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenNonce(InvalidIdToken):
    """Raised on mismatching `nonce` value in an ID Token.

    This happens when the authorization request includes a `nonce` but the returned ID Token include
    a different value.

    """

    def __init__(self, nonce: str, expected: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(f"nonce from token '{nonce}' does not match expected value '{expected}'", id_token, token)
        self.received = nonce
        self.expected = expected

MissingIdToken

Bases: InvalidIdToken

Raised when the Authorization Endpoint does not return a mandatory ID Token.

This happens when the Authorization Endpoint does not return an error, but does not return an ID Token either.

Source code in requests_oauth2client/tokens.py
class MissingIdToken(InvalidIdToken):
    """Raised when the Authorization Endpoint does not return a mandatory ID Token.

    This happens when the Authorization Endpoint does not return an error, but does not return an ID Token either.

    """

    def __init__(self, token: TokenResponse) -> None:
        super().__init__("An expected `id_token` is missing in the response.", token, None)

InvalidUri

Bases: ValueError

Raised when a URI does not pass validation by validate_endpoint_uri().

Source code in requests_oauth2client/utils.py
class InvalidUri(ValueError):
    """Raised when a URI does not pass validation by `validate_endpoint_uri()`."""

    def __init__(
        self, url: str, *, https: bool, no_credentials: bool, no_port: bool, no_fragment: bool, path: bool
    ) -> None:
        super().__init__("Invalid endpoint uri.")
        self.url = url
        self.https = https
        self.no_credentials = no_credentials
        self.no_port = no_port
        self.no_fragment = no_fragment
        self.path = path

    def errors(self) -> Iterator[str]:
        """Iterate over all error descriptions, as str."""
        if self.https:
            yield "must use https"
        if self.no_credentials:
            yield "must not contain basic credentials"
        if self.no_port:
            yield "no custom port number allowed"
        if self.no_fragment:
            yield "must not contain a uri fragment"
        if self.path:
            yield "must include a path other than /"

    def __str__(self) -> str:
        all_errors = ", ".join(self.errors())
        return f"Invalid URI: {all_errors}"

errors()

Iterate over all error descriptions, as str.

Source code in requests_oauth2client/utils.py
def errors(self) -> Iterator[str]:
    """Iterate over all error descriptions, as str."""
    if self.https:
        yield "must use https"
    if self.no_credentials:
        yield "must not contain basic credentials"
    if self.no_port:
        yield "no custom port number allowed"
    if self.no_fragment:
        yield "must not contain a uri fragment"
    if self.path:
        yield "must include a path other than /"

oauth2_discovery_document_url(issuer)

Construct the standardised OAuth 2.0 discovery document url for a given issuer.

Based an issuer identifier, returns the standardised URL where the OAuth20 server metadata can be retrieved.

The returned URL is built as specified in RFC8414.

Parameters:

Name Type Description Default
issuer str

an OAuth20 Authentication Server issuer

required

Returns:

Type Description
str

the standardised discovery document URL. Note that no attempt to fetch this document is

str

made.

Source code in requests_oauth2client/discovery.py
def oauth2_discovery_document_url(issuer: str) -> str:
    """Construct the standardised OAuth 2.0 discovery document url for a given `issuer`.

    Based an `issuer` identifier, returns the standardised URL where the OAuth20 server metadata can
    be retrieved.

    The returned URL is built as specified in
    [RFC8414](https://datatracker.ietf.org/doc/html/rfc8414).

    Args:
        issuer: an OAuth20 Authentication Server `issuer`

    Returns:
        the standardised discovery document URL. Note that no attempt to fetch this document is
        made.

    """
    return well_known_uri(issuer, "oauth-authorization-server", at_root=True)

oidc_discovery_document_url(issuer)

Construct the OIDC discovery document url for a given issuer.

Given an issuer identifier, return the standardised URL where the OIDC discovery document can be retrieved.

The returned URL is biuilt as specified in OpenID Connect Discovery 1.0.

Parameters:

Name Type Description Default
issuer str

an OIDC Authentication Server issuer

required

Returns:

Type Description
str

the standardised discovery document URL. Note that no attempt to fetch this document is

str

made.

Source code in requests_oauth2client/discovery.py
def oidc_discovery_document_url(issuer: str) -> str:
    """Construct the OIDC discovery document url for a given `issuer`.

    Given an `issuer` identifier, return the standardised URL where the OIDC discovery document can
    be retrieved.

    The returned URL is biuilt as specified in [OpenID Connect Discovery
    1.0](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata).

    Args:
        issuer: an OIDC Authentication Server `issuer`

    Returns:
        the standardised discovery document URL. Note that no attempt to fetch this document is
        made.

    """
    return well_known_uri(issuer, "openid-configuration", at_root=False)

well_known_uri(origin, name, *, at_root=True)

Return the location of a well-known document on an origin url.

See RFC8615 and OIDC Discovery.

Parameters:

Name Type Description Default
origin str

origin to use to build the well-known uri.

required
name str

document name to use to build the well-known uri.

required
at_root bool

if True, assume the well-known document is at root level (as defined in RFC8615). If False, assume the well-known location is per-directory, as defined in OpenID Connect Discovery 1.0.

True

Returns:

Type Description
str

the well-know uri, relative to origin, where the well-known document named name should be

str

found.

Source code in requests_oauth2client/discovery.py
def well_known_uri(origin: str, name: str, *, at_root: bool = True) -> str:
    """Return the location of a well-known document on an origin url.

    See [RFC8615](https://datatracker.ietf.org/doc/html/rfc8615) and [OIDC
    Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata).

    Args:
        origin: origin to use to build the well-known uri.
        name: document name to use to build the well-known uri.
        at_root: if `True`, assume the well-known document is at root level (as defined in [RFC8615](https://datatracker.ietf.org/doc/html/rfc8615)).
            If `False`, assume the well-known location is per-directory, as defined in [OpenID
            Connect Discovery
            1.0](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata).

    Returns:
        the well-know uri, relative to origin, where the well-known document named `name` should be
        found.

    """
    url = furl(origin)
    if at_root:
        url.path = Path(".well-known") / url.path / name
    else:
        url.path.add(Path(".well-known") / name)
    return str(url)

validate_dpop_proof(proof, *, htm, htu, ath=None, nonce=None, leeway=60, alg=None, algs=())

Validate a DPoP proof.

Parameters:

Name Type Description Default
proof str | bytes

The serialized DPoP proof.

required
htm str

The value of the HTTP method of the request to which the JWT is attached.

required
htu str

The HTTP target URI of the request to which the JWT is attached, without query and fragment parts.

required
ath str | None

The Hash of the access token.

None
nonce str | None

A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS.

None
leeway int

A leeway, in number of seconds, to validate the proof iat claim.

60
alg str | None

Allowed signature alg, if there is only one. Use this or algs.

None
algs Sequence[str]

Allowed signature algs, if there is several. Use this or alg.

()

Returns:

Type Description
SignedJwt

The validated DPoP proof, as a SignedJwt.

Source code in requests_oauth2client/dpop.py
def validate_dpop_proof(  # noqa: C901
    proof: str | bytes,
    *,
    htm: str,
    htu: str,
    ath: str | None = None,
    nonce: str | None = None,
    leeway: int = 60,
    alg: str | None = None,
    algs: Sequence[str] = (),
) -> jwskate.SignedJwt:
    """Validate a DPoP proof.

    Args:
        proof: The serialized DPoP proof.
        htm: The value of the HTTP method of the request to which the JWT is attached.
        htu: The HTTP target URI of the request to which the JWT is attached, without query and fragment parts.
        ath: The Hash of the access token.
        nonce: A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS.
        leeway: A leeway, in number of seconds, to validate the proof `iat` claim.
        alg: Allowed signature alg, if there is only one. Use this or `algs`.
        algs: Allowed signature algs, if there is several. Use this or `alg`.

    Returns:
        The validated DPoP proof, as a `SignedJwt`.

    """
    if not isinstance(proof, bytes):
        proof = proof.encode()
    try:
        proof_jwt = jwskate.SignedJwt(proof)
    except jwskate.InvalidJwt as exc:
        raise InvalidDPoPProof(proof, "not a syntactically valid JWT") from exc
    if proof_jwt.typ != "dpop+jwt":
        raise InvalidDPoPProof(proof, f"typ '{proof_jwt.typ}' is not the expected 'dpop+jwt'.")
    if "jwk" not in proof_jwt.headers:
        raise InvalidDPoPProof(proof, "'jwk' header is missing")
    try:
        public_jwk = jwskate.Jwk(proof_jwt.headers["jwk"])
    except jwskate.InvalidJwk as exc:
        raise InvalidDPoPProof(proof, "'jwk' header is not a valid JWK key.") from exc
    if public_jwk.is_private or public_jwk.is_symmetric:
        raise InvalidDPoPProof(proof, "'jwk' header is a private or symmetric key.")

    if not proof_jwt.verify_signature(public_jwk, alg=alg, algs=algs):
        raise InvalidDPoPProof(proof, "signature does not verify.")

    if proof_jwt.issued_at is None:
        raise InvalidDPoPProof(proof, "a Issued At (iat) claim is missing.")
    now = datetime.now(tz=timezone.utc)
    if not now - timedelta(seconds=leeway) < proof_jwt.issued_at < now + timedelta(seconds=leeway):
        msg = f"""\
Issued At timestamp (iat) is too far away in the past or future (received: {proof_jwt.issued_at}, now: {now})."""
        raise InvalidDPoPProof(
            proof,
            msg,
        )
    if proof_jwt.jwt_token_id is None:
        raise InvalidDPoPProof(proof, "a Unique Identifier (jti) claim is missing.")
    if "htm" not in proof_jwt.claims:
        raise InvalidDPoPProof(proof, "the HTTP method (htm) claim is missing.")
    if proof_jwt.htm != htm:
        raise InvalidDPoPProof(proof, f"HTTP Method (htm) '{proof_jwt.htm}' does not matches expected '{htm}'.")
    if "htu" not in proof_jwt.claims:
        raise InvalidDPoPProof(proof, "the HTTP URI (htu) claim is missing.")
    if proof_jwt.htu != htu:
        raise InvalidDPoPProof(proof, f"HTTP URI (htu) '{proof_jwt.htu}' does not matches expected '{htu}'.")
    if ath:
        if "ath" not in proof_jwt.claims:
            raise InvalidDPoPProof(proof, "the Access Token hash (ath) claim is missing.")
        if proof_jwt.ath != ath:
            raise InvalidDPoPProof(
                proof, f"Access Token Hash (ath) value '{proof_jwt.ath}' does not match expected '{ath}'."
            )
    if nonce:
        if "nonce" not in proof_jwt.claims:
            raise InvalidDPoPProof(proof, "the DPoP Nonce (nonce) claim is missing.")
        if proof_jwt.nonce != nonce:
            raise InvalidDPoPProof(
                proof, f"DPoP Nonce (nonce) value '{proof_jwt.nonce}' does not match expected '{nonce}'."
            )

    return proof_jwt

validate_endpoint_uri(uri, *, https=True, no_credentials=True, no_port=False, no_fragment=True, path=True)

Validate that a URI is suitable as an endpoint URI.

It checks:

  • that the scheme is https
  • that no custom port number is being used
  • that no username or password are included
  • that no fragment is included
  • that a path is present

Those checks can be individually disabled by using the parameters.

Parameters:

Name Type Description Default
uri str

the uri

required
https bool

if True, check that the uri is https

True
no_port bool

if True, check that no custom port number is included

False
no_credentials bool

if True, check that no username/password are included

True
no_fragment bool

if True, check that the uri contains no fragment

True
path bool

if True, check that the uri contains a path component

True

Raises:

Type Description
ValueError

if the supplied url is not suitable

Returns:

Type Description
str

the endpoint URI, if all checks passed

Source code in requests_oauth2client/utils.py
def validate_endpoint_uri(
    uri: str,
    *,
    https: bool = True,
    no_credentials: bool = True,
    no_port: bool = False,
    no_fragment: bool = True,
    path: bool = True,
) -> str:
    """Validate that a URI is suitable as an endpoint URI.

    It checks:

    - that the scheme is `https`
    - that no custom port number is being used
    - that no username or password are included
    - that no fragment is included
    - that a path is present

    Those checks can be individually disabled by using the parameters.

    Args:
        uri: the uri
        https: if `True`, check that the uri is https
        no_port: if `True`, check that no custom port number is included
        no_credentials: if ` True`, check that no username/password are included
        no_fragment: if `True`, check that the uri contains no fragment
        path: if `True`, check that the uri contains a path component

    Raises:
        ValueError: if the supplied url is not suitable

    Returns:
        the endpoint URI, if all checks passed

    """
    url = furl(uri)
    if https and url.scheme == "https":
        https = False
    if no_port and url.port == 443:  # noqa: PLR2004
        no_port = False
    if no_credentials and not url.username and not url.password:
        no_credentials = False
    if no_fragment and not url.fragment:
        no_fragment = False
    if path and url.path and url.path != "/":
        path = False

    if https or no_port or no_credentials or no_fragment or path:
        raise InvalidUri(
            uri, https=https, no_port=no_port, no_credentials=no_credentials, no_fragment=no_fragment, path=path
        )

    return uri

validate_issuer_uri(uri)

Validate that an Issuer Identifier URI is valid.

This is almost the same as a valid endpoint URI, but a path is not mandatory.

Source code in requests_oauth2client/utils.py
def validate_issuer_uri(uri: str) -> str:
    """Validate that an Issuer Identifier URI is valid.

    This is almost the same as a valid endpoint URI, but a path is not mandatory.

    """
    return validate_endpoint_uri(uri, path=False)

api_client

ApiClient main module.

InvalidBoolFieldsParam

Bases: ValueError

Raised when an invalid value is passed as 'bool_fields' parameter.

Source code in requests_oauth2client/api_client.py
class InvalidBoolFieldsParam(ValueError):
    """Raised when an invalid value is passed as 'bool_fields' parameter."""

    def __init__(self, bool_fields: object) -> None:
        super().__init__("""\
Invalid value for `bool_fields` parameter. It must be an iterable of 2 `str` values:
- first one for the `True` value,
- second one for the `False` value.
Boolean fields in `data` or `params` with a boolean value (`True` or `False`)
will be serialized to the corresponding value.
Default is `('true', 'false')`
Use this parameter when the target API expects some other values, e.g.:
- ('on', 'off')
- ('1', '0')
- ('yes', 'no')
""")
        self.value = bool_fields

InvalidPathParam

Bases: TypeError, ValueError

Raised when an unexpected path is passed as 'url' parameter.

Source code in requests_oauth2client/api_client.py
class InvalidPathParam(TypeError, ValueError):
    """Raised when an unexpected path is passed as 'url' parameter."""

    def __init__(self, path: None | str | bytes | Iterable[str | bytes | int]) -> None:
        super().__init__("""\
Unexpected path. Please provide a path that is relative to the configured `base_url`:
- `None` (default) to call the base_url
- a `str` or `bytes`, that will be joined to the base_url (with a / separator, if required)
- or an iterable of string-able objects, which will be joined to the base_url with / separators
""")
        self.url = path

ApiClient

A Wrapper around requests.Session with extra features for REST API calls.

Additional features compared to using a requests.Session directly:

  • You must set a root url at creation time, which then allows passing relative urls at request time.
  • It may also raise exceptions instead of returning error responses.
  • You can also pass additional kwargs at init time, which will be used to configure the Session, instead of setting them later.
  • for parameters passed as json, params or data, values that are None can be automatically discarded from the request
  • boolean values in data or params fields can be serialized to values that are suitable for the target API, like "true" or "false", or "1" / "0", instead of the default values "True" or "False",
  • you may pass cookies and headers, which will be added to the session cookie handler or request headers respectively.
  • you may use the user_agent parameter to change the User-Agent header easily. Set it to None to remove that header.

base_url will serve as root for relative urls passed to ApiClient.request(), ApiClient.get(), etc.

A requests.HTTPError will be raised everytime an API call returns an error code (>= 400), unless you set raise_for_status to False. Additional parameters passed at init time, including auth will be used to configure the Session.

Example
from requests_oauth2client import ApiClient

api = ApiClient("https://myapi.local/resource", timeout=10)
resp = api.get("/myid")  # this will send a GET request
# to https://myapi.local/resource/myid

# you can pass an underlying requests.Session at init time
session = requests.Session()
session.proxies = {"https": "https://localhost:3128"}
api = ApiClient("https://myapi.local/resource", session=session)

# or you can let ApiClient init its own session and provide additional configuration
# parameters:
api = ApiClient(
    "https://myapi.local/resource",
    proxies={"https": "https://localhost:3128"},
)

Parameters:

Name Type Description Default
base_url str

the base api url, that is the root for all the target API endpoints.

required
auth AuthBase | None

the requests.auth.AuthBase to use as authentication handler.

None
timeout int | None

the default timeout, in seconds, to use for each request from this ApiClient. Can be set to None to disable timeout.

60
raise_for_status bool

if True, exceptions will be raised everytime a request returns an error code (>= 400).

True
none_fields Literal['include', 'exclude', 'empty']

defines what to do with parameters with value None in data or json fields.

  • if "exclude" (default), fields whose values are None are not included in the request.
  • if "include", they are included with string value None. This is the default behavior of requests. Note that they will be serialized to null in JSON.
  • if "empty", they are included with an empty value (as an empty string).
'exclude'
bool_fields tuple[Any, Any] | None

a tuple of (true_value, false_value). Fields from data or params with a boolean value (True or False) will be serialized to the corresponding value. This can be useful since some APIs expect a 'true' or 'false' value as boolean, and requests serializes True to 'True' and False to 'False'. Set it to None to restore default requests behavior.

('true', 'false')
cookies Mapping[str, Any] | None

a mapping of cookies to set in the underlying requests.Session.

None
headers Mapping[str, Any] | None

a mapping of headers to set in the underlying requests.Session.

None
session Session | None

a preconfigured requests.Session to use with this ApiClient.

None
**session_kwargs Any

additional kwargs to configure the underlying requests.Session.

{}

Raises:

Type Description
InvalidBoolFieldsParam

if the provided bool_fields parameter is invalid.

Source code in requests_oauth2client/api_client.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
@frozen(init=False)
class ApiClient:
    """A Wrapper around [requests.Session][] with extra features for REST API calls.

    Additional features compared to using a [requests.Session][] directly:

    - You must set a root url at creation time, which then allows passing relative urls at request time.
    - It may also raise exceptions instead of returning error responses.
    - You can also pass additional kwargs at init time, which will be used to configure the
    [Session][requests.Session], instead of setting them later.
    - for parameters passed as `json`, `params` or `data`, values that are `None` can be
    automatically discarded from the request
    - boolean values in `data` or `params` fields can be serialized to values that are suitable
    for the target API, like `"true"`  or `"false"`, or `"1"` / `"0"`, instead of the default
    values `"True"` or `"False"`,
    - you may pass `cookies` and `headers`, which will be added to the session cookie handler or
    request headers respectively.
    - you may use the `user_agent` parameter to change the `User-Agent` header easily. Set it to
      `None` to remove that header.

    `base_url` will serve as root for relative urls passed to
    [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request],
    [ApiClient.get()][requests_oauth2client.api_client.ApiClient.get], etc.

    A [requests.HTTPError][] will be raised everytime an API call returns an error code (>= 400), unless
    you set `raise_for_status` to `False`. Additional parameters passed at init time, including
    `auth` will be used to configure the [Session][requests.Session].

    Example:
        ```python
        from requests_oauth2client import ApiClient

        api = ApiClient("https://myapi.local/resource", timeout=10)
        resp = api.get("/myid")  # this will send a GET request
        # to https://myapi.local/resource/myid

        # you can pass an underlying requests.Session at init time
        session = requests.Session()
        session.proxies = {"https": "https://localhost:3128"}
        api = ApiClient("https://myapi.local/resource", session=session)

        # or you can let ApiClient init its own session and provide additional configuration
        # parameters:
        api = ApiClient(
            "https://myapi.local/resource",
            proxies={"https": "https://localhost:3128"},
        )
        ```

    Args:
        base_url: the base api url, that is the root for all the target API endpoints.
        auth: the [requests.auth.AuthBase][] to use as authentication handler.
        timeout: the default timeout, in seconds, to use for each request from this `ApiClient`.
            Can be set to `None` to disable timeout.
        raise_for_status: if `True`, exceptions will be raised everytime a request returns an
            error code (>= 400).
        none_fields: defines what to do with parameters with value `None` in `data` or `json` fields.

            - if `"exclude"` (default), fields whose values are `None` are not included in the request.
            - if `"include"`, they are included with string value `None`. This is
            the default behavior of `requests`. Note that they will be serialized to `null` in JSON.
            - if `"empty"`, they are included with an empty value (as an empty string).
        bool_fields: a tuple of `(true_value, false_value)`. Fields from `data` or `params` with
            a boolean value (`True` or `False`) will be serialized to the corresponding value.
            This can be useful since some APIs expect a `'true'` or `'false'` value as boolean,
            and `requests` serializes `True` to `'True'` and `False` to `'False'`.
            Set it to `None` to restore default requests behavior.
        cookies: a mapping of cookies to set in the underlying `requests.Session`.
        headers: a mapping of headers to set in the underlying `requests.Session`.
        session: a preconfigured `requests.Session` to use with this `ApiClient`.
        **session_kwargs: additional kwargs to configure the underlying `requests.Session`.

    Raises:
        InvalidBoolFieldsParam: if the provided `bool_fields` parameter is invalid.

    """

    base_url: str
    auth: requests.auth.AuthBase | None
    timeout: int | None
    raise_for_status: bool
    none_fields: Literal["include", "exclude", "empty"]
    bool_fields: tuple[Any, Any] | None
    session: requests.Session

    def __init__(
        self,
        base_url: str,
        *,
        auth: requests.auth.AuthBase | None = None,
        timeout: int | None = 60,
        raise_for_status: bool = True,
        none_fields: Literal["include", "exclude", "empty"] = "exclude",
        bool_fields: tuple[Any, Any] | None = ("true", "false"),
        cookies: Mapping[str, Any] | None = None,
        headers: Mapping[str, Any] | None = None,
        user_agent: str | None = requests.utils.default_user_agent(),
        session: requests.Session | None = None,
        **session_kwargs: Any,
    ) -> None:
        session = session or requests.Session()

        if cookies:
            for key, val in cookies.items():
                session.cookies[key] = str(val)

        if headers:
            for key, val in headers.items():
                session.headers[key] = str(val)

        if user_agent is None:
            session.headers.pop("User-Agent", None)
        else:
            session.headers["User-Agent"] = str(user_agent)

        for key, val in session_kwargs.items():
            setattr(session, key, val)

        if bool_fields is None:
            bool_fields = ("True", "False")
        else:
            validate_bool_fields(bool_fields)

        self.__attrs_init__(
            base_url=base_url,
            auth=auth,
            raise_for_status=raise_for_status,
            none_fields=none_fields,
            bool_fields=bool_fields,
            timeout=timeout,
            session=session,
        )

    def request(  # noqa: C901, PLR0913, D417
        self,
        method: str,
        path: None | str | bytes | Iterable[str | bytes | int] = None,
        *,
        params: None | bytes | MutableMapping[str, str] = None,
        data: (
            Iterable[bytes]
            | str
            | bytes
            | list[tuple[Any, Any]]
            | tuple[tuple[Any, Any], ...]
            | Mapping[Any, Any]
            | None
        ) = None,
        headers: MutableMapping[str, str] | None = None,
        cookies: None | RequestsCookieJar | MutableMapping[str, str] = None,
        files: MutableMapping[str, IO[Any]] | None = None,
        auth: (
            None
            | tuple[str, str]
            | requests.auth.AuthBase
            | Callable[[requests.PreparedRequest], requests.PreparedRequest]
        ) = None,
        timeout: None | float | tuple[float, float] | tuple[float, None] = None,
        allow_redirects: bool = False,
        proxies: MutableMapping[str, str] | None = None,
        hooks: None
        | (
            MutableMapping[
                str,
                (Iterable[Callable[[requests.Response], Any]] | Callable[[requests.Response], Any]),
            ]
        ) = None,
        stream: bool | None = None,
        verify: str | bool | None = None,
        cert: str | tuple[str, str] | None = None,
        json: Mapping[str, Any] | None = None,
        raise_for_status: bool | None = None,
        none_fields: Literal["include", "exclude", "empty"] | None = None,
        bool_fields: tuple[Any, Any] | None = None,
    ) -> requests.Response:
        """A wrapper around [requests.Session.request][] method with extra features.

        Additional features are described in
        [ApiClient][requests_oauth2client.api_client.ApiClient] documentation.

        All parameters will be passed as-is to [requests.Session.request][], expected those
        described below which have a special behavior.

        Args:
          path: the url where the request will be sent to. Can be:

            - a path, as `str`: that path will be joined to the configured API url,
            - an iterable of path segments: that will be joined to the root url.
          raise_for_status: like the parameter of the same name from
            [ApiClient][requests_oauth2client.api_client.ApiClient],
            but this will be applied for this request only.
          none_fields: like the parameter of the same name from
            [ApiClient][requests_oauth2client.api_client.ApiClient],
            but this will be applied for this request only.
          bool_fields: like the parameter of the same name from
            [ApiClient][requests_oauth2client.api_client.ApiClient],
            but this will be applied for this request only.

        Returns:
          a Response as returned by requests

        Raises:
            InvalidBoolFieldsParam: if the provided `bool_fields` parameter is invalid.

        """
        path = self.to_absolute_url(path)

        if none_fields is None:
            none_fields = self.none_fields

        if none_fields == "exclude":
            if isinstance(data, Mapping):
                data = {key: val for key, val in data.items() if val is not None}
            if isinstance(json, Mapping):
                json = {key: val for key, val in json.items() if val is not None}
        elif none_fields == "empty":
            if isinstance(data, Mapping):
                data = {key: val if val is not None else "" for key, val in data.items()}
            if isinstance(json, Mapping):
                json = {key: val if val is not None else "" for key, val in json.items()}

        if bool_fields is None:
            bool_fields = self.bool_fields

        if bool_fields:
            true_value, false_value = validate_bool_fields(bool_fields)
            if isinstance(data, MutableMapping):
                for key, val in data.items():
                    if val is True:
                        data[key] = true_value
                    elif val is False:
                        data[key] = false_value
            if isinstance(params, MutableMapping):
                for key, val in params.items():
                    if val is True:
                        params[key] = true_value
                    elif val is False:
                        params[key] = false_value

        timeout = timeout or self.timeout

        response = self.session.request(
            method,
            path,
            params=params,
            data=data,
            headers=headers,
            cookies=cookies,
            files=files,
            auth=auth or self.auth,
            timeout=timeout,
            allow_redirects=allow_redirects,
            proxies=proxies,
            hooks=hooks,
            stream=stream,
            verify=verify,
            cert=cert,
            json=json,
        )

        if raise_for_status is None:
            raise_for_status = self.raise_for_status
        if raise_for_status:
            response.raise_for_status()
        return response

    def to_absolute_url(self, path: None | str | bytes | Iterable[str | bytes | int] = None) -> str:
        """Convert a relative url to an absolute url.

        Given a `path`, return the matching absolute url, based on the `base_url` that is
        configured for this API.

        The result of this method is different from a standard `urljoin()`, because a relative_url
        that starts with a "/" will not override the path from the base url. You can also pass an
        iterable of path parts as relative url, which will be properly joined with "/". Those parts
        may be `str` (which will be urlencoded) or `bytes` (which will be decoded as UTF-8 first) or
        any other type (which will be converted to `str` first, using the `str() function`). See the
        table below for example results which would exhibit most cases:

        | base_url | relative_url | result_url |
        |---------------------------|-----------------------------|-------------------------------------------|
        | `"https://myhost.com/root"` | `"/path"` | `"https://myhost.com/root/path"` |
        | `"https://myhost.com/root"` | `"/path"` | `"https://myhost.com/root/path"` |
        | `"https://myhost.com/root"` | `b"/path"` | `"https://myhost.com/root/path"` |
        | `"https://myhost.com/root"` | `"path"` | `"https://myhost.com/root/path"` |
        | `"https://myhost.com/root"` | `None` | `"https://myhost.com/root"` |
        | `"https://myhost.com/root"` |  `("user", 1, "resource")` | `"https://myhost.com/root/user/1/resource"` |
        | `"https://myhost.com/root"` | `"https://otherhost.org/foo"` | `ValueError` |

        Args:
          path: a relative url

        Returns:
          the resulting absolute url

        Raises:
            InvalidPathParam: if the provided path does not allow constructing a valid url

        """
        url = path

        if url is None:
            url = self.base_url
        else:
            if not isinstance(url, (str, bytes)):
                try:
                    url = "/".join(
                        [urlencode(part.decode() if isinstance(part, bytes) else str(part)) for part in url if part],
                    )
                except Exception as exc:
                    raise InvalidPathParam(url) from exc

            if isinstance(url, bytes):
                url = url.decode()

            if "://" in url:
                raise InvalidPathParam(url)

            url = urljoin(self.base_url + "/", url.lstrip("/"))

        if url is None or not isinstance(url, str):
            raise InvalidPathParam(url)  # pragma: no cover

        return url

    def get(
        self,
        path: None | str | bytes | Iterable[str | bytes | int] = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a GET request and return a [Response][requests.Response] object.

        The passed `url` is relative to the `base_url` passed at initialization time.
        It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
            path: the path where the request will be sent.
            raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
            **kwargs: additional kwargs for `requests.request()`

        Returns:
            a response object.

        Raises:
            requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("GET", path, raise_for_status=raise_for_status, **kwargs)

    def post(
        self,
        path: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a POST request and return a [Response][requests.Response] object.

        The passed `url` is relative to the `base_url` passed at initialization time.
        It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
          path: the path where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: additional kwargs for `requests.request()`

        Returns:
          a response object.

        Raises:
          requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("POST", path, raise_for_status=raise_for_status, **kwargs)

    def patch(
        self,
        path: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a PATCH request. Return a [Response][requests.Response] object.

        The passed `url` is relative to the `base_url` passed at initialization time.
        It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
          path: the path where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: additional kwargs for `requests.request()`

        Returns:
          a [Response][requests.Response] object.

        Raises:
          requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("PATCH", path, raise_for_status=raise_for_status, **kwargs)

    def put(
        self,
        path: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a PUT request. Return a [Response][requests.Response] object.

        The passed `url` is relative to the `base_url` passed at initialization time.
        It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
          path: the path where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: additional kwargs for `requests.request()`

        Returns:
          a [Response][requests.Response] object.

        Raises:
          requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("PUT", path, raise_for_status=raise_for_status, **kwargs)

    def delete(
        self,
        path: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a DELETE request. Return a [Response][requests.Response] object.

        The passed `url` may be relative to the url passed at initialization time. It takes the same
        parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

        Args:
          path: the path where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: additional kwargs for `requests.request()`.

        Returns:
          a response object.

        Raises:
          requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

        """
        return self.request("DELETE", path, raise_for_status=raise_for_status, **kwargs)

    def __getattr__(self, item: str) -> ApiClient:
        """Allow access sub resources with an attribute-based syntax.

        Args:
            item: a subpath

        Returns:
            a new `ApiClient` initialized on the new base url

        Example:
            ```python
            from requests_oauth2client import ApiClient

            api = ApiClient("https://myapi.local")
            resource1 = api.resource1.get()  # GET https://myapi.local/resource1
            resource2 = api.resource2.get()  # GET https://myapi.local/resource2
            ```

        """
        return self[item]

    def __getitem__(self, item: str) -> ApiClient:
        """Allow access to sub resources with a subscription-based syntax.

        Args:
            item: a subpath

        Returns:
            a new `ApiClient` initialized on the new base url

        Example:
            ```python
            from requests_oauth2client import ApiClient

            api = ApiClient("https://myapi.local")
            resource1 = api["resource1"].get()  # GET https://myapi.local/resource1
            resource2 = api["resource2"].get()  # GET https://myapi.local/resource2
            ```

        """
        new_base_uri = self.to_absolute_url(item)
        return ApiClient(
            new_base_uri,
            session=self.session,
            none_fields=self.none_fields,
            bool_fields=self.bool_fields,
            timeout=self.timeout,
            raise_for_status=self.raise_for_status,
        )

    def __enter__(self) -> Self:
        """Allow `ApiClient` to act as a context manager.

        You can then use an `ApiClient` instance in a `with` clause, the same way as
        `requests.Session`. The underlying request.Session will be closed on exit.

        Example:
            ```python
            with ApiClient("https://myapi.com/path") as client:
                resp = client.get("resource")
            ```

        """
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        """Close the underlying requests.Session on exit."""
        self.session.close()
request(method, path=None, *, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=False, proxies=None, hooks=None, stream=None, verify=None, cert=None, json=None, raise_for_status=None, none_fields=None, bool_fields=None)

A wrapper around requests.Session.request method with extra features.

Additional features are described in ApiClient documentation.

All parameters will be passed as-is to requests.Session.request, expected those described below which have a special behavior.

Parameters:

Name Type Description Default
path None | str | bytes | Iterable[str | bytes | int]

the url where the request will be sent to. Can be:

  • a path, as str: that path will be joined to the configured API url,
  • an iterable of path segments: that will be joined to the root url.
None
raise_for_status bool | None

like the parameter of the same name from ApiClient, but this will be applied for this request only.

None
none_fields Literal['include', 'exclude', 'empty'] | None

like the parameter of the same name from ApiClient, but this will be applied for this request only.

None
bool_fields tuple[Any, Any] | None

like the parameter of the same name from ApiClient, but this will be applied for this request only.

None

Returns:

Type Description
Response

a Response as returned by requests

Raises:

Type Description
InvalidBoolFieldsParam

if the provided bool_fields parameter is invalid.

Source code in requests_oauth2client/api_client.py
def request(  # noqa: C901, PLR0913, D417
    self,
    method: str,
    path: None | str | bytes | Iterable[str | bytes | int] = None,
    *,
    params: None | bytes | MutableMapping[str, str] = None,
    data: (
        Iterable[bytes]
        | str
        | bytes
        | list[tuple[Any, Any]]
        | tuple[tuple[Any, Any], ...]
        | Mapping[Any, Any]
        | None
    ) = None,
    headers: MutableMapping[str, str] | None = None,
    cookies: None | RequestsCookieJar | MutableMapping[str, str] = None,
    files: MutableMapping[str, IO[Any]] | None = None,
    auth: (
        None
        | tuple[str, str]
        | requests.auth.AuthBase
        | Callable[[requests.PreparedRequest], requests.PreparedRequest]
    ) = None,
    timeout: None | float | tuple[float, float] | tuple[float, None] = None,
    allow_redirects: bool = False,
    proxies: MutableMapping[str, str] | None = None,
    hooks: None
    | (
        MutableMapping[
            str,
            (Iterable[Callable[[requests.Response], Any]] | Callable[[requests.Response], Any]),
        ]
    ) = None,
    stream: bool | None = None,
    verify: str | bool | None = None,
    cert: str | tuple[str, str] | None = None,
    json: Mapping[str, Any] | None = None,
    raise_for_status: bool | None = None,
    none_fields: Literal["include", "exclude", "empty"] | None = None,
    bool_fields: tuple[Any, Any] | None = None,
) -> requests.Response:
    """A wrapper around [requests.Session.request][] method with extra features.

    Additional features are described in
    [ApiClient][requests_oauth2client.api_client.ApiClient] documentation.

    All parameters will be passed as-is to [requests.Session.request][], expected those
    described below which have a special behavior.

    Args:
      path: the url where the request will be sent to. Can be:

        - a path, as `str`: that path will be joined to the configured API url,
        - an iterable of path segments: that will be joined to the root url.
      raise_for_status: like the parameter of the same name from
        [ApiClient][requests_oauth2client.api_client.ApiClient],
        but this will be applied for this request only.
      none_fields: like the parameter of the same name from
        [ApiClient][requests_oauth2client.api_client.ApiClient],
        but this will be applied for this request only.
      bool_fields: like the parameter of the same name from
        [ApiClient][requests_oauth2client.api_client.ApiClient],
        but this will be applied for this request only.

    Returns:
      a Response as returned by requests

    Raises:
        InvalidBoolFieldsParam: if the provided `bool_fields` parameter is invalid.

    """
    path = self.to_absolute_url(path)

    if none_fields is None:
        none_fields = self.none_fields

    if none_fields == "exclude":
        if isinstance(data, Mapping):
            data = {key: val for key, val in data.items() if val is not None}
        if isinstance(json, Mapping):
            json = {key: val for key, val in json.items() if val is not None}
    elif none_fields == "empty":
        if isinstance(data, Mapping):
            data = {key: val if val is not None else "" for key, val in data.items()}
        if isinstance(json, Mapping):
            json = {key: val if val is not None else "" for key, val in json.items()}

    if bool_fields is None:
        bool_fields = self.bool_fields

    if bool_fields:
        true_value, false_value = validate_bool_fields(bool_fields)
        if isinstance(data, MutableMapping):
            for key, val in data.items():
                if val is True:
                    data[key] = true_value
                elif val is False:
                    data[key] = false_value
        if isinstance(params, MutableMapping):
            for key, val in params.items():
                if val is True:
                    params[key] = true_value
                elif val is False:
                    params[key] = false_value

    timeout = timeout or self.timeout

    response = self.session.request(
        method,
        path,
        params=params,
        data=data,
        headers=headers,
        cookies=cookies,
        files=files,
        auth=auth or self.auth,
        timeout=timeout,
        allow_redirects=allow_redirects,
        proxies=proxies,
        hooks=hooks,
        stream=stream,
        verify=verify,
        cert=cert,
        json=json,
    )

    if raise_for_status is None:
        raise_for_status = self.raise_for_status
    if raise_for_status:
        response.raise_for_status()
    return response
to_absolute_url(path=None)

Convert a relative url to an absolute url.

Given a path, return the matching absolute url, based on the base_url that is configured for this API.

The result of this method is different from a standard urljoin(), because a relative_url that starts with a "/" will not override the path from the base url. You can also pass an iterable of path parts as relative url, which will be properly joined with "/". Those parts may be str (which will be urlencoded) or bytes (which will be decoded as UTF-8 first) or any other type (which will be converted to str first, using the str() function). See the table below for example results which would exhibit most cases:

base_url relative_url result_url
"https://myhost.com/root" "/path" "https://myhost.com/root/path"
"https://myhost.com/root" "/path" "https://myhost.com/root/path"
"https://myhost.com/root" b"/path" "https://myhost.com/root/path"
"https://myhost.com/root" "path" "https://myhost.com/root/path"
"https://myhost.com/root" None "https://myhost.com/root"
"https://myhost.com/root" ("user", 1, "resource") "https://myhost.com/root/user/1/resource"
"https://myhost.com/root" "https://otherhost.org/foo" ValueError

Parameters:

Name Type Description Default
path None | str | bytes | Iterable[str | bytes | int]

a relative url

None

Returns:

Type Description
str

the resulting absolute url

Raises:

Type Description
InvalidPathParam

if the provided path does not allow constructing a valid url

Source code in requests_oauth2client/api_client.py
def to_absolute_url(self, path: None | str | bytes | Iterable[str | bytes | int] = None) -> str:
    """Convert a relative url to an absolute url.

    Given a `path`, return the matching absolute url, based on the `base_url` that is
    configured for this API.

    The result of this method is different from a standard `urljoin()`, because a relative_url
    that starts with a "/" will not override the path from the base url. You can also pass an
    iterable of path parts as relative url, which will be properly joined with "/". Those parts
    may be `str` (which will be urlencoded) or `bytes` (which will be decoded as UTF-8 first) or
    any other type (which will be converted to `str` first, using the `str() function`). See the
    table below for example results which would exhibit most cases:

    | base_url | relative_url | result_url |
    |---------------------------|-----------------------------|-------------------------------------------|
    | `"https://myhost.com/root"` | `"/path"` | `"https://myhost.com/root/path"` |
    | `"https://myhost.com/root"` | `"/path"` | `"https://myhost.com/root/path"` |
    | `"https://myhost.com/root"` | `b"/path"` | `"https://myhost.com/root/path"` |
    | `"https://myhost.com/root"` | `"path"` | `"https://myhost.com/root/path"` |
    | `"https://myhost.com/root"` | `None` | `"https://myhost.com/root"` |
    | `"https://myhost.com/root"` |  `("user", 1, "resource")` | `"https://myhost.com/root/user/1/resource"` |
    | `"https://myhost.com/root"` | `"https://otherhost.org/foo"` | `ValueError` |

    Args:
      path: a relative url

    Returns:
      the resulting absolute url

    Raises:
        InvalidPathParam: if the provided path does not allow constructing a valid url

    """
    url = path

    if url is None:
        url = self.base_url
    else:
        if not isinstance(url, (str, bytes)):
            try:
                url = "/".join(
                    [urlencode(part.decode() if isinstance(part, bytes) else str(part)) for part in url if part],
                )
            except Exception as exc:
                raise InvalidPathParam(url) from exc

        if isinstance(url, bytes):
            url = url.decode()

        if "://" in url:
            raise InvalidPathParam(url)

        url = urljoin(self.base_url + "/", url.lstrip("/"))

    if url is None or not isinstance(url, str):
        raise InvalidPathParam(url)  # pragma: no cover

    return url
get(path=None, raise_for_status=None, **kwargs)

Send a GET request and return a Response object.

The passed url is relative to the base_url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path None | str | bytes | Iterable[str | bytes | int]

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request()

{}

Returns:

Type Description
Response

a response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def get(
    self,
    path: None | str | bytes | Iterable[str | bytes | int] = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a GET request and return a [Response][requests.Response] object.

    The passed `url` is relative to the `base_url` passed at initialization time.
    It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
        path: the path where the request will be sent.
        raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
        **kwargs: additional kwargs for `requests.request()`

    Returns:
        a response object.

    Raises:
        requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("GET", path, raise_for_status=raise_for_status, **kwargs)
post(path=None, raise_for_status=None, **kwargs)

Send a POST request and return a Response object.

The passed url is relative to the base_url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path str | bytes | Iterable[str | bytes] | None

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request()

{}

Returns:

Type Description
Response

a response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def post(
    self,
    path: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a POST request and return a [Response][requests.Response] object.

    The passed `url` is relative to the `base_url` passed at initialization time.
    It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
      path: the path where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: additional kwargs for `requests.request()`

    Returns:
      a response object.

    Raises:
      requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("POST", path, raise_for_status=raise_for_status, **kwargs)
patch(path=None, raise_for_status=None, **kwargs)

Send a PATCH request. Return a Response object.

The passed url is relative to the base_url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path str | bytes | Iterable[str | bytes] | None

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request()

{}

Returns:

Type Description
Response

a Response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def patch(
    self,
    path: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a PATCH request. Return a [Response][requests.Response] object.

    The passed `url` is relative to the `base_url` passed at initialization time.
    It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
      path: the path where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: additional kwargs for `requests.request()`

    Returns:
      a [Response][requests.Response] object.

    Raises:
      requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("PATCH", path, raise_for_status=raise_for_status, **kwargs)
put(path=None, raise_for_status=None, **kwargs)

Send a PUT request. Return a Response object.

The passed url is relative to the base_url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path str | bytes | Iterable[str | bytes] | None

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request()

{}

Returns:

Type Description
Response

a Response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def put(
    self,
    path: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a PUT request. Return a [Response][requests.Response] object.

    The passed `url` is relative to the `base_url` passed at initialization time.
    It takes the same parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
      path: the path where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: additional kwargs for `requests.request()`

    Returns:
      a [Response][requests.Response] object.

    Raises:
      requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("PUT", path, raise_for_status=raise_for_status, **kwargs)
delete(path=None, raise_for_status=None, **kwargs)

Send a DELETE request. Return a Response object.

The passed url may be relative to the url passed at initialization time. It takes the same parameters as ApiClient.request().

Parameters:

Name Type Description Default
path str | bytes | Iterable[str | bytes] | None

the path where the request will be sent.

None
raise_for_status bool | None

overrides the raises_for_status parameter passed at initialization time.

None
**kwargs Any

additional kwargs for requests.request().

{}

Returns:

Type Description
Response

a response object.

Raises:

Type Description
HTTPError

if raises_for_status is True and an error response is returned.

Source code in requests_oauth2client/api_client.py
def delete(
    self,
    path: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a DELETE request. Return a [Response][requests.Response] object.

    The passed `url` may be relative to the url passed at initialization time. It takes the same
    parameters as [ApiClient.request()][requests_oauth2client.api_client.ApiClient.request].

    Args:
      path: the path where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: additional kwargs for `requests.request()`.

    Returns:
      a response object.

    Raises:
      requests.HTTPError: if `raises_for_status` is `True` and an error response is returned.

    """
    return self.request("DELETE", path, raise_for_status=raise_for_status, **kwargs)

validate_bool_fields(bool_fields)

Validate the bool_fields parameter.

It must be a sequence of 2 values. First one is the True value, second one is the False value. Both must be str or string-able values.

Source code in requests_oauth2client/api_client.py
def validate_bool_fields(bool_fields: tuple[str, str]) -> tuple[str, str]:
    """Validate the `bool_fields` parameter.

    It must be a sequence of 2 values. First one is the `True` value, second one is the `False` value.
    Both must be `str` or string-able values.

    """
    try:
        true_value, false_value = bool_fields
    except ValueError:
        raise InvalidBoolFieldsParam(bool_fields) from None
    else:
        return str(true_value), str(false_value)

auth

This module contains requests-compatible Auth Handlers that implement OAuth 2.0.

NonRenewableTokenError

Bases: Exception

Raised when attempting to renew a token non-interactively when missing renewing material.

Source code in requests_oauth2client/auth.py
class NonRenewableTokenError(Exception):
    """Raised when attempting to renew a token non-interactively when missing renewing material."""

OAuth2AccessTokenAuth

Bases: AuthBase

Authentication Handler for OAuth 2.0 Access Tokens and (optional) Refresh Tokens.

This Requests Auth handler implementation uses an access token as Bearer or DPoP token, and can automatically refresh it when expired, if a refresh token is available.

Token can be a simple str containing a raw access token value, or a BearerToken that can contain a refresh_token.

In addition to adding a properly formatted Authorization header, this will obtain a new token once the current token is expired. Expiration is detected based on the expires_in hint returned by the AS. A configurable leeway, in number of seconds, will make sure that a new token is obtained some seconds before the actual expiration is reached. This may help in situations where the client, AS and RS have slightly offset clocks.

Parameters:

Name Type Description Default
client OAuth2Client

the client to use to refresh tokens.

required
token str | BearerToken

an initial Access Token, if you have one already. In most cases, leave None.

required
leeway int

expiration leeway, in number of seconds.

20
**token_kwargs Any

additional kwargs to pass to the token endpoint.

{}
Example
1
2
3
4
5
6
7
8
from requests_oauth2client import BearerToken, OAuth2Client, OAuth2AccessTokenAuth, requests

client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
# obtain a BearerToken any way you see fit, optionally including a refresh token
# for this example, the token value is hardcoded
token = BearerToken(access_token="access_token", expires_in=600, refresh_token="refresh_token")
auth = OAuth2AccessTokenAuth(client, token, scope="my_scope")
resp = requests.post("https://my.api.local/resource", auth=auth)
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2AccessTokenAuth(requests.auth.AuthBase):
    """Authentication Handler for OAuth 2.0 Access Tokens and (optional) Refresh Tokens.

    This [Requests Auth handler][requests.auth.AuthBase] implementation uses an access token as
    Bearer or DPoP token, and can automatically refresh it when expired, if a refresh token is available.

    Token can be a simple `str` containing a raw access token value, or a
    [BearerToken][requests_oauth2client.tokens.BearerToken] that can contain a `refresh_token`.

    In addition to adding a properly formatted `Authorization` header, this will obtain a new token
    once the current token is expired. Expiration is detected based on the `expires_in` hint
    returned by the AS. A configurable `leeway`, in number of seconds, will make sure that a new
    token is obtained some seconds before the actual expiration is reached. This may help in
    situations where the client, AS and RS have slightly offset clocks.

    Args:
        client: the client to use to refresh tokens.
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        leeway: expiration leeway, in number of seconds.
        **token_kwargs: additional kwargs to pass to the token endpoint.

    Example:
        ```python
        from requests_oauth2client import BearerToken, OAuth2Client, OAuth2AccessTokenAuth, requests

        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        # obtain a BearerToken any way you see fit, optionally including a refresh token
        # for this example, the token value is hardcoded
        token = BearerToken(access_token="access_token", expires_in=600, refresh_token="refresh_token")
        auth = OAuth2AccessTokenAuth(client, token, scope="my_scope")
        resp = requests.post("https://my.api.local/resource", auth=auth)
        ```

    """

    client: OAuth2Client = field(on_setattr=setters.frozen)
    token: BearerToken | None
    leeway: int = field(on_setattr=setters.frozen)
    token_kwargs: dict[str, Any] = field(on_setattr=setters.frozen)

    def __init__(
        self, client: OAuth2Client, token: str | BearerToken, *, leeway: int = 20, **token_kwargs: Any
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(client=client, token=token, leeway=leeway, token_kwargs=token_kwargs)

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the Access Token to the request.

        If Access Token is not specified or expired, obtain a new one first.

        Raises:
            NonRenewableTokenError: if the token is not renewable

        """
        if self.token is None or self.token.is_expired(self.leeway):
            self.renew_token()
        if self.token is None:
            raise NonRenewableTokenError  # pragma: no cover
        return self.token(request)

    def renew_token(self) -> None:
        """Obtain a new Bearer Token.

        This will try to use the `refresh_token`, if there is one.

        """
        if self.token is not None and self.token.refresh_token is not None:
            self.token = self.client.refresh_token(refresh_token=self.token, **self.token_kwargs)

    def forget_token(self) -> None:
        """Forget the current token, forcing a renewal on the next HTTP request."""
        self.token = None
renew_token()

Obtain a new Bearer Token.

This will try to use the refresh_token, if there is one.

Source code in requests_oauth2client/auth.py
def renew_token(self) -> None:
    """Obtain a new Bearer Token.

    This will try to use the `refresh_token`, if there is one.

    """
    if self.token is not None and self.token.refresh_token is not None:
        self.token = self.client.refresh_token(refresh_token=self.token, **self.token_kwargs)
forget_token()

Forget the current token, forcing a renewal on the next HTTP request.

Source code in requests_oauth2client/auth.py
def forget_token(self) -> None:
    """Forget the current token, forcing a renewal on the next HTTP request."""
    self.token = None

OAuth2ClientCredentialsAuth

Bases: OAuth2AccessTokenAuth

An Auth Handler for the Client Credentials grant.

This requests AuthBase automatically gets Access Tokens from an OAuth 2.0 Token Endpoint with the Client Credentials grant, and will get a new one once the current one is expired.

Parameters:

Name Type Description Default
client OAuth2Client

the OAuth2Client to use to obtain Access Tokens.

required
token str | BearerToken | None

an initial Access Token, if you have one already. In most cases, leave None.

None
leeway int

expiration leeway, in number of seconds

20
**token_kwargs Any

extra kw parameters to pass to the Token Endpoint. May include scope, resource, etc.

{}
Example
1
2
3
4
5
from requests_oauth2client import OAuth2Client, OAuth2ClientCredentialsAuth, requests

client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
oauth2cc = OAuth2ClientCredentialsAuth(client, scope="my_scope")
resp = requests.post("https://my.api.local/resource", auth=oauth2cc)
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2ClientCredentialsAuth(OAuth2AccessTokenAuth):
    """An Auth Handler for the [Client Credentials grant](https://www.rfc-editor.org/rfc/rfc6749#section-4.4).

    This [requests AuthBase][requests.auth.AuthBase] automatically gets Access Tokens from an OAuth
    2.0 Token Endpoint with the Client Credentials grant, and will get a new one once the current
    one is expired.

    Args:
        client: the [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to obtain Access Tokens.
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        leeway: expiration leeway, in number of seconds
        **token_kwargs: extra kw parameters to pass to the Token Endpoint. May include `scope`, `resource`, etc.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client, OAuth2ClientCredentialsAuth, requests

        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        oauth2cc = OAuth2ClientCredentialsAuth(client, scope="my_scope")
        resp = requests.post("https://my.api.local/resource", auth=oauth2cc)
        ```

    """

    def __init__(
        self, client: OAuth2Client, *, leeway: int = 20, token: str | BearerToken | None = None, **token_kwargs: Any
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(client=client, token=token, leeway=leeway, token_kwargs=token_kwargs)

    @override
    def renew_token(self) -> None:
        """Obtain a new token for use within this Auth Handler."""
        self.token = self.client.client_credentials(**self.token_kwargs)
renew_token()

Obtain a new token for use within this Auth Handler.

Source code in requests_oauth2client/auth.py
@override
def renew_token(self) -> None:
    """Obtain a new token for use within this Auth Handler."""
    self.token = self.client.client_credentials(**self.token_kwargs)

OAuth2AuthorizationCodeAuth

Bases: OAuth2AccessTokenAuth

Authentication handler for the Authorization Code grant.

This Requests Auth handler implementation exchanges an Authorization Code for an access token, then automatically refreshes it once it is expired.

Parameters:

Name Type Description Default
client OAuth2Client

the client to use to obtain Access Tokens.

required
code str | AuthorizationResponse | None

an Authorization Code that has been obtained from the AS.

required
token str | BearerToken | None

an initial Access Token, if you have one already. In most cases, leave None.

None
leeway int

expiration leeway, in number of seconds.

20
**token_kwargs Any

additional kwargs to pass to the token endpoint.

{}
Example
1
2
3
4
5
from requests_oauth2client import ApiClient, OAuth2Client, OAuth2AuthorizationCodeAuth

client = OAuth2Client(token_endpoint="https://myas.local/token", auth=("client_id", "client_secret"))
code = "my_code"  # you must obtain this code yourself
api = ApiClient("https://my.api.local/resource", auth=OAuth2AuthorizationCodeAuth(client, code))
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2AuthorizationCodeAuth(OAuth2AccessTokenAuth):  # type: ignore[override]
    """Authentication handler for the [Authorization Code grant](https://www.rfc-editor.org/rfc/rfc6749#section-4.1).

    This [Requests Auth handler][requests.auth.AuthBase] implementation exchanges an Authorization
    Code for an access token, then automatically refreshes it once it is expired.

    Args:
        client: the client to use to obtain Access Tokens.
        code: an Authorization Code that has been obtained from the AS.
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        leeway: expiration leeway, in number of seconds.
        **token_kwargs: additional kwargs to pass to the token endpoint.

    Example:
        ```python
        from requests_oauth2client import ApiClient, OAuth2Client, OAuth2AuthorizationCodeAuth

        client = OAuth2Client(token_endpoint="https://myas.local/token", auth=("client_id", "client_secret"))
        code = "my_code"  # you must obtain this code yourself
        api = ApiClient("https://my.api.local/resource", auth=OAuth2AuthorizationCodeAuth(client, code))
        ```

    """

    code: str | AuthorizationResponse | None

    def __init__(
        self,
        client: OAuth2Client,
        code: str | AuthorizationResponse | None,
        *,
        leeway: int = 20,
        token: str | BearerToken | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(
            client=client,
            token=token,
            code=code,
            leeway=leeway,
            token_kwargs=token_kwargs,
        )

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Implement the Authorization Code grant as an Authentication Handler.

        This exchanges an Authorization Code for an access token and adds it in the request.

        Args:
            request: the request

        Returns:
            the request, with an Access Token added in Authorization Header

        """
        if self.token is None or self.token.is_expired():
            self.exchange_code_for_token()
        return super().__call__(request)

    def exchange_code_for_token(self) -> None:
        """Exchange the authorization code for an access token."""
        if self.code:  # pragma: no branch
            self.token = self.client.authorization_code(code=self.code, **self.token_kwargs)
            self.code = None
exchange_code_for_token()

Exchange the authorization code for an access token.

Source code in requests_oauth2client/auth.py
def exchange_code_for_token(self) -> None:
    """Exchange the authorization code for an access token."""
    if self.code:  # pragma: no branch
        self.token = self.client.authorization_code(code=self.code, **self.token_kwargs)
        self.code = None

OAuth2ResourceOwnerPasswordAuth

Bases: OAuth2AccessTokenAuth

Authentication Handler for the Resource Owner Password Credentials Flow.

This Requests Auth handler implementation exchanges the user credentials for an Access Token, then automatically repeats the process to get a new one once the current one is expired.

Note that this flow is considered deprecated, and the Authorization Code flow should be used whenever possible. Among other bad things, ROPC:

  • does not support SSO between multiple apps,
  • does not support MFA or risk-based adaptative authentication,
  • depends on the user typing its credentials directly inside the application, instead of on a dedicated, centralized login page managed by the AS, which makes it totally insecure for 3rd party apps.

It needs the username and password and an OAuth2Client to be able to get a token from the AS Token Endpoint just before the first request using this Auth Handler is being sent.

Parameters:

Name Type Description Default
client OAuth2Client

the client to use to obtain Access Tokens

required
username str

the username

required
password str

the user password

required
leeway int

an amount of time, in seconds

20
token str | BearerToken | None

an initial Access Token, if you have one already. In most cases, leave None.

None
**token_kwargs Any

additional kwargs to pass to the token endpoint

{}
Example
from requests_oauth2client import ApiClient, OAuth2Client, OAuth2ResourceOwnerPasswordAuth

client = OAuth2Client(
    token_endpoint="https://myas.local/token",
    auth=("client_id", "client_secret"),
)
username = "my_username"
password = "my_password"  # you must obtain those credentials from the user
auth = OAuth2ResourceOwnerPasswordAuth(client, username=username, password=password)
api = ApiClient("https://myapi.local", auth=auth)
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2ResourceOwnerPasswordAuth(OAuth2AccessTokenAuth):  # type: ignore[override]
    """Authentication Handler for the [Resource Owner Password Credentials Flow](https://www.rfc-editor.org/rfc/rfc6749#section-4.3).

    This [Requests Auth handler][requests.auth.AuthBase] implementation exchanges the user
    credentials for an Access Token, then automatically repeats the process to get a new one
    once the current one is expired.

    Note that this flow is considered *deprecated*, and the Authorization Code flow should be
    used whenever possible.
    Among other bad things, ROPC:

    - does not support SSO between multiple apps,
    - does not support MFA or risk-based adaptative authentication,
    - depends on the user typing its credentials directly inside the application, instead of on a
    dedicated, centralized login page managed by the AS, which makes it totally insecure for 3rd party apps.

    It needs the username and password and an
    [OAuth2Client][requests_oauth2client.client.OAuth2Client] to be able to get a token from
    the AS Token Endpoint just before the first request using this Auth Handler is being sent.

    Args:
        client: the client to use to obtain Access Tokens
        username: the username
        password: the user password
        leeway: an amount of time, in seconds
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        **token_kwargs: additional kwargs to pass to the token endpoint

    Example:
        ```python
        from requests_oauth2client import ApiClient, OAuth2Client, OAuth2ResourceOwnerPasswordAuth

        client = OAuth2Client(
            token_endpoint="https://myas.local/token",
            auth=("client_id", "client_secret"),
        )
        username = "my_username"
        password = "my_password"  # you must obtain those credentials from the user
        auth = OAuth2ResourceOwnerPasswordAuth(client, username=username, password=password)
        api = ApiClient("https://myapi.local", auth=auth)
        ```
    """

    username: str
    password: str

    def __init__(
        self,
        client: OAuth2Client,
        *,
        username: str,
        password: str,
        leeway: int = 20,
        token: str | BearerToken | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(
            client=client,
            token=token,
            leeway=leeway,
            token_kwargs=token_kwargs,
            username=username,
            password=password,
        )

    @override
    def renew_token(self) -> None:
        """Exchange the user credentials for an Access Token."""
        self.token = self.client.resource_owner_password(
            username=self.username,
            password=self.password,
            **self.token_kwargs,
        )
renew_token()

Exchange the user credentials for an Access Token.

Source code in requests_oauth2client/auth.py
@override
def renew_token(self) -> None:
    """Exchange the user credentials for an Access Token."""
    self.token = self.client.resource_owner_password(
        username=self.username,
        password=self.password,
        **self.token_kwargs,
    )

OAuth2DeviceCodeAuth

Bases: OAuth2AccessTokenAuth

Authentication Handler for the Device Code Flow.

This Requests Auth handler implementation exchanges a Device Code for an Access Token, then automatically refreshes it once it is expired.

It needs a Device Code and an OAuth2Client to be able to get a token from the AS Token Endpoint just before the first request using this Auth Handler is being sent.

Parameters:

Name Type Description Default
client OAuth2Client

the OAuth2Client to use to obtain Access Tokens.

required
device_code str | DeviceAuthorizationResponse

a Device Code obtained from the AS.

required
interval int

the interval to use to pool the Token Endpoint, in seconds.

5
expires_in int

the lifetime of the token, in seconds.

360
token str | BearerToken | None

an initial Access Token, if you have one already. In most cases, leave None.

None
leeway int

expiration leeway, in number of seconds.

20
**token_kwargs Any

additional kwargs to pass to the token endpoint.

{}
Example
1
2
3
4
5
6
from requests_oauth2client import OAuth2Client, OAuth2DeviceCodeAuth, requests

client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
device_code = client.device_authorization()
auth = OAuth2DeviceCodeAuth(client, device_code)
resp = requests.post("https://my.api.local/resource", auth=auth)
Source code in requests_oauth2client/auth.py
@define(init=False)
class OAuth2DeviceCodeAuth(OAuth2AccessTokenAuth):  # type: ignore[override]
    """Authentication Handler for the [Device Code Flow](https://www.rfc-editor.org/rfc/rfc8628).

    This [Requests Auth handler][requests.auth.AuthBase] implementation exchanges a Device Code for
    an Access Token, then automatically refreshes it once it is expired.

    It needs a Device Code and an [OAuth2Client][requests_oauth2client.client.OAuth2Client] to be
    able to get a token from the AS Token Endpoint just before the first request using this Auth
    Handler is being sent.

    Args:
        client: the [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to obtain Access Tokens.
        device_code: a Device Code obtained from the AS.
        interval: the interval to use to pool the Token Endpoint, in seconds.
        expires_in: the lifetime of the token, in seconds.
        token: an initial Access Token, if you have one already. In most cases, leave `None`.
        leeway: expiration leeway, in number of seconds.
        **token_kwargs: additional kwargs to pass to the token endpoint.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client, OAuth2DeviceCodeAuth, requests

        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        device_code = client.device_authorization()
        auth = OAuth2DeviceCodeAuth(client, device_code)
        resp = requests.post("https://my.api.local/resource", auth=auth)
        ```

    """

    device_code: str | DeviceAuthorizationResponse | None
    interval: int
    expires_in: int

    def __init__(
        self,
        client: OAuth2Client,
        *,
        device_code: str | DeviceAuthorizationResponse,
        leeway: int = 20,
        interval: int = 5,
        expires_in: int = 360,
        token: str | BearerToken | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(token, str):
            token = BearerToken(token)
        self.__attrs_init__(
            client=client,
            token=token,
            leeway=leeway,
            token_kwargs=token_kwargs,
            device_code=device_code,
            interval=interval,
            expires_in=expires_in,
        )

    @override
    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Implement the Device Code grant as a request Authentication Handler.

        This exchanges a Device Code for an access token and adds it in HTTP requests.

        Args:
            request: a [requests.PreparedRequest][]

        Returns:
            a [requests.PreparedRequest][] with an Access Token added in Authorization Header

        """
        if self.token is None:
            self.exchange_device_code_for_token()
        return super().__call__(request)

    def exchange_device_code_for_token(self) -> None:
        """Exchange the Device Code for an access token.

        This will poll the Token Endpoint until the user finishes the authorization process.

        """
        from .device_authorization import DeviceAuthorizationPoolingJob

        if self.device_code:  # pragma: no branch
            pooling_job = DeviceAuthorizationPoolingJob(
                client=self.client,
                device_code=self.device_code,
                interval=self.interval,
            )
            token = None
            while token is None:
                token = pooling_job()
            self.token = token
            self.device_code = None
exchange_device_code_for_token()

Exchange the Device Code for an access token.

This will poll the Token Endpoint until the user finishes the authorization process.

Source code in requests_oauth2client/auth.py
def exchange_device_code_for_token(self) -> None:
    """Exchange the Device Code for an access token.

    This will poll the Token Endpoint until the user finishes the authorization process.

    """
    from .device_authorization import DeviceAuthorizationPoolingJob

    if self.device_code:  # pragma: no branch
        pooling_job = DeviceAuthorizationPoolingJob(
            client=self.client,
            device_code=self.device_code,
            interval=self.interval,
        )
        token = None
        while token is None:
            token = pooling_job()
        self.token = token
        self.device_code = None

authorization_request

Classes and utilities related to Authorization Requests and Responses.

ResponseTypes

Bases: str, Enum

All standardised response_type values.

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

Source code in requests_oauth2client/authorization_request.py
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"

CodeChallengeMethods

Bases: str, Enum

All standardised code_challenge_method values.

You should always use S256.

Source code in requests_oauth2client/authorization_request.py
class CodeChallengeMethods(str, Enum):
    """All standardised `code_challenge_method` values.

    You should always use `S256`.

    """

    S256 = "S256"
    plain = "plain"

UnsupportedCodeChallengeMethod

Bases: ValueError

Raised when an unsupported code_challenge_method is provided.

Source code in requests_oauth2client/authorization_request.py
class UnsupportedCodeChallengeMethod(ValueError):
    """Raised when an unsupported `code_challenge_method` is provided."""

InvalidCodeVerifierParam

Bases: ValueError

Raised when an invalid code_verifier is supplied.

Source code in requests_oauth2client/authorization_request.py
class InvalidCodeVerifierParam(ValueError):
    """Raised when an invalid code_verifier is supplied."""

    def __init__(self, code_verifier: str) -> None:
        super().__init__("""\
Invalid 'code_verifier'. It must be a 43 to 128 characters long string, with:
- lowercase letters
- uppercase letters
- digits
- underscore, dash, tilde, or dot (_-~.)
""")
        self.code_verifier = code_verifier

PkceUtils

Contains helper methods for PKCE, as described in RFC7636.

See RFC7636.

Source code in requests_oauth2client/authorization_request.py
class PkceUtils:
    """Contains helper methods for PKCE, as described in RFC7636.

    See [RFC7636](https://tools.ietf.org/html/rfc7636).

    """

    code_verifier_pattern = re.compile(r"^[a-zA-Z0-9_\-~.]{43,128}$")
    """A regex that matches valid code verifiers."""

    @classmethod
    def generate_code_verifier(cls) -> str:
        """Generate a valid `code_verifier`.

        Returns:
            a `code_verifier` ready to use for PKCE

        """
        return secrets.token_urlsafe(96)

    @classmethod
    def derive_challenge(cls, verifier: str | bytes, method: str = CodeChallengeMethods.S256) -> str:
        """Derive the `code_challenge` from a given `code_verifier`.

        Args:
            verifier: a code verifier
            method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

        Returns:
            a `code_challenge` derived from the given verifier

        Raises:
            InvalidCodeVerifierParam: if the `verifier` does not match `code_verifier_pattern`
            UnsupportedCodeChallengeMethod: if the method is not supported

        """
        if isinstance(verifier, bytes):
            verifier = verifier.decode()

        if not cls.code_verifier_pattern.match(verifier):
            raise InvalidCodeVerifierParam(verifier)

        if method == CodeChallengeMethods.S256:
            return BinaPy(verifier).to("sha256").to("b64u").ascii()
        if method == CodeChallengeMethods.plain:
            return verifier

        raise UnsupportedCodeChallengeMethod(method)

    @classmethod
    def generate_code_verifier_and_challenge(cls, method: str = CodeChallengeMethods.S256) -> tuple[str, str]:
        """Generate a valid `code_verifier` and its matching `code_challenge`.

        Args:
            method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

        Returns:
            a `(code_verifier, code_challenge)` tuple.

        """
        verifier = cls.generate_code_verifier()
        challenge = cls.derive_challenge(verifier, method)
        return verifier, challenge

    @classmethod
    def validate_code_verifier(cls, verifier: str, challenge: str, method: str = CodeChallengeMethods.S256) -> bool:
        """Validate a `code_verifier` against a `code_challenge`.

        Args:
            verifier: the `code_verifier`, exactly as submitted by the client on token request.
            challenge: the `code_challenge`, exactly as submitted by the client on authorization request.
            method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

        Returns:
            `True` if verifier is valid, or `False` otherwise

        """
        return (
            cls.code_verifier_pattern.match(verifier) is not None
            and cls.derive_challenge(verifier, method) == challenge
        )
code_verifier_pattern = re.compile('^[a-zA-Z0-9_\\-~.]{43,128}$') class-attribute instance-attribute

A regex that matches valid code verifiers.

generate_code_verifier() classmethod

Generate a valid code_verifier.

Returns:

Type Description
str

a code_verifier ready to use for PKCE

Source code in requests_oauth2client/authorization_request.py
@classmethod
def generate_code_verifier(cls) -> str:
    """Generate a valid `code_verifier`.

    Returns:
        a `code_verifier` ready to use for PKCE

    """
    return secrets.token_urlsafe(96)
derive_challenge(verifier, method=CodeChallengeMethods.S256) classmethod

Derive the code_challenge from a given code_verifier.

Parameters:

Name Type Description Default
verifier str | bytes

a code verifier

required
method str

the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

S256

Returns:

Type Description
str

a code_challenge derived from the given verifier

Raises:

Type Description
InvalidCodeVerifierParam

if the verifier does not match code_verifier_pattern

UnsupportedCodeChallengeMethod

if the method is not supported

Source code in requests_oauth2client/authorization_request.py
@classmethod
def derive_challenge(cls, verifier: str | bytes, method: str = CodeChallengeMethods.S256) -> str:
    """Derive the `code_challenge` from a given `code_verifier`.

    Args:
        verifier: a code verifier
        method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

    Returns:
        a `code_challenge` derived from the given verifier

    Raises:
        InvalidCodeVerifierParam: if the `verifier` does not match `code_verifier_pattern`
        UnsupportedCodeChallengeMethod: if the method is not supported

    """
    if isinstance(verifier, bytes):
        verifier = verifier.decode()

    if not cls.code_verifier_pattern.match(verifier):
        raise InvalidCodeVerifierParam(verifier)

    if method == CodeChallengeMethods.S256:
        return BinaPy(verifier).to("sha256").to("b64u").ascii()
    if method == CodeChallengeMethods.plain:
        return verifier

    raise UnsupportedCodeChallengeMethod(method)
generate_code_verifier_and_challenge(method=CodeChallengeMethods.S256) classmethod

Generate a valid code_verifier and its matching code_challenge.

Parameters:

Name Type Description Default
method str

the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

S256

Returns:

Type Description
tuple[str, str]

a (code_verifier, code_challenge) tuple.

Source code in requests_oauth2client/authorization_request.py
@classmethod
def generate_code_verifier_and_challenge(cls, method: str = CodeChallengeMethods.S256) -> tuple[str, str]:
    """Generate a valid `code_verifier` and its matching `code_challenge`.

    Args:
        method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

    Returns:
        a `(code_verifier, code_challenge)` tuple.

    """
    verifier = cls.generate_code_verifier()
    challenge = cls.derive_challenge(verifier, method)
    return verifier, challenge
validate_code_verifier(verifier, challenge, method=CodeChallengeMethods.S256) classmethod

Validate a code_verifier against a code_challenge.

Parameters:

Name Type Description Default
verifier str

the code_verifier, exactly as submitted by the client on token request.

required
challenge str

the code_challenge, exactly as submitted by the client on authorization request.

required
method str

the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

S256

Returns:

Type Description
bool

True if verifier is valid, or False otherwise

Source code in requests_oauth2client/authorization_request.py
@classmethod
def validate_code_verifier(cls, verifier: str, challenge: str, method: str = CodeChallengeMethods.S256) -> bool:
    """Validate a `code_verifier` against a `code_challenge`.

    Args:
        verifier: the `code_verifier`, exactly as submitted by the client on token request.
        challenge: the `code_challenge`, exactly as submitted by the client on authorization request.
        method: the method to use for deriving the challenge. Accepts 'S256' or 'plain'.

    Returns:
        `True` if verifier is valid, or `False` otherwise

    """
    return (
        cls.code_verifier_pattern.match(verifier) is not None
        and cls.derive_challenge(verifier, method) == challenge
    )

UnsupportedResponseTypeParam

Bases: ValueError

Raised when an unsupported response_type is passed as parameter.

Source code in requests_oauth2client/authorization_request.py
class UnsupportedResponseTypeParam(ValueError):
    """Raised when an unsupported response_type is passed as parameter."""

    def __init__(self, response_type: str) -> None:
        super().__init__("""The only supported response type is 'code'.""", response_type)

MissingIssuerParam

Bases: ValueError

Raised when the 'issuer' parameter is required but not provided.

Source code in requests_oauth2client/authorization_request.py
class MissingIssuerParam(ValueError):
    """Raised when the 'issuer' parameter is required but not provided."""

    def __init__(self) -> None:
        super().__init__("""\
When 'authorization_response_iss_parameter_supported' is `True`, you must
provide the expected `issuer` as parameter.
""")

InvalidMaxAgeParam

Bases: ValueError

Raised when an invalid 'max_age' parameter is provided.

Source code in requests_oauth2client/authorization_request.py
class InvalidMaxAgeParam(ValueError):
    """Raised when an invalid 'max_age' parameter is provided."""

    def __init__(self) -> None:
        super().__init__("""\
Invalid 'max_age' parameter. It must be a positive number of seconds.
This specifies the allowable elapsed time in seconds since the last time
the End-User was actively authenticated by the OP.
""")

AuthorizationResponse

Represent a successful Authorization Response.

An Authorization Response is the redirection initiated by the AS to the client's redirection endpoint (redirect_uri), after an Authorization Request. This Response is typically created with a call to AuthorizationRequest.validate_callback() once the call to the client Redirection Endpoint is made. AuthorizationResponse contains the following attributes:

  • all the parameters that have been returned by the AS, most notably the code, and optional parameters such as state.
  • the redirect_uri that was used for the Authorization Request
  • the code_verifier matching the code_challenge that was used for the Authorization Request

Parameters redirect_uri and code_verifier must be those from the matching AuthorizationRequest. All other parameters including code and state must be those extracted from the Authorization Response parameters.

Parameters:

Name Type Description Default
code str

The authorization code returned by the AS.

required
redirect_uri str | None

The redirect_uri that was passed as parameter in the Authorization Request.

None
code_verifier str | None

the code_verifier matching the code_challenge that was passed as parameter in the Authorization Request.

None
state str | None

the state that was was passed as parameter in the Authorization Request and returned by the AS.

None
nonce str | None

the nonce that was was passed as parameter in the Authorization Request.

None
acr_values str | Sequence[str] | None

the acr_values that was passed as parameter in the Authorization Request.

None
max_age int | None

the max_age that was passed as parameter in the Authorization Request.

None
issuer str | None

the expected issuer identifier.

None
dpop_key DPoPKey | None

the DPoPKey that was used for Authorization Code DPoP binding.

None
**kwargs str

other parameters as returned by the AS.

{}
Source code in requests_oauth2client/authorization_request.py
@frozen(init=False)
class AuthorizationResponse:
    """Represent a successful Authorization Response.

    An Authorization Response is the redirection initiated by the AS to the client's redirection
    endpoint (redirect_uri), after an Authorization Request.
    This Response is typically created with a call to `AuthorizationRequest.validate_callback()`
    once the call to the client Redirection Endpoint is made.
    `AuthorizationResponse` contains the following attributes:

     - all the parameters that have been returned by the AS, most notably the `code`, and optional
       parameters such as `state`.
     - the `redirect_uri` that was used for the Authorization Request
     - the `code_verifier` matching the `code_challenge` that was used for the Authorization Request

    Parameters `redirect_uri` and `code_verifier` must be those from the matching
    `AuthorizationRequest`. All other parameters including `code` and `state` must be those
    extracted from the Authorization Response parameters.

    Args:
        code: The authorization `code` returned by the AS.
        redirect_uri: The `redirect_uri` that was passed as parameter in the Authorization Request.
        code_verifier: the `code_verifier` matching the `code_challenge` that was passed as
            parameter in the Authorization Request.
        state: the `state` that was was passed as parameter in the Authorization Request and returned by the AS.
        nonce: the `nonce` that was was passed as parameter in the Authorization Request.
        acr_values: the `acr_values` that was passed as parameter in the Authorization Request.
        max_age: the `max_age` that was passed as parameter in the Authorization Request.
        issuer: the expected `issuer` identifier.
        dpop_key: the `DPoPKey` that was used for Authorization Code DPoP binding.
        **kwargs: other parameters as returned by the AS.

    """

    code: str
    redirect_uri: str | None
    code_verifier: str | None
    state: str | None
    nonce: str | None
    acr_values: tuple[str, ...] | None
    max_age: int | None
    issuer: str | None
    dpop_key: DPoPKey | None
    kwargs: dict[str, Any]

    def __init__(
        self,
        *,
        code: str,
        redirect_uri: str | None = None,
        code_verifier: str | None = None,
        state: str | None = None,
        nonce: str | None = None,
        acr_values: str | Sequence[str] | None = None,
        max_age: int | None = None,
        issuer: str | None = None,
        dpop_key: DPoPKey | None = None,
        **kwargs: str,
    ) -> None:
        if not acr_values:
            acr_values = None
        elif isinstance(acr_values, str):
            acr_values = tuple(acr_values.split(" "))
        else:
            acr_values = tuple(acr_values)

        self.__attrs_init__(
            code=code,
            redirect_uri=redirect_uri,
            code_verifier=code_verifier,
            state=state,
            nonce=nonce,
            acr_values=acr_values,
            max_age=max_age,
            issuer=issuer,
            dpop_key=dpop_key,
            kwargs=kwargs,
        )

    def __getattr__(self, item: str) -> str | None:
        """Make additional parameters available as attributes.

        Args:
            item: the attribute name

        Returns:
            the attribute value, or None if it isn't part of the returned attributes

        """
        return self.kwargs.get(item)

AuthorizationRequest

Represent an Authorization Request.

This class makes it easy to generate valid Authorization Request URI (possibly including a state, nonce, PKCE, and custom args), to store all parameters, and to validate an Authorization Response.

All parameters passed at init time will be included in the request query parameters as-is, excepted for a few parameters which have a special behaviour:

  • state: if ... (default), a random state parameter will be generated for you. You may pass your own state as str, or set it to None so that the state parameter will not be included in the request. You may access that state in the state attribute from this request.
  • nonce: if ... (default) and scope includes 'openid', a random nonce will be generated and included in the request. You may access that nonce in the nonce attribute from this request.
  • code_verifier: if None, and code_challenge_method is 'S256' or 'plain', a valid code_challenge and code_verifier for PKCE will be automatically generated, and the code_challenge will be included in the request. You may pass your own code_verifier as a str parameter, in which case the appropriate code_challenge will be included in the request, according to the code_challenge_method.
  • authorization_response_iss_parameter_supported and issuer: those are used for Server Issuer Identification. By default:

    • If ìssuer is set and an issuer is included in the Authorization Response, then the consistency between those 2 values will be checked when using validate_callback().
    • If issuer is not included in the response, then no issuer check is performed.

    Set authorization_response_iss_parameter_supported to True to enforce server identification:

    • an issuer must also be provided as parameter, and the AS must return that same value for the response to be considered valid by validate_callback().
    • if no issuer is included in the Authorization Response, then an error will be raised.

Parameters:

Name Type Description Default
authorization_endpoint str

the uri for the authorization endpoint.

required
client_id str

the client_id to include in the request.

required
redirect_uri str | None

the redirect_uri to include in the request. This is required in OAuth 2.0 and optional in OAuth 2.1. Pass None if you don't need any redirect_uri in the Authorization Request.

None
scope None | str | Iterable[str]

the scope to include in the request, as an iterable of str, or a single space-separated str.

'openid'
response_type str

the response type to include in the request.

CODE
state str | ellipsis | None

the state to include in the request, or ... to autogenerate one (default).

...
nonce str | ellipsis | None

the nonce to include in the request, or ... to autogenerate one (default).

...
code_verifier str | None

the code verifier to include in the request. If left as None and code_challenge_method is set, a valid code_verifier will be generated.

None
code_challenge_method str | None

the method to use to derive the code_challenge from the code_verifier.

S256
acr_values str | Iterable[str] | None

requested Authentication Context Class Reference values.

None
issuer str | None

Issuer Identifier value from the OAuth/OIDC Server, if using Server Issuer Identification.

None
**kwargs Any

extra parameters to include in the request, as-is.

{}
Example
1
2
3
4
5
6
7
8
9
from requests_oauth2client import AuthorizationRequest

azr = AuthorizationRequest(
    authorization_endpoint="https://url.to.the/authorization_endpoint",
    client_id="my_client_id",
    redirect_uri="http://localhost/callback",
    scope="openid email profile",
)
print(azr)

Raises:

Type Description
InvalidMaxAgeParam

if the max_age parameter is invalid.

MissingIssuerParam

if authorization_response_iss_parameter_supported is set to True but the issuer parameter is not provided.

UnsupportedResponseTypeParam

if response_type is not supported.

Source code in requests_oauth2client/authorization_request.py
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
@frozen(init=False, repr=False)
class AuthorizationRequest:
    """Represent an Authorization Request.

    This class makes it easy to generate valid Authorization Request URI (possibly including a
    state, nonce, PKCE, and custom args), to store all parameters, and to validate an Authorization
    Response.

    All parameters passed at init time will be included in the request query parameters as-is,
    excepted for a few parameters which have a special behaviour:

    - `state`: if `...` (default), a random `state` parameter will be generated for you.
      You may pass your own `state` as `str`, or set it to `None` so that the `state` parameter
      will not be included in the request. You may access that state in the `state` attribute
      from this request.
    - `nonce`: if `...` (default) and `scope` includes 'openid', a random `nonce` will be
      generated and included in the request. You may access that `nonce` in the `nonce` attribute
      from this request.
    - `code_verifier`: if `None`, and `code_challenge_method` is `'S256'` or `'plain'`,
      a valid `code_challenge` and `code_verifier` for PKCE will be automatically generated,
      and the `code_challenge` will be included in the request.
      You may pass your own `code_verifier` as a `str` parameter, in which case the
      appropriate `code_challenge` will be included in the request, according to the
      `code_challenge_method`.
    - `authorization_response_iss_parameter_supported` and `issuer`:
       those are used for Server Issuer Identification. By default:

        - If `ìssuer` is set and an issuer is included in the Authorization Response,
        then the consistency between those 2 values will be checked when using `validate_callback()`.
        - If issuer is not included in the response, then no issuer check is performed.

        Set `authorization_response_iss_parameter_supported` to `True` to enforce server identification:

        - an `issuer` must also be provided as parameter, and the AS must return that same value
        for the response to be considered valid by `validate_callback()`.
        - if no issuer is included in the Authorization Response, then an error will be raised.

    Args:
        authorization_endpoint: the uri for the authorization endpoint.
        client_id: the client_id to include in the request.
        redirect_uri: the redirect_uri to include in the request. This is required in OAuth 2.0 and optional
            in OAuth 2.1. Pass `None` if you don't need any redirect_uri in the Authorization
            Request.
        scope: the scope to include in the request, as an iterable of `str`, or a single space-separated `str`.
        response_type: the response type to include in the request.
        state: the state to include in the request, or `...` to autogenerate one (default).
        nonce: the nonce to include in the request, or `...` to autogenerate one (default).
        code_verifier: the code verifier to include in the request.
            If left as `None` and `code_challenge_method` is set, a valid code_verifier
            will be generated.
        code_challenge_method: the method to use to derive the `code_challenge` from the `code_verifier`.
        acr_values: requested Authentication Context Class Reference values.
        issuer: Issuer Identifier value from the OAuth/OIDC Server, if using Server Issuer Identification.
        **kwargs: extra parameters to include in the request, as-is.

    Example:
        ```python
        from requests_oauth2client import AuthorizationRequest

        azr = AuthorizationRequest(
            authorization_endpoint="https://url.to.the/authorization_endpoint",
            client_id="my_client_id",
            redirect_uri="http://localhost/callback",
            scope="openid email profile",
        )
        print(azr)
        ```

    Raises:
        InvalidMaxAgeParam: if the `max_age` parameter is invalid.
        MissingIssuerParam: if `authorization_response_iss_parameter_supported` is set to `True`
            but the `issuer` parameter is not provided.
        UnsupportedResponseTypeParam: if `response_type` is not supported.

    """

    authorization_endpoint: str

    client_id: str = field(metadata={"query": True})
    redirect_uri: str | None = field(metadata={"query": True})
    scope: tuple[str, ...] | None = field(metadata={"query": True})
    response_type: str = field(metadata={"query": True})
    state: str | None = field(metadata={"query": True})
    nonce: str | None = field(metadata={"query": True})
    code_challenge_method: str | None = field(metadata={"query": True})
    acr_values: tuple[str, ...] | None = field(metadata={"query": True})
    max_age: int | None = field(metadata={"query": True})
    kwargs: dict[str, Any]

    code_verifier: str | None
    authorization_response_iss_parameter_supported: bool
    issuer: str | None

    dpop_key: DPoPKey | None = None

    exception_classes: ClassVar[dict[str, type[AuthorizationResponseError]]] = {
        "interaction_required": InteractionRequired,
        "login_required": LoginRequired,
        "session_selection_required": SessionSelectionRequired,
        "consent_required": ConsentRequired,
    }

    @classmethod
    def generate_state(cls) -> str:
        """Generate a random `state` parameter."""
        return secrets.token_urlsafe(32)

    @classmethod
    def generate_nonce(cls) -> str:
        """Generate a random `nonce`."""
        return secrets.token_urlsafe(32)

    def __init__(  # noqa: PLR0913, C901
        self,
        authorization_endpoint: str,
        *,
        client_id: str,
        redirect_uri: str | None = None,
        scope: None | str | Iterable[str] = "openid",
        response_type: str = ResponseTypes.CODE,
        state: str | ellipsis | None = ...,  # noqa: F821
        nonce: str | ellipsis | None = ...,  # noqa: F821
        code_verifier: str | None = None,
        code_challenge_method: str | None = CodeChallengeMethods.S256,
        acr_values: str | Iterable[str] | None = None,
        max_age: int | None = None,
        issuer: str | None = None,
        authorization_response_iss_parameter_supported: bool = False,
        dpop: bool = False,
        dpop_alg: str = SignatureAlgs.ES256,
        dpop_key: DPoPKey | None = None,
        **kwargs: Any,
    ) -> None:
        if response_type != ResponseTypes.CODE:
            raise UnsupportedResponseTypeParam(response_type)

        if authorization_response_iss_parameter_supported and not issuer:
            raise MissingIssuerParam

        if state is ...:
            state = self.generate_state()
        if state is not None and not isinstance(state, str):
            state = str(state)  # pragma: no cover

        if nonce is ...:
            nonce = self.generate_nonce() if scope is not None and "openid" in scope else None
        if nonce is not None and not isinstance(nonce, str):
            nonce = str(nonce)  # pragma: no cover

        if not scope:
            scope = None

        if scope is not None:
            scope = tuple(scope.split(" ")) if isinstance(scope, str) else tuple(scope)

        if acr_values is not None:
            acr_values = tuple(acr_values.split()) if isinstance(acr_values, str) else tuple(acr_values)

        if max_age is not None and max_age < 0:
            raise InvalidMaxAgeParam

        if "code_challenge" in kwargs:
            msg = (
                "A `code_challenge` must not be passed as parameter. Pass the `code_verifier`"
                " instead, and the appropriate `code_challenge` will automatically be derived"
                " from it and included in the request, based on `code_challenge_method`."
            )
            raise ValueError(msg)

        if code_challenge_method:
            if not code_verifier:
                code_verifier = PkceUtils.generate_code_verifier()
        else:
            code_verifier = None

        if dpop and not dpop_key:
            dpop_key = DPoPKey.generate(dpop_alg)

        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            redirect_uri=redirect_uri,
            issuer=issuer,
            response_type=response_type,
            scope=scope,
            state=state,
            nonce=nonce,
            code_verifier=code_verifier,
            code_challenge_method=code_challenge_method,
            acr_values=acr_values,
            max_age=max_age,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            dpop_key=dpop_key,
            kwargs=kwargs,
        )

    @cached_property
    def code_challenge(self) -> str | None:
        """The `code_challenge` that matches `code_verifier` and `code_challenge_method`."""
        if self.code_verifier and self.code_challenge_method:
            return PkceUtils.derive_challenge(self.code_verifier, self.code_challenge_method)
        return None

    @cached_property
    def dpop_jkt(self) -> str | None:
        """The DPoP JWK thumbprint that matches ``dpop_key`."""
        if self.dpop_key:
            return self.dpop_key.dpop_jkt
        return None

    def as_dict(self) -> dict[str, Any]:
        """Return the full argument dict.

        This can be used to serialize this request and/or to initialize a similar request.

        """
        d = asdict(self)
        d.update(**d.pop("kwargs", {}))
        return d

    @property
    def args(self) -> dict[str, Any]:
        """Return a dict with all the query parameters from this AuthorizationRequest.

        Returns:
            a dict of parameters

        """
        d = {field.name: getattr(self, field.name) for field in fields(type(self)) if field.metadata.get("query")}
        if d["scope"]:
            d["scope"] = " ".join(d["scope"])
        d["code_challenge"] = self.code_challenge
        d["dpop_jkt"] = self.dpop_jkt
        d.update(self.kwargs)

        return {key: val for key, val in d.items() if val is not None}

    def validate_callback(self, response: str) -> AuthorizationResponse:
        """Validate an Authorization Response against this Request.

        Validate a given Authorization Response URI against this Authorization Request, and return
        an [AuthorizationResponse][requests_oauth2client.authorization_request.AuthorizationResponse].

        This includes matching the `state` parameter, checking for returned errors, and extracting
        the returned `code` and other parameters.

        Args:
            response: the Authorization Response URI. This can be the full URL, or just the
                query parameters (still encoded as x-www-form-urlencoded).

        Returns:
            the extracted code, if all checks are successful

        Raises:
            MissingAuthCode: if the `code` is missing in the response
            MissingIssuer: if Server Issuer verification is active and the response does
                not contain an `iss`.
            MismatchingIssuer: if the 'iss' received from the response does not match the
                expected value.
            MismatchingState: if the response `state` does not match the expected value.
            OAuth2Error: if the response includes an error.
            MissingAuthCode: if the response does not contain a `code`.
            UnsupportedResponseTypeParam: if response_type anything else than 'code'.

        """
        try:
            response_url = furl(response)
        except ValueError:
            return self.on_response_error(response)

        # validate 'iss' according to RFC9207
        received_issuer = response_url.args.get("iss")
        if self.authorization_response_iss_parameter_supported or received_issuer:
            if received_issuer is None:
                raise MissingIssuer(self, response)
            if self.issuer and received_issuer != self.issuer:
                raise MismatchingIssuer(self.issuer, received_issuer, self, response)

        # validate state
        requested_state = self.state
        if requested_state:
            received_state = response_url.args.get("state")
            if requested_state != received_state:
                raise MismatchingState(requested_state, received_state, self, response)

        error = response_url.args.get("error")
        if error:
            return self.on_response_error(response)

        if self.response_type == ResponseTypes.CODE:
            code: str = response_url.args.get("code")
            if code is None:
                raise MissingAuthCode(self, response)
        else:
            raise UnsupportedResponseTypeParam(self.response_type)  # pragma: no cover

        return AuthorizationResponse(
            code_verifier=self.code_verifier,
            redirect_uri=self.redirect_uri,
            nonce=self.nonce,
            acr_values=self.acr_values,
            max_age=self.max_age,
            dpop_key=self.dpop_key,
            **response_url.args,
        )

    def sign_request_jwt(
        self,
        jwk: Jwk | dict[str, Any],
        alg: str | None = None,
        lifetime: int | None = None,
    ) -> SignedJwt:
        """Sign the `request` object that matches this Authorization Request parameters.

        Args:
            jwk: the JWK to use to sign the request
            alg: the alg to use to sign the request, if the provided `jwk` has no `alg` parameter.
            lifetime: an optional number of seconds of validity for the signed request.
                If present, `iat` an `exp` claims will be included in the signed JWT.

        Returns:
            a `Jwt` that contains the signed request object.

        """
        claims = self.args
        if lifetime:
            claims["iat"] = Jwt.timestamp()
            claims["exp"] = Jwt.timestamp(lifetime)
        return Jwt.sign(
            claims,
            key=jwk,
            alg=alg,
        )

    def sign(
        self,
        jwk: Jwk | dict[str, Any],
        alg: str | None = None,
        lifetime: int | None = None,
        **kwargs: Any,
    ) -> RequestParameterAuthorizationRequest:
        """Sign this Authorization Request and return a new one.

        This replaces all parameters with a signed `request` JWT.

        Args:
            jwk: the JWK to use to sign the request
            alg: the alg to use to sign the request, if the provided `jwk` has no `alg` parameter.
            lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
                By default, don't use an 'exp' claim.
            kwargs: additional query parameters to include in the signed authorization request

        Returns:
            the signed Authorization Request

        """
        request_jwt = self.sign_request_jwt(jwk, alg, lifetime)
        return RequestParameterAuthorizationRequest(
            authorization_endpoint=self.authorization_endpoint,
            client_id=self.client_id,
            request=str(request_jwt),
            expires_at=request_jwt.expires_at,
            **kwargs,
        )

    def sign_and_encrypt_request_jwt(
        self,
        sign_jwk: Jwk | dict[str, Any],
        enc_jwk: Jwk | dict[str, Any],
        sign_alg: str | None = None,
        enc_alg: str | None = None,
        enc: str = "A128CBC-HS256",
        lifetime: int | None = None,
    ) -> JweCompact:
        """Sign and encrypt a `request` object for this Authorization Request.

        The signed `request` will contain the same parameters as this AuthorizationRequest.

        Args:
            sign_jwk: the JWK to use to sign the request
            enc_jwk: the JWK to use to encrypt the request
            sign_alg: the alg to use to sign the request, if `sign_jwk` has no `alg` parameter.
            enc_alg: the alg to use to encrypt the request, if `enc_jwk` has no `alg` parameter.
            enc: the encoding to use to encrypt the request.
            lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
                By default, do not include an 'exp' claim.

        Returns:
            the signed and encrypted request object, as a `jwskate.Jwt`

        """
        claims = self.args
        if lifetime:
            claims["iat"] = Jwt.timestamp()
            claims["exp"] = Jwt.timestamp(lifetime)
        return Jwt.sign_and_encrypt(
            claims=claims,
            sign_key=sign_jwk,
            sign_alg=sign_alg,
            enc_key=enc_jwk,
            enc_alg=enc_alg,
            enc=enc,
        )

    def sign_and_encrypt(
        self,
        sign_jwk: Jwk | dict[str, Any],
        enc_jwk: Jwk | dict[str, Any],
        sign_alg: str | None = None,
        enc_alg: str | None = None,
        enc: str = "A128CBC-HS256",
        lifetime: int | None = None,
    ) -> RequestParameterAuthorizationRequest:
        """Sign and encrypt the current Authorization Request.

        This replaces all parameters with a matching `request` object.

        Args:
            sign_jwk: the JWK to use to sign the request
            enc_jwk: the JWK to use to encrypt the request
            sign_alg: the alg to use to sign the request, if `sign_jwk` has no `alg` parameter.
            enc_alg: the alg to use to encrypt the request, if `enc_jwk` has no `alg` parameter.
            enc: the encoding to use to encrypt the request.
            lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
                By default, do not include an 'exp' claim.

        Returns:
            a `RequestParameterAuthorizationRequest`, with a request object as parameter

        """
        request_jwt = self.sign_and_encrypt_request_jwt(
            sign_jwk=sign_jwk,
            enc_jwk=enc_jwk,
            sign_alg=sign_alg,
            enc_alg=enc_alg,
            enc=enc,
            lifetime=lifetime,
        )
        return RequestParameterAuthorizationRequest(
            authorization_endpoint=self.authorization_endpoint,
            client_id=self.client_id,
            request=str(request_jwt),
        )

    def on_response_error(self, response: str) -> AuthorizationResponse:
        """Error handler for Authorization Response errors.

        Triggered by
        [validate_callback()][requests_oauth2client.authorization_request.AuthorizationRequest.validate_callback]
        if the response uri contains an error.

        Args:
            response: the Authorization Response URI. This can be the full URL, or just the query parameters.

        Returns:
            may return a default code that will be returned by `validate_callback`. But this method
            will most likely raise exceptions instead.

        Raises:
            AuthorizationResponseError: if the response contains an `error`. The raised exception may be a subclass

        """
        response_url = furl(response)
        error = response_url.args.get("error")
        error_description = response_url.args.get("error_description")
        error_uri = response_url.args.get("error_uri")
        exception_class = self.exception_classes.get(error, AuthorizationResponseError)
        raise exception_class(
            request=self, response=response, error=error, description=error_description, uri=error_uri
        )

    @property
    def furl(self) -> furl:
        """Return the Authorization Request URI, as a `furl`."""
        return furl(
            self.authorization_endpoint,
            args=self.args,
        )

    @property
    def uri(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return str(self.furl.url)

    def __getattr__(self, item: str) -> Any:
        """Allow attribute access to extra parameters."""
        return self.kwargs[item]

    def __repr__(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return self.uri
code_challenge cached property

The code_challenge that matches code_verifier and code_challenge_method.

dpop_jkt cached property

The DPoP JWK thumbprint that matches `dpop_key.

args property

Return a dict with all the query parameters from this AuthorizationRequest.

Returns:

Type Description
dict[str, Any]

a dict of parameters

furl property

Return the Authorization Request URI, as a furl.

uri property

Return the Authorization Request URI, as a str.

generate_state() classmethod

Generate a random state parameter.

Source code in requests_oauth2client/authorization_request.py
@classmethod
def generate_state(cls) -> str:
    """Generate a random `state` parameter."""
    return secrets.token_urlsafe(32)
generate_nonce() classmethod

Generate a random nonce.

Source code in requests_oauth2client/authorization_request.py
@classmethod
def generate_nonce(cls) -> str:
    """Generate a random `nonce`."""
    return secrets.token_urlsafe(32)
as_dict()

Return the full argument dict.

This can be used to serialize this request and/or to initialize a similar request.

Source code in requests_oauth2client/authorization_request.py
def as_dict(self) -> dict[str, Any]:
    """Return the full argument dict.

    This can be used to serialize this request and/or to initialize a similar request.

    """
    d = asdict(self)
    d.update(**d.pop("kwargs", {}))
    return d
validate_callback(response)

Validate an Authorization Response against this Request.

Validate a given Authorization Response URI against this Authorization Request, and return an AuthorizationResponse.

This includes matching the state parameter, checking for returned errors, and extracting the returned code and other parameters.

Parameters:

Name Type Description Default
response str

the Authorization Response URI. This can be the full URL, or just the query parameters (still encoded as x-www-form-urlencoded).

required

Returns:

Type Description
AuthorizationResponse

the extracted code, if all checks are successful

Raises:

Type Description
MissingAuthCode

if the code is missing in the response

MissingIssuer

if Server Issuer verification is active and the response does not contain an iss.

MismatchingIssuer

if the 'iss' received from the response does not match the expected value.

MismatchingState

if the response state does not match the expected value.

OAuth2Error

if the response includes an error.

MissingAuthCode

if the response does not contain a code.

UnsupportedResponseTypeParam

if response_type anything else than 'code'.

Source code in requests_oauth2client/authorization_request.py
def validate_callback(self, response: str) -> AuthorizationResponse:
    """Validate an Authorization Response against this Request.

    Validate a given Authorization Response URI against this Authorization Request, and return
    an [AuthorizationResponse][requests_oauth2client.authorization_request.AuthorizationResponse].

    This includes matching the `state` parameter, checking for returned errors, and extracting
    the returned `code` and other parameters.

    Args:
        response: the Authorization Response URI. This can be the full URL, or just the
            query parameters (still encoded as x-www-form-urlencoded).

    Returns:
        the extracted code, if all checks are successful

    Raises:
        MissingAuthCode: if the `code` is missing in the response
        MissingIssuer: if Server Issuer verification is active and the response does
            not contain an `iss`.
        MismatchingIssuer: if the 'iss' received from the response does not match the
            expected value.
        MismatchingState: if the response `state` does not match the expected value.
        OAuth2Error: if the response includes an error.
        MissingAuthCode: if the response does not contain a `code`.
        UnsupportedResponseTypeParam: if response_type anything else than 'code'.

    """
    try:
        response_url = furl(response)
    except ValueError:
        return self.on_response_error(response)

    # validate 'iss' according to RFC9207
    received_issuer = response_url.args.get("iss")
    if self.authorization_response_iss_parameter_supported or received_issuer:
        if received_issuer is None:
            raise MissingIssuer(self, response)
        if self.issuer and received_issuer != self.issuer:
            raise MismatchingIssuer(self.issuer, received_issuer, self, response)

    # validate state
    requested_state = self.state
    if requested_state:
        received_state = response_url.args.get("state")
        if requested_state != received_state:
            raise MismatchingState(requested_state, received_state, self, response)

    error = response_url.args.get("error")
    if error:
        return self.on_response_error(response)

    if self.response_type == ResponseTypes.CODE:
        code: str = response_url.args.get("code")
        if code is None:
            raise MissingAuthCode(self, response)
    else:
        raise UnsupportedResponseTypeParam(self.response_type)  # pragma: no cover

    return AuthorizationResponse(
        code_verifier=self.code_verifier,
        redirect_uri=self.redirect_uri,
        nonce=self.nonce,
        acr_values=self.acr_values,
        max_age=self.max_age,
        dpop_key=self.dpop_key,
        **response_url.args,
    )
sign_request_jwt(jwk, alg=None, lifetime=None)

Sign the request object that matches this Authorization Request parameters.

Parameters:

Name Type Description Default
jwk Jwk | dict[str, Any]

the JWK to use to sign the request

required
alg str | None

the alg to use to sign the request, if the provided jwk has no alg parameter.

None
lifetime int | None

an optional number of seconds of validity for the signed request. If present, iat an exp claims will be included in the signed JWT.

None

Returns:

Type Description
SignedJwt

a Jwt that contains the signed request object.

Source code in requests_oauth2client/authorization_request.py
def sign_request_jwt(
    self,
    jwk: Jwk | dict[str, Any],
    alg: str | None = None,
    lifetime: int | None = None,
) -> SignedJwt:
    """Sign the `request` object that matches this Authorization Request parameters.

    Args:
        jwk: the JWK to use to sign the request
        alg: the alg to use to sign the request, if the provided `jwk` has no `alg` parameter.
        lifetime: an optional number of seconds of validity for the signed request.
            If present, `iat` an `exp` claims will be included in the signed JWT.

    Returns:
        a `Jwt` that contains the signed request object.

    """
    claims = self.args
    if lifetime:
        claims["iat"] = Jwt.timestamp()
        claims["exp"] = Jwt.timestamp(lifetime)
    return Jwt.sign(
        claims,
        key=jwk,
        alg=alg,
    )
sign(jwk, alg=None, lifetime=None, **kwargs)

Sign this Authorization Request and return a new one.

This replaces all parameters with a signed request JWT.

Parameters:

Name Type Description Default
jwk Jwk | dict[str, Any]

the JWK to use to sign the request

required
alg str | None

the alg to use to sign the request, if the provided jwk has no alg parameter.

None
lifetime int | None

lifetime of the resulting Jwt (used to calculate the 'exp' claim). By default, don't use an 'exp' claim.

None
kwargs Any

additional query parameters to include in the signed authorization request

{}

Returns:

Type Description
RequestParameterAuthorizationRequest

the signed Authorization Request

Source code in requests_oauth2client/authorization_request.py
def sign(
    self,
    jwk: Jwk | dict[str, Any],
    alg: str | None = None,
    lifetime: int | None = None,
    **kwargs: Any,
) -> RequestParameterAuthorizationRequest:
    """Sign this Authorization Request and return a new one.

    This replaces all parameters with a signed `request` JWT.

    Args:
        jwk: the JWK to use to sign the request
        alg: the alg to use to sign the request, if the provided `jwk` has no `alg` parameter.
        lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
            By default, don't use an 'exp' claim.
        kwargs: additional query parameters to include in the signed authorization request

    Returns:
        the signed Authorization Request

    """
    request_jwt = self.sign_request_jwt(jwk, alg, lifetime)
    return RequestParameterAuthorizationRequest(
        authorization_endpoint=self.authorization_endpoint,
        client_id=self.client_id,
        request=str(request_jwt),
        expires_at=request_jwt.expires_at,
        **kwargs,
    )
sign_and_encrypt_request_jwt(sign_jwk, enc_jwk, sign_alg=None, enc_alg=None, enc='A128CBC-HS256', lifetime=None)

Sign and encrypt a request object for this Authorization Request.

The signed request will contain the same parameters as this AuthorizationRequest.

Parameters:

Name Type Description Default
sign_jwk Jwk | dict[str, Any]

the JWK to use to sign the request

required
enc_jwk Jwk | dict[str, Any]

the JWK to use to encrypt the request

required
sign_alg str | None

the alg to use to sign the request, if sign_jwk has no alg parameter.

None
enc_alg str | None

the alg to use to encrypt the request, if enc_jwk has no alg parameter.

None
enc str

the encoding to use to encrypt the request.

'A128CBC-HS256'
lifetime int | None

lifetime of the resulting Jwt (used to calculate the 'exp' claim). By default, do not include an 'exp' claim.

None

Returns:

Type Description
JweCompact

the signed and encrypted request object, as a jwskate.Jwt

Source code in requests_oauth2client/authorization_request.py
def sign_and_encrypt_request_jwt(
    self,
    sign_jwk: Jwk | dict[str, Any],
    enc_jwk: Jwk | dict[str, Any],
    sign_alg: str | None = None,
    enc_alg: str | None = None,
    enc: str = "A128CBC-HS256",
    lifetime: int | None = None,
) -> JweCompact:
    """Sign and encrypt a `request` object for this Authorization Request.

    The signed `request` will contain the same parameters as this AuthorizationRequest.

    Args:
        sign_jwk: the JWK to use to sign the request
        enc_jwk: the JWK to use to encrypt the request
        sign_alg: the alg to use to sign the request, if `sign_jwk` has no `alg` parameter.
        enc_alg: the alg to use to encrypt the request, if `enc_jwk` has no `alg` parameter.
        enc: the encoding to use to encrypt the request.
        lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
            By default, do not include an 'exp' claim.

    Returns:
        the signed and encrypted request object, as a `jwskate.Jwt`

    """
    claims = self.args
    if lifetime:
        claims["iat"] = Jwt.timestamp()
        claims["exp"] = Jwt.timestamp(lifetime)
    return Jwt.sign_and_encrypt(
        claims=claims,
        sign_key=sign_jwk,
        sign_alg=sign_alg,
        enc_key=enc_jwk,
        enc_alg=enc_alg,
        enc=enc,
    )
sign_and_encrypt(sign_jwk, enc_jwk, sign_alg=None, enc_alg=None, enc='A128CBC-HS256', lifetime=None)

Sign and encrypt the current Authorization Request.

This replaces all parameters with a matching request object.

Parameters:

Name Type Description Default
sign_jwk Jwk | dict[str, Any]

the JWK to use to sign the request

required
enc_jwk Jwk | dict[str, Any]

the JWK to use to encrypt the request

required
sign_alg str | None

the alg to use to sign the request, if sign_jwk has no alg parameter.

None
enc_alg str | None

the alg to use to encrypt the request, if enc_jwk has no alg parameter.

None
enc str

the encoding to use to encrypt the request.

'A128CBC-HS256'
lifetime int | None

lifetime of the resulting Jwt (used to calculate the 'exp' claim). By default, do not include an 'exp' claim.

None

Returns:

Type Description
RequestParameterAuthorizationRequest

a RequestParameterAuthorizationRequest, with a request object as parameter

Source code in requests_oauth2client/authorization_request.py
def sign_and_encrypt(
    self,
    sign_jwk: Jwk | dict[str, Any],
    enc_jwk: Jwk | dict[str, Any],
    sign_alg: str | None = None,
    enc_alg: str | None = None,
    enc: str = "A128CBC-HS256",
    lifetime: int | None = None,
) -> RequestParameterAuthorizationRequest:
    """Sign and encrypt the current Authorization Request.

    This replaces all parameters with a matching `request` object.

    Args:
        sign_jwk: the JWK to use to sign the request
        enc_jwk: the JWK to use to encrypt the request
        sign_alg: the alg to use to sign the request, if `sign_jwk` has no `alg` parameter.
        enc_alg: the alg to use to encrypt the request, if `enc_jwk` has no `alg` parameter.
        enc: the encoding to use to encrypt the request.
        lifetime: lifetime of the resulting Jwt (used to calculate the 'exp' claim).
            By default, do not include an 'exp' claim.

    Returns:
        a `RequestParameterAuthorizationRequest`, with a request object as parameter

    """
    request_jwt = self.sign_and_encrypt_request_jwt(
        sign_jwk=sign_jwk,
        enc_jwk=enc_jwk,
        sign_alg=sign_alg,
        enc_alg=enc_alg,
        enc=enc,
        lifetime=lifetime,
    )
    return RequestParameterAuthorizationRequest(
        authorization_endpoint=self.authorization_endpoint,
        client_id=self.client_id,
        request=str(request_jwt),
    )
on_response_error(response)

Error handler for Authorization Response errors.

Triggered by validate_callback() if the response uri contains an error.

Parameters:

Name Type Description Default
response str

the Authorization Response URI. This can be the full URL, or just the query parameters.

required

Returns:

Type Description
AuthorizationResponse

may return a default code that will be returned by validate_callback. But this method

AuthorizationResponse

will most likely raise exceptions instead.

Raises:

Type Description
AuthorizationResponseError

if the response contains an error. The raised exception may be a subclass

Source code in requests_oauth2client/authorization_request.py
def on_response_error(self, response: str) -> AuthorizationResponse:
    """Error handler for Authorization Response errors.

    Triggered by
    [validate_callback()][requests_oauth2client.authorization_request.AuthorizationRequest.validate_callback]
    if the response uri contains an error.

    Args:
        response: the Authorization Response URI. This can be the full URL, or just the query parameters.

    Returns:
        may return a default code that will be returned by `validate_callback`. But this method
        will most likely raise exceptions instead.

    Raises:
        AuthorizationResponseError: if the response contains an `error`. The raised exception may be a subclass

    """
    response_url = furl(response)
    error = response_url.args.get("error")
    error_description = response_url.args.get("error_description")
    error_uri = response_url.args.get("error_uri")
    exception_class = self.exception_classes.get(error, AuthorizationResponseError)
    raise exception_class(
        request=self, response=response, error=error, description=error_description, uri=error_uri
    )

RequestParameterAuthorizationRequest

Represent an Authorization Request that includes a request JWT.

To construct such a request yourself, the easiest way is to initialize an AuthorizationRequest then sign it with AuthorizationRequest.sign().

Parameters:

Name Type Description Default
authorization_endpoint str

the Authorization Endpoint uri

required
client_id str

the client_id

required
request Jwt | str

the request JWT

required
expires_at datetime | None

the expiration date for this request

None
kwargs Any

extra parameters to include in the request

{}
Source code in requests_oauth2client/authorization_request.py
@frozen(init=False, repr=False)
class RequestParameterAuthorizationRequest:
    """Represent an Authorization Request that includes a `request` JWT.

    To construct such a request yourself, the easiest way is to initialize
    an [`AuthorizationRequest`][requests_oauth2client.authorization_request.AuthorizationRequest]
    then sign it with
    [`AuthorizationRequest.sign()`][requests_oauth2client.authorization_request.AuthorizationRequest.sign].

    Args:
        authorization_endpoint: the Authorization Endpoint uri
        client_id: the client_id
        request: the request JWT
        expires_at: the expiration date for this request
        kwargs: extra parameters to include in the request

    """

    authorization_endpoint: str
    client_id: str
    request: Jwt
    expires_at: datetime | None
    dpop_key: DPoPKey | None
    kwargs: dict[str, Any]

    @accepts_expires_in
    def __init__(
        self,
        authorization_endpoint: str,
        client_id: str,
        request: Jwt | str,
        expires_at: datetime | None = None,
        dpop_key: DPoPKey | None = None,
        **kwargs: Any,
    ) -> None:
        if isinstance(request, str):
            request = Jwt(request)

        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            request=request,
            expires_at=expires_at,
            dpop_key=dpop_key,
            kwargs=kwargs,
        )

    @property
    def furl(self) -> furl:
        """Return the Authorization Request URI, as a `furl` instance."""
        return furl(
            self.authorization_endpoint,
            args={"client_id": self.client_id, "request": str(self.request), **self.kwargs},
        )

    @property
    def uri(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return str(self.furl.url)

    def __getattr__(self, item: str) -> Any:
        """Allow attribute access to extra parameters."""
        return self.kwargs[item]

    def __repr__(self) -> str:
        """Return the Authorization Request URI, as a `str`.

        Returns:
             the Authorization Request URI

        """
        return self.uri
furl property

Return the Authorization Request URI, as a furl instance.

uri property

Return the Authorization Request URI, as a str.

RequestUriParameterAuthorizationRequest

Represent an Authorization Request that includes a request_uri parameter.

Parameters:

Name Type Description Default
authorization_endpoint str

The Authorization Endpoint uri.

required
client_id str

The Client ID.

required
request_uri str

The request_uri.

required
expires_at datetime | None

The expiration date for this request.

None
kwargs Any

Extra query parameters to include in the request.

{}
Source code in requests_oauth2client/authorization_request.py
@frozen(init=False)
class RequestUriParameterAuthorizationRequest:
    """Represent an Authorization Request that includes a `request_uri` parameter.

    Args:
        authorization_endpoint: The Authorization Endpoint uri.
        client_id: The Client ID.
        request_uri: The `request_uri`.
        expires_at: The expiration date for this request.
        kwargs: Extra query parameters to include in the request.

    """

    authorization_endpoint: str
    client_id: str
    request_uri: str
    expires_at: datetime | None
    dpop_key: DPoPKey | None
    kwargs: dict[str, Any]

    @accepts_expires_in
    def __init__(
        self,
        authorization_endpoint: str,
        *,
        client_id: str,
        request_uri: str,
        expires_at: datetime | None = None,
        dpop_key: DPoPKey | None = None,
        **kwargs: Any,
    ) -> None:
        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            request_uri=request_uri,
            expires_at=expires_at,
            dpop_key=dpop_key,
            kwargs=kwargs,
        )

    @property
    def furl(self) -> furl:
        """Return the Authorization Request URI, as a `furl` instance."""
        return furl(
            self.authorization_endpoint,
            args={"client_id": self.client_id, "request_uri": self.request_uri, **self.kwargs},
        )

    @property
    def uri(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return str(self.furl.url)

    def __getattr__(self, item: str) -> Any:
        """Allow attribute access to extra parameters."""
        return self.kwargs[item]

    def __repr__(self) -> str:
        """Return the Authorization Request URI, as a `str`."""
        return self.uri
furl property

Return the Authorization Request URI, as a furl instance.

uri property

Return the Authorization Request URI, as a str.

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.

Source code in requests_oauth2client/authorization_request.py
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)
default_dumper(azr) staticmethod

Provide a default dumper implementation.

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

Parameters:

Name Type Description Default
azr AuthorizationRequest

the AuthorizationRequest to serialize

required

Returns:

Type Description
str

the serialized value

Source code in requests_oauth2client/authorization_request.py
@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()
default_loader(serialized, azr_class=AuthorizationRequest) staticmethod

Provide a default deserializer implementation.

This does the opposite operations than default_dumper.

Parameters:

Name Type Description Default
serialized str

the serialized AuthorizationRequest

required
azr_class type[AuthorizationRequest]

the class to deserialize the Authorization Request to

AuthorizationRequest

Returns:

Type Description
AuthorizationRequest

an AuthorizationRequest

Source code in requests_oauth2client/authorization_request.py
@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)
dumps(azr)

Serialize and compress a given AuthorizationRequest for easier storage.

Parameters:

Name Type Description Default
azr AuthorizationRequest

an AuthorizationRequest to serialize

required

Returns:

Type Description
str

the serialized AuthorizationRequest, as a str

Source code in requests_oauth2client/authorization_request.py
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)
loads(serialized)

Deserialize a serialized AuthorizationRequest.

Parameters:

Name Type Description Default
serialized str

the serialized AuthorizationRequest

required

Returns:

Type Description
AuthorizationRequest

the deserialized AuthorizationRequest

Source code in requests_oauth2client/authorization_request.py
def loads(self, serialized: str) -> AuthorizationRequest:
    """Deserialize a serialized AuthorizationRequest.

    Args:
        serialized: the serialized AuthorizationRequest

    Returns:
        the deserialized AuthorizationRequest

    """
    return self.loader(serialized)

backchannel_authentication

Implementation of CIBA.

CIBA stands for Client Initiated BackChannel Authentication and is standardised by the OpenID Fundation. https://openid.net/specs/openid-client-initiated-backchannel- authentication-core-1_0.html.

BackChannelAuthenticationResponse

Represent a BackChannel Authentication Response.

This contains all the parameters that are returned by the AS as a result of a BackChannel Authentication Request, such as auth_req_id (required), and the optional expires_at, interval, and/or any custom parameters.

Parameters:

Name Type Description Default
auth_req_id str

the auth_req_id as returned by the AS.

required
expires_at datetime | None

the date when the auth_req_id expires. Note that this request also accepts an expires_in parameter, in seconds.

None
interval int | None

the Token Endpoint pooling interval, in seconds, as returned by the AS.

20
**kwargs Any

any additional custom parameters as returned by the AS.

{}
Source code in requests_oauth2client/backchannel_authentication.py
class BackChannelAuthenticationResponse:
    """Represent a BackChannel Authentication Response.

    This contains all the parameters that are returned by the AS as a result of a BackChannel
    Authentication Request, such as `auth_req_id` (required), and the optional `expires_at`,
    `interval`, and/or any custom parameters.

    Args:
        auth_req_id: the `auth_req_id` as returned by the AS.
        expires_at: the date when the `auth_req_id` expires.
            Note that this request also accepts an `expires_in` parameter, in seconds.
        interval: the Token Endpoint pooling interval, in seconds, as returned by the AS.
        **kwargs: any additional custom parameters as returned by the AS.

    """

    @accepts_expires_in
    def __init__(
        self,
        auth_req_id: str,
        expires_at: datetime | None = None,
        interval: int | None = 20,
        **kwargs: Any,
    ) -> None:
        self.auth_req_id = auth_req_id
        self.expires_at = expires_at
        self.interval = interval
        self.other = kwargs

    def is_expired(self, leeway: int = 0) -> bool | None:
        """Return `True` if the `auth_req_id` within this response is expired.

        Expiration is evaluated at the time of the call. If there is no "expires_at" hint (which is
        derived from the `expires_in` hint returned by the AS BackChannel Authentication endpoint),
        this will return `None`.

        Returns:
            `True` if the auth_req_id is expired, `False` if it is still valid, `None` if there is
            no `expires_in` hint.

        """
        if self.expires_at:
            return datetime.now(tz=timezone.utc) - timedelta(seconds=leeway) > self.expires_at
        return None

    @property
    def expires_in(self) -> int | None:
        """Number of seconds until expiration."""
        if self.expires_at:
            return ceil((self.expires_at - datetime.now(tz=timezone.utc)).total_seconds())
        return None

    def __getattr__(self, key: str) -> Any:
        """Return attributes from this `BackChannelAuthenticationResponse`.

        Allows accessing response parameters with `token_response.expires_in` or
        `token_response.any_custom_attribute`.

        Args:
            key: a key

        Returns:
            the associated value in this token response

        Raises:
            AttributeError: if the attribute is not present in the response

        """
        return self.other.get(key) or super().__getattribute__(key)
expires_in property

Number of seconds until expiration.

is_expired(leeway=0)

Return True if the auth_req_id within this response is expired.

Expiration is evaluated at the time of the call. If there is no "expires_at" hint (which is derived from the expires_in hint returned by the AS BackChannel Authentication endpoint), this will return None.

Returns:

Type Description
bool | None

True if the auth_req_id is expired, False if it is still valid, None if there is

bool | None

no expires_in hint.

Source code in requests_oauth2client/backchannel_authentication.py
def is_expired(self, leeway: int = 0) -> bool | None:
    """Return `True` if the `auth_req_id` within this response is expired.

    Expiration is evaluated at the time of the call. If there is no "expires_at" hint (which is
    derived from the `expires_in` hint returned by the AS BackChannel Authentication endpoint),
    this will return `None`.

    Returns:
        `True` if the auth_req_id is expired, `False` if it is still valid, `None` if there is
        no `expires_in` hint.

    """
    if self.expires_at:
        return datetime.now(tz=timezone.utc) - timedelta(seconds=leeway) > self.expires_at
    return None

BackChannelAuthenticationPoolingJob

Bases: BaseTokenEndpointPoolingJob

A pooling job for the BackChannel Authentication flow.

This will poll the Token Endpoint until the user finishes with its authentication.

Parameters:

Name Type Description Default
client OAuth2Client

an OAuth2Client that will be used to pool the token endpoint.

required
auth_req_id str | BackChannelAuthenticationResponse

an auth_req_id as str or a BackChannelAuthenticationResponse.

required
interval int | None

The pooling interval, in seconds, to use. This overrides the one in auth_req_id if it is a BackChannelAuthenticationResponse. Defaults to 5 seconds.

None
slow_down_interval int

Number of seconds to add to the pooling interval when the AS returns a slow down request.

5
requests_kwargs dict[str, Any] | None

Additional parameters for the underlying calls to requests.request.

None
**token_kwargs Any

Additional parameters for the token request.

{}
Example
1
2
3
4
5
6
7
8
9
client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
pool_job = BackChannelAuthenticationPoolingJob(
    client=client,
    auth_req_id="my_auth_req_id",
)

token = None
while token is None:
    token = pool_job()
Source code in requests_oauth2client/backchannel_authentication.py
@define(init=False)
class BackChannelAuthenticationPoolingJob(BaseTokenEndpointPoolingJob):
    """A pooling job for the BackChannel Authentication flow.

    This will poll the Token Endpoint until the user finishes with its authentication.

    Args:
        client: an OAuth2Client that will be used to pool the token endpoint.
        auth_req_id: an `auth_req_id` as `str` or a `BackChannelAuthenticationResponse`.
        interval: The pooling interval, in seconds, to use. This overrides
            the one in `auth_req_id` if it is a `BackChannelAuthenticationResponse`.
            Defaults to 5 seconds.
        slow_down_interval: Number of seconds to add to the pooling interval when the AS returns
            a slow down request.
        requests_kwargs: Additional parameters for the underlying calls to [requests.request][].
        **token_kwargs: Additional parameters for the token request.

    Example:
        ```python
        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        pool_job = BackChannelAuthenticationPoolingJob(
            client=client,
            auth_req_id="my_auth_req_id",
        )

        token = None
        while token is None:
            token = pool_job()
        ```

    """

    auth_req_id: str

    def __init__(
        self,
        client: OAuth2Client,
        auth_req_id: str | BackChannelAuthenticationResponse,
        *,
        interval: int | None = None,
        slow_down_interval: int = 5,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(auth_req_id, BackChannelAuthenticationResponse):
            interval = interval or auth_req_id.interval
            auth_req_id = auth_req_id.auth_req_id

        self.__attrs_init__(
            client=client,
            auth_req_id=auth_req_id,
            interval=interval or 5,
            slow_down_interval=slow_down_interval,
            requests_kwargs=requests_kwargs or {},
            token_kwargs=token_kwargs,
        )

    def token_request(self) -> BearerToken:
        """Implement the CIBA token request.

        This actually calls [OAuth2Client.ciba(auth_req_id)] on `client`.

        Returns:
            a [BearerToken][requests_oauth2client.tokens.BearerToken]

        """
        return self.client.ciba(self.auth_req_id, requests_kwargs=self.requests_kwargs, **self.token_kwargs)
token_request()

Implement the CIBA token request.

This actually calls [OAuth2Client.ciba(auth_req_id)] on client.

Returns:

Type Description
BearerToken
Source code in requests_oauth2client/backchannel_authentication.py
def token_request(self) -> BearerToken:
    """Implement the CIBA token request.

    This actually calls [OAuth2Client.ciba(auth_req_id)] on `client`.

    Returns:
        a [BearerToken][requests_oauth2client.tokens.BearerToken]

    """
    return self.client.ciba(self.auth_req_id, requests_kwargs=self.requests_kwargs, **self.token_kwargs)

client

This module contains the OAuth2Client class.

InvalidParam

Bases: ValueError

Base class for invalid parameters errors.

Source code in requests_oauth2client/client.py
class InvalidParam(ValueError):
    """Base class for invalid parameters errors."""

MissingIdTokenEncryptedResponseAlgParam

Bases: InvalidParam

Raised when an ID Token encryption is required but not provided.

Source code in requests_oauth2client/client.py
class MissingIdTokenEncryptedResponseAlgParam(InvalidParam):
    """Raised when an ID Token encryption is required but not provided."""

    def __init__(self) -> None:
        super().__init__("""\
An ID Token decryption key has been provided but no decryption algorithm is defined.
You can either pass an `id_token_encrypted_response_alg` parameter with the alg identifier,
or include an `alg` attribute in the decryption key, if it is in Jwk format.
""")

InvalidEndpointUri

Bases: InvalidParam

Raised when an invalid endpoint uri is provided.

Source code in requests_oauth2client/client.py
class InvalidEndpointUri(InvalidParam):
    """Raised when an invalid endpoint uri is provided."""

    def __init__(self, endpoint: str, uri: str, exc: InvalidUri) -> None:
        super().__init__(f"Invalid endpoint uri '{uri}' for '{endpoint}': {exc}")
        self.endpoint = endpoint
        self.uri = uri

InvalidIssuer

Bases: InvalidEndpointUri

Raised when an invalid issuer parameter is provided.

Source code in requests_oauth2client/client.py
class InvalidIssuer(InvalidEndpointUri):
    """Raised when an invalid issuer parameter is provided."""

InvalidScopeParam

Bases: InvalidParam

Raised when an invalid scope parameter is provided.

Source code in requests_oauth2client/client.py
class InvalidScopeParam(InvalidParam):
    """Raised when an invalid scope parameter is provided."""

    def __init__(self, scope: object) -> None:
        super().__init__("""\
Unsupported scope value. It must be one of:
- a space separated `str` of scopes names
- an iterable of scope names as `str`
""")
        self.scope = scope

MissingRefreshToken

Bases: ValueError

Raised when a refresh token is required but not present.

Source code in requests_oauth2client/client.py
class MissingRefreshToken(ValueError):
    """Raised when a refresh token is required but not present."""

    def __init__(self, token: TokenResponse) -> None:
        super().__init__("A refresh_token is required but is not present in this Access Token.")
        self.token = token

MissingDeviceCode

Bases: ValueError

Raised when a device_code is required but not provided.

Source code in requests_oauth2client/client.py
class MissingDeviceCode(ValueError):
    """Raised when a device_code is required but not provided."""

    def __init__(self, dar: DeviceAuthorizationResponse) -> None:
        super().__init__("A device_code is missing in this DeviceAuthorizationResponse")
        self.device_authorization_response = dar

MissingAuthRequestId

Bases: ValueError

Raised when an 'auth_req_id' is missing in a BackChannelAuthenticationResponse.

Source code in requests_oauth2client/client.py
class MissingAuthRequestId(ValueError):
    """Raised when an 'auth_req_id' is missing in a BackChannelAuthenticationResponse."""

    def __init__(self, bcar: BackChannelAuthenticationResponse) -> None:
        super().__init__("An 'auth_req_id' is required but is missing from this BackChannelAuthenticationResponse.")
        self.backchannel_authentication_response = bcar

UnknownTokenType

Bases: InvalidParam, TypeError

Raised when the type of a token cannot be determined automatically.

Source code in requests_oauth2client/client.py
class UnknownTokenType(InvalidParam, TypeError):
    """Raised when the type of a token cannot be determined automatically."""

    def __init__(self, message: str, token: object, token_type: str | None) -> None:
        super().__init__(f"Unable to determine the type of token provided: {message}")
        self.token = token
        self.token_type = token_type

UnknownSubjectTokenType

Bases: UnknownTokenType

Raised when the type of subject_token cannot be determined automatically.

Source code in requests_oauth2client/client.py
class UnknownSubjectTokenType(UnknownTokenType):
    """Raised when the type of subject_token cannot be determined automatically."""

    def __init__(self, subject_token: object, subject_token_type: str | None) -> None:
        super().__init__("subject_token", subject_token, subject_token_type)

UnknownActorTokenType

Bases: UnknownTokenType

Raised when the type of actor_token cannot be determined automatically.

Source code in requests_oauth2client/client.py
class UnknownActorTokenType(UnknownTokenType):
    """Raised when the type of actor_token cannot be determined automatically."""

    def __init__(self, actor_token: object, actor_token_type: str | None) -> None:
        super().__init__("actor_token", token=actor_token, token_type=actor_token_type)

InvalidBackchannelAuthenticationRequestHintParam

Bases: InvalidParam

Raised when an invalid hint is provided in a backchannel authentication request.

Source code in requests_oauth2client/client.py
class InvalidBackchannelAuthenticationRequestHintParam(InvalidParam):
    """Raised when an invalid hint is provided in a backchannel authentication request."""

InvalidAcrValuesParam

Bases: InvalidParam

Raised when an invalid 'acr_values' parameter is provided.

Source code in requests_oauth2client/client.py
class InvalidAcrValuesParam(InvalidParam):
    """Raised when an invalid 'acr_values' parameter is provided."""

    def __init__(self, acr_values: object) -> None:
        super().__init__(f"Invalid 'acr_values' parameter: {acr_values}")
        self.acr_values = acr_values

InvalidDiscoveryDocument

Bases: ValueError

Raised when handling an invalid Discovery Document.

Source code in requests_oauth2client/client.py
class InvalidDiscoveryDocument(ValueError):
    """Raised when handling an invalid Discovery Document."""

    def __init__(self, message: str, discovery_document: dict[str, Any]) -> None:
        super().__init__(f"Invalid discovery document: {message}")
        self.discovery_document = discovery_document

Endpoints

Bases: 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.

Source code in requests_oauth2client/client.py
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"

MissingEndpointUri

Bases: AttributeError

Raised when a required endpoint uri is not known.

Source code in requests_oauth2client/client.py
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.")

GrantTypes

Bases: str, Enum

An enum of standardized grant_type values.

Source code in requests_oauth2client/client.py
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"

OAuth2Client

An OAuth 2.x Client that can send requests to an OAuth 2.x Authorization Server.

OAuth2Client is able to obtain tokens from the Token Endpoint using any of the standardised Grant Types, and to communicate with the various backend endpoints like the Revocation, Introspection, and UserInfo Endpoint.

To init an OAuth2Client, you only need the url to the Token Endpoint and the Credentials (a client_id and one of a secret or private_key) that will be used to authenticate to that endpoint. Other endpoint urls, such as the Authorization Endpoint, Revocation Endpoint, etc. can be passed as parameter as well if you intend to use them.

This class is not intended to help with the end-user authentication or any request that goes in a browser. For authentication requests, see AuthorizationRequest. You may use the method authorization_request() to generate AuthorizationRequests with the preconfigured authorization_endpoint, client_id and `redirect_uri' from this client.

Parameters:

Name Type Description Default
token_endpoint str

the Token Endpoint URI where this client will get access tokens

required
auth AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None

the authentication handler to use for client authentication on the token endpoint. Can be:

None
client_id str | None

client ID (use either this or auth)

None
client_secret str | None

client secret (use either this or auth)

None
private_key Jwk | dict[str, Any] | None

private_key to use for client authentication (use either this or auth)

None
revocation_endpoint str | None

the Revocation Endpoint URI to use for revoking tokens

None
introspection_endpoint str | None

the Introspection Endpoint URI to use to get info about tokens

None
userinfo_endpoint str | None

the Userinfo Endpoint URI to use to get information about the user

None
authorization_endpoint str | None

the Authorization Endpoint URI, used for initializing Authorization Requests

None
redirect_uri str | None

the redirect_uri for this client

None
backchannel_authentication_endpoint str | None

the BackChannel Authentication URI

None
device_authorization_endpoint str | None

the Device Authorization Endpoint URI to use to authorize devices

None
jwks_uri str | None

the JWKS URI to use to obtain the AS public keys

None
code_challenge_method str

challenge method to use for PKCE (should always be 'S256')

S256
session Session | None

a requests Session to use when sending HTTP requests. Useful if some extra parameters such as proxy or client certificate must be used to connect to the AS.

None
token_class type[BearerToken]

a custom BearerToken class, if required

BearerToken
dpop_bound_access_tokens bool

if True, DPoP will be used by default for every token request. otherwise, you can enable DPoP by passing dpop=True when doing a token request.

False
dpop_key_generator Callable[[str], DPoPKey]

a callable that generates a DPoPKey, for whill be called when doing a token request with DPoP enabled.

generate
testing bool

if True, don't verify the validity of the endpoint urls that are passed as parameter.

False
**extra_metadata Any

additional metadata for this client, unused by this class, but may be used by subclasses. Those will be accessible with the extra_metadata attribute.

{}
Example
client = OAuth2Client(
    token_endpoint="https://my.as.local/token",
    revocation_endpoint="https://my.as.local/revoke",
    client_id="client_id",
    client_secret="client_secret",
)

# once initialized, a client can send requests to its configured endpoints
cc_token = client.client_credentials(scope="my_scope")
ac_token = client.authorization_code(code="my_code")
client.revoke_access_token(cc_token)

Raises:

Type Description
MissingIDTokenEncryptedResponseAlgParam

if an id_token_decryption_key is provided but no decryption alg is provided, either:

  • using id_token_encrypted_response_alg,
  • or in the alg parameter of the Jwk key
MissingIssuerParam

if authorization_response_iss_parameter_supported is set to True but the issuer is not provided.

InvalidEndpointUri

if a provided endpoint uri is not considered valid. For the rare cases where those checks must be disabled, you can use testing=True.

InvalidIssuer

if the issuer value is not considered valid.

Source code in requests_oauth2client/client.py
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
@frozen(init=False)
class OAuth2Client:
    """An OAuth 2.x Client that can send requests to an OAuth 2.x Authorization Server.

    `OAuth2Client` is able to obtain tokens from the Token Endpoint using any of the standardised
    Grant Types, and to communicate with the various backend endpoints like the Revocation,
    Introspection, and UserInfo Endpoint.

    To init an OAuth2Client, you only need the url to the Token Endpoint and the Credentials
    (a client_id and one of a secret or private_key) that will be used to authenticate to that endpoint.
    Other endpoint urls, such as the Authorization Endpoint, Revocation Endpoint, etc. can be passed as
    parameter as well if you intend to use them.


    This class is not intended to help with the end-user authentication or any request that goes in
    a browser. For authentication requests, see
    [AuthorizationRequest][requests_oauth2client.authorization_request.AuthorizationRequest]. You
    may use the method `authorization_request()` to generate `AuthorizationRequest`s with the
    preconfigured `authorization_endpoint`, `client_id` and `redirect_uri' from this client.

    Args:
        token_endpoint: the Token Endpoint URI where this client will get access tokens
        auth: the authentication handler to use for client authentication on the token endpoint.
            Can be:

            - a [requests.auth.AuthBase][] instance (which will be used as-is)
            - a tuple of `(client_id, client_secret)` which will initialize an instance
            of [ClientSecretPost][requests_oauth2client.client_authentication.ClientSecretPost]
            - a `(client_id, jwk)` to initialize
            a [PrivateKeyJwt][requests_oauth2client.client_authentication.PrivateKeyJwt],
            - or a `client_id` which will
            use [PublicApp][requests_oauth2client.client_authentication.PublicApp] authentication.

        client_id: client ID (use either this or `auth`)
        client_secret: client secret (use either this or `auth`)
        private_key: private_key to use for client authentication (use either this or `auth`)
        revocation_endpoint: the Revocation Endpoint URI to use for revoking tokens
        introspection_endpoint: the Introspection Endpoint URI to use to get info about tokens
        userinfo_endpoint: the Userinfo Endpoint URI to use to get information about the user
        authorization_endpoint: the Authorization Endpoint URI, used for initializing Authorization Requests
        redirect_uri: the redirect_uri for this client
        backchannel_authentication_endpoint: the BackChannel Authentication URI
        device_authorization_endpoint: the Device Authorization Endpoint URI to use to authorize devices
        jwks_uri: the JWKS URI to use to obtain the AS public keys
        code_challenge_method: challenge method to use for PKCE (should always be 'S256')
        session: a requests Session to use when sending HTTP requests.
            Useful if some extra parameters such as proxy or client certificate must be used
            to connect to the AS.
        token_class: a custom BearerToken class, if required
        dpop_bound_access_tokens: if `True`, DPoP will be used by default for every token request.
            otherwise, you can enable DPoP by passing `dpop=True` when doing a token request.
        dpop_key_generator: a callable that generates a DPoPKey, for whill be called when doing a token request
            with DPoP enabled.
        testing: if `True`, don't verify the validity of the endpoint urls that are passed as parameter.
        **extra_metadata: additional metadata for this client, unused by this class, but may be
            used by subclasses. Those will be accessible with the `extra_metadata` attribute.

    Example:
        ```python
        client = OAuth2Client(
            token_endpoint="https://my.as.local/token",
            revocation_endpoint="https://my.as.local/revoke",
            client_id="client_id",
            client_secret="client_secret",
        )

        # once initialized, a client can send requests to its configured endpoints
        cc_token = client.client_credentials(scope="my_scope")
        ac_token = client.authorization_code(code="my_code")
        client.revoke_access_token(cc_token)
        ```

    Raises:
        MissingIDTokenEncryptedResponseAlgParam: if an `id_token_decryption_key` is provided
            but no decryption alg is provided, either:

            - using `id_token_encrypted_response_alg`,
            - or in the `alg` parameter of the `Jwk` key
        MissingIssuerParam: if `authorization_response_iss_parameter_supported` is set to `True`
            but the `issuer` is not provided.
        InvalidEndpointUri: if a provided endpoint uri is not considered valid. For the rare cases
            where those checks must be disabled, you can use `testing=True`.
        InvalidIssuer: if the `issuer` value is not considered valid.

    """

    auth: requests.auth.AuthBase
    token_endpoint: str = field()
    revocation_endpoint: str | None = field()
    introspection_endpoint: str | None = field()
    userinfo_endpoint: str | None = field()
    authorization_endpoint: str | None = field()
    redirect_uri: str | None = field()
    backchannel_authentication_endpoint: str | None = field()
    device_authorization_endpoint: str | None = field()
    pushed_authorization_request_endpoint: str | None = field()
    jwks_uri: str | None = field()
    authorization_server_jwks: JwkSet
    issuer: str | None = field()
    id_token_signed_response_alg: str | None
    id_token_encrypted_response_alg: str | None
    id_token_decryption_key: Jwk | None
    code_challenge_method: str | None
    authorization_response_iss_parameter_supported: bool
    session: requests.Session
    extra_metadata: dict[str, Any]
    testing: bool

    dpop_bound_access_tokens: bool
    dpop_key_generator: Callable[[str], DPoPKey]
    dpop_alg: str

    token_class: type[BearerToken]

    exception_classes: ClassVar[dict[str, type[EndpointError]]] = {
        "server_error": ServerError,
        "invalid_request": InvalidRequest,
        "invalid_client": InvalidClient,
        "invalid_scope": InvalidScope,
        "invalid_target": InvalidTarget,
        "invalid_grant": InvalidGrant,
        "access_denied": AccessDenied,
        "unauthorized_client": UnauthorizedClient,
        "authorization_pending": AuthorizationPending,
        "slow_down": SlowDown,
        "expired_token": ExpiredToken,
        "use_dpop_nonce": UseDPoPNonce,
        "unsupported_token_type": UnsupportedTokenType,
    }

    def __init__(  # noqa: PLR0913
        self,
        token_endpoint: str,
        auth: (
            requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
        ) = None,
        *,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_key: Jwk | dict[str, Any] | None = None,
        revocation_endpoint: str | None = None,
        introspection_endpoint: str | None = None,
        userinfo_endpoint: str | None = None,
        authorization_endpoint: str | None = None,
        redirect_uri: str | None = None,
        backchannel_authentication_endpoint: str | None = None,
        device_authorization_endpoint: str | None = None,
        pushed_authorization_request_endpoint: str | None = None,
        jwks_uri: str | None = None,
        authorization_server_jwks: JwkSet | dict[str, Any] | None = None,
        issuer: str | None = None,
        id_token_signed_response_alg: str | None = SignatureAlgs.RS256,
        id_token_encrypted_response_alg: str | None = None,
        id_token_decryption_key: Jwk | dict[str, Any] | None = None,
        code_challenge_method: str = CodeChallengeMethods.S256,
        authorization_response_iss_parameter_supported: bool = False,
        token_class: type[BearerToken] = BearerToken,
        session: requests.Session | None = None,
        dpop_bound_access_tokens: bool = False,
        dpop_key_generator: Callable[[str], DPoPKey] = DPoPKey.generate,
        dpop_alg: str = SignatureAlgs.ES256,
        testing: bool = False,
        **extra_metadata: Any,
    ) -> None:
        if authorization_response_iss_parameter_supported and not issuer:
            raise MissingIssuerParam

        auth = client_auth_factory(
            auth,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            default_auth_handler=ClientSecretPost,
        )

        if authorization_server_jwks is None:
            authorization_server_jwks = JwkSet()
        elif not isinstance(authorization_server_jwks, JwkSet):
            authorization_server_jwks = JwkSet(authorization_server_jwks)

        if id_token_decryption_key is not None and not isinstance(id_token_decryption_key, Jwk):
            id_token_decryption_key = Jwk(id_token_decryption_key)

        if id_token_decryption_key is not None and id_token_encrypted_response_alg is None:
            if id_token_decryption_key.alg:
                id_token_encrypted_response_alg = id_token_decryption_key.alg
            else:
                raise MissingIdTokenEncryptedResponseAlgParam

        if dpop_alg not in SignatureAlgs.ALL_ASYMMETRIC:
            raise InvalidDPoPAlg(dpop_alg)

        if session is None:
            session = requests.Session()

        self.__attrs_init__(
            testing=testing,
            token_endpoint=token_endpoint,
            revocation_endpoint=revocation_endpoint,
            introspection_endpoint=introspection_endpoint,
            userinfo_endpoint=userinfo_endpoint,
            authorization_endpoint=authorization_endpoint,
            redirect_uri=redirect_uri,
            backchannel_authentication_endpoint=backchannel_authentication_endpoint,
            device_authorization_endpoint=device_authorization_endpoint,
            pushed_authorization_request_endpoint=pushed_authorization_request_endpoint,
            jwks_uri=jwks_uri,
            authorization_server_jwks=authorization_server_jwks,
            issuer=issuer,
            session=session,
            auth=auth,
            id_token_signed_response_alg=id_token_signed_response_alg,
            id_token_encrypted_response_alg=id_token_encrypted_response_alg,
            id_token_decryption_key=id_token_decryption_key,
            code_challenge_method=code_challenge_method,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            extra_metadata=extra_metadata,
            token_class=token_class,
            dpop_key_generator=dpop_key_generator,
            dpop_bound_access_tokens=dpop_bound_access_tokens,
            dpop_alg=dpop_alg,
        )

    @token_endpoint.validator
    @revocation_endpoint.validator
    @introspection_endpoint.validator
    @userinfo_endpoint.validator
    @authorization_endpoint.validator
    @backchannel_authentication_endpoint.validator
    @device_authorization_endpoint.validator
    @pushed_authorization_request_endpoint.validator
    @jwks_uri.validator
    def validate_endpoint_uri(self, attribute: Attribute[str | None], uri: str | None) -> str | None:
        """Validate that an endpoint URI is suitable for use.

        If you need to disable some checks (for AS testing purposes only!), provide a different method here.

        """
        if self.testing or uri is None:
            return uri
        try:
            return validate_endpoint_uri(uri)
        except InvalidUri as exc:
            raise InvalidEndpointUri(endpoint=attribute.name, uri=uri, exc=exc) from exc

    @issuer.validator
    def validate_issuer_uri(self, attribute: Attribute[str | None], uri: str | None) -> str | None:
        """Validate that an Issuer identifier is suitable for use.

        This is the same check as an endpoint URI, but the path may be (and usually is) empty.

        """
        if self.testing or uri is None:
            return uri
        try:
            return validate_issuer_uri(uri)
        except InvalidUri as exc:
            raise InvalidIssuer(attribute.name, uri, exc) from exc

    @property
    def client_id(self) -> str:
        """Client ID."""
        if hasattr(self.auth, "client_id"):
            return self.auth.client_id  # type: ignore[no-any-return]
        msg = "This client uses a custom authentication method without client_id."
        raise AttributeError(msg)  # pragma: no cover

    @property
    def client_secret(self) -> str | None:
        """Client Secret."""
        if hasattr(self.auth, "client_secret"):
            return self.auth.client_secret  # type: ignore[no-any-return]
        return None

    @property
    def client_jwks(self) -> JwkSet:
        """A `JwkSet` containing the public keys for this client.

        Keys are:

        - the public key for client assertion signature verification (if using private_key_jwt)
        - the ID Token encryption key

        """
        jwks = JwkSet()
        if isinstance(self.auth, PrivateKeyJwt):
            jwks.add_jwk(self.auth.private_jwk.public_jwk().with_usage_parameters())
        if self.id_token_decryption_key:
            jwks.add_jwk(self.id_token_decryption_key.public_jwk().with_usage_parameters())
        return jwks

    def _request(
        self,
        endpoint: str,
        *,
        on_success: Callable[[requests.Response, DefaultNamedArg(DPoPKey | None, "dpop_key")], T],
        on_failure: Callable[[requests.Response, DefaultNamedArg(DPoPKey | None, "dpop_key")], T],
        dpop_key: DPoPKey | None = None,
        accept: str = "application/json",
        method: str = "POST",
        **requests_kwargs: Any,
    ) -> T:
        """Send a request to one of the endpoints.

        This is a helper method that takes care of the following tasks:

        - make sure the endpoint as been configured
        - set `Accept: application/json` header
        - send the HTTP POST request, then
            - apply `on_success` to a successful response
            - or apply `on_failure` otherwise
        - return the result

        Args:
            endpoint: name of the endpoint to use
            on_success: a callable to apply to successful responses
            on_failure: a callable to apply to error responses
            dpop_key: a `DPoPKey` to proof the request. If `None` (default), no DPoP proofing is done.
            accept: the Accept header to include in the request
            method: the HTTP method to use
            **requests_kwargs: keyword arguments for the request

        Raises:
            InvalidTokenResponse: if the AS response contains a `use_dpop_nonce` error but:
              - the response comes in reply to a non-DPoP request
              - the DPoPKey.handle_as_provided_dpop_nonce() method raises an exception. This should happen:
                    - if the response does not include a DPoP-Nonce HTTP header with the requested nonce value
                    - or if the requested nonce is the same value that was sent in the request DPoP proof
              - a new nonce value is requested again for the 3rd time in a row

        """
        endpoint_uri = self._require_endpoint(endpoint)
        requests_kwargs.setdefault("headers", {})
        requests_kwargs["headers"]["Accept"] = accept

        for _ in range(3):
            if dpop_key:
                dpop_proof = dpop_key.proof(htm="POST", htu=endpoint_uri, nonce=dpop_key.as_nonce)
                requests_kwargs.setdefault("headers", {})
                requests_kwargs["headers"]["DPoP"] = str(dpop_proof)

            response = self.session.request(
                method,
                endpoint_uri,
                **requests_kwargs,
            )
            if response.ok:
                return on_success(response, dpop_key=dpop_key)

            try:
                return on_failure(response, dpop_key=dpop_key)
            except UseDPoPNonce as exc:
                if dpop_key is None:
                    raise InvalidTokenResponse(
                        response,
                        self,
                        """\
Authorization Server requested client to include a DPoP `nonce` in its DPoP proof,
but the initial request did not include a DPoP proof.
""",
                    ) from exc
                try:
                    dpop_key.handle_as_provided_dpop_nonce(response)
                except (MissingDPoPNonce, RepeatedDPoPNonce) as exc:
                    raise InvalidTokenResponse(response, self, str(exc)) from exc

        raise InvalidTokenResponse(
            response,
            self,
            """\
Authorization Server requested client to use a different DPoP `nonce` for the third time in row.
This should never happen. This exception is raised to avoid a potential endless loop where the client
keeps trying to obey the new DPoP `nonce` values as provided by the Authorization Server after each token request.
""",
        )

    def token_request(
        self,
        data: dict[str, Any],
        *,
        timeout: int = 10,
        dpop: bool | None = None,
        dpop_key: DPoPKey | None = None,
        **requests_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint.

        Authentication will be added automatically based on the defined `auth` for this client.

        Args:
          data: parameters to send to the token endpoint. Items with a `None`
               or empty value will not be sent in the request.
          timeout: a timeout value for the call
          dpop: toggles DPoP-proofing for the token request:

                - if `False`, disable it,
                - if `True`, enable it,
                - if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
          dpop_key: a chosen `DPoPKey` for this request. If `None`, a new key will be generated automatically
                with a call to this client `dpop_key_generator`.
          **requests_kwargs: additional parameters for requests.post()

        Returns:
            the token endpoint response

        """
        if dpop is None:
            dpop = self.dpop_bound_access_tokens
        if dpop and not dpop_key:
            dpop_key = self.dpop_key_generator(self.dpop_alg)

        return self._request(
            Endpoints.TOKEN,
            auth=self.auth,
            data=data,
            timeout=timeout,
            dpop_key=dpop_key,
            on_success=self.parse_token_response,
            on_failure=self.on_token_error,
            **requests_kwargs,
        )

    def parse_token_response(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> BearerToken:
        """Parse a Response returned by the Token Endpoint.

        Invoked by [token_request][requests_oauth2client.client.OAuth2Client.token_request] to parse
        responses returned by the Token Endpoint. Those responses contain an `access_token` and
        additional attributes.

        Args:
            response: the `Response` returned by the Token Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            a `BearerToken` based on the response contents.

        """
        token_class = dpop_key.dpop_token_class if dpop_key is not None else self.token_class
        try:
            token_response = token_class(**response.json(), _dpop_key=dpop_key)
        except Exception:  # noqa: BLE001
            return self.on_token_error(response, dpop_key=dpop_key)
        else:
            return token_response

    def on_token_error(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> BearerToken:
        """Error handler for `token_request()`.

        Invoked by [token_request][requests_oauth2client.client.OAuth2Client.token_request] when the
        Token Endpoint returns an error.

        Args:
            response: the `Response` returned by the Token Endpoint.
            dpop_key: the DPoPKey that was used to proof the token request, if any.

        Returns:
            nothing, and raises an exception instead. But a subclass may return a
            `BearerToken` to implement a default behaviour if needed.

        Raises:
            InvalidTokenResponse: if the error response does not contain an OAuth 2.0 standard
                error response.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, UnknownTokenEndpointError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise InvalidTokenResponse(
                response=response,
                client=self,
                description=f"An error happened while processing the error response: {exc}",
            ) from exc
        raise exception

    def client_credentials(
        self,
        scope: str | Iterable[str] | None = None,
        *,
        dpop: bool | None = None,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint using the `client_credentials` grant.

        Args:
            scope: the scope to send with the request. Can be a str, or an iterable of str.
                to pass that way include `scope`, `audience`, `resource`, etc.
            dpop: toggles DPoP-proofing for the token request:

                - if `False`, disable it,
                - if `True`, enable it,
                - if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
            dpop_key: a chosen `DPoPKey` for this request. If `None`, a new key will be generated automatically
                with a call to `dpop_key_generator`.
            requests_kwargs: additional parameters for the call to requests
            **token_kwargs: additional parameters that will be added in the form data for the token endpoint,
                 alongside `grant_type`.

        Returns:
            a `BearerToken` or `DPoPToken`, depending on the AS response.

        Raises:
            InvalidScopeParam: if the `scope` parameter is not suitable

        """
        requests_kwargs = requests_kwargs or {}

        if scope and not isinstance(scope, str):
            try:
                scope = " ".join(scope)
            except Exception as exc:
                raise InvalidScopeParam(scope) from exc

        data = dict(grant_type=GrantTypes.CLIENT_CREDENTIALS, scope=scope, **token_kwargs)
        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def authorization_code(
        self,
        code: str | AuthorizationResponse,
        *,
        validate: bool = True,
        dpop: bool = False,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint with the `authorization_code` grant.

        You can either pass an authorization code, as a `str`, or pass an `AuthorizationResponse` instance as
        returned by `AuthorizationRequest.validate_callback()` (recommended). If you do the latter, this will
        automatically:

        - add the appropriate `redirect_uri` value that was initially passed in the Authorization Request parameters.
        This is no longer mandatory in OAuth 2.1, but a lot of Authorization Servers are still expecting it since it was
        part of the OAuth 2.0 specifications.
        - add the appropriate `code_verifier` for PKCE that was generated before sending the AuthorizationRequest.
        - handle DPoP binding based on the same `DPoPKey` that was used to initialize the `AuthenticationRequest` and
        whose JWK thumbprint was passed as `dpop_jkt` parameter in the Auth Request.

        Args:
            code: An authorization code or an `AuthorizationResponse` to exchange for tokens.
            validate: If `True`, validate the ID Token (this works only if `code` is an `AuthorizationResponse`).
            dpop: Toggles DPoP binding for the Access Token,
                 even if Authorization Code DPoP binding was not initially done.
            dpop_key: A chosen DPoP key. Leave `None` to automatically generate a key, if `dpop` is `True`.
            requests_kwargs: Additional parameters for the call to the underlying HTTP `requests` call.
            **token_kwargs: Additional parameters that will be added in the form data for the token endpoint,
                alongside `grant_type`, `code`, etc.

        Returns:
            The Token Endpoint Response.

        """
        azr: AuthorizationResponse | None = None
        if isinstance(code, AuthorizationResponse):
            token_kwargs.setdefault("code_verifier", code.code_verifier)
            token_kwargs.setdefault("redirect_uri", code.redirect_uri)
            azr = code
            dpop_key = code.dpop_key
            code = code.code

        requests_kwargs = requests_kwargs or {}

        data = dict(grant_type=GrantTypes.AUTHORIZATION_CODE, code=code, **token_kwargs)
        token = self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
        if validate and token.id_token and isinstance(azr, AuthorizationResponse):
            return token.validate_id_token(self, azr)
        return token

    def refresh_token(
        self,
        refresh_token: str | BearerToken,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint with the `refresh_token` grant.

        If `refresh_token` is a `DPoPToken` instance, (which means that DPoP was used to obtain the initial
        Access/Refresh Tokens), then the same DPoP key will be used to DPoP proof the refresh token request,
        as defined in RFC9449.

        Args:
            refresh_token: A refresh_token, as a string, or as a `BearerToken`.
                That `BearerToken` must have a `refresh_token`.
            requests_kwargs: Additional parameters for the call to `requests`.
            **token_kwargs: Additional parameters for the token endpoint,
                alongside `grant_type`, `refresh_token`, etc.

        Returns:
            The token endpoint response.

        Raises:
            MissingRefreshToken: If `refresh_token` is a `BearerToken` instance but does not
                contain a `refresh_token`.

        """
        dpop_key: DPoPKey | None = None
        if isinstance(refresh_token, BearerToken):
            if refresh_token.refresh_token is None or not isinstance(refresh_token.refresh_token, str):
                raise MissingRefreshToken(refresh_token)
            if isinstance(refresh_token, DPoPToken):
                dpop_key = refresh_token.dpop_key
            refresh_token = refresh_token.refresh_token

        requests_kwargs = requests_kwargs or {}
        data = dict(grant_type=GrantTypes.REFRESH_TOKEN, refresh_token=refresh_token, **token_kwargs)
        return self.token_request(data, dpop_key=dpop_key, **requests_kwargs)

    def device_code(
        self,
        device_code: str | DeviceAuthorizationResponse,
        *,
        dpop: bool = False,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request to the token endpoint using the Device Code grant.

        The grant_type is `urn:ietf:params:oauth:grant-type:device_code`. This needs a Device Code,
        or a `DeviceAuthorizationResponse` as parameter.

        Args:
            device_code: A device code, or a `DeviceAuthorizationResponse`.
            dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
            requests_kwargs: Additional parameters for the call to requests.
            **token_kwargs: Additional parameters for the token endpoint, alongside `grant_type`, `device_code`, etc.

        Returns:
            The Token Endpoint response.

        Raises:
            MissingDeviceCode: if `device_code` is a DeviceAuthorizationResponse but does not
                contain a `device_code`.

        """
        if isinstance(device_code, DeviceAuthorizationResponse):
            if device_code.device_code is None or not isinstance(device_code.device_code, str):
                raise MissingDeviceCode(device_code)
            device_code = device_code.device_code

        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantTypes.DEVICE_CODE,
            device_code=device_code,
            **token_kwargs,
        )
        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def ciba(
        self,
        auth_req_id: str | BackChannelAuthenticationResponse,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a CIBA request to the Token Endpoint.

        A CIBA request is a Token Request using the `urn:openid:params:grant-type:ciba` grant.

        Args:
            auth_req_id: an authentication request ID, as returned by the AS
            requests_kwargs: additional parameters for the call to requests
            **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `auth_req_id`, etc.

        Returns:
            The Token Endpoint response.

        Raises:
            MissingAuthRequestId: if `auth_req_id` is a BackChannelAuthenticationResponse but does not contain
                an `auth_req_id`.

        """
        if isinstance(auth_req_id, BackChannelAuthenticationResponse):
            if auth_req_id.auth_req_id is None or not isinstance(auth_req_id.auth_req_id, str):
                raise MissingAuthRequestId(auth_req_id)
            auth_req_id = auth_req_id.auth_req_id

        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantTypes.CLIENT_INITIATED_BACKCHANNEL_AUTHENTICATION,
            auth_req_id=auth_req_id,
            **token_kwargs,
        )
        return self.token_request(data, **requests_kwargs)

    def token_exchange(
        self,
        *,
        subject_token: str | BearerToken | IdToken,
        subject_token_type: str | None = None,
        actor_token: None | str | BearerToken | IdToken = None,
        actor_token_type: str | None = None,
        requested_token_type: str | None = None,
        dpop: bool = False,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a Token Exchange request.

        A Token Exchange request is actually a request to the Token Endpoint with a grant_type
        `urn:ietf:params:oauth:grant-type:token-exchange`.

        Args:
            subject_token: The subject token to exchange for a new token.
            subject_token_type: A token type identifier for the subject_token, mandatory if it cannot be guessed based
                on `type(subject_token)`.
            actor_token: The actor token to include in the request, if any.
            actor_token_type: A token type identifier for the actor_token, mandatory if it cannot be guessed based
                on `type(actor_token)`.
            requested_token_type: A token type identifier for the requested token.
            dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
            requests_kwargs: Additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: Additional parameters to include in the request body.

        Returns:
            The Token Endpoint response.

        Raises:
            UnknownSubjectTokenType: If the type of `subject_token` cannot be determined automatically.
            UnknownActorTokenType: If the type of `actor_token` cannot be determined automatically.

        """
        requests_kwargs = requests_kwargs or {}

        try:
            subject_token_type = self.get_token_type(subject_token_type, subject_token)
        except ValueError as exc:
            raise UnknownSubjectTokenType(subject_token, subject_token_type) from exc
        if actor_token:  # pragma: no branch
            try:
                actor_token_type = self.get_token_type(actor_token_type, actor_token)
            except ValueError as exc:
                raise UnknownActorTokenType(actor_token, actor_token_type) from exc

        data = dict(
            grant_type=GrantTypes.TOKEN_EXCHANGE,
            subject_token=subject_token,
            subject_token_type=subject_token_type,
            actor_token=actor_token,
            actor_token_type=actor_token_type,
            requested_token_type=requested_token_type,
            **token_kwargs,
        )
        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def jwt_bearer(
        self,
        assertion: Jwt | str,
        *,
        dpop: bool = False,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request using a JWT as authorization grant.

        This is defined in (RFC7523 $2.1)[https://www.rfc-editor.org/rfc/rfc7523.html#section-2.1).

        Args:
            assertion: A JWT (as an instance of `jwskate.Jwt` or as a `str`) to use as authorization grant.
            dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
            requests_kwargs: Additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: Additional parameters to include in the request body.

        Returns:
            The Token Endpoint response.

        """
        requests_kwargs = requests_kwargs or {}

        if not isinstance(assertion, Jwt):
            assertion = Jwt(assertion)

        data = dict(
            grant_type=GrantTypes.JWT_BEARER,
            assertion=assertion,
            **token_kwargs,
        )

        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def resource_owner_password(
        self,
        username: str,
        password: str,
        *,
        dpop: bool | None = None,
        dpop_key: DPoPKey | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> BearerToken:
        """Send a request using the Resource Owner Password Grant.

        This Grant Type is deprecated and should only be used when there is no other choice.

        Args:
            username: the resource owner user name
            password: the resource owner password
            dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
            requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: additional parameters to include in the request body.

        Returns:
            The Token Endpoint response.

        """
        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantTypes.RESOURCE_OWNER_PASSWORD,
            username=username,
            password=password,
            **token_kwargs,
        )

        return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)

    def authorization_request(
        self,
        *,
        scope: None | str | Iterable[str] = "openid",
        response_type: str = ResponseTypes.CODE,
        redirect_uri: str | None = None,
        state: str | ellipsis | None = ...,  # noqa: F821
        nonce: str | ellipsis | None = ...,  # noqa: F821
        code_verifier: str | None = None,
        dpop: bool | None = None,
        dpop_key: DPoPKey | None = None,
        dpop_alg: str | None = None,
        **kwargs: Any,
    ) -> AuthorizationRequest:
        """Generate an Authorization Request for this client.

        Args:
            scope: The `scope` to use.
            response_type: The `response_type` to use.
            redirect_uri: The `redirect_uri` to include in the request. By default,
                the `redirect_uri` defined at init time is used.
            state: The `state` parameter to use. Leave default to generate a random value.
            nonce: A `nonce`. Leave default to generate a random value.
            dpop: Toggles DPoP binding.
                - if `True`, DPoP binding is used
                - if `False`, DPoP is not used
                - if `None`, defaults to `self.dpop_bound_access_tokens`
            dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for you.
            dpop_alg: A signature alg to sign the DPoP proof. If `None`, this defaults to `self.dpop_alg`.
                If DPoP is not used, or a chosen `dpop_key` is provided, this is ignored.
                This affects the key type if a DPoP key must be generated.
            code_verifier: The PKCE `code_verifier` to use. Leave default to generate a random value.
            **kwargs: Additional query parameters to include in the auth request.

        Returns:
            The Token Endpoint response.

        """
        authorization_endpoint = self._require_endpoint("authorization_endpoint")

        redirect_uri = redirect_uri or self.redirect_uri

        if dpop is None:
            dpop = self.dpop_bound_access_tokens
        if dpop_alg is None:
            dpop_alg = self.dpop_alg

        return AuthorizationRequest(
            authorization_endpoint=authorization_endpoint,
            client_id=self.client_id,
            redirect_uri=redirect_uri,
            issuer=self.issuer,
            response_type=response_type,
            scope=scope,
            state=state,
            nonce=nonce,
            code_verifier=code_verifier,
            code_challenge_method=self.code_challenge_method,
            dpop=dpop,
            dpop_key=dpop_key,
            dpop_alg=dpop_alg,
            **kwargs,
        )

    def pushed_authorization_request(
        self,
        authorization_request: AuthorizationRequest,
        requests_kwargs: dict[str, Any] | None = None,
    ) -> RequestUriParameterAuthorizationRequest:
        """Send a Pushed Authorization Request.

        This sends a request to the Pushed Authorization Request Endpoint, and returns a
        `RequestUriParameterAuthorizationRequest` initialized with the AS response.

        Args:
            authorization_request: The authorization request to send.
            requests_kwargs: Additional parameters for `requests.request()`.

        Returns:
            The `RequestUriParameterAuthorizationRequest` initialized based on the AS response.

        """
        requests_kwargs = requests_kwargs or {}
        return self._request(
            Endpoints.PUSHED_AUTHORIZATION_REQUEST,
            data=authorization_request.args,
            auth=self.auth,
            on_success=self.parse_pushed_authorization_response,
            on_failure=self.on_pushed_authorization_request_error,
            dpop_key=authorization_request.dpop_key,
            **requests_kwargs,
        )

    def parse_pushed_authorization_response(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,
    ) -> RequestUriParameterAuthorizationRequest:
        """Parse the response obtained by `pushed_authorization_request()`.

        Args:
            response: The `requests.Response` returned by the PAR endpoint.
            dpop_key: The `DPoPKey` that was used to proof the token request, if any.

        Returns:
            A `RequestUriParameterAuthorizationRequest` instance initialized based on the PAR endpoint response.

        """
        response_json = response.json()
        request_uri = response_json.get("request_uri")
        expires_in = response_json.get("expires_in")

        return RequestUriParameterAuthorizationRequest(
            authorization_endpoint=self.authorization_endpoint,
            client_id=self.client_id,
            request_uri=request_uri,
            expires_in=expires_in,
            dpop_key=dpop_key,
        )

    def on_pushed_authorization_request_error(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> RequestUriParameterAuthorizationRequest:
        """Error Handler for Pushed Authorization Endpoint errors.

        Args:
            response: The HTTP response as returned by the AS PAR endpoint.
            dpop_key: The `DPoPKey` that was used to proof the token request, if any.

        Returns:
            Should not return anything, but raise an Exception instead. A `RequestUriParameterAuthorizationRequest`
            may be returned by subclasses for testing purposes.

        Raises:
            EndpointError: A subclass of this error depending on the error returned by the AS.
            InvalidPushedAuthorizationResponse: If the returned response is not following the specifications.
            UnknownTokenEndpointError: For unknown/unhandled errors.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, UnknownTokenEndpointError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise InvalidPushedAuthorizationResponse(response=response, client=self) from exc
        raise exception

    def userinfo(self, access_token: BearerToken | str) -> Any:
        """Call the UserInfo endpoint.

        This sends a request to the UserInfo endpoint, with the specified access_token, and returns
        the parsed result.

        Args:
            access_token: the access token to use

        Returns:
            the [Response][requests.Response] returned by the userinfo endpoint.

        """
        if isinstance(access_token, str):
            access_token = BearerToken(access_token)
        return self._request(
            Endpoints.USER_INFO,
            auth=access_token,
            on_success=self.parse_userinfo_response,
            on_failure=self.on_userinfo_error,
        )

    def parse_userinfo_response(self, resp: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
        """Parse the response obtained by `userinfo()`.

        Invoked by [userinfo()][requests_oauth2client.client.OAuth2Client.userinfo] to parse the
        response from the UserInfo endpoint, this will extract and return its JSON content.

        Args:
            resp: a [Response][requests.Response] returned from the UserInfo endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            the parsed JSON content from this response.

        """
        return resp.json()

    def on_userinfo_error(self, resp: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
        """Parse UserInfo error response.

        Args:
            resp: a [Response][requests.Response] returned from the UserInfo endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            nothing, raises exception instead.

        """
        resp.raise_for_status()

    @classmethod
    def get_token_type(  # noqa: C901
        cls,
        token_type: str | None = None,
        token: None | str | BearerToken | IdToken = None,
    ) -> str:
        """Get standardized token type identifiers.

        Return a standardized token type identifier, based on a short `token_type` hint and/or a
        token value.

        Args:
            token_type: a token_type hint, as `str`. May be "access_token", "refresh_token"
                or "id_token"
            token: a token value, as an instance of `BearerToken` or IdToken, or as a `str`.

        Returns:
            the token_type as defined in the Token Exchange RFC8693.

        Raises:
            UnknownTokenType: if the type of token cannot be determined

        """
        if not (token_type or token):
            msg = "Cannot determine type of an empty token without a token_type hint"
            raise UnknownTokenType(msg, token, token_type)

        if token_type is None:
            if isinstance(token, str):
                msg = """\
Cannot determine the type of provided token when it is a bare `str`. Please specify a 'token_type'.
"""
                raise UnknownTokenType(msg, token, token_type)
            if isinstance(token, BearerToken):
                return "urn:ietf:params:oauth:token-type:access_token"
            if isinstance(token, IdToken):
                return "urn:ietf:params:oauth:token-type:id_token"
            msg = f"Unknown token type {type(token)}"
            raise UnknownTokenType(msg, token, token_type)
        if token_type == TokenType.ACCESS_TOKEN:
            if token is not None and not isinstance(token, (str, BearerToken)):
                msg = f"""\
The supplied token is of type '{type(token)}' which is inconsistent with token_type '{token_type}'.
A BearerToken or an access_token as a `str` is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:access_token"
        if token_type == TokenType.REFRESH_TOKEN:
            if token is not None and isinstance(token, BearerToken) and not token.refresh_token:
                msg = f"""\
The supplied BearerToken does not contain a refresh_token, which is inconsistent with token_type '{token_type}'.
A BearerToken containing a refresh_token is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:refresh_token"
        if token_type == TokenType.ID_TOKEN:
            if token is not None and not isinstance(token, (str, IdToken)):
                msg = f"""\
The supplied token is of type '{type(token)}' which is inconsistent with token_type '{token_type}'.
An IdToken or a string representation of it is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:id_token"

        return {
            "saml1": "urn:ietf:params:oauth:token-type:saml1",
            "saml2": "urn:ietf:params:oauth:token-type:saml2",
            "jwt": "urn:ietf:params:oauth:token-type:jwt",
        }.get(token_type, token_type)

    def revoke_access_token(
        self,
        access_token: BearerToken | str,
        requests_kwargs: dict[str, Any] | None = None,
        **revoke_kwargs: Any,
    ) -> bool:
        """Send a request to the Revocation Endpoint to revoke an access token.

        Args:
            access_token: the access token to revoke
            requests_kwargs: additional parameters for the underlying requests.post() call
            **revoke_kwargs: additional parameters to pass to the revocation endpoint

        """
        return self.revoke_token(
            access_token,
            token_type_hint=TokenType.ACCESS_TOKEN,
            requests_kwargs=requests_kwargs,
            **revoke_kwargs,
        )

    def revoke_refresh_token(
        self,
        refresh_token: str | BearerToken,
        requests_kwargs: dict[str, Any] | None = None,
        **revoke_kwargs: Any,
    ) -> bool:
        """Send a request to the Revocation Endpoint to revoke a refresh token.

        Args:
            refresh_token: the refresh token to revoke.
            requests_kwargs: additional parameters to pass to the revocation endpoint.
            **revoke_kwargs: additional parameters to pass to the revocation endpoint.

        Returns:
            `True` if the revocation request is successful, `False` if this client has no configured
            revocation endpoint.

        Raises:
            MissingRefreshToken: when `refresh_token` is a [BearerToken][requests_oauth2client.tokens.BearerToken]
                but does not contain a `refresh_token`.

        """
        if isinstance(refresh_token, BearerToken):
            if refresh_token.refresh_token is None:
                raise MissingRefreshToken(refresh_token)
            refresh_token = refresh_token.refresh_token

        return self.revoke_token(
            refresh_token,
            token_type_hint=TokenType.REFRESH_TOKEN,
            requests_kwargs=requests_kwargs,
            **revoke_kwargs,
        )

    def revoke_token(
        self,
        token: str | BearerToken,
        token_type_hint: str | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **revoke_kwargs: Any,
    ) -> bool:
        """Send a Token Revocation request.

        By default, authentication will be the same than the one used for the Token Endpoint.

        Args:
            token: the token to revoke.
            token_type_hint: a token_type_hint to send to the revocation endpoint.
            requests_kwargs: additional parameters to the underling call to requests.post()
            **revoke_kwargs: additional parameters to send to the revocation endpoint.

        Returns:
            the result from `parse_revocation_response` on the returned AS response.

        Raises:
            MissingEndpointUri: if the Revocation Endpoint URI is not configured.
            MissingRefreshToken: if `token_type_hint` is `"refresh_token"` and `token` is a BearerToken
                but does not contain a `refresh_token`.

        """
        requests_kwargs = requests_kwargs or {}

        if token_type_hint == TokenType.REFRESH_TOKEN and isinstance(token, BearerToken):
            if token.refresh_token is None:
                raise MissingRefreshToken(token)
            token = token.refresh_token

        data = dict(revoke_kwargs, token=str(token))
        if token_type_hint:
            data["token_type_hint"] = token_type_hint

        return self._request(
            Endpoints.REVOCATION,
            data=data,
            auth=self.auth,
            on_success=self.parse_revocation_response,
            on_failure=self.on_revocation_error,
            **requests_kwargs,
        )

    def parse_revocation_response(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> bool:  # noqa: ARG002
        """Parse reponses from the Revocation Endpoint.

        Since those do not return any meaningful information in a standardised fashion, this just returns `True`.

        Args:
            response: the `requests.Response` as returned by the Revocation Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            `True` if the revocation succeeds, `False` if no revocation endpoint is present or a
            non-standardised error is returned.

        """
        return True

    def on_revocation_error(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> bool:  # noqa: ARG002
        """Error handler for `revoke_token()`.

        Invoked by [revoke_token()][requests_oauth2client.client.OAuth2Client.revoke_token] when the
        revocation endpoint returns an error.

        Args:
            response: the `requests.Response` as returned by the Revocation Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            `False` to signal that an error occurred. May raise exceptions instead depending on the
            revocation response.

        Raises:
            EndpointError: if the response contains a standardised OAuth 2.0 error.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, RevocationError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception:  # noqa: BLE001
            return False
        raise exception

    def introspect_token(
        self,
        token: str | BearerToken,
        token_type_hint: str | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **introspect_kwargs: Any,
    ) -> Any:
        """Send a request to the Introspection Endpoint.

        Parameter `token` can be:

        - a `str`
        - a `BearerToken` instance

        You may pass any arbitrary `token` and `token_type_hint` values as `str`. Those will
        be included in the request, as-is.
        If `token` is a `BearerToken`, then `token_type_hint` must be either:

        - `None`: the access_token will be instrospected and no token_type_hint will be included
        in the request
        - `access_token`: same as `None`, but the token_type_hint will be included
        - or `refresh_token`: only available if a Refresh Token is present in the BearerToken.

        Args:
            token: the token to instrospect
            token_type_hint: the `token_type_hint` to include in the request.
            requests_kwargs: additional parameters to the underling call to requests.post()
            **introspect_kwargs: additional parameters to send to the introspection endpoint.

        Returns:
            the response as returned by the Introspection Endpoint.

        Raises:
            MissingRefreshToken: if `token_type_hint` is `"refresh_token"` and `token` is a BearerToken
                but does not contain a `refresh_token`.
            UnknownTokenType: if `token_type_hint` is neither `None`, `"access_token"` or `"refresh_token"`.

        """
        requests_kwargs = requests_kwargs or {}

        if isinstance(token, BearerToken):
            if token_type_hint is None or token_type_hint == TokenType.ACCESS_TOKEN:
                token = token.access_token
            elif token_type_hint == TokenType.REFRESH_TOKEN:
                if token.refresh_token is None:
                    raise MissingRefreshToken(token)

                token = token.refresh_token
            else:
                msg = """\
Invalid `token_type_hint`. To test arbitrary `token_type_hint` values, you must provide `token` as a `str`."""
                raise UnknownTokenType(msg, token, token_type_hint)

        data = dict(introspect_kwargs, token=str(token))
        if token_type_hint:
            data["token_type_hint"] = token_type_hint

        return self._request(
            Endpoints.INTROSPECTION,
            data=data,
            auth=self.auth,
            on_success=self.parse_introspection_response,
            on_failure=self.on_introspection_error,
            **requests_kwargs,
        )

    def parse_introspection_response(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> Any:
        """Parse Token Introspection Responses received by `introspect_token()`.

        Invoked by [introspect_token()][requests_oauth2client.client.OAuth2Client.introspect_token]
        to parse the returned response. This decodes the JSON content if possible, otherwise it
        returns the response as a string.

        Args:
            response: the [Response][requests.Response] as returned by the Introspection Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            the decoded JSON content, or a `str` with the content.

        """
        try:
            return response.json()
        except ValueError:
            return response.text

    def on_introspection_error(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
        """Error handler for `introspect_token()`.

        Invoked by [introspect_token()][requests_oauth2client.client.OAuth2Client.introspect_token]
        to parse the returned response in the case an error is returned.

        Args:
            response: the response as returned by the Introspection Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            usually raises exceptions. A subclass can return a default response instead.

        Raises:
            EndpointError: (or one of its subclasses) if the response contains a standard OAuth 2.0 error.
            UnknownIntrospectionError: if the response is not a standard error response.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, IntrospectionError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise UnknownIntrospectionError(response=response, client=self) from exc
        raise exception

    def backchannel_authentication_request(  # noqa: PLR0913
        self,
        scope: None | str | Iterable[str] = "openid",
        *,
        client_notification_token: str | None = None,
        acr_values: None | str | Iterable[str] = None,
        login_hint_token: str | None = None,
        id_token_hint: str | None = None,
        login_hint: str | None = None,
        binding_message: str | None = None,
        user_code: str | None = None,
        requested_expiry: int | None = None,
        private_jwk: Jwk | dict[str, Any] | None = None,
        alg: str | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **ciba_kwargs: Any,
    ) -> BackChannelAuthenticationResponse:
        """Send a CIBA Authentication Request.

        Args:
             scope: the scope to include in the request.
             client_notification_token: the Client Notification Token to include in the request.
             acr_values: the acr values to include in the request.
             login_hint_token: the Login Hint Token to include in the request.
             id_token_hint: the ID Token Hint to include in the request.
             login_hint: the Login Hint to include in the request.
             binding_message: the Binding Message to include in the request.
             user_code: the User Code to include in the request
             requested_expiry: the Requested Expiry, in seconds, to include in the request.
             private_jwk: the JWK to use to sign the request (optional)
             alg: the alg to use to sign the request, if the provided JWK does not include an "alg" parameter.
             requests_kwargs: additional parameters for
             **ciba_kwargs: additional parameters to include in the request.

        Returns:
            a BackChannelAuthenticationResponse as returned by AS

        Raises:
            InvalidBackchannelAuthenticationRequestHintParam: if none of `login_hint`, `login_hint_token`
                or `id_token_hint` is provided, or more than one of them is provided.
            InvalidScopeParam: if the `scope` parameter is invalid.
            InvalidAcrValuesParam: if the `acr_values` parameter is invalid.

        """
        if not (login_hint or login_hint_token or id_token_hint):
            msg = "One of `login_hint`, `login_hint_token` or `ìd_token_hint` must be provided"
            raise InvalidBackchannelAuthenticationRequestHintParam(msg)

        if (login_hint_token and id_token_hint) or (login_hint and id_token_hint) or (login_hint_token and login_hint):
            msg = "Only one of `login_hint`, `login_hint_token` or `ìd_token_hint` must be provided"
            raise InvalidBackchannelAuthenticationRequestHintParam(msg)

        requests_kwargs = requests_kwargs or {}

        if scope is not None and not isinstance(scope, str):
            try:
                scope = " ".join(scope)
            except Exception as exc:
                raise InvalidScopeParam(scope) from exc

        if acr_values is not None and not isinstance(acr_values, str):
            try:
                acr_values = " ".join(acr_values)
            except Exception as exc:
                raise InvalidAcrValuesParam(acr_values) from exc

        data = dict(
            ciba_kwargs,
            scope=scope,
            client_notification_token=client_notification_token,
            acr_values=acr_values,
            login_hint_token=login_hint_token,
            id_token_hint=id_token_hint,
            login_hint=login_hint,
            binding_message=binding_message,
            user_code=user_code,
            requested_expiry=requested_expiry,
        )

        if private_jwk is not None:
            data = {"request": str(Jwt.sign(data, key=private_jwk, alg=alg))}

        return self._request(
            Endpoints.BACKCHANNEL_AUTHENTICATION,
            data=data,
            auth=self.auth,
            on_success=self.parse_backchannel_authentication_response,
            on_failure=self.on_backchannel_authentication_error,
            **requests_kwargs,
        )

    def parse_backchannel_authentication_response(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> BackChannelAuthenticationResponse:
        """Parse a response received by `backchannel_authentication_request()`.

        Invoked by
        [backchannel_authentication_request()][requests_oauth2client.client.OAuth2Client.backchannel_authentication_request]
        to parse the response returned by the BackChannel Authentication Endpoint.

        Args:
            response: the response returned by the BackChannel Authentication Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            a `BackChannelAuthenticationResponse`

        Raises:
            InvalidBackChannelAuthenticationResponse: if the response does not contain a standard
                BackChannel Authentication response.

        """
        try:
            return BackChannelAuthenticationResponse(**response.json())
        except TypeError as exc:
            raise InvalidBackChannelAuthenticationResponse(response=response, client=self) from exc

    def on_backchannel_authentication_error(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> BackChannelAuthenticationResponse:
        """Error handler for `backchannel_authentication_request()`.

        Invoked by
        [backchannel_authentication_request()][requests_oauth2client.client.OAuth2Client.backchannel_authentication_request]
        to parse the response returned by the BackChannel Authentication Endpoint, when it is an
        error.

        Args:
            response: the response returned by the BackChannel Authentication Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            usually raises an exception. But a subclass can return a default response instead.

        Raises:
            EndpointError: (or one of its subclasses) if the response contains a standard OAuth 2.0 error.
            InvalidBackChannelAuthenticationResponse: for non-standard error responses.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, BackChannelAuthenticationError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise InvalidBackChannelAuthenticationResponse(response=response, client=self) from exc
        raise exception

    def authorize_device(
        self,
        requests_kwargs: dict[str, Any] | None = None,
        **data: Any,
    ) -> DeviceAuthorizationResponse:
        """Send a Device Authorization Request.

        Args:
            **data: additional data to send to the Device Authorization Endpoint
            requests_kwargs: additional parameters for `requests.request()`

        Returns:
            a Device Authorization Response

        Raises:
            MissingEndpointUri: if the Device Authorization URI is not configured

        """
        requests_kwargs = requests_kwargs or {}

        return self._request(
            Endpoints.DEVICE_AUTHORIZATION,
            data=data,
            auth=self.auth,
            on_success=self.parse_device_authorization_response,
            on_failure=self.on_device_authorization_error,
            **requests_kwargs,
        )

    def parse_device_authorization_response(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> DeviceAuthorizationResponse:
        """Parse a Device Authorization Response received by `authorize_device()`.

        Invoked by [authorize_device()][requests_oauth2client.client.OAuth2Client.authorize_device]
        to parse the response returned by the Device Authorization Endpoint.

        Args:
            response: the response returned by the Device Authorization Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            a `DeviceAuthorizationResponse` as returned by AS

        """
        return DeviceAuthorizationResponse(**response.json())

    def on_device_authorization_error(
        self,
        response: requests.Response,
        *,
        dpop_key: DPoPKey | None = None,  # noqa: ARG002
    ) -> DeviceAuthorizationResponse:
        """Error handler for `authorize_device()`.

        Invoked by [authorize_device()][requests_oauth2client.client.OAuth2Client.authorize_device]
        to parse the response returned by the Device Authorization Endpoint, when that response is
        an error.

        Args:
            response: the response returned by the Device Authorization Endpoint.
            dpop_key: the `DPoPKey` that was used to proof the token request, if any.

        Returns:
            usually raises an Exception. But a subclass may return a default response instead.

        Raises:
            EndpointError: for standard OAuth 2.0 errors
            InvalidDeviceAuthorizationResponse: for non-standard error responses.

        """
        try:
            data = response.json()
            error = data["error"]
            error_description = data.get("error_description")
            error_uri = data.get("error_uri")
            exception_class = self.exception_classes.get(error, DeviceAuthorizationError)
            exception = exception_class(
                response=response,
                client=self,
                error=error,
                description=error_description,
                uri=error_uri,
            )
        except Exception as exc:
            raise InvalidDeviceAuthorizationResponse(response=response, client=self) from exc
        raise exception

    def update_authorization_server_public_keys(self, requests_kwargs: dict[str, Any] | None = None) -> JwkSet:
        """Update the cached AS public keys by retrieving them from its `jwks_uri`.

        Public keys are returned by this method, as a `jwskate.JwkSet`. They are also
        available in attribute `authorization_server_jwks`.

        Returns:
            the retrieved public keys

        Raises:
            ValueError: if no `jwks_uri` is configured

        """
        requests_kwargs = requests_kwargs or {}
        requests_kwargs.setdefault("auth", None)

        jwks_uri = self._require_endpoint(Endpoints.JWKS)
        resp = self.session.get(jwks_uri, **requests_kwargs)
        resp.raise_for_status()
        jwks = resp.json()
        self.authorization_server_jwks.update(jwks)
        return self.authorization_server_jwks

    @classmethod
    def from_discovery_endpoint(
        cls,
        url: str | None = None,
        issuer: str | None = None,
        *,
        auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_key: Jwk | dict[str, Any] | None = None,
        session: requests.Session | None = None,
        testing: bool = False,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialize an `OAuth2Client` using an AS Discovery Document endpoint.

        If an `url` is provided, an HTTPS request will be done to that URL to obtain the Authorization Server Metadata.

        If an `issuer` is provided, the OpenID Connect Discovery document url will be automatically
        derived from it, as specified in [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest).

        Once the standardized metadata document is obtained, this will extract
        all Endpoint Uris from that document, will fetch the current public keys from its
        `jwks_uri`, then will initialize an OAuth2Client based on those endpoints.

        Args:
          url: The url where the server metadata will be retrieved.
          issuer: The issuer value that is expected in the discovery document.
            If not `url` is given, the OpenID Connect Discovery url for this issuer will be retrieved.
          auth: The authentication handler to use for client authentication.
          client_id: Client ID.
          client_secret: Client secret to use to authenticate the client.
          private_key: Private key to sign client assertions.
          session: A `requests.Session` to use to retrieve the document and initialise the client with.
          testing: If `True`, do not try to validate the issuer uri nor the endpoint urls
            that are part of the document.
          **kwargs: Additional keyword parameters to pass to `OAuth2Client`.

        Returns:
          An `OAuth2Client` with endpoints initialized based on the obtained metadata.

        Raises:
          InvalidIssuer: If `issuer` is not using https, or contains credentials or fragment.
          InvalidParam: If neither `url` nor `issuer` are suitable urls.
          requests.HTTPError: If an error happens while fetching the documents.

        Example:
            ```python
            from requests_oauth2client import OAuth2Client

            client = OAuth2Client.from_discovery_endpoint(
                issuer="https://myserver.net",
                client_id="my_client_id,
                client_secret="my_client_secret",
            )
            ```

        """
        if issuer is not None and not testing:
            try:
                validate_issuer_uri(issuer)
            except InvalidUri as exc:
                raise InvalidIssuer("issuer", issuer, exc) from exc  # noqa: EM101
        if url is None and issuer is not None:
            url = oidc_discovery_document_url(issuer)
        if url is None:
            msg = "Please specify at least one of `issuer` or `url`"
            raise InvalidParam(msg)

        if not testing:
            validate_endpoint_uri(url, path=False)

        session = session or requests.Session()
        discovery = session.get(url).json()

        jwks_uri = discovery.get("jwks_uri")
        jwks = JwkSet(session.get(jwks_uri).json()) if jwks_uri else None

        return cls.from_discovery_document(
            discovery,
            issuer=issuer,
            auth=auth,
            session=session,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            authorization_server_jwks=jwks,
            testing=testing,
            **kwargs,
        )

    @classmethod
    def from_discovery_document(
        cls,
        discovery: dict[str, Any],
        issuer: str | None = None,
        *,
        auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_key: Jwk | dict[str, Any] | None = None,
        authorization_server_jwks: JwkSet | dict[str, Any] | None = None,
        https: bool = True,
        testing: bool = False,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialize an `OAuth2Client`, based on an AS Discovery Document.

        Args:
          discovery: A `dict` of server metadata, in the same format as retrieved from a discovery endpoint.
          issuer: If an issuer is given, check that it matches the one mentioned in the document.
          auth: The authentication handler to use for client authentication.
          client_id: Client ID.
          client_secret: Client secret to use to authenticate the client.
          private_key: Private key to sign client assertions.
          authorization_server_jwks: The current authorization server JWKS keys.
          https: (deprecated) If `True`, validates that urls in the discovery document use the https scheme.
          testing: If `True`, don't try to validate the endpoint urls that are part of the document.
          **kwargs: Additional args that will be passed to `OAuth2Client`.

        Returns:
            An `OAuth2Client` initialized with the endpoints from the discovery document.

        Raises:
            InvalidDiscoveryDocument: If the document does not contain at least a `"token_endpoint"`.

        Examples:
            ```python
            from requests_oauth2client import OAuth2Client

            client = OAuth2Client.from_discovery_document(
                {
                    "issuer": "https://myas.local",
                    "token_endpoint": "https://myas.local/token",
                },
                client_id="client_id",
                client_secret="client_secret",
            )
            ```

        """
        if not https:
            warnings.warn(
                """\
The `https` parameter is deprecated.
To disable endpoint uri validation, set `testing=True` when initializing your `OAuth2Client`.""",
                stacklevel=1,
            )
            testing = True
        if issuer and discovery.get("issuer") != issuer:
            msg = f"""\
Mismatching `issuer` value in discovery document (received '{discovery.get("issuer")}', expected '{issuer}')."""
            raise InvalidParam(
                msg,
                issuer,
                discovery.get("issuer"),
            )
        if issuer is None:
            issuer = discovery.get("issuer")

        token_endpoint = discovery.get(Endpoints.TOKEN)
        if token_endpoint is None:
            msg = "token_endpoint not found in that discovery document"
            raise InvalidDiscoveryDocument(msg, discovery)
        authorization_endpoint = discovery.get(Endpoints.AUTHORIZATION)
        revocation_endpoint = discovery.get(Endpoints.REVOCATION)
        introspection_endpoint = discovery.get(Endpoints.INTROSPECTION)
        userinfo_endpoint = discovery.get(Endpoints.USER_INFO)
        pushed_authorization_request_endpoint = discovery.get(Endpoints.PUSHED_AUTHORIZATION_REQUEST)
        jwks_uri = discovery.get(Endpoints.JWKS)
        if jwks_uri is not None and not testing:
            validate_endpoint_uri(jwks_uri)
        authorization_response_iss_parameter_supported = discovery.get(
            "authorization_response_iss_parameter_supported",
            False,
        )

        return cls(
            token_endpoint=token_endpoint,
            authorization_endpoint=authorization_endpoint,
            revocation_endpoint=revocation_endpoint,
            introspection_endpoint=introspection_endpoint,
            userinfo_endpoint=userinfo_endpoint,
            pushed_authorization_request_endpoint=pushed_authorization_request_endpoint,
            jwks_uri=jwks_uri,
            authorization_server_jwks=authorization_server_jwks,
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            issuer=issuer,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            testing=testing,
            **kwargs,
        )

    def __enter__(self) -> Self:
        """Allow using `OAuth2Client` as a context-manager.

        The Authorization Server public keys are retrieved on `__enter__`.

        """
        self.update_authorization_server_public_keys()
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> bool:
        return True

    def _require_endpoint(self, endpoint: str) -> str:
        """Check that a required endpoint url is set."""
        url = getattr(self, endpoint, None)
        if not url:
            raise MissingEndpointUri(endpoint)

        return str(url)
client_id property

Client ID.

client_secret property

Client Secret.

client_jwks property

A JwkSet containing the public keys for this client.

Keys are:

  • the public key for client assertion signature verification (if using private_key_jwt)
  • the ID Token encryption key
validate_endpoint_uri(attribute, uri)

Validate that an endpoint URI is suitable for use.

If you need to disable some checks (for AS testing purposes only!), provide a different method here.

Source code in requests_oauth2client/client.py
@token_endpoint.validator
@revocation_endpoint.validator
@introspection_endpoint.validator
@userinfo_endpoint.validator
@authorization_endpoint.validator
@backchannel_authentication_endpoint.validator
@device_authorization_endpoint.validator
@pushed_authorization_request_endpoint.validator
@jwks_uri.validator
def validate_endpoint_uri(self, attribute: Attribute[str | None], uri: str | None) -> str | None:
    """Validate that an endpoint URI is suitable for use.

    If you need to disable some checks (for AS testing purposes only!), provide a different method here.

    """
    if self.testing or uri is None:
        return uri
    try:
        return validate_endpoint_uri(uri)
    except InvalidUri as exc:
        raise InvalidEndpointUri(endpoint=attribute.name, uri=uri, exc=exc) from exc
validate_issuer_uri(attribute, uri)

Validate that an Issuer identifier is suitable for use.

This is the same check as an endpoint URI, but the path may be (and usually is) empty.

Source code in requests_oauth2client/client.py
@issuer.validator
def validate_issuer_uri(self, attribute: Attribute[str | None], uri: str | None) -> str | None:
    """Validate that an Issuer identifier is suitable for use.

    This is the same check as an endpoint URI, but the path may be (and usually is) empty.

    """
    if self.testing or uri is None:
        return uri
    try:
        return validate_issuer_uri(uri)
    except InvalidUri as exc:
        raise InvalidIssuer(attribute.name, uri, exc) from exc
token_request(data, *, timeout=10, dpop=None, dpop_key=None, **requests_kwargs)

Send a request to the token endpoint.

Authentication will be added automatically based on the defined auth for this client.

Parameters:

Name Type Description Default
data dict[str, Any]

parameters to send to the token endpoint. Items with a None or empty value will not be sent in the request.

required
timeout int

a timeout value for the call

10
dpop bool | None

toggles DPoP-proofing for the token request:

1
2
3
- if `False`, disable it,
- if `True`, enable it,
- if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
None
dpop_key DPoPKey | None

a chosen DPoPKey for this request. If None, a new key will be generated automatically with a call to this client dpop_key_generator.

None
**requests_kwargs Any

additional parameters for requests.post()

{}

Returns:

Type Description
BearerToken

the token endpoint response

Source code in requests_oauth2client/client.py
def token_request(
    self,
    data: dict[str, Any],
    *,
    timeout: int = 10,
    dpop: bool | None = None,
    dpop_key: DPoPKey | None = None,
    **requests_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint.

    Authentication will be added automatically based on the defined `auth` for this client.

    Args:
      data: parameters to send to the token endpoint. Items with a `None`
           or empty value will not be sent in the request.
      timeout: a timeout value for the call
      dpop: toggles DPoP-proofing for the token request:

            - if `False`, disable it,
            - if `True`, enable it,
            - if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
      dpop_key: a chosen `DPoPKey` for this request. If `None`, a new key will be generated automatically
            with a call to this client `dpop_key_generator`.
      **requests_kwargs: additional parameters for requests.post()

    Returns:
        the token endpoint response

    """
    if dpop is None:
        dpop = self.dpop_bound_access_tokens
    if dpop and not dpop_key:
        dpop_key = self.dpop_key_generator(self.dpop_alg)

    return self._request(
        Endpoints.TOKEN,
        auth=self.auth,
        data=data,
        timeout=timeout,
        dpop_key=dpop_key,
        on_success=self.parse_token_response,
        on_failure=self.on_token_error,
        **requests_kwargs,
    )
parse_token_response(response, *, dpop_key=None)

Parse a Response returned by the Token Endpoint.

Invoked by token_request to parse responses returned by the Token Endpoint. Those responses contain an access_token and additional attributes.

Parameters:

Name Type Description Default
response Response

the Response returned by the Token Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
BearerToken

a BearerToken based on the response contents.

Source code in requests_oauth2client/client.py
def parse_token_response(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> BearerToken:
    """Parse a Response returned by the Token Endpoint.

    Invoked by [token_request][requests_oauth2client.client.OAuth2Client.token_request] to parse
    responses returned by the Token Endpoint. Those responses contain an `access_token` and
    additional attributes.

    Args:
        response: the `Response` returned by the Token Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        a `BearerToken` based on the response contents.

    """
    token_class = dpop_key.dpop_token_class if dpop_key is not None else self.token_class
    try:
        token_response = token_class(**response.json(), _dpop_key=dpop_key)
    except Exception:  # noqa: BLE001
        return self.on_token_error(response, dpop_key=dpop_key)
    else:
        return token_response
on_token_error(response, *, dpop_key=None)

Error handler for token_request().

Invoked by token_request when the Token Endpoint returns an error.

Parameters:

Name Type Description Default
response Response

the Response returned by the Token Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
BearerToken

nothing, and raises an exception instead. But a subclass may return a

BearerToken

BearerToken to implement a default behaviour if needed.

Raises:

Type Description
InvalidTokenResponse

if the error response does not contain an OAuth 2.0 standard error response.

Source code in requests_oauth2client/client.py
def on_token_error(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> BearerToken:
    """Error handler for `token_request()`.

    Invoked by [token_request][requests_oauth2client.client.OAuth2Client.token_request] when the
    Token Endpoint returns an error.

    Args:
        response: the `Response` returned by the Token Endpoint.
        dpop_key: the DPoPKey that was used to proof the token request, if any.

    Returns:
        nothing, and raises an exception instead. But a subclass may return a
        `BearerToken` to implement a default behaviour if needed.

    Raises:
        InvalidTokenResponse: if the error response does not contain an OAuth 2.0 standard
            error response.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, UnknownTokenEndpointError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise InvalidTokenResponse(
            response=response,
            client=self,
            description=f"An error happened while processing the error response: {exc}",
        ) from exc
    raise exception
client_credentials(scope=None, *, dpop=None, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint using the client_credentials grant.

Parameters:

Name Type Description Default
scope str | Iterable[str] | None

the scope to send with the request. Can be a str, or an iterable of str. to pass that way include scope, audience, resource, etc.

None
dpop bool | None

toggles DPoP-proofing for the token request:

  • if False, disable it,
  • if True, enable it,
  • if None, defaults to dpop_bound_access_tokens configuration parameter for the client.
None
dpop_key DPoPKey | None

a chosen DPoPKey for this request. If None, a new key will be generated automatically with a call to dpop_key_generator.

None
requests_kwargs dict[str, Any] | None

additional parameters for the call to requests

None
**token_kwargs Any

additional parameters that will be added in the form data for the token endpoint, alongside grant_type.

{}

Returns:

Type Description
BearerToken

a BearerToken or DPoPToken, depending on the AS response.

Raises:

Type Description
InvalidScopeParam

if the scope parameter is not suitable

Source code in requests_oauth2client/client.py
def client_credentials(
    self,
    scope: str | Iterable[str] | None = None,
    *,
    dpop: bool | None = None,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint using the `client_credentials` grant.

    Args:
        scope: the scope to send with the request. Can be a str, or an iterable of str.
            to pass that way include `scope`, `audience`, `resource`, etc.
        dpop: toggles DPoP-proofing for the token request:

            - if `False`, disable it,
            - if `True`, enable it,
            - if `None`, defaults to `dpop_bound_access_tokens` configuration parameter for the client.
        dpop_key: a chosen `DPoPKey` for this request. If `None`, a new key will be generated automatically
            with a call to `dpop_key_generator`.
        requests_kwargs: additional parameters for the call to requests
        **token_kwargs: additional parameters that will be added in the form data for the token endpoint,
             alongside `grant_type`.

    Returns:
        a `BearerToken` or `DPoPToken`, depending on the AS response.

    Raises:
        InvalidScopeParam: if the `scope` parameter is not suitable

    """
    requests_kwargs = requests_kwargs or {}

    if scope and not isinstance(scope, str):
        try:
            scope = " ".join(scope)
        except Exception as exc:
            raise InvalidScopeParam(scope) from exc

    data = dict(grant_type=GrantTypes.CLIENT_CREDENTIALS, scope=scope, **token_kwargs)
    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
authorization_code(code, *, validate=True, dpop=False, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint with the authorization_code grant.

You can either pass an authorization code, as a str, or pass an AuthorizationResponse instance as returned by AuthorizationRequest.validate_callback() (recommended). If you do the latter, this will automatically:

  • add the appropriate redirect_uri value that was initially passed in the Authorization Request parameters. This is no longer mandatory in OAuth 2.1, but a lot of Authorization Servers are still expecting it since it was part of the OAuth 2.0 specifications.
  • add the appropriate code_verifier for PKCE that was generated before sending the AuthorizationRequest.
  • handle DPoP binding based on the same DPoPKey that was used to initialize the AuthenticationRequest and whose JWK thumbprint was passed as dpop_jkt parameter in the Auth Request.

Parameters:

Name Type Description Default
code str | AuthorizationResponse

An authorization code or an AuthorizationResponse to exchange for tokens.

required
validate bool

If True, validate the ID Token (this works only if code is an AuthorizationResponse).

True
dpop bool

Toggles DPoP binding for the Access Token, even if Authorization Code DPoP binding was not initially done.

False
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to automatically generate a key, if dpop is True.

None
requests_kwargs dict[str, Any] | None

Additional parameters for the call to the underlying HTTP requests call.

None
**token_kwargs Any

Additional parameters that will be added in the form data for the token endpoint, alongside grant_type, code, etc.

{}

Returns:

Type Description
BearerToken

The Token Endpoint Response.

Source code in requests_oauth2client/client.py
def authorization_code(
    self,
    code: str | AuthorizationResponse,
    *,
    validate: bool = True,
    dpop: bool = False,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint with the `authorization_code` grant.

    You can either pass an authorization code, as a `str`, or pass an `AuthorizationResponse` instance as
    returned by `AuthorizationRequest.validate_callback()` (recommended). If you do the latter, this will
    automatically:

    - add the appropriate `redirect_uri` value that was initially passed in the Authorization Request parameters.
    This is no longer mandatory in OAuth 2.1, but a lot of Authorization Servers are still expecting it since it was
    part of the OAuth 2.0 specifications.
    - add the appropriate `code_verifier` for PKCE that was generated before sending the AuthorizationRequest.
    - handle DPoP binding based on the same `DPoPKey` that was used to initialize the `AuthenticationRequest` and
    whose JWK thumbprint was passed as `dpop_jkt` parameter in the Auth Request.

    Args:
        code: An authorization code or an `AuthorizationResponse` to exchange for tokens.
        validate: If `True`, validate the ID Token (this works only if `code` is an `AuthorizationResponse`).
        dpop: Toggles DPoP binding for the Access Token,
             even if Authorization Code DPoP binding was not initially done.
        dpop_key: A chosen DPoP key. Leave `None` to automatically generate a key, if `dpop` is `True`.
        requests_kwargs: Additional parameters for the call to the underlying HTTP `requests` call.
        **token_kwargs: Additional parameters that will be added in the form data for the token endpoint,
            alongside `grant_type`, `code`, etc.

    Returns:
        The Token Endpoint Response.

    """
    azr: AuthorizationResponse | None = None
    if isinstance(code, AuthorizationResponse):
        token_kwargs.setdefault("code_verifier", code.code_verifier)
        token_kwargs.setdefault("redirect_uri", code.redirect_uri)
        azr = code
        dpop_key = code.dpop_key
        code = code.code

    requests_kwargs = requests_kwargs or {}

    data = dict(grant_type=GrantTypes.AUTHORIZATION_CODE, code=code, **token_kwargs)
    token = self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
    if validate and token.id_token and isinstance(azr, AuthorizationResponse):
        return token.validate_id_token(self, azr)
    return token
refresh_token(refresh_token, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint with the refresh_token grant.

If refresh_token is a DPoPToken instance, (which means that DPoP was used to obtain the initial Access/Refresh Tokens), then the same DPoP key will be used to DPoP proof the refresh token request, as defined in RFC9449.

Parameters:

Name Type Description Default
refresh_token str | BearerToken

A refresh_token, as a string, or as a BearerToken. That BearerToken must have a refresh_token.

required
requests_kwargs dict[str, Any] | None

Additional parameters for the call to requests.

None
**token_kwargs Any

Additional parameters for the token endpoint, alongside grant_type, refresh_token, etc.

{}

Returns:

Type Description
BearerToken

The token endpoint response.

Raises:

Type Description
MissingRefreshToken

If refresh_token is a BearerToken instance but does not contain a refresh_token.

Source code in requests_oauth2client/client.py
def refresh_token(
    self,
    refresh_token: str | BearerToken,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint with the `refresh_token` grant.

    If `refresh_token` is a `DPoPToken` instance, (which means that DPoP was used to obtain the initial
    Access/Refresh Tokens), then the same DPoP key will be used to DPoP proof the refresh token request,
    as defined in RFC9449.

    Args:
        refresh_token: A refresh_token, as a string, or as a `BearerToken`.
            That `BearerToken` must have a `refresh_token`.
        requests_kwargs: Additional parameters for the call to `requests`.
        **token_kwargs: Additional parameters for the token endpoint,
            alongside `grant_type`, `refresh_token`, etc.

    Returns:
        The token endpoint response.

    Raises:
        MissingRefreshToken: If `refresh_token` is a `BearerToken` instance but does not
            contain a `refresh_token`.

    """
    dpop_key: DPoPKey | None = None
    if isinstance(refresh_token, BearerToken):
        if refresh_token.refresh_token is None or not isinstance(refresh_token.refresh_token, str):
            raise MissingRefreshToken(refresh_token)
        if isinstance(refresh_token, DPoPToken):
            dpop_key = refresh_token.dpop_key
        refresh_token = refresh_token.refresh_token

    requests_kwargs = requests_kwargs or {}
    data = dict(grant_type=GrantTypes.REFRESH_TOKEN, refresh_token=refresh_token, **token_kwargs)
    return self.token_request(data, dpop_key=dpop_key, **requests_kwargs)
device_code(device_code, *, dpop=False, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint using the Device Code grant.

The grant_type is urn:ietf:params:oauth:grant-type:device_code. This needs a Device Code, or a DeviceAuthorizationResponse as parameter.

Parameters:

Name Type Description Default
device_code str | DeviceAuthorizationResponse

A device code, or a DeviceAuthorizationResponse.

required
dpop bool

Toggles DPoP Binding. If None, defaults to self.dpop_bound_access_tokens.

False
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for this request.

None
requests_kwargs dict[str, Any] | None

Additional parameters for the call to requests.

None
**token_kwargs Any

Additional parameters for the token endpoint, alongside grant_type, device_code, etc.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Raises:

Type Description
MissingDeviceCode

if device_code is a DeviceAuthorizationResponse but does not contain a device_code.

Source code in requests_oauth2client/client.py
def device_code(
    self,
    device_code: str | DeviceAuthorizationResponse,
    *,
    dpop: bool = False,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint using the Device Code grant.

    The grant_type is `urn:ietf:params:oauth:grant-type:device_code`. This needs a Device Code,
    or a `DeviceAuthorizationResponse` as parameter.

    Args:
        device_code: A device code, or a `DeviceAuthorizationResponse`.
        dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
        requests_kwargs: Additional parameters for the call to requests.
        **token_kwargs: Additional parameters for the token endpoint, alongside `grant_type`, `device_code`, etc.

    Returns:
        The Token Endpoint response.

    Raises:
        MissingDeviceCode: if `device_code` is a DeviceAuthorizationResponse but does not
            contain a `device_code`.

    """
    if isinstance(device_code, DeviceAuthorizationResponse):
        if device_code.device_code is None or not isinstance(device_code.device_code, str):
            raise MissingDeviceCode(device_code)
        device_code = device_code.device_code

    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantTypes.DEVICE_CODE,
        device_code=device_code,
        **token_kwargs,
    )
    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
ciba(auth_req_id, requests_kwargs=None, **token_kwargs)

Send a CIBA request to the Token Endpoint.

A CIBA request is a Token Request using the urn:openid:params:grant-type:ciba grant.

Parameters:

Name Type Description Default
auth_req_id str | BackChannelAuthenticationResponse

an authentication request ID, as returned by the AS

required
requests_kwargs dict[str, Any] | None

additional parameters for the call to requests

None
**token_kwargs Any

additional parameters for the token endpoint, alongside grant_type, auth_req_id, etc.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Raises:

Type Description
MissingAuthRequestId

if auth_req_id is a BackChannelAuthenticationResponse but does not contain an auth_req_id.

Source code in requests_oauth2client/client.py
def ciba(
    self,
    auth_req_id: str | BackChannelAuthenticationResponse,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a CIBA request to the Token Endpoint.

    A CIBA request is a Token Request using the `urn:openid:params:grant-type:ciba` grant.

    Args:
        auth_req_id: an authentication request ID, as returned by the AS
        requests_kwargs: additional parameters for the call to requests
        **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `auth_req_id`, etc.

    Returns:
        The Token Endpoint response.

    Raises:
        MissingAuthRequestId: if `auth_req_id` is a BackChannelAuthenticationResponse but does not contain
            an `auth_req_id`.

    """
    if isinstance(auth_req_id, BackChannelAuthenticationResponse):
        if auth_req_id.auth_req_id is None or not isinstance(auth_req_id.auth_req_id, str):
            raise MissingAuthRequestId(auth_req_id)
        auth_req_id = auth_req_id.auth_req_id

    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantTypes.CLIENT_INITIATED_BACKCHANNEL_AUTHENTICATION,
        auth_req_id=auth_req_id,
        **token_kwargs,
    )
    return self.token_request(data, **requests_kwargs)
token_exchange(*, subject_token, subject_token_type=None, actor_token=None, actor_token_type=None, requested_token_type=None, dpop=False, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a Token Exchange request.

A Token Exchange request is actually a request to the Token Endpoint with a grant_type urn:ietf:params:oauth:grant-type:token-exchange.

Parameters:

Name Type Description Default
subject_token str | BearerToken | IdToken

The subject token to exchange for a new token.

required
subject_token_type str | None

A token type identifier for the subject_token, mandatory if it cannot be guessed based on type(subject_token).

None
actor_token None | str | BearerToken | IdToken

The actor token to include in the request, if any.

None
actor_token_type str | None

A token type identifier for the actor_token, mandatory if it cannot be guessed based on type(actor_token).

None
requested_token_type str | None

A token type identifier for the requested token.

None
dpop bool

Toggles DPoP Binding. If None, defaults to self.dpop_bound_access_tokens.

False
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for this request.

None
requests_kwargs dict[str, Any] | None

Additional parameters to pass to the underlying requests.post() call.

None
**token_kwargs Any

Additional parameters to include in the request body.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Raises:

Type Description
UnknownSubjectTokenType

If the type of subject_token cannot be determined automatically.

UnknownActorTokenType

If the type of actor_token cannot be determined automatically.

Source code in requests_oauth2client/client.py
def token_exchange(
    self,
    *,
    subject_token: str | BearerToken | IdToken,
    subject_token_type: str | None = None,
    actor_token: None | str | BearerToken | IdToken = None,
    actor_token_type: str | None = None,
    requested_token_type: str | None = None,
    dpop: bool = False,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a Token Exchange request.

    A Token Exchange request is actually a request to the Token Endpoint with a grant_type
    `urn:ietf:params:oauth:grant-type:token-exchange`.

    Args:
        subject_token: The subject token to exchange for a new token.
        subject_token_type: A token type identifier for the subject_token, mandatory if it cannot be guessed based
            on `type(subject_token)`.
        actor_token: The actor token to include in the request, if any.
        actor_token_type: A token type identifier for the actor_token, mandatory if it cannot be guessed based
            on `type(actor_token)`.
        requested_token_type: A token type identifier for the requested token.
        dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
        requests_kwargs: Additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: Additional parameters to include in the request body.

    Returns:
        The Token Endpoint response.

    Raises:
        UnknownSubjectTokenType: If the type of `subject_token` cannot be determined automatically.
        UnknownActorTokenType: If the type of `actor_token` cannot be determined automatically.

    """
    requests_kwargs = requests_kwargs or {}

    try:
        subject_token_type = self.get_token_type(subject_token_type, subject_token)
    except ValueError as exc:
        raise UnknownSubjectTokenType(subject_token, subject_token_type) from exc
    if actor_token:  # pragma: no branch
        try:
            actor_token_type = self.get_token_type(actor_token_type, actor_token)
        except ValueError as exc:
            raise UnknownActorTokenType(actor_token, actor_token_type) from exc

    data = dict(
        grant_type=GrantTypes.TOKEN_EXCHANGE,
        subject_token=subject_token,
        subject_token_type=subject_token_type,
        actor_token=actor_token,
        actor_token_type=actor_token_type,
        requested_token_type=requested_token_type,
        **token_kwargs,
    )
    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
jwt_bearer(assertion, *, dpop=False, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request using a JWT as authorization grant.

This is defined in (RFC7523 $2.1)[https://www.rfc-editor.org/rfc/rfc7523.html#section-2.1).

Parameters:

Name Type Description Default
assertion Jwt | str

A JWT (as an instance of jwskate.Jwt or as a str) to use as authorization grant.

required
dpop bool

Toggles DPoP Binding. If None, defaults to self.dpop_bound_access_tokens.

False
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for this request.

None
requests_kwargs dict[str, Any] | None

Additional parameters to pass to the underlying requests.post() call.

None
**token_kwargs Any

Additional parameters to include in the request body.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Source code in requests_oauth2client/client.py
def jwt_bearer(
    self,
    assertion: Jwt | str,
    *,
    dpop: bool = False,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request using a JWT as authorization grant.

    This is defined in (RFC7523 $2.1)[https://www.rfc-editor.org/rfc/rfc7523.html#section-2.1).

    Args:
        assertion: A JWT (as an instance of `jwskate.Jwt` or as a `str`) to use as authorization grant.
        dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
        requests_kwargs: Additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: Additional parameters to include in the request body.

    Returns:
        The Token Endpoint response.

    """
    requests_kwargs = requests_kwargs or {}

    if not isinstance(assertion, Jwt):
        assertion = Jwt(assertion)

    data = dict(
        grant_type=GrantTypes.JWT_BEARER,
        assertion=assertion,
        **token_kwargs,
    )

    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
resource_owner_password(username, password, *, dpop=None, dpop_key=None, requests_kwargs=None, **token_kwargs)

Send a request using the Resource Owner Password Grant.

This Grant Type is deprecated and should only be used when there is no other choice.

Parameters:

Name Type Description Default
username str

the resource owner user name

required
password str

the resource owner password

required
dpop bool | None

Toggles DPoP Binding. If None, defaults to self.dpop_bound_access_tokens.

None
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for this request.

None
requests_kwargs dict[str, Any] | None

additional parameters to pass to the underlying requests.post() call.

None
**token_kwargs Any

additional parameters to include in the request body.

{}

Returns:

Type Description
BearerToken

The Token Endpoint response.

Source code in requests_oauth2client/client.py
def resource_owner_password(
    self,
    username: str,
    password: str,
    *,
    dpop: bool | None = None,
    dpop_key: DPoPKey | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request using the Resource Owner Password Grant.

    This Grant Type is deprecated and should only be used when there is no other choice.

    Args:
        username: the resource owner user name
        password: the resource owner password
        dpop: Toggles DPoP Binding. If `None`, defaults to `self.dpop_bound_access_tokens`.
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for this request.
        requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: additional parameters to include in the request body.

    Returns:
        The Token Endpoint response.

    """
    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantTypes.RESOURCE_OWNER_PASSWORD,
        username=username,
        password=password,
        **token_kwargs,
    )

    return self.token_request(data, dpop=dpop, dpop_key=dpop_key, **requests_kwargs)
authorization_request(*, scope='openid', response_type=ResponseTypes.CODE, redirect_uri=None, state=..., nonce=..., code_verifier=None, dpop=None, dpop_key=None, dpop_alg=None, **kwargs)

Generate an Authorization Request for this client.

Parameters:

Name Type Description Default
scope None | str | Iterable[str]

The scope to use.

'openid'
response_type str

The response_type to use.

CODE
redirect_uri str | None

The redirect_uri to include in the request. By default, the redirect_uri defined at init time is used.

None
state str | ellipsis | None

The state parameter to use. Leave default to generate a random value.

...
nonce str | ellipsis | None

A nonce. Leave default to generate a random value.

...
dpop bool | None

Toggles DPoP binding. - if True, DPoP binding is used - if False, DPoP is not used - if None, defaults to self.dpop_bound_access_tokens

None
dpop_key DPoPKey | None

A chosen DPoP key. Leave None to have a new key generated for you.

None
dpop_alg str | None

A signature alg to sign the DPoP proof. If None, this defaults to self.dpop_alg. If DPoP is not used, or a chosen dpop_key is provided, this is ignored. This affects the key type if a DPoP key must be generated.

None
code_verifier str | None

The PKCE code_verifier to use. Leave default to generate a random value.

None
**kwargs Any

Additional query parameters to include in the auth request.

{}

Returns:

Type Description
AuthorizationRequest

The Token Endpoint response.

Source code in requests_oauth2client/client.py
def authorization_request(
    self,
    *,
    scope: None | str | Iterable[str] = "openid",
    response_type: str = ResponseTypes.CODE,
    redirect_uri: str | None = None,
    state: str | ellipsis | None = ...,  # noqa: F821
    nonce: str | ellipsis | None = ...,  # noqa: F821
    code_verifier: str | None = None,
    dpop: bool | None = None,
    dpop_key: DPoPKey | None = None,
    dpop_alg: str | None = None,
    **kwargs: Any,
) -> AuthorizationRequest:
    """Generate an Authorization Request for this client.

    Args:
        scope: The `scope` to use.
        response_type: The `response_type` to use.
        redirect_uri: The `redirect_uri` to include in the request. By default,
            the `redirect_uri` defined at init time is used.
        state: The `state` parameter to use. Leave default to generate a random value.
        nonce: A `nonce`. Leave default to generate a random value.
        dpop: Toggles DPoP binding.
            - if `True`, DPoP binding is used
            - if `False`, DPoP is not used
            - if `None`, defaults to `self.dpop_bound_access_tokens`
        dpop_key: A chosen DPoP key. Leave `None` to have a new key generated for you.
        dpop_alg: A signature alg to sign the DPoP proof. If `None`, this defaults to `self.dpop_alg`.
            If DPoP is not used, or a chosen `dpop_key` is provided, this is ignored.
            This affects the key type if a DPoP key must be generated.
        code_verifier: The PKCE `code_verifier` to use. Leave default to generate a random value.
        **kwargs: Additional query parameters to include in the auth request.

    Returns:
        The Token Endpoint response.

    """
    authorization_endpoint = self._require_endpoint("authorization_endpoint")

    redirect_uri = redirect_uri or self.redirect_uri

    if dpop is None:
        dpop = self.dpop_bound_access_tokens
    if dpop_alg is None:
        dpop_alg = self.dpop_alg

    return AuthorizationRequest(
        authorization_endpoint=authorization_endpoint,
        client_id=self.client_id,
        redirect_uri=redirect_uri,
        issuer=self.issuer,
        response_type=response_type,
        scope=scope,
        state=state,
        nonce=nonce,
        code_verifier=code_verifier,
        code_challenge_method=self.code_challenge_method,
        dpop=dpop,
        dpop_key=dpop_key,
        dpop_alg=dpop_alg,
        **kwargs,
    )
pushed_authorization_request(authorization_request, requests_kwargs=None)

Send a Pushed Authorization Request.

This sends a request to the Pushed Authorization Request Endpoint, and returns a RequestUriParameterAuthorizationRequest initialized with the AS response.

Parameters:

Name Type Description Default
authorization_request AuthorizationRequest

The authorization request to send.

required
requests_kwargs dict[str, Any] | None

Additional parameters for requests.request().

None

Returns:

Type Description
RequestUriParameterAuthorizationRequest

The RequestUriParameterAuthorizationRequest initialized based on the AS response.

Source code in requests_oauth2client/client.py
def pushed_authorization_request(
    self,
    authorization_request: AuthorizationRequest,
    requests_kwargs: dict[str, Any] | None = None,
) -> RequestUriParameterAuthorizationRequest:
    """Send a Pushed Authorization Request.

    This sends a request to the Pushed Authorization Request Endpoint, and returns a
    `RequestUriParameterAuthorizationRequest` initialized with the AS response.

    Args:
        authorization_request: The authorization request to send.
        requests_kwargs: Additional parameters for `requests.request()`.

    Returns:
        The `RequestUriParameterAuthorizationRequest` initialized based on the AS response.

    """
    requests_kwargs = requests_kwargs or {}
    return self._request(
        Endpoints.PUSHED_AUTHORIZATION_REQUEST,
        data=authorization_request.args,
        auth=self.auth,
        on_success=self.parse_pushed_authorization_response,
        on_failure=self.on_pushed_authorization_request_error,
        dpop_key=authorization_request.dpop_key,
        **requests_kwargs,
    )
parse_pushed_authorization_response(response, *, dpop_key=None)

Parse the response obtained by pushed_authorization_request().

Parameters:

Name Type Description Default
response Response

The requests.Response returned by the PAR endpoint.

required
dpop_key DPoPKey | None

The DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
RequestUriParameterAuthorizationRequest

A RequestUriParameterAuthorizationRequest instance initialized based on the PAR endpoint response.

Source code in requests_oauth2client/client.py
def parse_pushed_authorization_response(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,
) -> RequestUriParameterAuthorizationRequest:
    """Parse the response obtained by `pushed_authorization_request()`.

    Args:
        response: The `requests.Response` returned by the PAR endpoint.
        dpop_key: The `DPoPKey` that was used to proof the token request, if any.

    Returns:
        A `RequestUriParameterAuthorizationRequest` instance initialized based on the PAR endpoint response.

    """
    response_json = response.json()
    request_uri = response_json.get("request_uri")
    expires_in = response_json.get("expires_in")

    return RequestUriParameterAuthorizationRequest(
        authorization_endpoint=self.authorization_endpoint,
        client_id=self.client_id,
        request_uri=request_uri,
        expires_in=expires_in,
        dpop_key=dpop_key,
    )
on_pushed_authorization_request_error(response, *, dpop_key=None)

Error Handler for Pushed Authorization Endpoint errors.

Parameters:

Name Type Description Default
response Response

The HTTP response as returned by the AS PAR endpoint.

required
dpop_key DPoPKey | None

The DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
RequestUriParameterAuthorizationRequest

Should not return anything, but raise an Exception instead. A RequestUriParameterAuthorizationRequest

RequestUriParameterAuthorizationRequest

may be returned by subclasses for testing purposes.

Raises:

Type Description
EndpointError

A subclass of this error depending on the error returned by the AS.

InvalidPushedAuthorizationResponse

If the returned response is not following the specifications.

UnknownTokenEndpointError

For unknown/unhandled errors.

Source code in requests_oauth2client/client.py
def on_pushed_authorization_request_error(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> RequestUriParameterAuthorizationRequest:
    """Error Handler for Pushed Authorization Endpoint errors.

    Args:
        response: The HTTP response as returned by the AS PAR endpoint.
        dpop_key: The `DPoPKey` that was used to proof the token request, if any.

    Returns:
        Should not return anything, but raise an Exception instead. A `RequestUriParameterAuthorizationRequest`
        may be returned by subclasses for testing purposes.

    Raises:
        EndpointError: A subclass of this error depending on the error returned by the AS.
        InvalidPushedAuthorizationResponse: If the returned response is not following the specifications.
        UnknownTokenEndpointError: For unknown/unhandled errors.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, UnknownTokenEndpointError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise InvalidPushedAuthorizationResponse(response=response, client=self) from exc
    raise exception
userinfo(access_token)

Call the UserInfo endpoint.

This sends a request to the UserInfo endpoint, with the specified access_token, and returns the parsed result.

Parameters:

Name Type Description Default
access_token BearerToken | str

the access token to use

required

Returns:

Type Description
Any

the Response returned by the userinfo endpoint.

Source code in requests_oauth2client/client.py
def userinfo(self, access_token: BearerToken | str) -> Any:
    """Call the UserInfo endpoint.

    This sends a request to the UserInfo endpoint, with the specified access_token, and returns
    the parsed result.

    Args:
        access_token: the access token to use

    Returns:
        the [Response][requests.Response] returned by the userinfo endpoint.

    """
    if isinstance(access_token, str):
        access_token = BearerToken(access_token)
    return self._request(
        Endpoints.USER_INFO,
        auth=access_token,
        on_success=self.parse_userinfo_response,
        on_failure=self.on_userinfo_error,
    )
parse_userinfo_response(resp, *, dpop_key=None)

Parse the response obtained by userinfo().

Invoked by userinfo() to parse the response from the UserInfo endpoint, this will extract and return its JSON content.

Parameters:

Name Type Description Default
resp Response

a Response returned from the UserInfo endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
Any

the parsed JSON content from this response.

Source code in requests_oauth2client/client.py
def parse_userinfo_response(self, resp: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
    """Parse the response obtained by `userinfo()`.

    Invoked by [userinfo()][requests_oauth2client.client.OAuth2Client.userinfo] to parse the
    response from the UserInfo endpoint, this will extract and return its JSON content.

    Args:
        resp: a [Response][requests.Response] returned from the UserInfo endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        the parsed JSON content from this response.

    """
    return resp.json()
on_userinfo_error(resp, *, dpop_key=None)

Parse UserInfo error response.

Parameters:

Name Type Description Default
resp Response

a Response returned from the UserInfo endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
Any

nothing, raises exception instead.

Source code in requests_oauth2client/client.py
def on_userinfo_error(self, resp: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
    """Parse UserInfo error response.

    Args:
        resp: a [Response][requests.Response] returned from the UserInfo endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        nothing, raises exception instead.

    """
    resp.raise_for_status()
get_token_type(token_type=None, token=None) classmethod

Get standardized token type identifiers.

Return a standardized token type identifier, based on a short token_type hint and/or a token value.

Parameters:

Name Type Description Default
token_type str | None

a token_type hint, as str. May be "access_token", "refresh_token" or "id_token"

None
token None | str | BearerToken | IdToken

a token value, as an instance of BearerToken or IdToken, or as a str.

None

Returns:

Type Description
str

the token_type as defined in the Token Exchange RFC8693.

Raises:

Type Description
UnknownTokenType

if the type of token cannot be determined

Source code in requests_oauth2client/client.py
    @classmethod
    def get_token_type(  # noqa: C901
        cls,
        token_type: str | None = None,
        token: None | str | BearerToken | IdToken = None,
    ) -> str:
        """Get standardized token type identifiers.

        Return a standardized token type identifier, based on a short `token_type` hint and/or a
        token value.

        Args:
            token_type: a token_type hint, as `str`. May be "access_token", "refresh_token"
                or "id_token"
            token: a token value, as an instance of `BearerToken` or IdToken, or as a `str`.

        Returns:
            the token_type as defined in the Token Exchange RFC8693.

        Raises:
            UnknownTokenType: if the type of token cannot be determined

        """
        if not (token_type or token):
            msg = "Cannot determine type of an empty token without a token_type hint"
            raise UnknownTokenType(msg, token, token_type)

        if token_type is None:
            if isinstance(token, str):
                msg = """\
Cannot determine the type of provided token when it is a bare `str`. Please specify a 'token_type'.
"""
                raise UnknownTokenType(msg, token, token_type)
            if isinstance(token, BearerToken):
                return "urn:ietf:params:oauth:token-type:access_token"
            if isinstance(token, IdToken):
                return "urn:ietf:params:oauth:token-type:id_token"
            msg = f"Unknown token type {type(token)}"
            raise UnknownTokenType(msg, token, token_type)
        if token_type == TokenType.ACCESS_TOKEN:
            if token is not None and not isinstance(token, (str, BearerToken)):
                msg = f"""\
The supplied token is of type '{type(token)}' which is inconsistent with token_type '{token_type}'.
A BearerToken or an access_token as a `str` is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:access_token"
        if token_type == TokenType.REFRESH_TOKEN:
            if token is not None and isinstance(token, BearerToken) and not token.refresh_token:
                msg = f"""\
The supplied BearerToken does not contain a refresh_token, which is inconsistent with token_type '{token_type}'.
A BearerToken containing a refresh_token is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:refresh_token"
        if token_type == TokenType.ID_TOKEN:
            if token is not None and not isinstance(token, (str, IdToken)):
                msg = f"""\
The supplied token is of type '{type(token)}' which is inconsistent with token_type '{token_type}'.
An IdToken or a string representation of it is expected.
"""
                raise UnknownTokenType(msg, token, token_type)
            return "urn:ietf:params:oauth:token-type:id_token"

        return {
            "saml1": "urn:ietf:params:oauth:token-type:saml1",
            "saml2": "urn:ietf:params:oauth:token-type:saml2",
            "jwt": "urn:ietf:params:oauth:token-type:jwt",
        }.get(token_type, token_type)
revoke_access_token(access_token, requests_kwargs=None, **revoke_kwargs)

Send a request to the Revocation Endpoint to revoke an access token.

Parameters:

Name Type Description Default
access_token BearerToken | str

the access token to revoke

required
requests_kwargs dict[str, Any] | None

additional parameters for the underlying requests.post() call

None
**revoke_kwargs Any

additional parameters to pass to the revocation endpoint

{}
Source code in requests_oauth2client/client.py
def revoke_access_token(
    self,
    access_token: BearerToken | str,
    requests_kwargs: dict[str, Any] | None = None,
    **revoke_kwargs: Any,
) -> bool:
    """Send a request to the Revocation Endpoint to revoke an access token.

    Args:
        access_token: the access token to revoke
        requests_kwargs: additional parameters for the underlying requests.post() call
        **revoke_kwargs: additional parameters to pass to the revocation endpoint

    """
    return self.revoke_token(
        access_token,
        token_type_hint=TokenType.ACCESS_TOKEN,
        requests_kwargs=requests_kwargs,
        **revoke_kwargs,
    )
revoke_refresh_token(refresh_token, requests_kwargs=None, **revoke_kwargs)

Send a request to the Revocation Endpoint to revoke a refresh token.

Parameters:

Name Type Description Default
refresh_token str | BearerToken

the refresh token to revoke.

required
requests_kwargs dict[str, Any] | None

additional parameters to pass to the revocation endpoint.

None
**revoke_kwargs Any

additional parameters to pass to the revocation endpoint.

{}

Returns:

Type Description
bool

True if the revocation request is successful, False if this client has no configured

bool

revocation endpoint.

Raises:

Type Description
MissingRefreshToken

when refresh_token is a BearerToken but does not contain a refresh_token.

Source code in requests_oauth2client/client.py
def revoke_refresh_token(
    self,
    refresh_token: str | BearerToken,
    requests_kwargs: dict[str, Any] | None = None,
    **revoke_kwargs: Any,
) -> bool:
    """Send a request to the Revocation Endpoint to revoke a refresh token.

    Args:
        refresh_token: the refresh token to revoke.
        requests_kwargs: additional parameters to pass to the revocation endpoint.
        **revoke_kwargs: additional parameters to pass to the revocation endpoint.

    Returns:
        `True` if the revocation request is successful, `False` if this client has no configured
        revocation endpoint.

    Raises:
        MissingRefreshToken: when `refresh_token` is a [BearerToken][requests_oauth2client.tokens.BearerToken]
            but does not contain a `refresh_token`.

    """
    if isinstance(refresh_token, BearerToken):
        if refresh_token.refresh_token is None:
            raise MissingRefreshToken(refresh_token)
        refresh_token = refresh_token.refresh_token

    return self.revoke_token(
        refresh_token,
        token_type_hint=TokenType.REFRESH_TOKEN,
        requests_kwargs=requests_kwargs,
        **revoke_kwargs,
    )
revoke_token(token, token_type_hint=None, requests_kwargs=None, **revoke_kwargs)

Send a Token Revocation request.

By default, authentication will be the same than the one used for the Token Endpoint.

Parameters:

Name Type Description Default
token str | BearerToken

the token to revoke.

required
token_type_hint str | None

a token_type_hint to send to the revocation endpoint.

None
requests_kwargs dict[str, Any] | None

additional parameters to the underling call to requests.post()

None
**revoke_kwargs Any

additional parameters to send to the revocation endpoint.

{}

Returns:

Type Description
bool

the result from parse_revocation_response on the returned AS response.

Raises:

Type Description
MissingEndpointUri

if the Revocation Endpoint URI is not configured.

MissingRefreshToken

if token_type_hint is "refresh_token" and token is a BearerToken but does not contain a refresh_token.

Source code in requests_oauth2client/client.py
def revoke_token(
    self,
    token: str | BearerToken,
    token_type_hint: str | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **revoke_kwargs: Any,
) -> bool:
    """Send a Token Revocation request.

    By default, authentication will be the same than the one used for the Token Endpoint.

    Args:
        token: the token to revoke.
        token_type_hint: a token_type_hint to send to the revocation endpoint.
        requests_kwargs: additional parameters to the underling call to requests.post()
        **revoke_kwargs: additional parameters to send to the revocation endpoint.

    Returns:
        the result from `parse_revocation_response` on the returned AS response.

    Raises:
        MissingEndpointUri: if the Revocation Endpoint URI is not configured.
        MissingRefreshToken: if `token_type_hint` is `"refresh_token"` and `token` is a BearerToken
            but does not contain a `refresh_token`.

    """
    requests_kwargs = requests_kwargs or {}

    if token_type_hint == TokenType.REFRESH_TOKEN and isinstance(token, BearerToken):
        if token.refresh_token is None:
            raise MissingRefreshToken(token)
        token = token.refresh_token

    data = dict(revoke_kwargs, token=str(token))
    if token_type_hint:
        data["token_type_hint"] = token_type_hint

    return self._request(
        Endpoints.REVOCATION,
        data=data,
        auth=self.auth,
        on_success=self.parse_revocation_response,
        on_failure=self.on_revocation_error,
        **requests_kwargs,
    )
parse_revocation_response(response, *, dpop_key=None)

Parse reponses from the Revocation Endpoint.

Since those do not return any meaningful information in a standardised fashion, this just returns True.

Parameters:

Name Type Description Default
response Response

the requests.Response as returned by the Revocation Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
bool

True if the revocation succeeds, False if no revocation endpoint is present or a

bool

non-standardised error is returned.

Source code in requests_oauth2client/client.py
def parse_revocation_response(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> bool:  # noqa: ARG002
    """Parse reponses from the Revocation Endpoint.

    Since those do not return any meaningful information in a standardised fashion, this just returns `True`.

    Args:
        response: the `requests.Response` as returned by the Revocation Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        `True` if the revocation succeeds, `False` if no revocation endpoint is present or a
        non-standardised error is returned.

    """
    return True
on_revocation_error(response, *, dpop_key=None)

Error handler for revoke_token().

Invoked by revoke_token() when the revocation endpoint returns an error.

Parameters:

Name Type Description Default
response Response

the requests.Response as returned by the Revocation Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
bool

False to signal that an error occurred. May raise exceptions instead depending on the

bool

revocation response.

Raises:

Type Description
EndpointError

if the response contains a standardised OAuth 2.0 error.

Source code in requests_oauth2client/client.py
def on_revocation_error(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> bool:  # noqa: ARG002
    """Error handler for `revoke_token()`.

    Invoked by [revoke_token()][requests_oauth2client.client.OAuth2Client.revoke_token] when the
    revocation endpoint returns an error.

    Args:
        response: the `requests.Response` as returned by the Revocation Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        `False` to signal that an error occurred. May raise exceptions instead depending on the
        revocation response.

    Raises:
        EndpointError: if the response contains a standardised OAuth 2.0 error.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, RevocationError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception:  # noqa: BLE001
        return False
    raise exception
introspect_token(token, token_type_hint=None, requests_kwargs=None, **introspect_kwargs)

Send a request to the Introspection Endpoint.

Parameter token can be:

  • a str
  • a BearerToken instance

You may pass any arbitrary token and token_type_hint values as str. Those will be included in the request, as-is. If token is a BearerToken, then token_type_hint must be either:

  • None: the access_token will be instrospected and no token_type_hint will be included in the request
  • access_token: same as None, but the token_type_hint will be included
  • or refresh_token: only available if a Refresh Token is present in the BearerToken.

Parameters:

Name Type Description Default
token str | BearerToken

the token to instrospect

required
token_type_hint str | None

the token_type_hint to include in the request.

None
requests_kwargs dict[str, Any] | None

additional parameters to the underling call to requests.post()

None
**introspect_kwargs Any

additional parameters to send to the introspection endpoint.

{}

Returns:

Type Description
Any

the response as returned by the Introspection Endpoint.

Raises:

Type Description
MissingRefreshToken

if token_type_hint is "refresh_token" and token is a BearerToken but does not contain a refresh_token.

UnknownTokenType

if token_type_hint is neither None, "access_token" or "refresh_token".

Source code in requests_oauth2client/client.py
    def introspect_token(
        self,
        token: str | BearerToken,
        token_type_hint: str | None = None,
        requests_kwargs: dict[str, Any] | None = None,
        **introspect_kwargs: Any,
    ) -> Any:
        """Send a request to the Introspection Endpoint.

        Parameter `token` can be:

        - a `str`
        - a `BearerToken` instance

        You may pass any arbitrary `token` and `token_type_hint` values as `str`. Those will
        be included in the request, as-is.
        If `token` is a `BearerToken`, then `token_type_hint` must be either:

        - `None`: the access_token will be instrospected and no token_type_hint will be included
        in the request
        - `access_token`: same as `None`, but the token_type_hint will be included
        - or `refresh_token`: only available if a Refresh Token is present in the BearerToken.

        Args:
            token: the token to instrospect
            token_type_hint: the `token_type_hint` to include in the request.
            requests_kwargs: additional parameters to the underling call to requests.post()
            **introspect_kwargs: additional parameters to send to the introspection endpoint.

        Returns:
            the response as returned by the Introspection Endpoint.

        Raises:
            MissingRefreshToken: if `token_type_hint` is `"refresh_token"` and `token` is a BearerToken
                but does not contain a `refresh_token`.
            UnknownTokenType: if `token_type_hint` is neither `None`, `"access_token"` or `"refresh_token"`.

        """
        requests_kwargs = requests_kwargs or {}

        if isinstance(token, BearerToken):
            if token_type_hint is None or token_type_hint == TokenType.ACCESS_TOKEN:
                token = token.access_token
            elif token_type_hint == TokenType.REFRESH_TOKEN:
                if token.refresh_token is None:
                    raise MissingRefreshToken(token)

                token = token.refresh_token
            else:
                msg = """\
Invalid `token_type_hint`. To test arbitrary `token_type_hint` values, you must provide `token` as a `str`."""
                raise UnknownTokenType(msg, token, token_type_hint)

        data = dict(introspect_kwargs, token=str(token))
        if token_type_hint:
            data["token_type_hint"] = token_type_hint

        return self._request(
            Endpoints.INTROSPECTION,
            data=data,
            auth=self.auth,
            on_success=self.parse_introspection_response,
            on_failure=self.on_introspection_error,
            **requests_kwargs,
        )
parse_introspection_response(response, *, dpop_key=None)

Parse Token Introspection Responses received by introspect_token().

Invoked by introspect_token() to parse the returned response. This decodes the JSON content if possible, otherwise it returns the response as a string.

Parameters:

Name Type Description Default
response Response

the Response as returned by the Introspection Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
Any

the decoded JSON content, or a str with the content.

Source code in requests_oauth2client/client.py
def parse_introspection_response(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> Any:
    """Parse Token Introspection Responses received by `introspect_token()`.

    Invoked by [introspect_token()][requests_oauth2client.client.OAuth2Client.introspect_token]
    to parse the returned response. This decodes the JSON content if possible, otherwise it
    returns the response as a string.

    Args:
        response: the [Response][requests.Response] as returned by the Introspection Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        the decoded JSON content, or a `str` with the content.

    """
    try:
        return response.json()
    except ValueError:
        return response.text
on_introspection_error(response, *, dpop_key=None)

Error handler for introspect_token().

Invoked by introspect_token() to parse the returned response in the case an error is returned.

Parameters:

Name Type Description Default
response Response

the response as returned by the Introspection Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
Any

usually raises exceptions. A subclass can return a default response instead.

Raises:

Type Description
EndpointError

(or one of its subclasses) if the response contains a standard OAuth 2.0 error.

UnknownIntrospectionError

if the response is not a standard error response.

Source code in requests_oauth2client/client.py
def on_introspection_error(self, response: requests.Response, *, dpop_key: DPoPKey | None = None) -> Any:  # noqa: ARG002
    """Error handler for `introspect_token()`.

    Invoked by [introspect_token()][requests_oauth2client.client.OAuth2Client.introspect_token]
    to parse the returned response in the case an error is returned.

    Args:
        response: the response as returned by the Introspection Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        usually raises exceptions. A subclass can return a default response instead.

    Raises:
        EndpointError: (or one of its subclasses) if the response contains a standard OAuth 2.0 error.
        UnknownIntrospectionError: if the response is not a standard error response.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, IntrospectionError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise UnknownIntrospectionError(response=response, client=self) from exc
    raise exception
backchannel_authentication_request(scope='openid', *, client_notification_token=None, acr_values=None, login_hint_token=None, id_token_hint=None, login_hint=None, binding_message=None, user_code=None, requested_expiry=None, private_jwk=None, alg=None, requests_kwargs=None, **ciba_kwargs)

Send a CIBA Authentication Request.

Parameters:

Name Type Description Default
scope None | str | Iterable[str]

the scope to include in the request.

'openid'
client_notification_token str | None

the Client Notification Token to include in the request.

None
acr_values None | str | Iterable[str]

the acr values to include in the request.

None
login_hint_token str | None

the Login Hint Token to include in the request.

None
id_token_hint str | None

the ID Token Hint to include in the request.

None
login_hint str | None

the Login Hint to include in the request.

None
binding_message str | None

the Binding Message to include in the request.

None
user_code str | None

the User Code to include in the request

None
requested_expiry int | None

the Requested Expiry, in seconds, to include in the request.

None
private_jwk Jwk | dict[str, Any] | None

the JWK to use to sign the request (optional)

None
alg str | None

the alg to use to sign the request, if the provided JWK does not include an "alg" parameter.

None
requests_kwargs dict[str, Any] | None

additional parameters for

None
**ciba_kwargs Any

additional parameters to include in the request.

{}

Returns:

Type Description
BackChannelAuthenticationResponse

a BackChannelAuthenticationResponse as returned by AS

Raises:

Type Description
InvalidBackchannelAuthenticationRequestHintParam

if none of login_hint, login_hint_token or id_token_hint is provided, or more than one of them is provided.

InvalidScopeParam

if the scope parameter is invalid.

InvalidAcrValuesParam

if the acr_values parameter is invalid.

Source code in requests_oauth2client/client.py
def backchannel_authentication_request(  # noqa: PLR0913
    self,
    scope: None | str | Iterable[str] = "openid",
    *,
    client_notification_token: str | None = None,
    acr_values: None | str | Iterable[str] = None,
    login_hint_token: str | None = None,
    id_token_hint: str | None = None,
    login_hint: str | None = None,
    binding_message: str | None = None,
    user_code: str | None = None,
    requested_expiry: int | None = None,
    private_jwk: Jwk | dict[str, Any] | None = None,
    alg: str | None = None,
    requests_kwargs: dict[str, Any] | None = None,
    **ciba_kwargs: Any,
) -> BackChannelAuthenticationResponse:
    """Send a CIBA Authentication Request.

    Args:
         scope: the scope to include in the request.
         client_notification_token: the Client Notification Token to include in the request.
         acr_values: the acr values to include in the request.
         login_hint_token: the Login Hint Token to include in the request.
         id_token_hint: the ID Token Hint to include in the request.
         login_hint: the Login Hint to include in the request.
         binding_message: the Binding Message to include in the request.
         user_code: the User Code to include in the request
         requested_expiry: the Requested Expiry, in seconds, to include in the request.
         private_jwk: the JWK to use to sign the request (optional)
         alg: the alg to use to sign the request, if the provided JWK does not include an "alg" parameter.
         requests_kwargs: additional parameters for
         **ciba_kwargs: additional parameters to include in the request.

    Returns:
        a BackChannelAuthenticationResponse as returned by AS

    Raises:
        InvalidBackchannelAuthenticationRequestHintParam: if none of `login_hint`, `login_hint_token`
            or `id_token_hint` is provided, or more than one of them is provided.
        InvalidScopeParam: if the `scope` parameter is invalid.
        InvalidAcrValuesParam: if the `acr_values` parameter is invalid.

    """
    if not (login_hint or login_hint_token or id_token_hint):
        msg = "One of `login_hint`, `login_hint_token` or `ìd_token_hint` must be provided"
        raise InvalidBackchannelAuthenticationRequestHintParam(msg)

    if (login_hint_token and id_token_hint) or (login_hint and id_token_hint) or (login_hint_token and login_hint):
        msg = "Only one of `login_hint`, `login_hint_token` or `ìd_token_hint` must be provided"
        raise InvalidBackchannelAuthenticationRequestHintParam(msg)

    requests_kwargs = requests_kwargs or {}

    if scope is not None and not isinstance(scope, str):
        try:
            scope = " ".join(scope)
        except Exception as exc:
            raise InvalidScopeParam(scope) from exc

    if acr_values is not None and not isinstance(acr_values, str):
        try:
            acr_values = " ".join(acr_values)
        except Exception as exc:
            raise InvalidAcrValuesParam(acr_values) from exc

    data = dict(
        ciba_kwargs,
        scope=scope,
        client_notification_token=client_notification_token,
        acr_values=acr_values,
        login_hint_token=login_hint_token,
        id_token_hint=id_token_hint,
        login_hint=login_hint,
        binding_message=binding_message,
        user_code=user_code,
        requested_expiry=requested_expiry,
    )

    if private_jwk is not None:
        data = {"request": str(Jwt.sign(data, key=private_jwk, alg=alg))}

    return self._request(
        Endpoints.BACKCHANNEL_AUTHENTICATION,
        data=data,
        auth=self.auth,
        on_success=self.parse_backchannel_authentication_response,
        on_failure=self.on_backchannel_authentication_error,
        **requests_kwargs,
    )
parse_backchannel_authentication_response(response, *, dpop_key=None)

Parse a response received by backchannel_authentication_request().

Invoked by backchannel_authentication_request() to parse the response returned by the BackChannel Authentication Endpoint.

Parameters:

Name Type Description Default
response Response

the response returned by the BackChannel Authentication Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
BackChannelAuthenticationResponse

a BackChannelAuthenticationResponse

Raises:

Type Description
InvalidBackChannelAuthenticationResponse

if the response does not contain a standard BackChannel Authentication response.

Source code in requests_oauth2client/client.py
def parse_backchannel_authentication_response(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> BackChannelAuthenticationResponse:
    """Parse a response received by `backchannel_authentication_request()`.

    Invoked by
    [backchannel_authentication_request()][requests_oauth2client.client.OAuth2Client.backchannel_authentication_request]
    to parse the response returned by the BackChannel Authentication Endpoint.

    Args:
        response: the response returned by the BackChannel Authentication Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        a `BackChannelAuthenticationResponse`

    Raises:
        InvalidBackChannelAuthenticationResponse: if the response does not contain a standard
            BackChannel Authentication response.

    """
    try:
        return BackChannelAuthenticationResponse(**response.json())
    except TypeError as exc:
        raise InvalidBackChannelAuthenticationResponse(response=response, client=self) from exc
on_backchannel_authentication_error(response, *, dpop_key=None)

Error handler for backchannel_authentication_request().

Invoked by backchannel_authentication_request() to parse the response returned by the BackChannel Authentication Endpoint, when it is an error.

Parameters:

Name Type Description Default
response Response

the response returned by the BackChannel Authentication Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
BackChannelAuthenticationResponse

usually raises an exception. But a subclass can return a default response instead.

Raises:

Type Description
EndpointError

(or one of its subclasses) if the response contains a standard OAuth 2.0 error.

InvalidBackChannelAuthenticationResponse

for non-standard error responses.

Source code in requests_oauth2client/client.py
def on_backchannel_authentication_error(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> BackChannelAuthenticationResponse:
    """Error handler for `backchannel_authentication_request()`.

    Invoked by
    [backchannel_authentication_request()][requests_oauth2client.client.OAuth2Client.backchannel_authentication_request]
    to parse the response returned by the BackChannel Authentication Endpoint, when it is an
    error.

    Args:
        response: the response returned by the BackChannel Authentication Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        usually raises an exception. But a subclass can return a default response instead.

    Raises:
        EndpointError: (or one of its subclasses) if the response contains a standard OAuth 2.0 error.
        InvalidBackChannelAuthenticationResponse: for non-standard error responses.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, BackChannelAuthenticationError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise InvalidBackChannelAuthenticationResponse(response=response, client=self) from exc
    raise exception
authorize_device(requests_kwargs=None, **data)

Send a Device Authorization Request.

Parameters:

Name Type Description Default
**data Any

additional data to send to the Device Authorization Endpoint

{}
requests_kwargs dict[str, Any] | None

additional parameters for requests.request()

None

Returns:

Type Description
DeviceAuthorizationResponse

a Device Authorization Response

Raises:

Type Description
MissingEndpointUri

if the Device Authorization URI is not configured

Source code in requests_oauth2client/client.py
def authorize_device(
    self,
    requests_kwargs: dict[str, Any] | None = None,
    **data: Any,
) -> DeviceAuthorizationResponse:
    """Send a Device Authorization Request.

    Args:
        **data: additional data to send to the Device Authorization Endpoint
        requests_kwargs: additional parameters for `requests.request()`

    Returns:
        a Device Authorization Response

    Raises:
        MissingEndpointUri: if the Device Authorization URI is not configured

    """
    requests_kwargs = requests_kwargs or {}

    return self._request(
        Endpoints.DEVICE_AUTHORIZATION,
        data=data,
        auth=self.auth,
        on_success=self.parse_device_authorization_response,
        on_failure=self.on_device_authorization_error,
        **requests_kwargs,
    )
parse_device_authorization_response(response, *, dpop_key=None)

Parse a Device Authorization Response received by authorize_device().

Invoked by authorize_device() to parse the response returned by the Device Authorization Endpoint.

Parameters:

Name Type Description Default
response Response

the response returned by the Device Authorization Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
DeviceAuthorizationResponse

a DeviceAuthorizationResponse as returned by AS

Source code in requests_oauth2client/client.py
def parse_device_authorization_response(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> DeviceAuthorizationResponse:
    """Parse a Device Authorization Response received by `authorize_device()`.

    Invoked by [authorize_device()][requests_oauth2client.client.OAuth2Client.authorize_device]
    to parse the response returned by the Device Authorization Endpoint.

    Args:
        response: the response returned by the Device Authorization Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        a `DeviceAuthorizationResponse` as returned by AS

    """
    return DeviceAuthorizationResponse(**response.json())
on_device_authorization_error(response, *, dpop_key=None)

Error handler for authorize_device().

Invoked by authorize_device() to parse the response returned by the Device Authorization Endpoint, when that response is an error.

Parameters:

Name Type Description Default
response Response

the response returned by the Device Authorization Endpoint.

required
dpop_key DPoPKey | None

the DPoPKey that was used to proof the token request, if any.

None

Returns:

Type Description
DeviceAuthorizationResponse

usually raises an Exception. But a subclass may return a default response instead.

Raises:

Type Description
EndpointError

for standard OAuth 2.0 errors

InvalidDeviceAuthorizationResponse

for non-standard error responses.

Source code in requests_oauth2client/client.py
def on_device_authorization_error(
    self,
    response: requests.Response,
    *,
    dpop_key: DPoPKey | None = None,  # noqa: ARG002
) -> DeviceAuthorizationResponse:
    """Error handler for `authorize_device()`.

    Invoked by [authorize_device()][requests_oauth2client.client.OAuth2Client.authorize_device]
    to parse the response returned by the Device Authorization Endpoint, when that response is
    an error.

    Args:
        response: the response returned by the Device Authorization Endpoint.
        dpop_key: the `DPoPKey` that was used to proof the token request, if any.

    Returns:
        usually raises an Exception. But a subclass may return a default response instead.

    Raises:
        EndpointError: for standard OAuth 2.0 errors
        InvalidDeviceAuthorizationResponse: for non-standard error responses.

    """
    try:
        data = response.json()
        error = data["error"]
        error_description = data.get("error_description")
        error_uri = data.get("error_uri")
        exception_class = self.exception_classes.get(error, DeviceAuthorizationError)
        exception = exception_class(
            response=response,
            client=self,
            error=error,
            description=error_description,
            uri=error_uri,
        )
    except Exception as exc:
        raise InvalidDeviceAuthorizationResponse(response=response, client=self) from exc
    raise exception
update_authorization_server_public_keys(requests_kwargs=None)

Update the cached AS public keys by retrieving them from its jwks_uri.

Public keys are returned by this method, as a jwskate.JwkSet. They are also available in attribute authorization_server_jwks.

Returns:

Type Description
JwkSet

the retrieved public keys

Raises:

Type Description
ValueError

if no jwks_uri is configured

Source code in requests_oauth2client/client.py
def update_authorization_server_public_keys(self, requests_kwargs: dict[str, Any] | None = None) -> JwkSet:
    """Update the cached AS public keys by retrieving them from its `jwks_uri`.

    Public keys are returned by this method, as a `jwskate.JwkSet`. They are also
    available in attribute `authorization_server_jwks`.

    Returns:
        the retrieved public keys

    Raises:
        ValueError: if no `jwks_uri` is configured

    """
    requests_kwargs = requests_kwargs or {}
    requests_kwargs.setdefault("auth", None)

    jwks_uri = self._require_endpoint(Endpoints.JWKS)
    resp = self.session.get(jwks_uri, **requests_kwargs)
    resp.raise_for_status()
    jwks = resp.json()
    self.authorization_server_jwks.update(jwks)
    return self.authorization_server_jwks
from_discovery_endpoint(url=None, issuer=None, *, auth=None, client_id=None, client_secret=None, private_key=None, session=None, testing=False, **kwargs) classmethod

Initialize an OAuth2Client using an AS Discovery Document endpoint.

If an url is provided, an HTTPS request will be done to that URL to obtain the Authorization Server Metadata.

If an issuer is provided, the OpenID Connect Discovery document url will be automatically derived from it, as specified in OpenID Connect Discovery.

Once the standardized metadata document is obtained, this will extract all Endpoint Uris from that document, will fetch the current public keys from its jwks_uri, then will initialize an OAuth2Client based on those endpoints.

Parameters:

Name Type Description Default
url str | None

The url where the server metadata will be retrieved.

None
issuer str | None

The issuer value that is expected in the discovery document. If not url is given, the OpenID Connect Discovery url for this issuer will be retrieved.

None
auth AuthBase | tuple[str, str] | str | None

The authentication handler to use for client authentication.

None
client_id str | None

Client ID.

None
client_secret str | None

Client secret to use to authenticate the client.

None
private_key Jwk | dict[str, Any] | None

Private key to sign client assertions.

None
session Session | None

A requests.Session to use to retrieve the document and initialise the client with.

None
testing bool

If True, do not try to validate the issuer uri nor the endpoint urls that are part of the document.

False
**kwargs Any

Additional keyword parameters to pass to OAuth2Client.

{}

Returns:

Type Description
OAuth2Client

An OAuth2Client with endpoints initialized based on the obtained metadata.

Raises:

Type Description
InvalidIssuer

If issuer is not using https, or contains credentials or fragment.

InvalidParam

If neither url nor issuer are suitable urls.

HTTPError

If an error happens while fetching the documents.

Example
1
2
3
4
5
6
7
from requests_oauth2client import OAuth2Client

client = OAuth2Client.from_discovery_endpoint(
    issuer="https://myserver.net",
    client_id="my_client_id,
    client_secret="my_client_secret",
)
Source code in requests_oauth2client/client.py
@classmethod
def from_discovery_endpoint(
    cls,
    url: str | None = None,
    issuer: str | None = None,
    *,
    auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_key: Jwk | dict[str, Any] | None = None,
    session: requests.Session | None = None,
    testing: bool = False,
    **kwargs: Any,
) -> OAuth2Client:
    """Initialize an `OAuth2Client` using an AS Discovery Document endpoint.

    If an `url` is provided, an HTTPS request will be done to that URL to obtain the Authorization Server Metadata.

    If an `issuer` is provided, the OpenID Connect Discovery document url will be automatically
    derived from it, as specified in [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest).

    Once the standardized metadata document is obtained, this will extract
    all Endpoint Uris from that document, will fetch the current public keys from its
    `jwks_uri`, then will initialize an OAuth2Client based on those endpoints.

    Args:
      url: The url where the server metadata will be retrieved.
      issuer: The issuer value that is expected in the discovery document.
        If not `url` is given, the OpenID Connect Discovery url for this issuer will be retrieved.
      auth: The authentication handler to use for client authentication.
      client_id: Client ID.
      client_secret: Client secret to use to authenticate the client.
      private_key: Private key to sign client assertions.
      session: A `requests.Session` to use to retrieve the document and initialise the client with.
      testing: If `True`, do not try to validate the issuer uri nor the endpoint urls
        that are part of the document.
      **kwargs: Additional keyword parameters to pass to `OAuth2Client`.

    Returns:
      An `OAuth2Client` with endpoints initialized based on the obtained metadata.

    Raises:
      InvalidIssuer: If `issuer` is not using https, or contains credentials or fragment.
      InvalidParam: If neither `url` nor `issuer` are suitable urls.
      requests.HTTPError: If an error happens while fetching the documents.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client

        client = OAuth2Client.from_discovery_endpoint(
            issuer="https://myserver.net",
            client_id="my_client_id,
            client_secret="my_client_secret",
        )
        ```

    """
    if issuer is not None and not testing:
        try:
            validate_issuer_uri(issuer)
        except InvalidUri as exc:
            raise InvalidIssuer("issuer", issuer, exc) from exc  # noqa: EM101
    if url is None and issuer is not None:
        url = oidc_discovery_document_url(issuer)
    if url is None:
        msg = "Please specify at least one of `issuer` or `url`"
        raise InvalidParam(msg)

    if not testing:
        validate_endpoint_uri(url, path=False)

    session = session or requests.Session()
    discovery = session.get(url).json()

    jwks_uri = discovery.get("jwks_uri")
    jwks = JwkSet(session.get(jwks_uri).json()) if jwks_uri else None

    return cls.from_discovery_document(
        discovery,
        issuer=issuer,
        auth=auth,
        session=session,
        client_id=client_id,
        client_secret=client_secret,
        private_key=private_key,
        authorization_server_jwks=jwks,
        testing=testing,
        **kwargs,
    )
from_discovery_document(discovery, issuer=None, *, auth=None, client_id=None, client_secret=None, private_key=None, authorization_server_jwks=None, https=True, testing=False, **kwargs) classmethod

Initialize an OAuth2Client, based on an AS Discovery Document.

Parameters:

Name Type Description Default
discovery dict[str, Any]

A dict of server metadata, in the same format as retrieved from a discovery endpoint.

required
issuer str | None

If an issuer is given, check that it matches the one mentioned in the document.

None
auth AuthBase | tuple[str, str] | str | None

The authentication handler to use for client authentication.

None
client_id str | None

Client ID.

None
client_secret str | None

Client secret to use to authenticate the client.

None
private_key Jwk | dict[str, Any] | None

Private key to sign client assertions.

None
authorization_server_jwks JwkSet | dict[str, Any] | None

The current authorization server JWKS keys.

None
https bool

(deprecated) If True, validates that urls in the discovery document use the https scheme.

True
testing bool

If True, don't try to validate the endpoint urls that are part of the document.

False
**kwargs Any

Additional args that will be passed to OAuth2Client.

{}

Returns:

Type Description
OAuth2Client

An OAuth2Client initialized with the endpoints from the discovery document.

Raises:

Type Description
InvalidDiscoveryDocument

If the document does not contain at least a "token_endpoint".

Examples:

from requests_oauth2client import OAuth2Client

client = OAuth2Client.from_discovery_document(
    {
        "issuer": "https://myas.local",
        "token_endpoint": "https://myas.local/token",
    },
    client_id="client_id",
    client_secret="client_secret",
)
Source code in requests_oauth2client/client.py
    @classmethod
    def from_discovery_document(
        cls,
        discovery: dict[str, Any],
        issuer: str | None = None,
        *,
        auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_key: Jwk | dict[str, Any] | None = None,
        authorization_server_jwks: JwkSet | dict[str, Any] | None = None,
        https: bool = True,
        testing: bool = False,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialize an `OAuth2Client`, based on an AS Discovery Document.

        Args:
          discovery: A `dict` of server metadata, in the same format as retrieved from a discovery endpoint.
          issuer: If an issuer is given, check that it matches the one mentioned in the document.
          auth: The authentication handler to use for client authentication.
          client_id: Client ID.
          client_secret: Client secret to use to authenticate the client.
          private_key: Private key to sign client assertions.
          authorization_server_jwks: The current authorization server JWKS keys.
          https: (deprecated) If `True`, validates that urls in the discovery document use the https scheme.
          testing: If `True`, don't try to validate the endpoint urls that are part of the document.
          **kwargs: Additional args that will be passed to `OAuth2Client`.

        Returns:
            An `OAuth2Client` initialized with the endpoints from the discovery document.

        Raises:
            InvalidDiscoveryDocument: If the document does not contain at least a `"token_endpoint"`.

        Examples:
            ```python
            from requests_oauth2client import OAuth2Client

            client = OAuth2Client.from_discovery_document(
                {
                    "issuer": "https://myas.local",
                    "token_endpoint": "https://myas.local/token",
                },
                client_id="client_id",
                client_secret="client_secret",
            )
            ```

        """
        if not https:
            warnings.warn(
                """\
The `https` parameter is deprecated.
To disable endpoint uri validation, set `testing=True` when initializing your `OAuth2Client`.""",
                stacklevel=1,
            )
            testing = True
        if issuer and discovery.get("issuer") != issuer:
            msg = f"""\
Mismatching `issuer` value in discovery document (received '{discovery.get("issuer")}', expected '{issuer}')."""
            raise InvalidParam(
                msg,
                issuer,
                discovery.get("issuer"),
            )
        if issuer is None:
            issuer = discovery.get("issuer")

        token_endpoint = discovery.get(Endpoints.TOKEN)
        if token_endpoint is None:
            msg = "token_endpoint not found in that discovery document"
            raise InvalidDiscoveryDocument(msg, discovery)
        authorization_endpoint = discovery.get(Endpoints.AUTHORIZATION)
        revocation_endpoint = discovery.get(Endpoints.REVOCATION)
        introspection_endpoint = discovery.get(Endpoints.INTROSPECTION)
        userinfo_endpoint = discovery.get(Endpoints.USER_INFO)
        pushed_authorization_request_endpoint = discovery.get(Endpoints.PUSHED_AUTHORIZATION_REQUEST)
        jwks_uri = discovery.get(Endpoints.JWKS)
        if jwks_uri is not None and not testing:
            validate_endpoint_uri(jwks_uri)
        authorization_response_iss_parameter_supported = discovery.get(
            "authorization_response_iss_parameter_supported",
            False,
        )

        return cls(
            token_endpoint=token_endpoint,
            authorization_endpoint=authorization_endpoint,
            revocation_endpoint=revocation_endpoint,
            introspection_endpoint=introspection_endpoint,
            userinfo_endpoint=userinfo_endpoint,
            pushed_authorization_request_endpoint=pushed_authorization_request_endpoint,
            jwks_uri=jwks_uri,
            authorization_server_jwks=authorization_server_jwks,
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            issuer=issuer,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            testing=testing,
            **kwargs,
        )

client_authentication

This module implements OAuth 2.0 Client Authentication Methods.

An OAuth 2.0 Client must authenticate to the AS whenever it sends a request to the Token Endpoint, by including appropriate credentials. This module contains helper classes and methods that implement the standardized and commonly used Client Authentication Methods.

InvalidRequestForClientAuthentication

Bases: RuntimeError

Raised when a request is not suitable for OAuth 2.0 client authentication.

Source code in requests_oauth2client/client_authentication.py
class InvalidRequestForClientAuthentication(RuntimeError):
    """Raised when a request is not suitable for OAuth 2.0 client authentication."""

    def __init__(self, request: requests.PreparedRequest) -> None:
        super().__init__("This request is not suitabe for OAuth 2.0 client authentication.")
        self.request = request

BaseClientAuthenticationMethod

Bases: AuthBase

Base class for all Client Authentication methods. This extends requests.auth.AuthBase.

This base class checks that requests are suitable to add Client Authentication parameters to, and does not modify the request.

Source code in requests_oauth2client/client_authentication.py
@frozen
class BaseClientAuthenticationMethod(requests.auth.AuthBase):
    """Base class for all Client Authentication methods. This extends [requests.auth.AuthBase][].

    This base class checks that requests are suitable to add Client Authentication parameters to, and does not modify
    the request.

    """

    client_id: str

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Check that the request is suitable for Client Authentication.

        It checks:

        * that the method is `POST`
        * that the Content-Type is "application/x-www-form-urlencoded" or None

        Args:
            request: a [requests.PreparedRequest][]

        Returns:
            a [requests.PreparedRequest][], unmodified

        Raises:
            RuntimeError: if the request is not suitable for OAuth 2.0 Client Authentication

        """
        if request.method != "POST" or request.headers.get("Content-Type") not in (
            "application/x-www-form-urlencoded",
            None,
        ):
            raise InvalidRequestForClientAuthentication(request)
        return request

ClientSecretBasic

Bases: BaseClientAuthenticationMethod

Implement client_secret_basic authentication.

With this method, the client sends its Client ID and Secret, in the HTTP Authorization header, with the Basic scheme, in each authenticated request to the Authorization Server.

Parameters:

Name Type Description Default
client_id str

Client ID

required
client_secret str

Client Secret

required
Example
1
2
3
4
from requests_oauth2client import ClientSecretBasic, OAuth2Client

auth = ClientSecretBasic("my_client_id", "my_client_secret")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen(init=False)
class ClientSecretBasic(BaseClientAuthenticationMethod):
    """Implement `client_secret_basic` authentication.

    With this method, the client sends its Client ID and Secret, in the HTTP `Authorization` header, with
    the `Basic` scheme, in each authenticated request to the Authorization Server.

    Args:
        client_id: Client ID
        client_secret: Client Secret

    Example:
        ```python
        from requests_oauth2client import ClientSecretBasic, OAuth2Client

        auth = ClientSecretBasic("my_client_id", "my_client_secret")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    client_secret: str

    def __init__(self, client_id: str, client_secret: str) -> None:
        self.__attrs_init__(
            client_id=client_id,
            client_secret=client_secret,
        )

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the appropriate `Authorization` header in each request.

        The Authorization header is formatted as such:
        `Authorization: Basic BASE64('<client_id:client_secret>')`

        Args:
            request: the request

        Returns:
            a [requests.PreparedRequest][] with the added Authorization header.

        """
        request = super().__call__(request)
        b64encoded_credentials = BinaPy(f"{self.client_id}:{self.client_secret}").to("b64").ascii()
        request.headers["Authorization"] = f"Basic {b64encoded_credentials}"
        return request

ClientSecretPost

Bases: BaseClientAuthenticationMethod

Implement client_secret_post client authentication method.

With this method, the client inserts its client_id and client_secret in each authenticated request to the AS.

Parameters:

Name Type Description Default
client_id str

Client ID

required
client_secret str

Client Secret

required
Example
1
2
3
4
from requests_oauth2client import ClientSecretPost, OAuth2Client

auth = ClientSecretPost("my_client_id", "my_client_secret")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen(init=False)
class ClientSecretPost(BaseClientAuthenticationMethod):
    """Implement `client_secret_post` client authentication method.

    With this method, the client inserts its client_id and client_secret in each authenticated
    request to the AS.

    Args:
        client_id: Client ID
        client_secret: Client Secret

    Example:
        ```python
        from requests_oauth2client import ClientSecretPost, OAuth2Client

        auth = ClientSecretPost("my_client_id", "my_client_secret")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    client_secret: str

    def __init__(self, client_id: str, client_secret: str) -> None:
        self.__attrs_init__(
            client_id=client_id,
            client_secret=client_secret,
        )

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the `client_id` and `client_secret` parameters in the request body.

        Args:
            request: a [requests.PreparedRequest][].

        Returns:
            a [requests.PreparedRequest][] with the added client credentials fields.

        """
        request = super().__call__(request)
        params = (
            parse_qs(request.body, strict_parsing=True, keep_blank_values=True)  # type: ignore[type-var]
            if isinstance(request.body, (str, bytes))
            else {}
        )
        params[b"client_id"] = [self.client_id.encode()]
        params[b"client_secret"] = [self.client_secret.encode()]
        request.prepare_body(params, files=None)
        return request

BaseClientAssertionAuthenticationMethod

Bases: BaseClientAuthenticationMethod

Base class for assertion-based client authentication methods.

Source code in requests_oauth2client/client_authentication.py
@frozen
class BaseClientAssertionAuthenticationMethod(BaseClientAuthenticationMethod):
    """Base class for assertion-based client authentication methods."""

    lifetime: int
    jti_gen: Callable[[], str]
    aud: str | None
    alg: str | None

    def client_assertion(self, audience: str) -> str:
        """Generate a Client Assertion for a specific audience.

        Args:
            audience: the audience to use for the `aud` claim of the generated Client Assertion.

        Returns:
            a Client Assertion, as `str`.

        """
        raise NotImplementedError

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add a `client_assertion` field in the request body.

        Args:
            request: a [requests.PreparedRequest][].

        Returns:
            a [requests.PreparedRequest][] with the added `client_assertion` field.

        """
        request = super().__call__(request)
        audience = self.aud or request.url
        if audience is None:
            raise InvalidRequestForClientAuthentication(request)  # pragma: no cover
        params = (
            parse_qs(request.body, strict_parsing=True, keep_blank_values=True)  # type: ignore[type-var]
            if request.body
            else {}
        )
        client_assertion = self.client_assertion(audience)
        params[b"client_id"] = [self.client_id.encode()]
        params[b"client_assertion"] = [client_assertion.encode()]
        params[b"client_assertion_type"] = [b"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"]
        request.prepare_body(params, files=None)
        return request
client_assertion(audience)

Generate a Client Assertion for a specific audience.

Parameters:

Name Type Description Default
audience str

the audience to use for the aud claim of the generated Client Assertion.

required

Returns:

Type Description
str

a Client Assertion, as str.

Source code in requests_oauth2client/client_authentication.py
def client_assertion(self, audience: str) -> str:
    """Generate a Client Assertion for a specific audience.

    Args:
        audience: the audience to use for the `aud` claim of the generated Client Assertion.

    Returns:
        a Client Assertion, as `str`.

    """
    raise NotImplementedError

ClientSecretJwt

Bases: BaseClientAssertionAuthenticationMethod

Implement client_secret_jwt client authentication method.

With this method, the client generates a client assertion, then symmetrically signs it with its Client Secret. The assertion is then sent to the AS in a client_assertion field with each authenticated request.

Parameters:

Name Type Description Default
client_id str

the client_id to use.

required
client_secret str

the client_secret to use to sign generated Client Assertions.

required
alg str

the alg to use to sign generated Client Assertions.

HS256
lifetime int

the lifetime to use for generated Client Assertions.

60
jti_gen Callable[[], str]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

lambda: str(uuid4())
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.

None
Example
1
2
3
4
from requests_oauth2client import OAuth2Client, ClientSecretJwt

auth = ClientSecretJwt("my_client_id", "my_client_secret")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen(init=False)
class ClientSecretJwt(BaseClientAssertionAuthenticationMethod):
    """Implement `client_secret_jwt` client authentication method.

    With this method, the client generates a client assertion, then symmetrically signs it with its Client Secret.
    The assertion is then sent to the AS in a `client_assertion` field with each authenticated request.

    Args:
        client_id: the `client_id` to use.
        client_secret: the `client_secret` to use to sign generated Client Assertions.
        alg: the alg to use to sign generated Client Assertions.
        lifetime: the lifetime to use for generated Client Assertions.
        jti_gen: a function to generate JWT Token Ids (`jti`) for generated Client Assertions.
        aud: the audience value to use. If `None` (default), the endpoint URL will be used.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client, ClientSecretJwt

        auth = ClientSecretJwt("my_client_id", "my_client_secret")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    client_secret: str

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        lifetime: int = 60,
        alg: str = SignatureAlgs.HS256,
        jti_gen: Callable[[], str] = lambda: str(uuid4()),
        aud: str | None = None,
    ) -> None:
        self.__attrs_init__(
            client_id=client_id,
            client_secret=client_secret,
            lifetime=lifetime,
            alg=alg,
            jti_gen=jti_gen,
            aud=aud,
        )

    def client_assertion(self, audience: str) -> str:
        """Generate a symmetrically signed Client Assertion.

        Assertion is signed with the `client_secret` as key and the `alg` passed at init time.

        Args:
            audience: the audience to use for the generated Client Assertion.

        Returns:
            a Client Assertion, as `str`.

        """
        iat = int(datetime.now(tz=timezone.utc).timestamp())
        exp = iat + self.lifetime
        jti = str(self.jti_gen())

        jwk = SymmetricJwk.from_bytes(self.client_secret.encode())

        jwt = Jwt.sign(
            claims={
                "iss": self.client_id,
                "sub": self.client_id,
                "aud": audience,
                "iat": iat,
                "exp": exp,
                "jti": jti,
            },
            key=jwk,
            alg=self.alg,
        )
        return str(jwt)
client_assertion(audience)

Generate a symmetrically signed Client Assertion.

Assertion is signed with the client_secret as key and the alg passed at init time.

Parameters:

Name Type Description Default
audience str

the audience to use for the generated Client Assertion.

required

Returns:

Type Description
str

a Client Assertion, as str.

Source code in requests_oauth2client/client_authentication.py
def client_assertion(self, audience: str) -> str:
    """Generate a symmetrically signed Client Assertion.

    Assertion is signed with the `client_secret` as key and the `alg` passed at init time.

    Args:
        audience: the audience to use for the generated Client Assertion.

    Returns:
        a Client Assertion, as `str`.

    """
    iat = int(datetime.now(tz=timezone.utc).timestamp())
    exp = iat + self.lifetime
    jti = str(self.jti_gen())

    jwk = SymmetricJwk.from_bytes(self.client_secret.encode())

    jwt = Jwt.sign(
        claims={
            "iss": self.client_id,
            "sub": self.client_id,
            "aud": audience,
            "iat": iat,
            "exp": exp,
            "jti": jti,
        },
        key=jwk,
        alg=self.alg,
    )
    return str(jwt)

InvalidClientAssertionSigningKeyOrAlg

Bases: ValueError

Raised when the client assertion signing alg is not specified or invalid.

Source code in requests_oauth2client/client_authentication.py
class InvalidClientAssertionSigningKeyOrAlg(ValueError):
    """Raised when the client assertion signing alg is not specified or invalid."""

    def __init__(self, alg: str | None) -> None:
        super().__init__("""\
An asymmetric private signing key, and an alg that is supported by the signing key is required.
It can be provided either:
- as part of the private `Jwk`, in the parameter 'alg'
- or passed as parameter `alg` when initializing a `PrivateKeyJwt`.
Examples of valid `alg` values and matching key type:
- 'RS256', 'RS512' (with a key of type RSA)
- 'ES256', 'ES512' (with a key of type EC)
The private key must include a Key ID (in its 'kid' parameter).
""")
        self.alg = alg

PrivateKeyJwt

Bases: BaseClientAssertionAuthenticationMethod

Implement private_key_jwt client authentication method.

With this method, the client generates and sends a client_assertion, that is asymmetrically signed with a private key, on each direct request to the Authorization Server.

The private key must be supplied as a jwskate.Jwk instance, or any key material that can be used to initialize one.

Parameters:

Name Type Description Default
client_id str

the client_id to use.

required
private_jwk Jwk | dict[str, Any] | Any

the private key to use to sign generated Client Assertions.

required
alg str | None

the alg to use to sign generated Client Assertions.

None
lifetime int

the lifetime to use for generated Client Assertions.

60
jti_gen Callable[[], str]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

lambda: str(uuid4())
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.k

None
Example
1
2
3
4
5
6
7
8
9
from jwskate import Jwk
from requests_oauth2client import OAuth2Client, PrivateKeyJwt

# load your private key from wherever it is stored:
with open("my_private_key.pem") as f:
    my_private_key = Jwk.from_pem(f.read(), password="my_private_key_password")

auth = PrivateKeyJwt("my_client_id", my_private_key, alg="RS256")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen(init=False)
class PrivateKeyJwt(BaseClientAssertionAuthenticationMethod):
    """Implement `private_key_jwt` client authentication method.

    With this method, the client generates and sends a client_assertion, that is asymmetrically
    signed with a private key, on each direct request to the Authorization Server.

    The private key must be supplied as a [`jwskate.Jwk`][jwskate.jwk.Jwk] instance,
    or any key material that can be used to initialize one.

    Args:
        client_id: the `client_id` to use.
        private_jwk: the private key to use to sign generated Client Assertions.
        alg: the alg to use to sign generated Client Assertions.
        lifetime: the lifetime to use for generated Client Assertions.
        jti_gen: a function to generate JWT Token Ids (`jti`) for generated Client Assertions.
        aud: the audience value to use. If `None` (default), the endpoint URL will be used.k

    Example:
        ```python
        from jwskate import Jwk
        from requests_oauth2client import OAuth2Client, PrivateKeyJwt

        # load your private key from wherever it is stored:
        with open("my_private_key.pem") as f:
            my_private_key = Jwk.from_pem(f.read(), password="my_private_key_password")

        auth = PrivateKeyJwt("my_client_id", my_private_key, alg="RS256")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    private_jwk: Jwk

    def __init__(
        self,
        client_id: str,
        private_jwk: Jwk | dict[str, Any] | Any,
        *,
        alg: str | None = None,
        lifetime: int = 60,
        jti_gen: Callable[[], str] = lambda: str(uuid4()),
        aud: str | None = None,
    ) -> None:
        private_jwk = to_jwk(private_jwk)

        alg = private_jwk.alg or alg
        if not alg:
            raise InvalidClientAssertionSigningKeyOrAlg(alg)

        if alg not in private_jwk.supported_signing_algorithms():
            raise InvalidClientAssertionSigningKeyOrAlg(alg)

        if not private_jwk.is_private or private_jwk.is_symmetric:
            raise InvalidClientAssertionSigningKeyOrAlg(alg)

        kid = private_jwk.get("kid")
        if not kid:
            raise InvalidClientAssertionSigningKeyOrAlg(alg)

        self.__attrs_init__(
            client_id=client_id,
            private_jwk=private_jwk,
            alg=alg,
            lifetime=lifetime,
            jti_gen=jti_gen,
            aud=aud,
        )

    def client_assertion(self, audience: str) -> str:
        """Generate a Client Assertion, asymmetrically signed with `private_jwk` as key.

        Args:
            audience: the audience to use for the generated Client Assertion.

        Returns:
            a Client Assertion.

        """
        iat = int(datetime.now(tz=timezone.utc).timestamp())
        exp = iat + self.lifetime
        jti = str(self.jti_gen())

        jwt = Jwt.sign(
            claims={
                "iss": self.client_id,
                "sub": self.client_id,
                "aud": audience,
                "iat": iat,
                "exp": exp,
                "jti": jti,
            },
            key=self.private_jwk,
            alg=self.alg,
        )
        return str(jwt)
client_assertion(audience)

Generate a Client Assertion, asymmetrically signed with private_jwk as key.

Parameters:

Name Type Description Default
audience str

the audience to use for the generated Client Assertion.

required

Returns:

Type Description
str

a Client Assertion.

Source code in requests_oauth2client/client_authentication.py
def client_assertion(self, audience: str) -> str:
    """Generate a Client Assertion, asymmetrically signed with `private_jwk` as key.

    Args:
        audience: the audience to use for the generated Client Assertion.

    Returns:
        a Client Assertion.

    """
    iat = int(datetime.now(tz=timezone.utc).timestamp())
    exp = iat + self.lifetime
    jti = str(self.jti_gen())

    jwt = Jwt.sign(
        claims={
            "iss": self.client_id,
            "sub": self.client_id,
            "aud": audience,
            "iat": iat,
            "exp": exp,
            "jti": jti,
        },
        key=self.private_jwk,
        alg=self.alg,
    )
    return str(jwt)

PublicApp

Bases: BaseClientAuthenticationMethod

Implement the none authentication method for public apps.

This scheme is used for Public Clients, which do not have any secret credentials. Those only send their client_id to the Authorization Server.

Example
1
2
3
4
from requests_oauth2client import OAuth2Client, PublicApp

auth = PublicApp("my_client_id")
client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
Source code in requests_oauth2client/client_authentication.py
@frozen
class PublicApp(BaseClientAuthenticationMethod):
    """Implement the `none` authentication method for public apps.

    This scheme is used for Public Clients, which do not have any secret credentials. Those only
    send their client_id to the Authorization Server.

    Example:
        ```python
        from requests_oauth2client import OAuth2Client, PublicApp

        auth = PublicApp("my_client_id")
        client = OAuth2Client("https://url.to.the/token_endpoint", auth=auth)
        ```

    """

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the `client_id` field in the request body.

        Args:
            request: a request.

        Returns:
            the request with the added `client_id` form field.

        """
        request = super().__call__(request)
        params = (
            parse_qs(request.body, strict_parsing=True, keep_blank_values=True)  # type: ignore[type-var]
            if request.body
            else {}
        )
        params[b"client_id"] = [self.client_id.encode()]
        request.prepare_body(params, files=None)
        return request

UnsupportedClientCredentials

Bases: TypeError, ValueError

Raised when unsupported client credentials are provided.

Source code in requests_oauth2client/client_authentication.py
class UnsupportedClientCredentials(TypeError, ValueError):
    """Raised when unsupported client credentials are provided."""

client_auth_factory(auth, *, client_id=None, client_secret=None, private_key=None, default_auth_handler=ClientSecretPost)

Initialize the appropriate Auth Handler based on the provided parameters.

This initializes a ClientAuthenticationMethod subclass based on the provided parameters.

Parameters:

Name Type Description Default
auth AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None

can be:

  • a requests.auth.AuthBase instance (which will be used directly)
  • a tuple of (client_id, client_secret) which will be used to initialize an instance of default_auth_handler,
  • a tuple of (client_id, jwk), used to initialize a PrivateKeyJwk (jwk being an instance of jwskate.Jwk or a dict),
  • a client_id, as str,
  • or None, to pass client_id and other credentials as dedicated parameters, see below.
required
client_id str | None

the Client ID to use for this client

None
client_secret str | None

the Client Secret to use for this client, if any (for clients using an authentication method based on a secret)

None
private_key Jwk | dict[str, Any] | None

the private key to use for private_key_jwt authentication method

None
default_auth_handler type[ClientSecretPost | ClientSecretBasic | ClientSecretJwt]

if a client_id and client_secret are provided, initialize an instance of this class with those 2 parameters. You can choose between ClientSecretBasic, ClientSecretPost, or ClientSecretJwt.

ClientSecretPost

Returns:

Type Description
AuthBase

an Auth Handler that will manage client authentication to the AS Token Endpoint or other

AuthBase

backend endpoints.

Raises:

Type Description
UnsupportedClientCredentials

if the provided parameters are not suitable to guess the desired authentication method.

Source code in requests_oauth2client/client_authentication.py
def client_auth_factory(
    auth: requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None,
    *,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_key: Jwk | dict[str, Any] | None = None,
    default_auth_handler: type[ClientSecretPost | ClientSecretBasic | ClientSecretJwt] = ClientSecretPost,
) -> requests.auth.AuthBase:
    """Initialize the appropriate Auth Handler based on the provided parameters.

    This initializes a `ClientAuthenticationMethod` subclass based on the provided parameters.

    Args:
        auth: can be:

            - a `requests.auth.AuthBase` instance (which will be used directly)
            - a tuple of (client_id, client_secret) which will be used to initialize an instance of
              `default_auth_handler`,
            - a tuple of (client_id, jwk), used to initialize a `PrivateKeyJwk` (`jwk` being an
              instance of `jwskate.Jwk` or a `dict`),
            - a `client_id`, as `str`,
            - or `None`, to pass `client_id` and other credentials as dedicated parameters, see
              below.
        client_id: the Client ID to use for this client
        client_secret: the Client Secret to use for this client, if any (for clients using
            an authentication method based on a secret)
        private_key: the private key to use for private_key_jwt authentication method
        default_auth_handler: if a client_id and client_secret are provided, initialize an
            instance of this class with those 2 parameters.
            You can choose between `ClientSecretBasic`, `ClientSecretPost`, or `ClientSecretJwt`.

    Returns:
        an Auth Handler that will manage client authentication to the AS Token Endpoint or other
        backend endpoints.

    Raises:
        UnsupportedClientCredentials: if the provided parameters are not suitable to guess the
            desired authentication method.

    """
    if auth is not None and (client_id is not None or client_secret is not None or private_key is not None):
        msg = """\
Please use either `auth` parameter to provide an authentication method,
or use `client_id` and one of `client_secret` or `private_key`.
"""
        raise UnsupportedClientCredentials(msg)

    if isinstance(auth, str):
        client_id = auth
    elif isinstance(auth, requests.auth.AuthBase):
        return auth
    elif isinstance(auth, tuple) and len(auth) == 2:  # noqa: PLR2004
        client_id, credential = auth
        if isinstance(credential, (Jwk, dict)):
            private_key = credential
        elif isinstance(credential, str):
            client_secret = credential
        else:
            msg = "This credential type is not supported:"
            raise UnsupportedClientCredentials(msg, type(credential), credential)

    if client_id is None:
        msg = "A client_id must be provided."
        raise UnsupportedClientCredentials(msg)

    if private_key is not None:
        return PrivateKeyJwt(client_id, private_jwk=private_key)
    if client_secret is None:
        return PublicApp(str(client_id))

    return default_auth_handler(str(client_id), str(client_secret))

device_authorization

Implements the Device Authorization Flow as defined in RFC8628.

See RFC8628.

DeviceAuthorizationResponse

Represent a response returned by the device Authorization Endpoint.

All parameters are those returned by the AS as response to a Device Authorization Request.

Parameters:

Name Type Description Default
device_code str

the device_code as returned by the AS.

required
user_code str

the device_code as returned by the AS.

required
verification_uri str

the device_code as returned by the AS.

required
verification_uri_complete str | None

the device_code as returned by the AS.

None
expires_at datetime | None

the expiration date for the device_code. Also accepts an expires_in parameter, as a number of seconds in the future.

None
interval int | None

the pooling interval as returned by the AS.

None
**kwargs Any

additional parameters as returned by the AS.

{}
Source code in requests_oauth2client/device_authorization.py
class DeviceAuthorizationResponse:
    """Represent a response returned by the device Authorization Endpoint.

    All parameters are those returned by the AS as response to a Device Authorization Request.

    Args:
        device_code: the `device_code` as returned by the AS.
        user_code: the `device_code` as returned by the AS.
        verification_uri: the `device_code` as returned by the AS.
        verification_uri_complete: the `device_code` as returned by the AS.
        expires_at: the expiration date for the device_code.
            Also accepts an `expires_in` parameter, as a number of seconds in the future.
        interval: the pooling `interval` as returned by the AS.
        **kwargs: additional parameters as returned by the AS.

    """

    @accepts_expires_in
    def __init__(
        self,
        device_code: str,
        user_code: str,
        verification_uri: str,
        verification_uri_complete: str | None = None,
        expires_at: datetime | None = None,
        interval: int | None = None,
        **kwargs: Any,
    ) -> None:
        self.device_code = device_code
        self.user_code = user_code
        self.verification_uri = verification_uri
        self.verification_uri_complete = verification_uri_complete
        self.expires_at = expires_at
        self.interval = interval
        self.other = kwargs

    def is_expired(self, leeway: int = 0) -> bool | None:
        """Check if the `device_code` within this response is expired.

        Returns:
            `True` if the device_code is expired, `False` if it is still valid, `None` if there is
            no `expires_in` hint.

        """
        if self.expires_at:
            return datetime.now(tz=timezone.utc) - timedelta(seconds=leeway) > self.expires_at
        return None
is_expired(leeway=0)

Check if the device_code within this response is expired.

Returns:

Type Description
bool | None

True if the device_code is expired, False if it is still valid, None if there is

bool | None

no expires_in hint.

Source code in requests_oauth2client/device_authorization.py
def is_expired(self, leeway: int = 0) -> bool | None:
    """Check if the `device_code` within this response is expired.

    Returns:
        `True` if the device_code is expired, `False` if it is still valid, `None` if there is
        no `expires_in` hint.

    """
    if self.expires_at:
        return datetime.now(tz=timezone.utc) - timedelta(seconds=leeway) > self.expires_at
    return None

DeviceAuthorizationPoolingJob

Bases: BaseTokenEndpointPoolingJob

A Token Endpoint pooling job for the Device Authorization Flow.

This periodically checks if the user has finished with his authorization in a Device Authorization flow.

Parameters:

Name Type Description Default
client OAuth2Client

an OAuth2Client that will be used to pool the token endpoint.

required
device_code str | DeviceAuthorizationResponse

a device_code as str or a DeviceAuthorizationResponse.

required
interval int | None

The pooling interval to use. This overrides the one in auth_req_id if it is a BackChannelAuthenticationResponse.

None
slow_down_interval int

Number of seconds to add to the pooling interval when the AS returns a slow-down request.

5
requests_kwargs dict[str, Any] | None

Additional parameters for the underlying calls to requests.request.

None
**token_kwargs Any

Additional parameters for the token request.

{}
Example
1
2
3
4
5
6
7
8
from requests_oauth2client import DeviceAuthorizationPoolingJob, OAuth2Client

client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
pooler = DeviceAuthorizationPoolingJob(client=client, device_code="my_device_code")

token = None
while token is None:
    token = pooler()
Source code in requests_oauth2client/device_authorization.py
@define(init=False)
class DeviceAuthorizationPoolingJob(BaseTokenEndpointPoolingJob):
    """A Token Endpoint pooling job for the Device Authorization Flow.

    This periodically checks if the user has finished with his authorization in a Device
    Authorization flow.

    Args:
        client: an OAuth2Client that will be used to pool the token endpoint.
        device_code: a `device_code` as `str` or a `DeviceAuthorizationResponse`.
        interval: The pooling interval to use. This overrides the one in `auth_req_id` if it is
            a `BackChannelAuthenticationResponse`.
        slow_down_interval: Number of seconds to add to the pooling interval when the AS returns
            a slow-down request.
        requests_kwargs: Additional parameters for the underlying calls to [requests.request][].
        **token_kwargs: Additional parameters for the token request.

    Example:
        ```python
        from requests_oauth2client import DeviceAuthorizationPoolingJob, OAuth2Client

        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        pooler = DeviceAuthorizationPoolingJob(client=client, device_code="my_device_code")

        token = None
        while token is None:
            token = pooler()
        ```

    """

    device_code: str

    def __init__(
        self,
        client: OAuth2Client,
        device_code: str | DeviceAuthorizationResponse,
        interval: int | None = None,
        slow_down_interval: int = 5,
        requests_kwargs: dict[str, Any] | None = None,
        **token_kwargs: Any,
    ) -> None:
        if isinstance(device_code, DeviceAuthorizationResponse):
            interval = interval or device_code.interval
            device_code = device_code.device_code

        self.__attrs_init__(
            client=client,
            device_code=device_code,
            interval=interval or 5,
            slow_down_interval=slow_down_interval,
            requests_kwargs=requests_kwargs or {},
            token_kwargs=token_kwargs,
        )

    def token_request(self) -> BearerToken:
        """Implement the Device Code token request.

        This actually calls [OAuth2Client.device_code(device_code)][requests_oauth2client.OAuth2Client.device_code]
        on `self.client`.

        Returns:
            a [BearerToken][requests_oauth2client.tokens.BearerToken]

        """
        return self.client.device_code(self.device_code, requests_kwargs=self.requests_kwargs, **self.token_kwargs)
token_request()

Implement the Device Code token request.

This actually calls OAuth2Client.device_code(device_code) on self.client.

Returns:

Type Description
BearerToken
Source code in requests_oauth2client/device_authorization.py
def token_request(self) -> BearerToken:
    """Implement the Device Code token request.

    This actually calls [OAuth2Client.device_code(device_code)][requests_oauth2client.OAuth2Client.device_code]
    on `self.client`.

    Returns:
        a [BearerToken][requests_oauth2client.tokens.BearerToken]

    """
    return self.client.device_code(self.device_code, requests_kwargs=self.requests_kwargs, **self.token_kwargs)

discovery

Implements Metadata discovery documents URLS.

This is as defined in RFC8615 and OpenID Connect Discovery 1.0.

well_known_uri(origin, name, *, at_root=True)

Return the location of a well-known document on an origin url.

See RFC8615 and OIDC Discovery.

Parameters:

Name Type Description Default
origin str

origin to use to build the well-known uri.

required
name str

document name to use to build the well-known uri.

required
at_root bool

if True, assume the well-known document is at root level (as defined in RFC8615). If False, assume the well-known location is per-directory, as defined in OpenID Connect Discovery 1.0.

True

Returns:

Type Description
str

the well-know uri, relative to origin, where the well-known document named name should be

str

found.

Source code in requests_oauth2client/discovery.py
def well_known_uri(origin: str, name: str, *, at_root: bool = True) -> str:
    """Return the location of a well-known document on an origin url.

    See [RFC8615](https://datatracker.ietf.org/doc/html/rfc8615) and [OIDC
    Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata).

    Args:
        origin: origin to use to build the well-known uri.
        name: document name to use to build the well-known uri.
        at_root: if `True`, assume the well-known document is at root level (as defined in [RFC8615](https://datatracker.ietf.org/doc/html/rfc8615)).
            If `False`, assume the well-known location is per-directory, as defined in [OpenID
            Connect Discovery
            1.0](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata).

    Returns:
        the well-know uri, relative to origin, where the well-known document named `name` should be
        found.

    """
    url = furl(origin)
    if at_root:
        url.path = Path(".well-known") / url.path / name
    else:
        url.path.add(Path(".well-known") / name)
    return str(url)

oidc_discovery_document_url(issuer)

Construct the OIDC discovery document url for a given issuer.

Given an issuer identifier, return the standardised URL where the OIDC discovery document can be retrieved.

The returned URL is biuilt as specified in OpenID Connect Discovery 1.0.

Parameters:

Name Type Description Default
issuer str

an OIDC Authentication Server issuer

required

Returns:

Type Description
str

the standardised discovery document URL. Note that no attempt to fetch this document is

str

made.

Source code in requests_oauth2client/discovery.py
def oidc_discovery_document_url(issuer: str) -> str:
    """Construct the OIDC discovery document url for a given `issuer`.

    Given an `issuer` identifier, return the standardised URL where the OIDC discovery document can
    be retrieved.

    The returned URL is biuilt as specified in [OpenID Connect Discovery
    1.0](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata).

    Args:
        issuer: an OIDC Authentication Server `issuer`

    Returns:
        the standardised discovery document URL. Note that no attempt to fetch this document is
        made.

    """
    return well_known_uri(issuer, "openid-configuration", at_root=False)

oauth2_discovery_document_url(issuer)

Construct the standardised OAuth 2.0 discovery document url for a given issuer.

Based an issuer identifier, returns the standardised URL where the OAuth20 server metadata can be retrieved.

The returned URL is built as specified in RFC8414.

Parameters:

Name Type Description Default
issuer str

an OAuth20 Authentication Server issuer

required

Returns:

Type Description
str

the standardised discovery document URL. Note that no attempt to fetch this document is

str

made.

Source code in requests_oauth2client/discovery.py
def oauth2_discovery_document_url(issuer: str) -> str:
    """Construct the standardised OAuth 2.0 discovery document url for a given `issuer`.

    Based an `issuer` identifier, returns the standardised URL where the OAuth20 server metadata can
    be retrieved.

    The returned URL is built as specified in
    [RFC8414](https://datatracker.ietf.org/doc/html/rfc8414).

    Args:
        issuer: an OAuth20 Authentication Server `issuer`

    Returns:
        the standardised discovery document URL. Note that no attempt to fetch this document is
        made.

    """
    return well_known_uri(issuer, "oauth-authorization-server", at_root=True)

dpop

Implementation of OAuth 2.0 Demonstrating Proof of Possession (DPoP) (RFC9449).

InvalidDPoPAccessToken

Bases: ValueError

Raised when an access token contains invalid characters.

Source code in requests_oauth2client/dpop.py
class InvalidDPoPAccessToken(ValueError):
    """Raised when an access token contains invalid characters."""

    def __init__(self, access_token: str) -> None:
        super().__init__("""\
This DPoP token contains invalid characters. DPoP tokens are limited to a set of 68 characters,
to avoid encoding inconsistencies when doing the token value hashing for the DPoP proof.""")
        self.access_token = access_token

InvalidDPoPKey

Bases: ValueError

Raised when a DPoPToken is initialized with a non-suitable key.

Source code in requests_oauth2client/dpop.py
class InvalidDPoPKey(ValueError):
    """Raised when a DPoPToken is initialized with a non-suitable key."""

    def __init__(self, key: Any) -> None:
        super().__init__("The key you are trying to use with DPoP is not an asymmetric private key.")
        self.key = key

InvalidDPoPAlg

Bases: ValueError

Raised when an invalid or unsupported DPoP alg is given.

Source code in requests_oauth2client/dpop.py
class InvalidDPoPAlg(ValueError):
    """Raised when an invalid or unsupported DPoP alg is given."""

    def __init__(self, alg: str) -> None:
        super().__init__("DPoP proofing require an asymmetric signing alg.")
        self.alg = alg

InvalidDPoPProof

Bases: ValueError

Raised when a DPoP proof does not verify.

Source code in requests_oauth2client/dpop.py
class InvalidDPoPProof(ValueError):
    """Raised when a DPoP proof does not verify."""

    def __init__(self, proof: bytes, message: str) -> None:
        super().__init__(f"Invalid DPoP proof: {message}")
        self.proof = proof

InvalidUseDPoPNonceResponse

Bases: Exception

Base class for invalid Responses with a use_dpop_nonce error.

Source code in requests_oauth2client/dpop.py
class InvalidUseDPoPNonceResponse(Exception):
    """Base class for invalid Responses with a `use_dpop_nonce` error."""

    def __init__(self, response: requests.Response, message: str) -> None:
        super().__init__(message)
        self.response = response

MissingDPoPNonce

Bases: InvalidUseDPoPNonceResponse

Raised when a server requests a DPoP nonce but none is provided in its response.

Source code in requests_oauth2client/dpop.py
class MissingDPoPNonce(InvalidUseDPoPNonceResponse):
    """Raised when a server requests a DPoP nonce but none is provided in its response."""

    def __init__(self, response: requests.Response) -> None:
        super().__init__(
            response,
            "Server requested client to use a DPoP `nonce`, but the `DPoP-Nonce` HTTP header is missing.",
        )

RepeatedDPoPNonce

Bases: InvalidUseDPoPNonceResponse

Raised when the server requests a DPoP nonce value that is the same as already included in the request.

Source code in requests_oauth2client/dpop.py
class RepeatedDPoPNonce(InvalidUseDPoPNonceResponse):
    """Raised when the server requests a DPoP nonce value that is the same as already included in the request."""

    def __init__(self, response: requests.Response) -> None:
        super().__init__(
            response,
            """\
Server requested client to use a DPoP `nonce`,
but provided the same value for that nonce that was already included in the DPoP proof.""",
        )

DPoPToken

Bases: BearerToken

Represent a DPoP token (RFC9449).

A DPoP is very much like a BearerToken, with an additional private key bound to it.

Source code in requests_oauth2client/dpop.py
@frozen(init=False)
class DPoPToken(BearerToken):  # type: ignore[override]
    """Represent a DPoP token (RFC9449).

    A DPoP is very much like a BearerToken, with an additional private key bound to it.

    """

    TOKEN_TYPE = AccessTokenTypes.DPOP.value
    AUTHORIZATION_SCHEME = AccessTokenTypes.DPOP.value
    DPOP_HEADER: ClassVar[str] = "DPoP"

    dpop_key: DPoPKey = field(kw_only=True)

    @accepts_expires_in
    def __init__(
        self,
        access_token: str,
        *,
        _dpop_key: DPoPKey,
        expires_at: datetime | None = None,
        scope: str | None = None,
        refresh_token: str | None = None,
        token_type: str = TOKEN_TYPE,
        id_token: str | bytes | IdToken | jwskate.JweCompact | None = None,
        **kwargs: Any,
    ) -> None:
        if not token68_pattern.match(access_token):
            raise InvalidDPoPAccessToken(access_token)

        id_token = id_token_converter(id_token)

        self.__attrs_init__(
            access_token=access_token,
            expires_at=expires_at,
            scope=scope,
            refresh_token=refresh_token,
            token_type=token_type,
            id_token=id_token,
            dpop_key=_dpop_key,
            kwargs=kwargs,
        )

    def _response_hook(self, response: requests.Response, **kwargs: Any) -> requests.Response:
        """Handles a Resource Server provided DPoP nonce."""
        if response.status_code == codes.unauthorized and response.headers.get("WWW-Authenticate", "").startswith(
            "DPoP"
        ):
            self.dpop_key.handle_rs_provided_dpop_nonce(response)
            new_request = response.request.copy()
            # remove the previously registered hook to avoid registering it multiple times
            new_request.deregister_hook("response", self._response_hook)  # type: ignore[no-untyped-call]
            new_request = self(new_request)  # another hook will be re-registered here in the __call__() method

            return response.connection.send(new_request, **kwargs)

        return response

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add a DPoP proof in each request."""
        request = super().__call__(request)
        add_dpop_proof(request, dpop_key=self.dpop_key, access_token=self.access_token, header_name=self.DPOP_HEADER)
        request.register_hook("response", self._response_hook)  # type: ignore[no-untyped-call]
        return request

DPoPKey

Wrapper around a DPoP proof signature key.

This handles DPoP proof generation. It also keeps track of a nonce, if provided by the Resource Server. Its behavior follows the standard DPoP specifications. You may subclass or otherwise customize this class to implement custom behavior, like adding or modifying claims to the proofs.

Parameters:

Name Type Description Default
private_key Any

the private key to use for DPoP proof signatures.

required
alg str | None

the alg to use for signatures, if not specified of the private_key.

None
jti_generator Callable[[], str]

a callable that generates unique JWT Token ID (jti) values to include in proofs.

lambda: str(uuid4())
iat_generator Callable[[], int]

a callable that generates the Issuer Date (iat) to include in proofs.

lambda: timestamp()
jwt_typ str

the token type (typ) header to include in the generated proofs.

'dpop+jwt'
dpop_token_class type[DPoPToken]

the class to use to represent DPoP tokens.

DPoPToken
rs_nonce str | None

an initial DPoP nonce to include in requests, for testing purposes. You should leave None.

None
Source code in requests_oauth2client/dpop.py
@define(init=False)
class DPoPKey:
    """Wrapper around a DPoP proof signature key.

    This handles DPoP proof generation. It also keeps track of a nonce, if provided
    by the Resource Server.
    Its behavior follows the standard DPoP specifications.
    You may subclass or otherwise customize this class to implement custom behavior,
    like adding or modifying claims to the proofs.

    Args:
        private_key: the private key to use for DPoP proof signatures.
        alg: the alg to use for signatures, if not specified of the `private_key`.
        jti_generator: a callable that generates unique JWT Token ID (jti) values to include in proofs.
        iat_generator: a callable that generates the Issuer Date (iat) to include in proofs.
        jwt_typ: the token type (`typ`) header to include in the generated proofs.
        dpop_token_class: the class to use to represent DPoP tokens.
        rs_nonce: an initial DPoP `nonce` to include in requests, for testing purposes. You should leave `None`.

    """

    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)
    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
    rs_nonce: str | None

    def __init__(
        self,
        private_key: Any,
        alg: str | None = None,
        jti_generator: Callable[[], str] = lambda: str(uuid4()),
        iat_generator: Callable[[], int] = lambda: jwskate.Jwt.timestamp(),
        jwt_typ: str = "dpop+jwt",
        dpop_token_class: type[DPoPToken] = DPoPToken,
        as_nonce: str | None = None,
        rs_nonce: str | None = None,
    ) -> None:
        try:
            private_key = jwskate.to_jwk(private_key).check(is_private=True, is_symmetric=False)
        except ValueError as exc:
            raise InvalidDPoPKey(private_key) from exc

        alg_name = jwskate.select_alg_class(private_key.SIGNATURE_ALGORITHMS, jwk_alg=private_key.alg, alg=alg).name

        self.__attrs_init__(
            alg=alg_name,
            private_key=private_key,
            jti_generator=jti_generator,
            iat_generator=iat_generator,
            jwt_typ=jwt_typ,
            dpop_token_class=dpop_token_class,
            as_nonce=as_nonce,
            rs_nonce=rs_nonce,
        )

    @classmethod
    def generate(
        cls,
        alg: str = jwskate.SignatureAlgs.ES256,
        jwt_typ: str = "dpop+jwt",
        jti_generator: Callable[[], str] = lambda: str(uuid4()),
        iat_generator: Callable[[], int] = lambda: jwskate.Jwt.timestamp(),
        dpop_token_class: type[DPoPToken] = DPoPToken,
        as_nonce: str | None = None,
        rs_nonce: str | None = None,
    ) -> Self:
        """Generate a new DPoPKey with a new private key that is suitable for the given `alg`."""
        if alg not in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
            raise InvalidDPoPAlg(alg)
        key = jwskate.Jwk.generate(alg=alg)
        return cls(
            private_key=key,
            jti_generator=jti_generator,
            iat_generator=iat_generator,
            jwt_typ=jwt_typ,
            dpop_token_class=dpop_token_class,
            as_nonce=as_nonce,
            rs_nonce=rs_nonce,
        )

    @cached_property
    def public_jwk(self) -> jwskate.Jwk:
        """The public JWK key that matches the private key."""
        return self.private_key.public_jwk()

    @cached_property
    def dpop_jkt(self) -> str:
        """The key thumbprint, used for Authorization Code DPoP binding."""
        return self.private_key.thumbprint()

    def proof(self, htm: str, htu: str, ath: str | None = None, nonce: str | None = None) -> jwskate.SignedJwt:
        """Generate a DPoP proof.

        Proof will contain the following claims:

            - The HTTP method (`htm`), target URI (`htu`), and Access Token hash (`ath`) that are passed as parameters.
            - The `iat` claim will be generated by the configured `iat_generator`, which defaults to current datetime.
            - The `jti` claim will be generated by the configured `jti_generator`, which defaults to a random UUID4.
            - The `nonce` claim will be the value stored in the `nonce` attribute. This attribute is updated
              automatically when using a `DPoPToken` or one of the provided Authentication handlers as a `requests`
              auth handler.

        The proof will be signed with the private key of this DPoPKey, using the configured `alg` signature algorithm.

        Args:
            htm: The HTTP method value of the request to which the proof is attached.
            htu: The HTTP target URI of the request to which the proof is attached. Query and Fragment parts will
                be automatically removed before being used as `htu` value in the generated proof.
            ath: The Access Token hash value.
            nonce: A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS.  If `None`, the
                value stored in `rs_nonce` will be used instead.
                In typical cases, you should never have to use this parameter. It is only used internally when
                requesting the AS token endpoint.

        Returns:
            the proof value (as a signed JWT)

        """
        htu = furl(htu).remove(query=True, fragment=True).url
        proof_claims = {"jti": self.jti_generator(), "htm": htm, "htu": htu, "iat": self.iat_generator()}
        if nonce:
            proof_claims["nonce"] = nonce
        elif self.rs_nonce:
            proof_claims["nonce"] = self.rs_nonce
        if ath:
            proof_claims["ath"] = ath
        return jwskate.SignedJwt.sign(
            proof_claims,
            key=self.private_key,
            alg=self.alg,
            typ=self.jwt_typ,
            extra_headers={"jwk": self.public_jwk},
        )

    def handle_as_provided_dpop_nonce(self, response: requests.Response) -> None:
        """Handle an Authorization Server response containing a `use_dpop_nonce` error.

        Args:
            response: the response from the AS.

        """
        nonce = response.headers.get("DPoP-Nonce")
        if not nonce:
            raise MissingDPoPNonce(response)
        if self.as_nonce == nonce:
            raise RepeatedDPoPNonce(response)
        self.as_nonce = nonce

    def handle_rs_provided_dpop_nonce(self, response: requests.Response) -> None:
        """Handle a Resource Server response containing a `use_dpop_nonce` error.

        Args:
            response: the response from the AS.

        """
        nonce = response.headers.get("DPoP-Nonce")
        if not nonce:
            raise MissingDPoPNonce(response)
        if self.rs_nonce == nonce:
            raise RepeatedDPoPNonce(response)
        self.rs_nonce = nonce
public_jwk cached property

The public JWK key that matches the private key.

dpop_jkt cached property

The key thumbprint, used for Authorization Code DPoP binding.

generate(alg=jwskate.SignatureAlgs.ES256, jwt_typ='dpop+jwt', jti_generator=lambda: str(uuid4()), iat_generator=lambda: jwskate.Jwt.timestamp(), dpop_token_class=DPoPToken, as_nonce=None, rs_nonce=None) classmethod

Generate a new DPoPKey with a new private key that is suitable for the given alg.

Source code in requests_oauth2client/dpop.py
@classmethod
def generate(
    cls,
    alg: str = jwskate.SignatureAlgs.ES256,
    jwt_typ: str = "dpop+jwt",
    jti_generator: Callable[[], str] = lambda: str(uuid4()),
    iat_generator: Callable[[], int] = lambda: jwskate.Jwt.timestamp(),
    dpop_token_class: type[DPoPToken] = DPoPToken,
    as_nonce: str | None = None,
    rs_nonce: str | None = None,
) -> Self:
    """Generate a new DPoPKey with a new private key that is suitable for the given `alg`."""
    if alg not in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
        raise InvalidDPoPAlg(alg)
    key = jwskate.Jwk.generate(alg=alg)
    return cls(
        private_key=key,
        jti_generator=jti_generator,
        iat_generator=iat_generator,
        jwt_typ=jwt_typ,
        dpop_token_class=dpop_token_class,
        as_nonce=as_nonce,
        rs_nonce=rs_nonce,
    )
proof(htm, htu, ath=None, nonce=None)

Generate a DPoP proof.

Proof will contain the following claims:

1
2
3
4
5
6
- The HTTP method (`htm`), target URI (`htu`), and Access Token hash (`ath`) that are passed as parameters.
- The `iat` claim will be generated by the configured `iat_generator`, which defaults to current datetime.
- The `jti` claim will be generated by the configured `jti_generator`, which defaults to a random UUID4.
- The `nonce` claim will be the value stored in the `nonce` attribute. This attribute is updated
  automatically when using a `DPoPToken` or one of the provided Authentication handlers as a `requests`
  auth handler.

The proof will be signed with the private key of this DPoPKey, using the configured alg signature algorithm.

Parameters:

Name Type Description Default
htm str

The HTTP method value of the request to which the proof is attached.

required
htu str

The HTTP target URI of the request to which the proof is attached. Query and Fragment parts will be automatically removed before being used as htu value in the generated proof.

required
ath str | None

The Access Token hash value.

None
nonce str | None

A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS. If None, the value stored in rs_nonce will be used instead. In typical cases, you should never have to use this parameter. It is only used internally when requesting the AS token endpoint.

None

Returns:

Type Description
SignedJwt

the proof value (as a signed JWT)

Source code in requests_oauth2client/dpop.py
def proof(self, htm: str, htu: str, ath: str | None = None, nonce: str | None = None) -> jwskate.SignedJwt:
    """Generate a DPoP proof.

    Proof will contain the following claims:

        - The HTTP method (`htm`), target URI (`htu`), and Access Token hash (`ath`) that are passed as parameters.
        - The `iat` claim will be generated by the configured `iat_generator`, which defaults to current datetime.
        - The `jti` claim will be generated by the configured `jti_generator`, which defaults to a random UUID4.
        - The `nonce` claim will be the value stored in the `nonce` attribute. This attribute is updated
          automatically when using a `DPoPToken` or one of the provided Authentication handlers as a `requests`
          auth handler.

    The proof will be signed with the private key of this DPoPKey, using the configured `alg` signature algorithm.

    Args:
        htm: The HTTP method value of the request to which the proof is attached.
        htu: The HTTP target URI of the request to which the proof is attached. Query and Fragment parts will
            be automatically removed before being used as `htu` value in the generated proof.
        ath: The Access Token hash value.
        nonce: A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS.  If `None`, the
            value stored in `rs_nonce` will be used instead.
            In typical cases, you should never have to use this parameter. It is only used internally when
            requesting the AS token endpoint.

    Returns:
        the proof value (as a signed JWT)

    """
    htu = furl(htu).remove(query=True, fragment=True).url
    proof_claims = {"jti": self.jti_generator(), "htm": htm, "htu": htu, "iat": self.iat_generator()}
    if nonce:
        proof_claims["nonce"] = nonce
    elif self.rs_nonce:
        proof_claims["nonce"] = self.rs_nonce
    if ath:
        proof_claims["ath"] = ath
    return jwskate.SignedJwt.sign(
        proof_claims,
        key=self.private_key,
        alg=self.alg,
        typ=self.jwt_typ,
        extra_headers={"jwk": self.public_jwk},
    )
handle_as_provided_dpop_nonce(response)

Handle an Authorization Server response containing a use_dpop_nonce error.

Parameters:

Name Type Description Default
response Response

the response from the AS.

required
Source code in requests_oauth2client/dpop.py
def handle_as_provided_dpop_nonce(self, response: requests.Response) -> None:
    """Handle an Authorization Server response containing a `use_dpop_nonce` error.

    Args:
        response: the response from the AS.

    """
    nonce = response.headers.get("DPoP-Nonce")
    if not nonce:
        raise MissingDPoPNonce(response)
    if self.as_nonce == nonce:
        raise RepeatedDPoPNonce(response)
    self.as_nonce = nonce
handle_rs_provided_dpop_nonce(response)

Handle a Resource Server response containing a use_dpop_nonce error.

Parameters:

Name Type Description Default
response Response

the response from the AS.

required
Source code in requests_oauth2client/dpop.py
def handle_rs_provided_dpop_nonce(self, response: requests.Response) -> None:
    """Handle a Resource Server response containing a `use_dpop_nonce` error.

    Args:
        response: the response from the AS.

    """
    nonce = response.headers.get("DPoP-Nonce")
    if not nonce:
        raise MissingDPoPNonce(response)
    if self.rs_nonce == nonce:
        raise RepeatedDPoPNonce(response)
    self.rs_nonce = nonce

add_dpop_proof(request, dpop_key, access_token, header_name='DPoP')

Add a valid DPoP proof to a request, in-place.

Parameters:

Name Type Description Default
request PreparedRequest

the request to add the proof to.

required
dpop_key DPoPKey

the DPoP key to use for the proof.

required
access_token str

the access token to hash in the proof.

required
header_name str

the name of the header to add the proof to.

'DPoP'
Source code in requests_oauth2client/dpop.py
def add_dpop_proof(
    request: requests.PreparedRequest,
    dpop_key: DPoPKey,
    access_token: str,
    header_name: str = "DPoP",
) -> None:
    """Add a valid DPoP proof to a request, in-place.

    Args:
        request: the request to add the proof to.
        dpop_key: the DPoP key to use for the proof.
        access_token: the access token to hash in the proof.
        header_name: the name of the header to add the proof to.

    """
    htu = request.url
    htm = request.method
    ath = BinaPy(access_token).to("sha256").to("b64u").decode()
    if htu is None or htm is None:  # pragma: no cover
        msg = "Request has no 'method' or 'url'! This should not happen."
        raise RuntimeError(msg)
    proof = dpop_key.proof(htm=htm, htu=htu, ath=ath)
    request.headers[header_name] = str(proof)

validate_dpop_proof(proof, *, htm, htu, ath=None, nonce=None, leeway=60, alg=None, algs=())

Validate a DPoP proof.

Parameters:

Name Type Description Default
proof str | bytes

The serialized DPoP proof.

required
htm str

The value of the HTTP method of the request to which the JWT is attached.

required
htu str

The HTTP target URI of the request to which the JWT is attached, without query and fragment parts.

required
ath str | None

The Hash of the access token.

None
nonce str | None

A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS.

None
leeway int

A leeway, in number of seconds, to validate the proof iat claim.

60
alg str | None

Allowed signature alg, if there is only one. Use this or algs.

None
algs Sequence[str]

Allowed signature algs, if there is several. Use this or alg.

()

Returns:

Type Description
SignedJwt

The validated DPoP proof, as a SignedJwt.

Source code in requests_oauth2client/dpop.py
def validate_dpop_proof(  # noqa: C901
    proof: str | bytes,
    *,
    htm: str,
    htu: str,
    ath: str | None = None,
    nonce: str | None = None,
    leeway: int = 60,
    alg: str | None = None,
    algs: Sequence[str] = (),
) -> jwskate.SignedJwt:
    """Validate a DPoP proof.

    Args:
        proof: The serialized DPoP proof.
        htm: The value of the HTTP method of the request to which the JWT is attached.
        htu: The HTTP target URI of the request to which the JWT is attached, without query and fragment parts.
        ath: The Hash of the access token.
        nonce: A recent nonce provided via the DPoP-Nonce HTTP header, from either the AS or RS.
        leeway: A leeway, in number of seconds, to validate the proof `iat` claim.
        alg: Allowed signature alg, if there is only one. Use this or `algs`.
        algs: Allowed signature algs, if there is several. Use this or `alg`.

    Returns:
        The validated DPoP proof, as a `SignedJwt`.

    """
    if not isinstance(proof, bytes):
        proof = proof.encode()
    try:
        proof_jwt = jwskate.SignedJwt(proof)
    except jwskate.InvalidJwt as exc:
        raise InvalidDPoPProof(proof, "not a syntactically valid JWT") from exc
    if proof_jwt.typ != "dpop+jwt":
        raise InvalidDPoPProof(proof, f"typ '{proof_jwt.typ}' is not the expected 'dpop+jwt'.")
    if "jwk" not in proof_jwt.headers:
        raise InvalidDPoPProof(proof, "'jwk' header is missing")
    try:
        public_jwk = jwskate.Jwk(proof_jwt.headers["jwk"])
    except jwskate.InvalidJwk as exc:
        raise InvalidDPoPProof(proof, "'jwk' header is not a valid JWK key.") from exc
    if public_jwk.is_private or public_jwk.is_symmetric:
        raise InvalidDPoPProof(proof, "'jwk' header is a private or symmetric key.")

    if not proof_jwt.verify_signature(public_jwk, alg=alg, algs=algs):
        raise InvalidDPoPProof(proof, "signature does not verify.")

    if proof_jwt.issued_at is None:
        raise InvalidDPoPProof(proof, "a Issued At (iat) claim is missing.")
    now = datetime.now(tz=timezone.utc)
    if not now - timedelta(seconds=leeway) < proof_jwt.issued_at < now + timedelta(seconds=leeway):
        msg = f"""\
Issued At timestamp (iat) is too far away in the past or future (received: {proof_jwt.issued_at}, now: {now})."""
        raise InvalidDPoPProof(
            proof,
            msg,
        )
    if proof_jwt.jwt_token_id is None:
        raise InvalidDPoPProof(proof, "a Unique Identifier (jti) claim is missing.")
    if "htm" not in proof_jwt.claims:
        raise InvalidDPoPProof(proof, "the HTTP method (htm) claim is missing.")
    if proof_jwt.htm != htm:
        raise InvalidDPoPProof(proof, f"HTTP Method (htm) '{proof_jwt.htm}' does not matches expected '{htm}'.")
    if "htu" not in proof_jwt.claims:
        raise InvalidDPoPProof(proof, "the HTTP URI (htu) claim is missing.")
    if proof_jwt.htu != htu:
        raise InvalidDPoPProof(proof, f"HTTP URI (htu) '{proof_jwt.htu}' does not matches expected '{htu}'.")
    if ath:
        if "ath" not in proof_jwt.claims:
            raise InvalidDPoPProof(proof, "the Access Token hash (ath) claim is missing.")
        if proof_jwt.ath != ath:
            raise InvalidDPoPProof(
                proof, f"Access Token Hash (ath) value '{proof_jwt.ath}' does not match expected '{ath}'."
            )
    if nonce:
        if "nonce" not in proof_jwt.claims:
            raise InvalidDPoPProof(proof, "the DPoP Nonce (nonce) claim is missing.")
        if proof_jwt.nonce != nonce:
            raise InvalidDPoPProof(
                proof, f"DPoP Nonce (nonce) value '{proof_jwt.nonce}' does not match expected '{nonce}'."
            )

    return proof_jwt

exceptions

This module contains all exception classes from requests_oauth2client.

OAuth2Error

Bases: Exception

Base class for Exceptions raised when a backend endpoint returns an error.

Parameters:

Name Type Description Default
response Response

the HTTP response containing the error

required
client

the OAuth2Client used to send the request

required
description str | None

description of the error

None
Source code in requests_oauth2client/exceptions.py
class OAuth2Error(Exception):
    """Base class for Exceptions raised when a backend endpoint returns an error.

    Args:
        response: the HTTP response containing the error
        client : the OAuth2Client used to send the request
        description: description of the error

    """

    def __init__(self, response: requests.Response, client: OAuth2Client, description: str | None = None) -> None:
        super().__init__(f"The remote endpoint returned an error: {description or 'no description provided'}")
        self.response = response
        self.client = client
        self.description = description

    @property
    def request(self) -> requests.PreparedRequest:
        """The request leading to the error."""
        return self.response.request
request property

The request leading to the error.

EndpointError

Bases: OAuth2Error

Base class for exceptions raised from backend endpoint errors.

This contains the error message, description and uri that are returned by the AS in the OAuth 2.0 standardised way.

Parameters:

Name Type Description Default
response Response

the raw response containing the error.

required
error str

the error identifier as returned by the AS.

required
description str | None

the error_description as returned by the AS.

None
uri str | None

the error_uri as returned by the AS.

None
Source code in requests_oauth2client/exceptions.py
class EndpointError(OAuth2Error):
    """Base class for exceptions raised from backend endpoint errors.

    This contains the error message, description and uri that are returned
    by the AS in the OAuth 2.0 standardised way.

    Args:
        response: the raw response containing the error.
        error: the `error` identifier as returned by the AS.
        description: the `error_description` as returned by the AS.
        uri: the `error_uri` as returned by the AS.

    """

    def __init__(
        self,
        response: requests.Response,
        client: OAuth2Client,
        error: str,
        description: str | None = None,
        uri: str | None = None,
    ) -> None:
        super().__init__(response=response, client=client, description=description)
        self.error = error
        self.uri = uri

InvalidTokenResponse

Bases: OAuth2Error

Raised when the Token Endpoint returns a non-standard response.

Source code in requests_oauth2client/exceptions.py
class InvalidTokenResponse(OAuth2Error):
    """Raised when the Token Endpoint returns a non-standard response."""

UnknownTokenEndpointError

Bases: EndpointError

Raised when the token endpoint returns an otherwise unknown error.

Source code in requests_oauth2client/exceptions.py
class UnknownTokenEndpointError(EndpointError):
    """Raised when the token endpoint returns an otherwise unknown error."""

ServerError

Bases: EndpointError

Raised when the token endpoint returns error = server_error.

Source code in requests_oauth2client/exceptions.py
class ServerError(EndpointError):
    """Raised when the token endpoint returns `error = server_error`."""

TokenEndpointError

Bases: EndpointError

Base class for errors that are specific to the token endpoint.

Source code in requests_oauth2client/exceptions.py
class TokenEndpointError(EndpointError):
    """Base class for errors that are specific to the token endpoint."""

InvalidRequest

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_request.

Source code in requests_oauth2client/exceptions.py
class InvalidRequest(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_request`."""

InvalidClient

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_client.

Source code in requests_oauth2client/exceptions.py
class InvalidClient(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_client`."""

InvalidScope

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_scope.

Source code in requests_oauth2client/exceptions.py
class InvalidScope(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_scope`."""

InvalidTarget

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_target.

Source code in requests_oauth2client/exceptions.py
class InvalidTarget(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_target`."""

InvalidGrant

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = invalid_grant.

Source code in requests_oauth2client/exceptions.py
class InvalidGrant(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_grant`."""

UseDPoPNonce

Bases: TokenEndpointError

Raised when the Token Endpoint raises error = use_dpop_nonce`.

Source code in requests_oauth2client/exceptions.py
class UseDPoPNonce(TokenEndpointError):
    """Raised when the Token Endpoint raises error = use_dpop_nonce`."""

AccessDenied

Bases: EndpointError

Raised when the Authorization Server returns error = access_denied.

Source code in requests_oauth2client/exceptions.py
class AccessDenied(EndpointError):
    """Raised when the Authorization Server returns `error = access_denied`."""

UnauthorizedClient

Bases: EndpointError

Raised when the Authorization Server returns error = unauthorized_client.

Source code in requests_oauth2client/exceptions.py
class UnauthorizedClient(EndpointError):
    """Raised when the Authorization Server returns `error = unauthorized_client`."""

RevocationError

Bases: EndpointError

Base class for Revocation Endpoint errors.

Source code in requests_oauth2client/exceptions.py
class RevocationError(EndpointError):
    """Base class for Revocation Endpoint errors."""

UnsupportedTokenType

Bases: RevocationError

Raised when the Revocation endpoint returns error = unsupported_token_type.

Source code in requests_oauth2client/exceptions.py
class UnsupportedTokenType(RevocationError):
    """Raised when the Revocation endpoint returns `error = unsupported_token_type`."""

IntrospectionError

Bases: EndpointError

Base class for Introspection Endpoint errors.

Source code in requests_oauth2client/exceptions.py
class IntrospectionError(EndpointError):
    """Base class for Introspection Endpoint errors."""

UnknownIntrospectionError

Bases: OAuth2Error

Raised when the Introspection Endpoint returns a non-standard error.

Source code in requests_oauth2client/exceptions.py
class UnknownIntrospectionError(OAuth2Error):
    """Raised when the Introspection Endpoint returns a non-standard error."""

DeviceAuthorizationError

Bases: EndpointError

Base class for Device Authorization Endpoint errors.

Source code in requests_oauth2client/exceptions.py
class DeviceAuthorizationError(EndpointError):
    """Base class for Device Authorization Endpoint errors."""

AuthorizationPending

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = authorization_pending.

Source code in requests_oauth2client/exceptions.py
class AuthorizationPending(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = authorization_pending`."""

SlowDown

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = slow_down.

Source code in requests_oauth2client/exceptions.py
class SlowDown(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = slow_down`."""

ExpiredToken

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = expired_token.

Source code in requests_oauth2client/exceptions.py
class ExpiredToken(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = expired_token`."""

InvalidDeviceAuthorizationResponse

Bases: OAuth2Error

Raised when the Device Authorization Endpoint returns a non-standard error response.

Source code in requests_oauth2client/exceptions.py
class InvalidDeviceAuthorizationResponse(OAuth2Error):
    """Raised when the Device Authorization Endpoint returns a non-standard error response."""

AuthorizationResponseError

Bases: Exception

Base class for error responses returned by the Authorization endpoint.

An AuthorizationResponseError contains the error message, description and uri that are returned by the AS.

Parameters:

Name Type Description Default
error str

the error identifier as returned by the AS

required
description str | None

the error_description as returned by the AS

None
uri str | None

the error_uri as returned by the AS

None
Source code in requests_oauth2client/exceptions.py
class AuthorizationResponseError(Exception):
    """Base class for error responses returned by the Authorization endpoint.

    An `AuthorizationResponseError` contains the error message, description and uri that are
    returned by the AS.

    Args:
        error: the `error` identifier as returned by the AS
        description: the `error_description` as returned by the AS
        uri: the `error_uri` as returned by the AS

    """

    def __init__(
        self,
        request: AuthorizationRequest,
        response: str,
        error: str,
        description: str | None = None,
        uri: str | None = None,
    ) -> None:
        self.error = error
        self.description = description
        self.uri = uri
        self.request = request
        self.response = response

InteractionRequired

Bases: AuthorizationResponseError

Raised when the Authorization Endpoint returns error = interaction_required.

Source code in requests_oauth2client/exceptions.py
class InteractionRequired(AuthorizationResponseError):
    """Raised when the Authorization Endpoint returns `error = interaction_required`."""

LoginRequired

Bases: InteractionRequired

Raised when the Authorization Endpoint returns error = login_required.

Source code in requests_oauth2client/exceptions.py
class LoginRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = login_required`."""

AccountSelectionRequired

Bases: InteractionRequired

Raised when the Authorization Endpoint returns error = account_selection_required.

Source code in requests_oauth2client/exceptions.py
class AccountSelectionRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = account_selection_required`."""

SessionSelectionRequired

Bases: InteractionRequired

Raised when the Authorization Endpoint returns error = session_selection_required.

Source code in requests_oauth2client/exceptions.py
class SessionSelectionRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = session_selection_required`."""

ConsentRequired

Bases: InteractionRequired

Raised when the Authorization Endpoint returns error = consent_required.

Source code in requests_oauth2client/exceptions.py
class ConsentRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = consent_required`."""

InvalidAuthResponse

Bases: ValueError

Raised when the Authorization Endpoint returns an invalid response.

Source code in requests_oauth2client/exceptions.py
class InvalidAuthResponse(ValueError):
    """Raised when the Authorization Endpoint returns an invalid response."""

    def __init__(self, message: str, request: AuthorizationRequest, response: str) -> None:
        super().__init__(f"The Authorization Response is invalid: {message}")
        self.request = request
        self.response = response

MissingAuthCode

Bases: InvalidAuthResponse

Raised when the Authorization Endpoint does not return the mandatory code.

This happens when the Authorization Endpoint does not return an error, but does not return an authorization code either.

Source code in requests_oauth2client/exceptions.py
class MissingAuthCode(InvalidAuthResponse):
    """Raised when the Authorization Endpoint does not return the mandatory `code`.

    This happens when the Authorization Endpoint does not return an error, but does not return an
    authorization `code` either.

    """

    def __init__(self, request: AuthorizationRequest, response: str) -> None:
        super().__init__("missing `code` query parameter in response", request, response)

MissingIssuer

Bases: InvalidAuthResponse

Raised when the Authorization Endpoint does not return an iss parameter as expected.

The Authorization Server advertises its support with a flag authorization_response_iss_parameter_supported in its discovery document. If it is set to true, it must include an iss parameter in its authorization responses, containing its issuer identifier.

Source code in requests_oauth2client/exceptions.py
class MissingIssuer(InvalidAuthResponse):
    """Raised when the Authorization Endpoint does not return an `iss` parameter as expected.

    The Authorization Server advertises its support with a flag
    `authorization_response_iss_parameter_supported` in its discovery document. If it is set to
    `true`, it must include an `iss` parameter in its authorization responses, containing its issuer
    identifier.

    """

    def __init__(self, request: AuthorizationRequest, response: str) -> None:
        super().__init__("missing `iss` query parameter in response", request, response)

MismatchingState

Bases: InvalidAuthResponse

Raised on mismatching state value.

This happens when the Authorization Endpoints returns a 'state' parameter that doesn't match the value passed in the Authorization Request.

Source code in requests_oauth2client/exceptions.py
class MismatchingState(InvalidAuthResponse):
    """Raised on mismatching `state` value.

    This happens when the Authorization Endpoints returns a 'state' parameter that doesn't match the value passed in the
    Authorization Request.

    """

    def __init__(self, received: str, expected: str, request: AuthorizationRequest, response: str) -> None:
        super().__init__(f"mismatching `state` (received '{received}', expected '{expected}')", request, response)
        self.received = received
        self.expected = expected

MismatchingIssuer

Bases: InvalidAuthResponse

Raised on mismatching iss value.

This happens when the Authorization Endpoints returns an 'iss' that doesn't match the expected value.

Source code in requests_oauth2client/exceptions.py
class MismatchingIssuer(InvalidAuthResponse):
    """Raised on mismatching `iss` value.

    This happens when the Authorization Endpoints returns an 'iss' that doesn't match the expected value.

    """

    def __init__(self, received: str, expected: str, request: AuthorizationRequest, response: str) -> None:
        super().__init__(f"mismatching `iss` (received '{received}', expected '{expected}')", request, response)
        self.received = received
        self.expected = expected

BackChannelAuthenticationError

Bases: EndpointError

Base class for errors returned by the BackChannel Authentication endpoint.

Source code in requests_oauth2client/exceptions.py
class BackChannelAuthenticationError(EndpointError):
    """Base class for errors returned by the BackChannel Authentication endpoint."""

InvalidBackChannelAuthenticationResponse

Bases: OAuth2Error

Raised when the BackChannel Authentication endpoint returns a non-standard response.

Source code in requests_oauth2client/exceptions.py
class InvalidBackChannelAuthenticationResponse(OAuth2Error):
    """Raised when the BackChannel Authentication endpoint returns a non-standard response."""

InvalidPushedAuthorizationResponse

Bases: OAuth2Error

Raised when the Pushed Authorization Endpoint returns an error.

Source code in requests_oauth2client/exceptions.py
class InvalidPushedAuthorizationResponse(OAuth2Error):
    """Raised when the Pushed Authorization Endpoint returns an error."""

flask

This module contains helper classes for the Flask Framework.

See Flask framework.

FlaskOAuth2ClientCredentialsAuth

Bases: FlaskSessionAuthMixin, OAuth2ClientCredentialsAuth

A requests Auth handler for CC grant that stores its token in Flask session.

It will automatically get Access Tokens from an OAuth 2.x AS with the Client Credentials grant (and can get a new one once the first one is expired), and stores the retrieved token, serialized in Flask session, so that each user has a different access token.

Source code in requests_oauth2client/flask/auth.py
class FlaskOAuth2ClientCredentialsAuth(FlaskSessionAuthMixin, OAuth2ClientCredentialsAuth):  # type: ignore[misc]
    """A `requests` Auth handler for CC grant that stores its token in Flask session.

    It will automatically get Access Tokens from an OAuth 2.x AS with the Client Credentials grant
    (and can get a new one once the first one is expired), and stores the retrieved token,
    serialized in Flask `session`, so that each user has a different access token.

    """

auth

Helper classes for the Flask framework.

FlaskSessionAuthMixin

A Mixin for auth handlers to store their tokens in Flask session.

Storing tokens in Flask session does ensure that each user of a Flask application has a different access token, and that tokens used for backend API access will be persisted between multiple requests to the front-end Flask app.

Parameters:

Name Type Description Default
session_key str

the key that will be used to store the access token in session.

required
serializer BearerTokenSerializer | None

the serializer that will be used to store the access token in session.

None
Source code in requests_oauth2client/flask/auth.py
class FlaskSessionAuthMixin:
    """A Mixin for auth handlers to store their tokens in Flask session.

    Storing tokens in Flask session does ensure that each user of a Flask application has a
    different access token, and that tokens used for backend API access will be persisted between
    multiple requests to the front-end Flask app.

    Args:
        session_key: the key that will be used to store the access token in session.
        serializer: the serializer that will be used to store the access token in session.

    """

    def __init__(
        self,
        session_key: str,
        serializer: BearerTokenSerializer | None = None,
        *args: Any,
        **token_kwargs: Any,
    ) -> None:
        self.serializer = serializer or BearerTokenSerializer()
        self.session_key = session_key
        super().__init__(*args, **token_kwargs)

    @property
    def token(self) -> BearerToken | None:
        """Return the Access Token stored in session.

        Returns:
            The current `BearerToken` for this session, if any.

        """
        serialized_token = session.get(self.session_key)
        if serialized_token is None:
            return None
        return self.serializer.loads(serialized_token)

    @token.setter
    def token(self, token: BearerToken | str | None) -> None:
        """Store an Access Token in session.

        Args:
            token: the token to store

        """
        if isinstance(token, str):
            token = BearerToken(token)  # pragma: no cover
        if token:
            serialized_token = self.serializer.dumps(token)
            session[self.session_key] = serialized_token
        elif session and self.session_key in session:
            session.pop(self.session_key, None)
token property writable

Return the Access Token stored in session.

Returns:

Type Description
BearerToken | None

The current BearerToken for this session, if any.

FlaskOAuth2ClientCredentialsAuth

Bases: FlaskSessionAuthMixin, OAuth2ClientCredentialsAuth

A requests Auth handler for CC grant that stores its token in Flask session.

It will automatically get Access Tokens from an OAuth 2.x AS with the Client Credentials grant (and can get a new one once the first one is expired), and stores the retrieved token, serialized in Flask session, so that each user has a different access token.

Source code in requests_oauth2client/flask/auth.py
class FlaskOAuth2ClientCredentialsAuth(FlaskSessionAuthMixin, OAuth2ClientCredentialsAuth):  # type: ignore[misc]
    """A `requests` Auth handler for CC grant that stores its token in Flask session.

    It will automatically get Access Tokens from an OAuth 2.x AS with the Client Credentials grant
    (and can get a new one once the first one is expired), and stores the retrieved token,
    serialized in Flask `session`, so that each user has a different access token.

    """

pooling

Contains base classes for pooling jobs.

BaseTokenEndpointPoolingJob

Base class for Token Endpoint pooling jobs.

This is used for decoupled flows like CIBA or Device Authorization.

This class must be subclassed to implement actual BackChannel flows. This needs an OAuth2Client that will be used to pool the token endpoint. The initial pooling interval is configurable.

Source code in requests_oauth2client/pooling.py
@define
class BaseTokenEndpointPoolingJob:
    """Base class for Token Endpoint pooling jobs.

    This is used for decoupled flows like CIBA or Device Authorization.

    This class must be subclassed to implement actual BackChannel flows. This needs an
    [OAuth2Client][requests_oauth2client.client.OAuth2Client] that will be used to pool the token
    endpoint. The initial pooling `interval` is configurable.

    """

    client: OAuth2Client = field(on_setattr=setters.frozen)
    requests_kwargs: dict[str, Any] = field(on_setattr=setters.frozen)
    token_kwargs: dict[str, Any] = field(on_setattr=setters.frozen)
    slow_down_interval: int = field(on_setattr=setters.frozen)
    interval: int

    def __call__(self) -> BearerToken | None:
        """Wrap the actual Token Endpoint call with a pooling interval.

        Everytime this method is called, it will wait for the entire duration of the pooling
        interval before calling
        [token_request()][requests_oauth2client.pooling.TokenEndpointPoolingJob.token_request]. So
        you can call it immediately after initiating the BackChannel flow, and it will wait before
        initiating the first call.

        This implements the logic to handle
        [AuthorizationPending][requests_oauth2client.exceptions.AuthorizationPending] or
        [SlowDown][requests_oauth2client.exceptions.SlowDown] requests by the AS.

        Returns:
            a `BearerToken` if the AS returns one, or `None` if the Authorization is still pending.

        """
        self.sleep()
        try:
            return self.token_request()
        except SlowDown:
            self.slow_down()
        except AuthorizationPending:
            self.authorization_pending()
        return None

    def sleep(self) -> None:
        """Implement the wait between two requests of the token endpoint.

        By default, relies on time.sleep().

        """
        time.sleep(self.interval)

    def slow_down(self) -> None:
        """Implement the behavior when receiving a 'slow_down' response from the AS.

        By default, it increases the pooling interval by the slow down interval.

        """
        self.interval += self.slow_down_interval

    def authorization_pending(self) -> None:
        """Implement the behavior when receiving an 'authorization_pending' response from the AS.

        By default, it does nothing.

        """

    def token_request(self) -> BearerToken:
        """Abstract method for the token endpoint call.

        Subclasses must implement this. This method must raise
        [AuthorizationPending][requests_oauth2client.exceptions.AuthorizationPending] to retry after
        the pooling interval, or [SlowDown][requests_oauth2client.exceptions.SlowDown] to increase
        the pooling interval by `slow_down_interval` seconds.

        Returns:
            a [BearerToken][requests_oauth2client.tokens.BearerToken]

        """
        raise NotImplementedError
sleep()

Implement the wait between two requests of the token endpoint.

By default, relies on time.sleep().

Source code in requests_oauth2client/pooling.py
def sleep(self) -> None:
    """Implement the wait between two requests of the token endpoint.

    By default, relies on time.sleep().

    """
    time.sleep(self.interval)
slow_down()

Implement the behavior when receiving a 'slow_down' response from the AS.

By default, it increases the pooling interval by the slow down interval.

Source code in requests_oauth2client/pooling.py
def slow_down(self) -> None:
    """Implement the behavior when receiving a 'slow_down' response from the AS.

    By default, it increases the pooling interval by the slow down interval.

    """
    self.interval += self.slow_down_interval
authorization_pending()

Implement the behavior when receiving an 'authorization_pending' response from the AS.

By default, it does nothing.

Source code in requests_oauth2client/pooling.py
def authorization_pending(self) -> None:
    """Implement the behavior when receiving an 'authorization_pending' response from the AS.

    By default, it does nothing.

    """
token_request()

Abstract method for the token endpoint call.

Subclasses must implement this. This method must raise AuthorizationPending to retry after the pooling interval, or SlowDown to increase the pooling interval by slow_down_interval seconds.

Returns:

Type Description
BearerToken
Source code in requests_oauth2client/pooling.py
def token_request(self) -> BearerToken:
    """Abstract method for the token endpoint call.

    Subclasses must implement this. This method must raise
    [AuthorizationPending][requests_oauth2client.exceptions.AuthorizationPending] to retry after
    the pooling interval, or [SlowDown][requests_oauth2client.exceptions.SlowDown] to increase
    the pooling interval by `slow_down_interval` seconds.

    Returns:
        a [BearerToken][requests_oauth2client.tokens.BearerToken]

    """
    raise NotImplementedError

tokens

This module contains classes that represent Tokens used in OAuth2.0 / OIDC.

TokenType

Bases: str, Enum

An enum of standardised token_type values.

Source code in requests_oauth2client/tokens.py
class TokenType(str, Enum):
    """An enum of standardised `token_type` values."""

    ACCESS_TOKEN = "access_token"
    REFRESH_TOKEN = "refresh_token"
    ID_TOKEN = "id_token"

AccessTokenTypes

Bases: str, Enum

An enum of standardised access_token types.

Source code in requests_oauth2client/tokens.py
class AccessTokenTypes(str, Enum):
    """An enum of standardised `access_token` types."""

    BEARER = "Bearer"
    DPOP = "DPoP"

UnsupportedTokenType

Bases: ValueError

Raised when an unsupported token_type is provided.

Source code in requests_oauth2client/tokens.py
class UnsupportedTokenType(ValueError):
    """Raised when an unsupported token_type is provided."""

    def __init__(self, token_type: str) -> None:
        super().__init__(f"Unsupported token_type: {token_type}")
        self.token_type = token_type

IdToken

Bases: SignedJwt

Represent an ID Token.

An ID Token is actually a Signed JWT. If the ID Token is encrypted, it must be decoded beforehand.

Source code in requests_oauth2client/tokens.py
class IdToken(jwskate.SignedJwt):
    """Represent an ID Token.

    An ID Token is actually a Signed JWT. If the ID Token is encrypted, it must be decoded beforehand.

    """

    @property
    def authorized_party(self) -> str | None:
        """The Authorized Party (azp)."""
        azp = self.claims.get("azp")
        if azp is None or isinstance(azp, str):
            return azp
        msg = "`azp` attribute must be a string."
        raise AttributeError(msg)

    @property
    def auth_datetime(self) -> datetime | None:
        """The last user authentication time (auth_time)."""
        auth_time = self.claims.get("auth_time")
        if auth_time is None:
            return None
        if isinstance(auth_time, int) and auth_time > 0:
            return self.timestamp_to_datetime(auth_time)
        msg = "`auth_time` must be a positive integer"
        raise AttributeError(msg)

    @classmethod
    def hash_method(cls, key: jwskate.Jwk, alg: str | None = None) -> Callable[[str], str]:
        """Returns a callable that generates valid OIDC hashes, such as `at_hash`, `c_hash`, etc.

        Args:
            key: the ID token signature verification public key
            alg: the ID token signature algorithm

        Returns:
            a callable that takes a string as input and produces a valid hash as a str output

        """
        alg_class = jwskate.select_alg_class(key.SIGNATURE_ALGORITHMS, jwk_alg=key.alg, alg=alg)
        if alg_class == jwskate.EdDsa:
            if key.crv == "Ed25519":

                def hash_method(token: str) -> str:
                    return BinaPy(token).to("sha512")[:32].to("b64u").decode()

            elif key.crv == "Ed448":

                def hash_method(token: str) -> str:
                    return BinaPy(token).to("shake256", 456).to("b64u").decode()

        else:
            hash_alg = alg_class.hashing_alg.name
            hash_size = alg_class.hashing_alg.digest_size

            def hash_method(token: str) -> str:
                return BinaPy(token).to(hash_alg)[: hash_size // 2].to("b64u").decode()

        return hash_method
authorized_party property

The Authorized Party (azp).

auth_datetime property

The last user authentication time (auth_time).

hash_method(key, alg=None) classmethod

Returns a callable that generates valid OIDC hashes, such as at_hash, c_hash, etc.

Parameters:

Name Type Description Default
key Jwk

the ID token signature verification public key

required
alg str | None

the ID token signature algorithm

None

Returns:

Type Description
Callable[[str], str]

a callable that takes a string as input and produces a valid hash as a str output

Source code in requests_oauth2client/tokens.py
@classmethod
def hash_method(cls, key: jwskate.Jwk, alg: str | None = None) -> Callable[[str], str]:
    """Returns a callable that generates valid OIDC hashes, such as `at_hash`, `c_hash`, etc.

    Args:
        key: the ID token signature verification public key
        alg: the ID token signature algorithm

    Returns:
        a callable that takes a string as input and produces a valid hash as a str output

    """
    alg_class = jwskate.select_alg_class(key.SIGNATURE_ALGORITHMS, jwk_alg=key.alg, alg=alg)
    if alg_class == jwskate.EdDsa:
        if key.crv == "Ed25519":

            def hash_method(token: str) -> str:
                return BinaPy(token).to("sha512")[:32].to("b64u").decode()

        elif key.crv == "Ed448":

            def hash_method(token: str) -> str:
                return BinaPy(token).to("shake256", 456).to("b64u").decode()

    else:
        hash_alg = alg_class.hashing_alg.name
        hash_size = alg_class.hashing_alg.digest_size

        def hash_method(token: str) -> str:
            return BinaPy(token).to(hash_alg)[: hash_size // 2].to("b64u").decode()

    return hash_method

InvalidIdToken

Bases: ValueError

Raised when trying to validate an invalid ID Token value.

Source code in requests_oauth2client/tokens.py
class InvalidIdToken(ValueError):
    """Raised when trying to validate an invalid ID Token value."""

    def __init__(
        self,
        message: str,
        id_token: IdToken | jwskate.JweCompact | object | None = None,
        token_resp: TokenResponse | None = None,
    ) -> None:
        super().__init__(f"Invalid ID Token: {message}")
        self.id_token = id_token
        self.token_resp = token_resp

MissingIdToken

Bases: InvalidIdToken

Raised when the Authorization Endpoint does not return a mandatory ID Token.

This happens when the Authorization Endpoint does not return an error, but does not return an ID Token either.

Source code in requests_oauth2client/tokens.py
class MissingIdToken(InvalidIdToken):
    """Raised when the Authorization Endpoint does not return a mandatory ID Token.

    This happens when the Authorization Endpoint does not return an error, but does not return an ID Token either.

    """

    def __init__(self, token: TokenResponse) -> None:
        super().__init__("An expected `id_token` is missing in the response.", token, None)

MismatchingIdTokenIssuer

Bases: InvalidIdToken

Raised on mismatching iss value in an ID Token.

This happens when the expected issuer value is different from the iss value in an obtained ID Token.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenIssuer(InvalidIdToken):
    """Raised on mismatching `iss` value in an ID Token.

    This happens when the expected `issuer` value is different from the `iss` value in an obtained ID Token.

    """

    def __init__(self, iss: str | None, expected: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(f"`iss` from token '{iss}' does not match expected value '{expected}'", id_token, token)
        self.received = iss
        self.expected = expected

MismatchingIdTokenNonce

Bases: InvalidIdToken

Raised on mismatching nonce value in an ID Token.

This happens when the authorization request includes a nonce but the returned ID Token include a different value.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenNonce(InvalidIdToken):
    """Raised on mismatching `nonce` value in an ID Token.

    This happens when the authorization request includes a `nonce` but the returned ID Token include
    a different value.

    """

    def __init__(self, nonce: str, expected: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(f"nonce from token '{nonce}' does not match expected value '{expected}'", id_token, token)
        self.received = nonce
        self.expected = expected

MismatchingIdTokenAcr

Bases: InvalidIdToken

Raised when the returned ID Token doesn't contain one of the requested ACR Values.

This happens when the authorization request includes an acr_values parameter but the returned ID Token includes a different value.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenAcr(InvalidIdToken):
    """Raised when the returned ID Token doesn't contain one of the requested ACR Values.

    This happens when the authorization request includes an `acr_values` parameter but the returned
    ID Token includes a different value.

    """

    def __init__(self, acr: str, expected: Sequence[str], token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(f"token contains acr '{acr}' while client expects one of '{expected}'", id_token, token)
        self.received = acr
        self.expected = expected

MismatchingIdTokenAudience

Bases: InvalidIdToken

Raised when the ID Token audience does not include the requesting Client ID.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenAudience(InvalidIdToken):
    """Raised when the ID Token audience does not include the requesting Client ID."""

    def __init__(self, audiences: Sequence[str], client_id: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(
            f"token audience (`aud`) '{audiences}' does not match client_id '{client_id}'",
            id_token,
            token,
        )
        self.received = audiences
        self.expected = client_id

MismatchingIdTokenAzp

Bases: InvalidIdToken

Raised when the ID Token Authorized Presenter (azp) claim is not the Client ID.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenAzp(InvalidIdToken):
    """Raised when the ID Token Authorized Presenter (azp) claim is not the Client ID."""

    def __init__(self, azp: str, client_id: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(
            f"token Authorized Presenter (`azp`) claim '{azp}' does not match client_id '{client_id}'", id_token, token
        )
        self.received = azp
        self.expected = client_id

MismatchingIdTokenAlg

Bases: InvalidIdToken

Raised when the returned ID Token is signed with an unexpected alg.

Source code in requests_oauth2client/tokens.py
class MismatchingIdTokenAlg(InvalidIdToken):
    """Raised when the returned ID Token is signed with an unexpected alg."""

    def __init__(self, token_alg: str, client_alg: str, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__(f"token is signed with alg {token_alg}, client expects {client_alg}", id_token, token)
        self.received = token_alg
        self.expected = client_alg

ExpiredIdToken

Bases: InvalidIdToken

Raised when the returned ID Token is expired.

Source code in requests_oauth2client/tokens.py
class ExpiredIdToken(InvalidIdToken):
    """Raised when the returned ID Token is expired."""

    def __init__(self, token: TokenResponse, id_token: IdToken) -> None:
        super().__init__("token is expired", id_token, token)
        self.received = id_token.expires_at
        self.expected = datetime.now(tz=timezone.utc)

UnsupportedIdTokenAlg

Bases: InvalidIdToken

Raised when the return ID Token is signed with an unsupported alg.

Source code in requests_oauth2client/tokens.py
class UnsupportedIdTokenAlg(InvalidIdToken):
    """Raised when the return ID Token is signed with an unsupported alg."""

    def __init__(self, token: TokenResponse, id_token: IdToken, alg: str) -> None:
        super().__init__(f"token is signed with an unsupported alg {alg}", id_token, token)
        self.alg = alg

TokenResponse

Base class for Token Endpoint Responses.

Source code in requests_oauth2client/tokens.py
class TokenResponse:
    """Base class for Token Endpoint Responses."""

    TOKEN_TYPE: ClassVar[str]

ExpiredAccessToken

Bases: RuntimeError

Raised when an expired access token is used.

Source code in requests_oauth2client/tokens.py
class ExpiredAccessToken(RuntimeError):
    """Raised when an expired access token is used."""

BearerToken

Bases: TokenResponse, AuthBase

Represents a Bearer Token as returned by a Token Endpoint.

This is a wrapper around a Bearer Token and associated parameters, such as expiration date and refresh token, as returned by an OAuth 2.x or OIDC 1.0 Token Endpoint.

All parameters are as returned by a Token Endpoint. The token expiration date can be passed as datetime in the expires_at parameter, or an expires_in parameter, as number of seconds in the future, can be passed instead.

Parameters:

Name Type Description Default
access_token str

an access_token, as returned by the AS.

required
expires_at datetime | None

an expiration date. This method also accepts an expires_in hint as returned by the AS, if any.

None
scope str | None

a scope, as returned by the AS, if any.

None
refresh_token str | None

a refresh_token, as returned by the AS, if any.

None
token_type str

a token_type, as returned by the AS.

TOKEN_TYPE
id_token str | bytes | IdToken | JweCompact | None

an id_token, as returned by the AS, if any.

None
**kwargs Any

additional parameters as returned by the AS, if any.

{}
Source code in requests_oauth2client/tokens.py
@frozen(init=False)
class BearerToken(TokenResponse, requests.auth.AuthBase):
    """Represents a Bearer Token as returned by a Token Endpoint.

    This is a wrapper around a Bearer Token and associated parameters, such as expiration date and
    refresh token, as returned by an OAuth 2.x or OIDC 1.0 Token Endpoint.

    All parameters are as returned by a Token Endpoint. The token expiration date can be passed as
    datetime in the `expires_at` parameter, or an `expires_in` parameter, as number of seconds in
    the future, can be passed instead.

    Args:
        access_token: an `access_token`, as returned by the AS.
        expires_at: an expiration date. This method also accepts an `expires_in` hint as
            returned by the AS, if any.
        scope: a `scope`, as returned by the AS, if any.
        refresh_token: a `refresh_token`, as returned by the AS, if any.
        token_type: a `token_type`, as returned by the AS.
        id_token: an `id_token`, as returned by the AS, if any.
        **kwargs: additional parameters as returned by the AS, if any.

    """

    TOKEN_TYPE: ClassVar[str] = AccessTokenTypes.BEARER.value
    AUTHORIZATION_HEADER: ClassVar[str] = "Authorization"
    AUTHORIZATION_SCHEME: ClassVar[str] = AccessTokenTypes.BEARER.value

    access_token: str
    expires_at: datetime | None
    scope: str | None
    refresh_token: str | None
    token_type: str
    id_token: IdToken | jwskate.JweCompact | None
    kwargs: dict[str, Any]

    @accepts_expires_in
    def __init__(
        self,
        access_token: str,
        *,
        expires_at: datetime | None = None,
        scope: str | None = None,
        refresh_token: str | None = None,
        token_type: str = TOKEN_TYPE,
        id_token: str | bytes | IdToken | jwskate.JweCompact | None = None,
        **kwargs: Any,
    ) -> None:
        if token_type.title() != self.TOKEN_TYPE.title():
            raise UnsupportedTokenType(token_type)

        id_token = id_token_converter(id_token)

        self.__attrs_init__(
            access_token=access_token,
            expires_at=expires_at,
            scope=scope,
            refresh_token=refresh_token,
            token_type=token_type,
            id_token=id_token,
            kwargs=kwargs,
        )

    def is_expired(self, leeway: int = 0) -> bool | None:
        """Check if the access token is expired.

        Args:
            leeway: If the token expires in the next given number of seconds,
                then consider it expired already.

        Returns:
            One of:

            - `True` if the access token is expired
            - `False` if it is still valid
            - `None` if there is no expires_in hint.

        """
        if self.expires_at:
            return datetime.now(tz=timezone.utc) + timedelta(seconds=leeway) > self.expires_at
        return None

    def authorization_header(self) -> str:
        """Return the appropriate Authorization Header value for this token.

        The value is formatted correctly according to RFC6750.

        Returns:
            the value to use in an HTTP Authorization Header

        """
        return f"{self.AUTHORIZATION_SCHEME} {self.access_token}"

    def validate_id_token(  # noqa: PLR0915, C901
        self, client: OAuth2Client, azr: AuthorizationResponse, exp_leeway: int = 0, auth_time_leeway: int = 10
    ) -> Self:
        """Validate the ID Token, and return a new instance with the decrypted ID Token.

        If the ID Token was not encrypted, the returned instance will contain the same ID Token.

        This will validate the id_token as described in [OIDC 1.0
        $3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).

        Args:
            client: the `OAuth2Client` that was used to obtain this token
            azr: the `AuthorizationResponse`, as obtained by a call to `AuthorizationRequest.validate()`
            exp_leeway: a leeway, in seconds, applied to the ID Token expiration date
            auth_time_leeway: a leeway, in seconds, applied to the `auth_time` validation

        Raises:
            MissingIdToken: if the ID Token is missing
            InvalidIdToken: this is a base exception class, which is raised:

                - if the ID Token is not a JWT
                - or is encrypted while a clear-text token is expected
                - or is clear-text while an encrypted token is expected
                - if token is encrypted but client does not have a decryption key
                - if the token does not contain an `alg` header
            MismatchingIdTokenAlg: if the `alg` header from the ID Token does not match
                the expected `client.id_token_signed_response_alg`.
            MismatchingIdTokenIssuer: if the `iss` claim from the ID Token does not match
                the expected `azr.issuer`.
            MismatchingIdTokenAcr: if the `acr` claim from the ID Token does not match
                on of the expected `azr.acr_values`.
            MismatchingIdTokenAudience: if the `aud` claim from the ID Token does not match
                the expected `client.client_id`.
            MismatchingIdTokenAzp: if the `azp` claim from the ID Token does not match
                the expected `client.client_id`.
            MismatchingIdTokenNonce: if the `nonce` claim from the ID Token does not match
                the expected `azr.nonce`.
            ExpiredIdToken: if the ID Token is expired at the time of the check.
            UnsupportedIdTokenAlg: if the signature alg for the ID Token is not supported.

        """
        if not self.id_token:
            raise MissingIdToken(self)

        raw_id_token = self.id_token

        if isinstance(raw_id_token, jwskate.JweCompact) and client.id_token_encrypted_response_alg is None:
            msg = "token is encrypted while it should be clear-text"
            raise InvalidIdToken(msg, raw_id_token, self)
        if isinstance(raw_id_token, IdToken) and client.id_token_encrypted_response_alg is not None:
            msg = "token is clear-text while it should be encrypted"
            raise InvalidIdToken(msg, raw_id_token, self)

        if isinstance(raw_id_token, jwskate.JweCompact):
            enc_jwk = client.id_token_decryption_key
            if enc_jwk is None:
                msg = "token is encrypted but client does not have a decryption key"
                raise InvalidIdToken(msg, raw_id_token, self)
            nested_id_token = raw_id_token.decrypt(enc_jwk)
            id_token = IdToken(nested_id_token)
        else:
            id_token = raw_id_token

        id_token_alg = id_token.get_header("alg")
        if id_token_alg is None:
            id_token_alg = client.id_token_signed_response_alg
        if id_token_alg is None:
            msg = """
token does not contain an `alg` parameter to specify the signature algorithm,
and no algorithm has been configured for the client (using param `id_token_signed_response_alg`).
"""
            raise InvalidIdToken(msg, id_token, self)
        if client.id_token_signed_response_alg is not None and id_token_alg != client.id_token_signed_response_alg:
            raise MismatchingIdTokenAlg(id_token.alg, client.id_token_signed_response_alg, self, id_token)

        verification_jwk: jwskate.Jwk

        if id_token_alg in jwskate.SignatureAlgs.ALL_SYMMETRIC:
            if not client.client_secret:
                msg = "token is symmetrically signed but this client does not have a Client Secret."
                raise InvalidIdToken(msg, id_token, self)
            verification_jwk = jwskate.SymmetricJwk.from_bytes(client.client_secret, alg=id_token_alg)
            id_token.verify_signature(verification_jwk, alg=id_token_alg)
        elif id_token_alg in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
            if not client.authorization_server_jwks:
                msg = "token is asymmetrically signed but the Authorization Server JWKS is not available."
                raise InvalidIdToken(msg, id_token, self)

            if id_token.get_header("kid") is None:
                msg = """
token does not contain a Key ID (kid) to specify the asymmetric key
to use for signature verification."""
                raise InvalidIdToken(msg, id_token, self)
            try:
                verification_jwk = client.authorization_server_jwks.get_jwk_by_kid(id_token.kid)
            except KeyError:
                msg = f"""\
token is asymmetrically signed but there is no key
with kid='{id_token.kid}' in the Authorization Server JWKS."""
                raise InvalidIdToken(msg, id_token, self) from None

            if id_token_alg not in verification_jwk.supported_signing_algorithms():
                msg = "token is asymmetrically signed but its algorithm is not supported by the verification key."
                raise InvalidIdToken(msg, id_token, self)
        else:
            raise UnsupportedIdTokenAlg(self, id_token, id_token_alg)

        id_token.verify(verification_jwk, alg=id_token_alg)

        if azr.issuer and id_token.issuer != azr.issuer:
            raise MismatchingIdTokenIssuer(id_token.issuer, azr.issuer, self, id_token)

        if id_token.audiences and client.client_id not in id_token.audiences:
            raise MismatchingIdTokenAudience(id_token.audiences, client.client_id, self, id_token)

        if id_token.authorized_party is not None and id_token.authorized_party != client.client_id:
            raise MismatchingIdTokenAzp(id_token.azp, client.client_id, self, id_token)

        if id_token.is_expired(leeway=exp_leeway):
            raise ExpiredIdToken(self, id_token)

        if azr.nonce and id_token.nonce != azr.nonce:
            raise MismatchingIdTokenNonce(id_token.nonce, azr.nonce, self, id_token)

        if azr.acr_values and id_token.acr not in azr.acr_values:
            raise MismatchingIdTokenAcr(id_token.acr, azr.acr_values, self, id_token)

        hash_function = IdToken.hash_method(verification_jwk, id_token_alg)

        at_hash = id_token.get_claim("at_hash")
        if at_hash is not None:
            expected_at_hash = hash_function(self.access_token)
            if expected_at_hash != at_hash:
                msg = f"mismatching 'at_hash' value (expected '{expected_at_hash}', got '{at_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        c_hash = id_token.get_claim("c_hash")
        if c_hash is not None:
            expected_c_hash = hash_function(azr.code)
            if expected_c_hash != c_hash:
                msg = f"mismatching 'c_hash' value (expected '{expected_c_hash}', got '{c_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        s_hash = id_token.get_claim("s_hash")
        if s_hash is not None:
            if azr.state is None:
                msg = "token has a 's_hash' claim but no state was included in the request."
                raise InvalidIdToken(msg, id_token, self)
            expected_s_hash = hash_function(azr.state)
            if expected_s_hash != s_hash:
                msg = f"mismatching 's_hash' value (expected '{expected_s_hash}', got '{s_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        if azr.max_age is not None:
            auth_time = id_token.auth_datetime
            if auth_time is None:
                msg = """
a `max_age` parameter was included in the authorization request,
but the ID Token does not contain an `auth_time` claim.
"""
                raise InvalidIdToken(msg, id_token, self) from None
            auth_age = datetime.now(tz=timezone.utc) - auth_time
            if auth_age.total_seconds() > azr.max_age + auth_time_leeway:
                msg = f"""
user authentication happened too far in the past.
The `auth_time` parameter from the ID Token indicate that
the last Authentication Time was at {auth_time} ({auth_age.total_seconds()} sec ago),
but the authorization request `max_age` parameter specified that it must
be a maximum of {azr.max_age} sec ago.
"""
                raise InvalidIdToken(msg, id_token, self)

        return self.__class__(
            access_token=self.access_token,
            expires_at=self.expires_at,
            scope=self.scope,
            refresh_token=self.refresh_token,
            token_type=self.token_type,
            id_token=id_token,
            **self.kwargs,
        )

    def __str__(self) -> str:
        """Return the access token value, as a string.

        Returns:
            the access token string

        """
        return self.access_token

    def as_dict(self) -> dict[str, Any]:
        """Return a dict of parameters.

        That is suitable for serialization or to init another BearerToken.

        """
        d = asdict(self)
        d.pop("expires_at")
        d["expires_in"] = self.expires_in
        d.update(**d.pop("kwargs", {}))
        return {key: val for key, val in d.items() if val is not None}

    @property
    def expires_in(self) -> int | None:
        """Number of seconds until expiration."""
        if self.expires_at:
            return ceil((self.expires_at - datetime.now(tz=timezone.utc)).total_seconds())
        return None

    def __getattr__(self, key: str) -> Any:
        """Return custom attributes from this BearerToken.

        Args:
            key: a key

        Returns:
            the associated value in this token response

        Raises:
            AttributeError: if the attribute is not found in this response.

        """
        return self.kwargs.get(key) or super().__getattribute__(key)

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Implement the usage of Bearer Tokens in requests.

        This will add a properly formatted `Authorization: Bearer <token>` header in the request.

        If the configured token is an instance of BearerToken with an expires_at attribute, raises
        [ExpiredAccessToken][requests_oauth2client.exceptions.ExpiredAccessToken] once the access
        token is expired.

        Args:
            request: the request

        Returns:
            the same request with an Access Token added in `Authorization` Header

        Raises:
            ExpiredAccessToken: if the token is expired

        """
        if self.access_token is None:
            return request  # pragma: no cover
        if self.is_expired():
            raise ExpiredAccessToken(self)
        request.headers[self.AUTHORIZATION_HEADER] = self.authorization_header()
        return request

    @cached_property
    def access_token_jwt(self) -> jwskate.SignedJwt:
        """If the access token is a JWT, return it as an instance of `jwskate.SignedJwt`.

        This method is just a helper for AS testing purposes. Note that, as an OAuth 2.0 Client, you should never have
        to decode or analyze an access token, since it is simply an abstract string value. It is not even mandatory that
        Access Tokens are JWTs, just an implementation choice. Only Resource Servers (APIs) should check for the
        contents of Access Tokens they receive.

        """
        return jwskate.SignedJwt(self.access_token)
expires_in property

Number of seconds until expiration.

access_token_jwt cached property

If the access token is a JWT, return it as an instance of jwskate.SignedJwt.

This method is just a helper for AS testing purposes. Note that, as an OAuth 2.0 Client, you should never have to decode or analyze an access token, since it is simply an abstract string value. It is not even mandatory that Access Tokens are JWTs, just an implementation choice. Only Resource Servers (APIs) should check for the contents of Access Tokens they receive.

is_expired(leeway=0)

Check if the access token is expired.

Parameters:

Name Type Description Default
leeway int

If the token expires in the next given number of seconds, then consider it expired already.

0

Returns:

Type Description
bool | None

One of:

bool | None
  • True if the access token is expired
bool | None
  • False if it is still valid
bool | None
  • None if there is no expires_in hint.
Source code in requests_oauth2client/tokens.py
def is_expired(self, leeway: int = 0) -> bool | None:
    """Check if the access token is expired.

    Args:
        leeway: If the token expires in the next given number of seconds,
            then consider it expired already.

    Returns:
        One of:

        - `True` if the access token is expired
        - `False` if it is still valid
        - `None` if there is no expires_in hint.

    """
    if self.expires_at:
        return datetime.now(tz=timezone.utc) + timedelta(seconds=leeway) > self.expires_at
    return None
authorization_header()

Return the appropriate Authorization Header value for this token.

The value is formatted correctly according to RFC6750.

Returns:

Type Description
str

the value to use in an HTTP Authorization Header

Source code in requests_oauth2client/tokens.py
def authorization_header(self) -> str:
    """Return the appropriate Authorization Header value for this token.

    The value is formatted correctly according to RFC6750.

    Returns:
        the value to use in an HTTP Authorization Header

    """
    return f"{self.AUTHORIZATION_SCHEME} {self.access_token}"
validate_id_token(client, azr, exp_leeway=0, auth_time_leeway=10)

Validate the ID Token, and return a new instance with the decrypted ID Token.

If the ID Token was not encrypted, the returned instance will contain the same ID Token.

This will validate the id_token as described in OIDC 1.0 $3.1.3.7.

Parameters:

Name Type Description Default
client OAuth2Client

the OAuth2Client that was used to obtain this token

required
azr AuthorizationResponse

the AuthorizationResponse, as obtained by a call to AuthorizationRequest.validate()

required
exp_leeway int

a leeway, in seconds, applied to the ID Token expiration date

0
auth_time_leeway int

a leeway, in seconds, applied to the auth_time validation

10

Raises:

Type Description
MissingIdToken

if the ID Token is missing

InvalidIdToken

this is a base exception class, which is raised:

  • if the ID Token is not a JWT
  • or is encrypted while a clear-text token is expected
  • or is clear-text while an encrypted token is expected
  • if token is encrypted but client does not have a decryption key
  • if the token does not contain an alg header
MismatchingIdTokenAlg

if the alg header from the ID Token does not match the expected client.id_token_signed_response_alg.

MismatchingIdTokenIssuer

if the iss claim from the ID Token does not match the expected azr.issuer.

MismatchingIdTokenAcr

if the acr claim from the ID Token does not match on of the expected azr.acr_values.

MismatchingIdTokenAudience

if the aud claim from the ID Token does not match the expected client.client_id.

MismatchingIdTokenAzp

if the azp claim from the ID Token does not match the expected client.client_id.

MismatchingIdTokenNonce

if the nonce claim from the ID Token does not match the expected azr.nonce.

ExpiredIdToken

if the ID Token is expired at the time of the check.

UnsupportedIdTokenAlg

if the signature alg for the ID Token is not supported.

Source code in requests_oauth2client/tokens.py
    def validate_id_token(  # noqa: PLR0915, C901
        self, client: OAuth2Client, azr: AuthorizationResponse, exp_leeway: int = 0, auth_time_leeway: int = 10
    ) -> Self:
        """Validate the ID Token, and return a new instance with the decrypted ID Token.

        If the ID Token was not encrypted, the returned instance will contain the same ID Token.

        This will validate the id_token as described in [OIDC 1.0
        $3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).

        Args:
            client: the `OAuth2Client` that was used to obtain this token
            azr: the `AuthorizationResponse`, as obtained by a call to `AuthorizationRequest.validate()`
            exp_leeway: a leeway, in seconds, applied to the ID Token expiration date
            auth_time_leeway: a leeway, in seconds, applied to the `auth_time` validation

        Raises:
            MissingIdToken: if the ID Token is missing
            InvalidIdToken: this is a base exception class, which is raised:

                - if the ID Token is not a JWT
                - or is encrypted while a clear-text token is expected
                - or is clear-text while an encrypted token is expected
                - if token is encrypted but client does not have a decryption key
                - if the token does not contain an `alg` header
            MismatchingIdTokenAlg: if the `alg` header from the ID Token does not match
                the expected `client.id_token_signed_response_alg`.
            MismatchingIdTokenIssuer: if the `iss` claim from the ID Token does not match
                the expected `azr.issuer`.
            MismatchingIdTokenAcr: if the `acr` claim from the ID Token does not match
                on of the expected `azr.acr_values`.
            MismatchingIdTokenAudience: if the `aud` claim from the ID Token does not match
                the expected `client.client_id`.
            MismatchingIdTokenAzp: if the `azp` claim from the ID Token does not match
                the expected `client.client_id`.
            MismatchingIdTokenNonce: if the `nonce` claim from the ID Token does not match
                the expected `azr.nonce`.
            ExpiredIdToken: if the ID Token is expired at the time of the check.
            UnsupportedIdTokenAlg: if the signature alg for the ID Token is not supported.

        """
        if not self.id_token:
            raise MissingIdToken(self)

        raw_id_token = self.id_token

        if isinstance(raw_id_token, jwskate.JweCompact) and client.id_token_encrypted_response_alg is None:
            msg = "token is encrypted while it should be clear-text"
            raise InvalidIdToken(msg, raw_id_token, self)
        if isinstance(raw_id_token, IdToken) and client.id_token_encrypted_response_alg is not None:
            msg = "token is clear-text while it should be encrypted"
            raise InvalidIdToken(msg, raw_id_token, self)

        if isinstance(raw_id_token, jwskate.JweCompact):
            enc_jwk = client.id_token_decryption_key
            if enc_jwk is None:
                msg = "token is encrypted but client does not have a decryption key"
                raise InvalidIdToken(msg, raw_id_token, self)
            nested_id_token = raw_id_token.decrypt(enc_jwk)
            id_token = IdToken(nested_id_token)
        else:
            id_token = raw_id_token

        id_token_alg = id_token.get_header("alg")
        if id_token_alg is None:
            id_token_alg = client.id_token_signed_response_alg
        if id_token_alg is None:
            msg = """
token does not contain an `alg` parameter to specify the signature algorithm,
and no algorithm has been configured for the client (using param `id_token_signed_response_alg`).
"""
            raise InvalidIdToken(msg, id_token, self)
        if client.id_token_signed_response_alg is not None and id_token_alg != client.id_token_signed_response_alg:
            raise MismatchingIdTokenAlg(id_token.alg, client.id_token_signed_response_alg, self, id_token)

        verification_jwk: jwskate.Jwk

        if id_token_alg in jwskate.SignatureAlgs.ALL_SYMMETRIC:
            if not client.client_secret:
                msg = "token is symmetrically signed but this client does not have a Client Secret."
                raise InvalidIdToken(msg, id_token, self)
            verification_jwk = jwskate.SymmetricJwk.from_bytes(client.client_secret, alg=id_token_alg)
            id_token.verify_signature(verification_jwk, alg=id_token_alg)
        elif id_token_alg in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
            if not client.authorization_server_jwks:
                msg = "token is asymmetrically signed but the Authorization Server JWKS is not available."
                raise InvalidIdToken(msg, id_token, self)

            if id_token.get_header("kid") is None:
                msg = """
token does not contain a Key ID (kid) to specify the asymmetric key
to use for signature verification."""
                raise InvalidIdToken(msg, id_token, self)
            try:
                verification_jwk = client.authorization_server_jwks.get_jwk_by_kid(id_token.kid)
            except KeyError:
                msg = f"""\
token is asymmetrically signed but there is no key
with kid='{id_token.kid}' in the Authorization Server JWKS."""
                raise InvalidIdToken(msg, id_token, self) from None

            if id_token_alg not in verification_jwk.supported_signing_algorithms():
                msg = "token is asymmetrically signed but its algorithm is not supported by the verification key."
                raise InvalidIdToken(msg, id_token, self)
        else:
            raise UnsupportedIdTokenAlg(self, id_token, id_token_alg)

        id_token.verify(verification_jwk, alg=id_token_alg)

        if azr.issuer and id_token.issuer != azr.issuer:
            raise MismatchingIdTokenIssuer(id_token.issuer, azr.issuer, self, id_token)

        if id_token.audiences and client.client_id not in id_token.audiences:
            raise MismatchingIdTokenAudience(id_token.audiences, client.client_id, self, id_token)

        if id_token.authorized_party is not None and id_token.authorized_party != client.client_id:
            raise MismatchingIdTokenAzp(id_token.azp, client.client_id, self, id_token)

        if id_token.is_expired(leeway=exp_leeway):
            raise ExpiredIdToken(self, id_token)

        if azr.nonce and id_token.nonce != azr.nonce:
            raise MismatchingIdTokenNonce(id_token.nonce, azr.nonce, self, id_token)

        if azr.acr_values and id_token.acr not in azr.acr_values:
            raise MismatchingIdTokenAcr(id_token.acr, azr.acr_values, self, id_token)

        hash_function = IdToken.hash_method(verification_jwk, id_token_alg)

        at_hash = id_token.get_claim("at_hash")
        if at_hash is not None:
            expected_at_hash = hash_function(self.access_token)
            if expected_at_hash != at_hash:
                msg = f"mismatching 'at_hash' value (expected '{expected_at_hash}', got '{at_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        c_hash = id_token.get_claim("c_hash")
        if c_hash is not None:
            expected_c_hash = hash_function(azr.code)
            if expected_c_hash != c_hash:
                msg = f"mismatching 'c_hash' value (expected '{expected_c_hash}', got '{c_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        s_hash = id_token.get_claim("s_hash")
        if s_hash is not None:
            if azr.state is None:
                msg = "token has a 's_hash' claim but no state was included in the request."
                raise InvalidIdToken(msg, id_token, self)
            expected_s_hash = hash_function(azr.state)
            if expected_s_hash != s_hash:
                msg = f"mismatching 's_hash' value (expected '{expected_s_hash}', got '{s_hash}')"
                raise InvalidIdToken(msg, id_token, self)

        if azr.max_age is not None:
            auth_time = id_token.auth_datetime
            if auth_time is None:
                msg = """
a `max_age` parameter was included in the authorization request,
but the ID Token does not contain an `auth_time` claim.
"""
                raise InvalidIdToken(msg, id_token, self) from None
            auth_age = datetime.now(tz=timezone.utc) - auth_time
            if auth_age.total_seconds() > azr.max_age + auth_time_leeway:
                msg = f"""
user authentication happened too far in the past.
The `auth_time` parameter from the ID Token indicate that
the last Authentication Time was at {auth_time} ({auth_age.total_seconds()} sec ago),
but the authorization request `max_age` parameter specified that it must
be a maximum of {azr.max_age} sec ago.
"""
                raise InvalidIdToken(msg, id_token, self)

        return self.__class__(
            access_token=self.access_token,
            expires_at=self.expires_at,
            scope=self.scope,
            refresh_token=self.refresh_token,
            token_type=self.token_type,
            id_token=id_token,
            **self.kwargs,
        )
as_dict()

Return a dict of parameters.

That is suitable for serialization or to init another BearerToken.

Source code in requests_oauth2client/tokens.py
def as_dict(self) -> dict[str, Any]:
    """Return a dict of parameters.

    That is suitable for serialization or to init another BearerToken.

    """
    d = asdict(self)
    d.pop("expires_at")
    d["expires_in"] = self.expires_in
    d.update(**d.pop("kwargs", {}))
    return {key: val for key, val in d.items() if val is not None}

BearerTokenSerializer

A helper class to serialize Token Response returned by an AS.

This may be used to store BearerTokens in session or cookies.

It needs a dumper and a loader functions that will respectively serialize and deserialize BearerTokens. Default implementations are provided with use gzip and base64url on the serialized JSON representation.

Parameters:

Name Type Description Default
dumper Callable[[BearerToken], str] | None

a function to serialize a token into a str.

None
loader Callable[[str], BearerToken] | None

a function to deserialize a serialized token representation.

None
Source code in requests_oauth2client/tokens.py
class BearerTokenSerializer:
    """A helper class to serialize Token Response returned by an AS.

    This may be used to store BearerTokens in session or cookies.

    It needs a `dumper` and a `loader` functions that will respectively serialize and deserialize
    BearerTokens. Default implementations are provided with use gzip and base64url on the serialized
    JSON representation.

    Args:
        dumper: a function to serialize a token into a `str`.
        loader: a function to deserialize a serialized token representation.

    """

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

    @staticmethod
    def default_dumper(token: BearerToken) -> str:
        """Serialize a token as JSON, then compress with deflate, then encodes as base64url.

        Args:
            token: the `BearerToken` to serialize

        Returns:
            the serialized value

        """
        d = asdict(token)
        d.update(**d.pop("kwargs", {}))
        return (
            BinaPy.serialize_to("json", {k: w for k, w in d.items() if w is not None}).to("deflate").to("b64u").ascii()
        )

    def default_loader(self, serialized: str, token_class: type[BearerToken] = BearerToken) -> BearerToken:
        """Deserialize a BearerToken.

        This does the opposite operations than `default_dumper`.

        Args:
            serialized: the serialized token
            token_class: class to use to deserialize the Token

        Returns:
            a BearerToken

        """
        attrs = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json")
        expires_at = attrs.get("expires_at")
        if expires_at:
            attrs["expires_at"] = datetime.fromtimestamp(expires_at, tz=timezone.utc)
        return token_class(**attrs)

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

        Args:
            token: a BearerToken to serialize

        Returns:
            the serialized token, as a str

        """
        return self.dumper(token)

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

        Args:
            serialized: the serialized token

        Returns:
            the deserialized token

        """
        return self.loader(serialized)
default_dumper(token) staticmethod

Serialize a token as JSON, then compress with deflate, then encodes as base64url.

Parameters:

Name Type Description Default
token BearerToken

the BearerToken to serialize

required

Returns:

Type Description
str

the serialized value

Source code in requests_oauth2client/tokens.py
@staticmethod
def default_dumper(token: BearerToken) -> str:
    """Serialize a token as JSON, then compress with deflate, then encodes as base64url.

    Args:
        token: the `BearerToken` to serialize

    Returns:
        the serialized value

    """
    d = asdict(token)
    d.update(**d.pop("kwargs", {}))
    return (
        BinaPy.serialize_to("json", {k: w for k, w in d.items() if w is not None}).to("deflate").to("b64u").ascii()
    )
default_loader(serialized, token_class=BearerToken)

Deserialize a BearerToken.

This does the opposite operations than default_dumper.

Parameters:

Name Type Description Default
serialized str

the serialized token

required
token_class type[BearerToken]

class to use to deserialize the Token

BearerToken

Returns:

Type Description
BearerToken

a BearerToken

Source code in requests_oauth2client/tokens.py
def default_loader(self, serialized: str, token_class: type[BearerToken] = BearerToken) -> BearerToken:
    """Deserialize a BearerToken.

    This does the opposite operations than `default_dumper`.

    Args:
        serialized: the serialized token
        token_class: class to use to deserialize the Token

    Returns:
        a BearerToken

    """
    attrs = BinaPy(serialized).decode_from("b64u").decode_from("deflate").parse_from("json")
    expires_at = attrs.get("expires_at")
    if expires_at:
        attrs["expires_at"] = datetime.fromtimestamp(expires_at, tz=timezone.utc)
    return token_class(**attrs)
dumps(token)

Serialize and compress a given token for easier storage.

Parameters:

Name Type Description Default
token BearerToken

a BearerToken to serialize

required

Returns:

Type Description
str

the serialized token, as a str

Source code in requests_oauth2client/tokens.py
def dumps(self, token: BearerToken) -> str:
    """Serialize and compress a given token for easier storage.

    Args:
        token: a BearerToken to serialize

    Returns:
        the serialized token, as a str

    """
    return self.dumper(token)
loads(serialized)

Deserialize a serialized token.

Parameters:

Name Type Description Default
serialized str

the serialized token

required

Returns:

Type Description
BearerToken

the deserialized token

Source code in requests_oauth2client/tokens.py
def loads(self, serialized: str) -> BearerToken:
    """Deserialize a serialized token.

    Args:
        serialized: the serialized token

    Returns:
        the deserialized token

    """
    return self.loader(serialized)

id_token_converter(id_token)

Utility method that converts an ID Token, as str | bytes, to an IdToken or JweCompact.

Source code in requests_oauth2client/tokens.py
def id_token_converter(
    id_token: str | bytes | IdToken | jwskate.JweCompact | None,
) -> IdToken | jwskate.JweCompact | None:
    """Utility method that converts an ID Token, as `str | bytes`, to an `IdToken` or `JweCompact`."""
    if id_token is not None and not isinstance(id_token, (IdToken, jwskate.JweCompact)):
        try:
            id_token = IdToken(id_token)
        except jwskate.InvalidJwt:
            try:
                id_token = jwskate.JweCompact(id_token)  # type: ignore[arg-type]
            except jwskate.InvalidJwe:
                msg = "token is neither a JWT or a JWE."
                raise InvalidIdToken(msg, id_token) from None
    return id_token

vendor_specific

Vendor-specific utilities.

This module contains vendor-specific subclasses of [requests_oauth2client] classes, that make it easier to work with specific OAuth 2.x providers and/or fix compatibility issues.

Auth0

Auth0-related utilities.

Source code in requests_oauth2client/vendor_specific/auth0.py
class Auth0:
    """Auth0-related utilities."""

    @classmethod
    def tenant(cls, tenant: str) -> str:
        """Given a short tenant name, returns the full tenant FQDN."""
        if not tenant:
            msg = "You must specify a tenant name."
            raise ValueError(msg)
        if "." not in tenant or tenant.endswith((".eu", ".us", ".au", ".jp")):
            tenant = f"{tenant}.auth0.com"
        if "://" in tenant:
            if tenant.startswith("https://"):
                return tenant[8:]
            msg = (
                "Invalid tenant name. "
                "It must be a tenant name like 'mytenant.myregion' "
                "or a full FQDN like 'mytenant.myregion.auth0.com'."
                "or an issuer like 'https://mytenant.myregion.auth0.com'"
            )
            raise ValueError(msg)
        return tenant

    @classmethod
    def client(
        cls,
        tenant: str,
        auth: (
            requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
        ) = None,
        *,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_jwk: Any | None = None,
        session: requests.Session | None = None,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialise an OAuth2Client for an Auth0 tenant."""
        tenant = cls.tenant(tenant)
        issuer = f"https://{tenant}"
        token_endpoint = f"{issuer}/oauth/token"
        authorization_endpoint = f"{issuer}/authorize"
        revocation_endpoint = f"{issuer}/oauth/revoke"
        userinfo_endpoint = f"{issuer}/userinfo"
        jwks_uri = f"{issuer}/.well-known/jwks.json"

        return OAuth2Client(
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_jwk=private_jwk,
            session=session,
            token_endpoint=token_endpoint,
            authorization_endpoint=authorization_endpoint,
            revocation_endpoint=revocation_endpoint,
            userinfo_endpoint=userinfo_endpoint,
            issuer=issuer,
            jwks_uri=jwks_uri,
            **kwargs,
        )

    @classmethod
    def management_api_client(
        cls,
        tenant: str,
        auth: (
            requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
        ) = None,
        *,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_jwk: Any | None = None,
        session: requests.Session | None = None,
        **kwargs: Any,
    ) -> ApiClient:
        """Initialize a client for the Auth0 Management API.

        See [Auth0 Management API v2](https://auth0.com/docs/api/management/v2). You must provide the
        target tenant name and the credentials for a client that is allowed access to the Management
        API.

        Args:
            tenant: the tenant name.
                Same definition as for [Auth0.client][requests_oauth2client.vendor_specific.auth0.Auth0.client]
            auth: client credentials.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            client_id: the Client ID.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            client_secret: the Client Secret.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            private_jwk: the private key to use for client authentication.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            session: requests session.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            **kwargs: additional kwargs to pass to the ApiClient base class

        Example:
            ```python
            from requests_oauth2client.vendor_specific import Auth0

            a0mgmt = Auth0.management_api_client("mytenant.eu", client_id=client_id, client_secret=client_secret)
            users = a0mgmt.get("users", params={"page": 0, "per_page": 100})
            ```

        """
        tenant = cls.tenant(tenant)
        client = cls.client(
            tenant,
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_jwk=private_jwk,
            session=session,
        )
        audience = f"https://{tenant}/api/v2/"
        api_auth = OAuth2ClientCredentialsAuth(client, audience=audience)
        return ApiClient(
            base_url=audience,
            auth=api_auth,
            session=session,
            **kwargs,
        )
tenant(tenant) classmethod

Given a short tenant name, returns the full tenant FQDN.

Source code in requests_oauth2client/vendor_specific/auth0.py
@classmethod
def tenant(cls, tenant: str) -> str:
    """Given a short tenant name, returns the full tenant FQDN."""
    if not tenant:
        msg = "You must specify a tenant name."
        raise ValueError(msg)
    if "." not in tenant or tenant.endswith((".eu", ".us", ".au", ".jp")):
        tenant = f"{tenant}.auth0.com"
    if "://" in tenant:
        if tenant.startswith("https://"):
            return tenant[8:]
        msg = (
            "Invalid tenant name. "
            "It must be a tenant name like 'mytenant.myregion' "
            "or a full FQDN like 'mytenant.myregion.auth0.com'."
            "or an issuer like 'https://mytenant.myregion.auth0.com'"
        )
        raise ValueError(msg)
    return tenant
client(tenant, auth=None, *, client_id=None, client_secret=None, private_jwk=None, session=None, **kwargs) classmethod

Initialise an OAuth2Client for an Auth0 tenant.

Source code in requests_oauth2client/vendor_specific/auth0.py
@classmethod
def client(
    cls,
    tenant: str,
    auth: (
        requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
    ) = None,
    *,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_jwk: Any | None = None,
    session: requests.Session | None = None,
    **kwargs: Any,
) -> OAuth2Client:
    """Initialise an OAuth2Client for an Auth0 tenant."""
    tenant = cls.tenant(tenant)
    issuer = f"https://{tenant}"
    token_endpoint = f"{issuer}/oauth/token"
    authorization_endpoint = f"{issuer}/authorize"
    revocation_endpoint = f"{issuer}/oauth/revoke"
    userinfo_endpoint = f"{issuer}/userinfo"
    jwks_uri = f"{issuer}/.well-known/jwks.json"

    return OAuth2Client(
        auth=auth,
        client_id=client_id,
        client_secret=client_secret,
        private_jwk=private_jwk,
        session=session,
        token_endpoint=token_endpoint,
        authorization_endpoint=authorization_endpoint,
        revocation_endpoint=revocation_endpoint,
        userinfo_endpoint=userinfo_endpoint,
        issuer=issuer,
        jwks_uri=jwks_uri,
        **kwargs,
    )
management_api_client(tenant, auth=None, *, client_id=None, client_secret=None, private_jwk=None, session=None, **kwargs) classmethod

Initialize a client for the Auth0 Management API.

See Auth0 Management API v2. You must provide the target tenant name and the credentials for a client that is allowed access to the Management API.

Parameters:

Name Type Description Default
tenant str

the tenant name. Same definition as for Auth0.client

required
auth AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None

client credentials. Same definition as for OAuth2Client

None
client_id str | None

the Client ID. Same definition as for OAuth2Client

None
client_secret str | None

the Client Secret. Same definition as for OAuth2Client

None
private_jwk Any | None

the private key to use for client authentication. Same definition as for OAuth2Client

None
session Session | None

requests session. Same definition as for OAuth2Client

None
**kwargs Any

additional kwargs to pass to the ApiClient base class

{}
Example
1
2
3
4
from requests_oauth2client.vendor_specific import Auth0

a0mgmt = Auth0.management_api_client("mytenant.eu", client_id=client_id, client_secret=client_secret)
users = a0mgmt.get("users", params={"page": 0, "per_page": 100})
Source code in requests_oauth2client/vendor_specific/auth0.py
@classmethod
def management_api_client(
    cls,
    tenant: str,
    auth: (
        requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
    ) = None,
    *,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_jwk: Any | None = None,
    session: requests.Session | None = None,
    **kwargs: Any,
) -> ApiClient:
    """Initialize a client for the Auth0 Management API.

    See [Auth0 Management API v2](https://auth0.com/docs/api/management/v2). You must provide the
    target tenant name and the credentials for a client that is allowed access to the Management
    API.

    Args:
        tenant: the tenant name.
            Same definition as for [Auth0.client][requests_oauth2client.vendor_specific.auth0.Auth0.client]
        auth: client credentials.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        client_id: the Client ID.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        client_secret: the Client Secret.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        private_jwk: the private key to use for client authentication.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        session: requests session.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        **kwargs: additional kwargs to pass to the ApiClient base class

    Example:
        ```python
        from requests_oauth2client.vendor_specific import Auth0

        a0mgmt = Auth0.management_api_client("mytenant.eu", client_id=client_id, client_secret=client_secret)
        users = a0mgmt.get("users", params={"page": 0, "per_page": 100})
        ```

    """
    tenant = cls.tenant(tenant)
    client = cls.client(
        tenant,
        auth=auth,
        client_id=client_id,
        client_secret=client_secret,
        private_jwk=private_jwk,
        session=session,
    )
    audience = f"https://{tenant}/api/v2/"
    api_auth = OAuth2ClientCredentialsAuth(client, audience=audience)
    return ApiClient(
        base_url=audience,
        auth=api_auth,
        session=session,
        **kwargs,
    )

Ping

Ping Identity related utilities.

Source code in requests_oauth2client/vendor_specific/ping.py
class Ping:
    """Ping Identity related utilities."""

    @classmethod
    def client(
        cls,
        issuer: str,
        auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_jwk: Any = None,
        session: requests.Session | None = None,
    ) -> OAuth2Client:
        """Initialize an OAuth2Client for PingFederate.

        This will configure all endpoints with PingID specific urls, without using the metadata.
        Excepted for avoiding a round-trip to get the metadata url, this does not provide any advantage
        over using `OAuth2Client.from_discovery_endpoint(issuer="https://myissuer.domain.tld")`.

        """
        if not issuer.startswith("https://"):
            if "://" in issuer:
                msg = "Invalid issuer. It must be an https:// url or a domain name without a scheme."
                raise ValueError(msg)
            issuer = f"https://{issuer}"
        if "." not in issuer:
            msg = "Invalid issuer. It must contain at least a dot in the domain name."
            raise ValueError(msg)

        return OAuth2Client(
            authorization_endpoint=f"{issuer}/as/authorization.oauth2",
            token_endpoint=f"{issuer}/as/token.oauth2",
            revocation_endpoint=f"{issuer}/as/revoke_token.oauth2",
            userinfo_endpoint=f"{issuer}/idp/userinfo.openid",
            introspection_endpoint=f"{issuer}/as/introspect.oauth2",
            jwks_uri=f"{issuer}/pf/JWKS",
            registration_endpoint=f"{issuer}/as/clients.oauth2",
            ping_revoked_sris_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/revokedSris",
            ping_session_management_sris_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/sessions",
            ping_session_management_users_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/users",
            ping_end_session_endpoint=f"{issuer}/idp/startSLO.ping",
            device_authorization_endpoint=f"{issuer}/as/device_authz.oauth2",
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_jwk=private_jwk,
            session=session,
        )
client(issuer, auth=None, client_id=None, client_secret=None, private_jwk=None, session=None) classmethod

Initialize an OAuth2Client for PingFederate.

This will configure all endpoints with PingID specific urls, without using the metadata. Excepted for avoiding a round-trip to get the metadata url, this does not provide any advantage over using OAuth2Client.from_discovery_endpoint(issuer="https://myissuer.domain.tld").

Source code in requests_oauth2client/vendor_specific/ping.py
@classmethod
def client(
    cls,
    issuer: str,
    auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_jwk: Any = None,
    session: requests.Session | None = None,
) -> OAuth2Client:
    """Initialize an OAuth2Client for PingFederate.

    This will configure all endpoints with PingID specific urls, without using the metadata.
    Excepted for avoiding a round-trip to get the metadata url, this does not provide any advantage
    over using `OAuth2Client.from_discovery_endpoint(issuer="https://myissuer.domain.tld")`.

    """
    if not issuer.startswith("https://"):
        if "://" in issuer:
            msg = "Invalid issuer. It must be an https:// url or a domain name without a scheme."
            raise ValueError(msg)
        issuer = f"https://{issuer}"
    if "." not in issuer:
        msg = "Invalid issuer. It must contain at least a dot in the domain name."
        raise ValueError(msg)

    return OAuth2Client(
        authorization_endpoint=f"{issuer}/as/authorization.oauth2",
        token_endpoint=f"{issuer}/as/token.oauth2",
        revocation_endpoint=f"{issuer}/as/revoke_token.oauth2",
        userinfo_endpoint=f"{issuer}/idp/userinfo.openid",
        introspection_endpoint=f"{issuer}/as/introspect.oauth2",
        jwks_uri=f"{issuer}/pf/JWKS",
        registration_endpoint=f"{issuer}/as/clients.oauth2",
        ping_revoked_sris_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/revokedSris",
        ping_session_management_sris_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/sessions",
        ping_session_management_users_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/users",
        ping_end_session_endpoint=f"{issuer}/idp/startSLO.ping",
        device_authorization_endpoint=f"{issuer}/as/device_authz.oauth2",
        auth=auth,
        client_id=client_id,
        client_secret=client_secret,
        private_jwk=private_jwk,
        session=session,
    )

auth0

Implements subclasses for Auth0.

Auth0

Auth0-related utilities.

Source code in requests_oauth2client/vendor_specific/auth0.py
class Auth0:
    """Auth0-related utilities."""

    @classmethod
    def tenant(cls, tenant: str) -> str:
        """Given a short tenant name, returns the full tenant FQDN."""
        if not tenant:
            msg = "You must specify a tenant name."
            raise ValueError(msg)
        if "." not in tenant or tenant.endswith((".eu", ".us", ".au", ".jp")):
            tenant = f"{tenant}.auth0.com"
        if "://" in tenant:
            if tenant.startswith("https://"):
                return tenant[8:]
            msg = (
                "Invalid tenant name. "
                "It must be a tenant name like 'mytenant.myregion' "
                "or a full FQDN like 'mytenant.myregion.auth0.com'."
                "or an issuer like 'https://mytenant.myregion.auth0.com'"
            )
            raise ValueError(msg)
        return tenant

    @classmethod
    def client(
        cls,
        tenant: str,
        auth: (
            requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
        ) = None,
        *,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_jwk: Any | None = None,
        session: requests.Session | None = None,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialise an OAuth2Client for an Auth0 tenant."""
        tenant = cls.tenant(tenant)
        issuer = f"https://{tenant}"
        token_endpoint = f"{issuer}/oauth/token"
        authorization_endpoint = f"{issuer}/authorize"
        revocation_endpoint = f"{issuer}/oauth/revoke"
        userinfo_endpoint = f"{issuer}/userinfo"
        jwks_uri = f"{issuer}/.well-known/jwks.json"

        return OAuth2Client(
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_jwk=private_jwk,
            session=session,
            token_endpoint=token_endpoint,
            authorization_endpoint=authorization_endpoint,
            revocation_endpoint=revocation_endpoint,
            userinfo_endpoint=userinfo_endpoint,
            issuer=issuer,
            jwks_uri=jwks_uri,
            **kwargs,
        )

    @classmethod
    def management_api_client(
        cls,
        tenant: str,
        auth: (
            requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
        ) = None,
        *,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_jwk: Any | None = None,
        session: requests.Session | None = None,
        **kwargs: Any,
    ) -> ApiClient:
        """Initialize a client for the Auth0 Management API.

        See [Auth0 Management API v2](https://auth0.com/docs/api/management/v2). You must provide the
        target tenant name and the credentials for a client that is allowed access to the Management
        API.

        Args:
            tenant: the tenant name.
                Same definition as for [Auth0.client][requests_oauth2client.vendor_specific.auth0.Auth0.client]
            auth: client credentials.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            client_id: the Client ID.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            client_secret: the Client Secret.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            private_jwk: the private key to use for client authentication.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            session: requests session.
                Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
            **kwargs: additional kwargs to pass to the ApiClient base class

        Example:
            ```python
            from requests_oauth2client.vendor_specific import Auth0

            a0mgmt = Auth0.management_api_client("mytenant.eu", client_id=client_id, client_secret=client_secret)
            users = a0mgmt.get("users", params={"page": 0, "per_page": 100})
            ```

        """
        tenant = cls.tenant(tenant)
        client = cls.client(
            tenant,
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_jwk=private_jwk,
            session=session,
        )
        audience = f"https://{tenant}/api/v2/"
        api_auth = OAuth2ClientCredentialsAuth(client, audience=audience)
        return ApiClient(
            base_url=audience,
            auth=api_auth,
            session=session,
            **kwargs,
        )
tenant(tenant) classmethod

Given a short tenant name, returns the full tenant FQDN.

Source code in requests_oauth2client/vendor_specific/auth0.py
@classmethod
def tenant(cls, tenant: str) -> str:
    """Given a short tenant name, returns the full tenant FQDN."""
    if not tenant:
        msg = "You must specify a tenant name."
        raise ValueError(msg)
    if "." not in tenant or tenant.endswith((".eu", ".us", ".au", ".jp")):
        tenant = f"{tenant}.auth0.com"
    if "://" in tenant:
        if tenant.startswith("https://"):
            return tenant[8:]
        msg = (
            "Invalid tenant name. "
            "It must be a tenant name like 'mytenant.myregion' "
            "or a full FQDN like 'mytenant.myregion.auth0.com'."
            "or an issuer like 'https://mytenant.myregion.auth0.com'"
        )
        raise ValueError(msg)
    return tenant
client(tenant, auth=None, *, client_id=None, client_secret=None, private_jwk=None, session=None, **kwargs) classmethod

Initialise an OAuth2Client for an Auth0 tenant.

Source code in requests_oauth2client/vendor_specific/auth0.py
@classmethod
def client(
    cls,
    tenant: str,
    auth: (
        requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
    ) = None,
    *,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_jwk: Any | None = None,
    session: requests.Session | None = None,
    **kwargs: Any,
) -> OAuth2Client:
    """Initialise an OAuth2Client for an Auth0 tenant."""
    tenant = cls.tenant(tenant)
    issuer = f"https://{tenant}"
    token_endpoint = f"{issuer}/oauth/token"
    authorization_endpoint = f"{issuer}/authorize"
    revocation_endpoint = f"{issuer}/oauth/revoke"
    userinfo_endpoint = f"{issuer}/userinfo"
    jwks_uri = f"{issuer}/.well-known/jwks.json"

    return OAuth2Client(
        auth=auth,
        client_id=client_id,
        client_secret=client_secret,
        private_jwk=private_jwk,
        session=session,
        token_endpoint=token_endpoint,
        authorization_endpoint=authorization_endpoint,
        revocation_endpoint=revocation_endpoint,
        userinfo_endpoint=userinfo_endpoint,
        issuer=issuer,
        jwks_uri=jwks_uri,
        **kwargs,
    )
management_api_client(tenant, auth=None, *, client_id=None, client_secret=None, private_jwk=None, session=None, **kwargs) classmethod

Initialize a client for the Auth0 Management API.

See Auth0 Management API v2. You must provide the target tenant name and the credentials for a client that is allowed access to the Management API.

Parameters:

Name Type Description Default
tenant str

the tenant name. Same definition as for Auth0.client

required
auth AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None

client credentials. Same definition as for OAuth2Client

None
client_id str | None

the Client ID. Same definition as for OAuth2Client

None
client_secret str | None

the Client Secret. Same definition as for OAuth2Client

None
private_jwk Any | None

the private key to use for client authentication. Same definition as for OAuth2Client

None
session Session | None

requests session. Same definition as for OAuth2Client

None
**kwargs Any

additional kwargs to pass to the ApiClient base class

{}
Example
1
2
3
4
from requests_oauth2client.vendor_specific import Auth0

a0mgmt = Auth0.management_api_client("mytenant.eu", client_id=client_id, client_secret=client_secret)
users = a0mgmt.get("users", params={"page": 0, "per_page": 100})
Source code in requests_oauth2client/vendor_specific/auth0.py
@classmethod
def management_api_client(
    cls,
    tenant: str,
    auth: (
        requests.auth.AuthBase | tuple[str, str] | tuple[str, Jwk] | tuple[str, dict[str, Any]] | str | None
    ) = None,
    *,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_jwk: Any | None = None,
    session: requests.Session | None = None,
    **kwargs: Any,
) -> ApiClient:
    """Initialize a client for the Auth0 Management API.

    See [Auth0 Management API v2](https://auth0.com/docs/api/management/v2). You must provide the
    target tenant name and the credentials for a client that is allowed access to the Management
    API.

    Args:
        tenant: the tenant name.
            Same definition as for [Auth0.client][requests_oauth2client.vendor_specific.auth0.Auth0.client]
        auth: client credentials.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        client_id: the Client ID.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        client_secret: the Client Secret.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        private_jwk: the private key to use for client authentication.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        session: requests session.
            Same definition as for [OAuth2Client][requests_oauth2client.client.OAuth2Client]
        **kwargs: additional kwargs to pass to the ApiClient base class

    Example:
        ```python
        from requests_oauth2client.vendor_specific import Auth0

        a0mgmt = Auth0.management_api_client("mytenant.eu", client_id=client_id, client_secret=client_secret)
        users = a0mgmt.get("users", params={"page": 0, "per_page": 100})
        ```

    """
    tenant = cls.tenant(tenant)
    client = cls.client(
        tenant,
        auth=auth,
        client_id=client_id,
        client_secret=client_secret,
        private_jwk=private_jwk,
        session=session,
    )
    audience = f"https://{tenant}/api/v2/"
    api_auth = OAuth2ClientCredentialsAuth(client, audience=audience)
    return ApiClient(
        base_url=audience,
        auth=api_auth,
        session=session,
        **kwargs,
    )

ping

PingID specific client.

Ping

Ping Identity related utilities.

Source code in requests_oauth2client/vendor_specific/ping.py
class Ping:
    """Ping Identity related utilities."""

    @classmethod
    def client(
        cls,
        issuer: str,
        auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
        client_id: str | None = None,
        client_secret: str | None = None,
        private_jwk: Any = None,
        session: requests.Session | None = None,
    ) -> OAuth2Client:
        """Initialize an OAuth2Client for PingFederate.

        This will configure all endpoints with PingID specific urls, without using the metadata.
        Excepted for avoiding a round-trip to get the metadata url, this does not provide any advantage
        over using `OAuth2Client.from_discovery_endpoint(issuer="https://myissuer.domain.tld")`.

        """
        if not issuer.startswith("https://"):
            if "://" in issuer:
                msg = "Invalid issuer. It must be an https:// url or a domain name without a scheme."
                raise ValueError(msg)
            issuer = f"https://{issuer}"
        if "." not in issuer:
            msg = "Invalid issuer. It must contain at least a dot in the domain name."
            raise ValueError(msg)

        return OAuth2Client(
            authorization_endpoint=f"{issuer}/as/authorization.oauth2",
            token_endpoint=f"{issuer}/as/token.oauth2",
            revocation_endpoint=f"{issuer}/as/revoke_token.oauth2",
            userinfo_endpoint=f"{issuer}/idp/userinfo.openid",
            introspection_endpoint=f"{issuer}/as/introspect.oauth2",
            jwks_uri=f"{issuer}/pf/JWKS",
            registration_endpoint=f"{issuer}/as/clients.oauth2",
            ping_revoked_sris_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/revokedSris",
            ping_session_management_sris_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/sessions",
            ping_session_management_users_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/users",
            ping_end_session_endpoint=f"{issuer}/idp/startSLO.ping",
            device_authorization_endpoint=f"{issuer}/as/device_authz.oauth2",
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_jwk=private_jwk,
            session=session,
        )
client(issuer, auth=None, client_id=None, client_secret=None, private_jwk=None, session=None) classmethod

Initialize an OAuth2Client for PingFederate.

This will configure all endpoints with PingID specific urls, without using the metadata. Excepted for avoiding a round-trip to get the metadata url, this does not provide any advantage over using OAuth2Client.from_discovery_endpoint(issuer="https://myissuer.domain.tld").

Source code in requests_oauth2client/vendor_specific/ping.py
@classmethod
def client(
    cls,
    issuer: str,
    auth: requests.auth.AuthBase | tuple[str, str] | str | None = None,
    client_id: str | None = None,
    client_secret: str | None = None,
    private_jwk: Any = None,
    session: requests.Session | None = None,
) -> OAuth2Client:
    """Initialize an OAuth2Client for PingFederate.

    This will configure all endpoints with PingID specific urls, without using the metadata.
    Excepted for avoiding a round-trip to get the metadata url, this does not provide any advantage
    over using `OAuth2Client.from_discovery_endpoint(issuer="https://myissuer.domain.tld")`.

    """
    if not issuer.startswith("https://"):
        if "://" in issuer:
            msg = "Invalid issuer. It must be an https:// url or a domain name without a scheme."
            raise ValueError(msg)
        issuer = f"https://{issuer}"
    if "." not in issuer:
        msg = "Invalid issuer. It must contain at least a dot in the domain name."
        raise ValueError(msg)

    return OAuth2Client(
        authorization_endpoint=f"{issuer}/as/authorization.oauth2",
        token_endpoint=f"{issuer}/as/token.oauth2",
        revocation_endpoint=f"{issuer}/as/revoke_token.oauth2",
        userinfo_endpoint=f"{issuer}/idp/userinfo.openid",
        introspection_endpoint=f"{issuer}/as/introspect.oauth2",
        jwks_uri=f"{issuer}/pf/JWKS",
        registration_endpoint=f"{issuer}/as/clients.oauth2",
        ping_revoked_sris_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/revokedSris",
        ping_session_management_sris_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/sessions",
        ping_session_management_users_endpoint=f"{issuer}/pf-ws/rest/sessionMgmt/users",
        ping_end_session_endpoint=f"{issuer}/idp/startSLO.ping",
        device_authorization_endpoint=f"{issuer}/as/device_authz.oauth2",
        auth=auth,
        client_id=client_id,
        client_secret=client_secret,
        private_jwk=private_jwk,
        session=session,
    )