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".

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

An 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.

Usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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']

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. Note that this is the default behavior of requests.
  • 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 behaviour.

('true', 'false')
session Session | None

a preconfigured requests.Session to use with this ApiClient.

None
**session_kwargs Any

additional kwargs to configure the underlying requests.Session.

{}
Source code in requests_oauth2client/api_client.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 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
@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"`.

    `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.

    An `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].

    Usage:
        ```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: 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`. Note that this is
            the default behavior of `requests`.
            - 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 behaviour.
        session: a preconfigured `requests.Session` to use with this `ApiClient`.
        **session_kwargs: additional kwargs to configure the underlying `requests.Session`.

    """

    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"
    session: requests.Session = field(factory=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"),
        session: requests.Session | None = None,
        **session_kwargs: Any,
    ):
        session = session or requests.Session()
        for key, val in session_kwargs.items():
            setattr(session, key, val)

        if bool_fields is None:
            bool_fields = (True, False)

        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,
        url: 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:
        """Overridden `request` method with extra features.

        Features added compared to plain request():

        - takes a relative path instead of a full url, which will be appended to the
          base_url
        - it can raise an exception when the API returns a non-success status code
        - allow_redirects is False by default (since API usually don't use redirects)
        - `data` or `json` fields with value `None` can either be included or excluded from the
          request
        - boolean fields can be serialized to `'true'` or `'false'` instead of `'True'` and
          `'False'`

        Args:
          method: the HTTP method to use
          url: 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. Can also be 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.__init__`,
            but this will be applied for this request only.
          none_fields: like the parameter of the same name from `ApiClient.__init__`,
            but this will be applied for this request only.
          bool_fields: like the parameter of the same name from `ApiClient.__init__`,
            but this will be applied for this request only.

        Returns:
          a [requests.Response][] as returned by requests

        """
        url = self.to_absolute_url(url)

        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:
            try:
                true_value, false_value = bool_fields
            except ValueError:
                msg = "Invalid value for 'bool_fields'. Must be a 2 value tuple, with (true_value, false_value)."
                raise ValueError(msg) from None
            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,
            url,
            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, relative_url: None | str | bytes | Iterable[str | bytes | int] = None) -> str:
        """Convert a relative url to an absolute url.

        Given a `relative_url`, 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:
          relative_url: a relative url

        Returns:
          the resulting absolute url

        """
        url = relative_url

        if self.base_url:
            if url is not None:
                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:
                        msg = (
                            "Unexpected url type, please pass a relative path as string or"
                            " bytes, or an iterable of string-able objects"
                        )
                        raise TypeError(
                            msg,
                            type(url),
                        ) from exc

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

                if "://" in url:
                    msg = "url must be relative to root_url"
                    raise ValueError(msg)

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

        if url is None or not isinstance(url, str):
            msg = "Unable to determine an absolute url."
            raise ValueError(msg)

        return url

    def get(
        self,
        url: None | str | bytes | Iterable[str | bytes | int] = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a GET 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:
            url: a url where the request will be sent.
            raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
            **kwargs: Optional arguments that [request()][requests.request] takes.

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

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

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

    def post(
        self,
        url: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a POST 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:
          url: an url where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: Optional arguments that ``request`` takes.

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

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

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

    def patch(
        self,
        url: 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` 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:
          url: an url where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: Optional arguments that ``request`` takes.

        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", url, raise_for_status=raise_for_status, **kwargs)

    def put(
        self,
        url: 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` 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:
          url: a url 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", url, raise_for_status=raise_for_status, **kwargs)

    def delete(
        self,
        url: 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:
          url: a url 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("DELETE", url, 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 initialised on the new base url

        Usage:
            ```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 initialised on the new base url

        Usage:
            ```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) -> ApiClient:
        """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.

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

        """
        return self

    def __exit__(self, *args: Any) -> None:
        """Close the underlying requests.Session on exit."""
        self.session.close()

request(method, url=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)

Overridden request method with extra features.

Features added compared to plain request():

  • takes a relative path instead of a full url, which will be appended to the base_url
  • it can raise an exception when the API returns a non-success status code
  • allow_redirects is False by default (since API usually don't use redirects)
  • data or json fields with value None can either be included or excluded from the request
  • boolean fields can be serialized to 'true' or 'false' instead of 'True' and 'False'

Parameters:

Name Type Description Default
method str

the HTTP method to use

required
url 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. Can also be 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.__init__, 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.__init__, 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.__init__, but this will be applied for this request only.

None

Returns:

Type Description
Response

a requests.Response as returned by requests

Source code in requests_oauth2client/api_client.py
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
def request(  # noqa: C901, PLR0913, D417
    self,
    method: str,
    url: 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:
    """Overridden `request` method with extra features.

    Features added compared to plain request():

    - takes a relative path instead of a full url, which will be appended to the
      base_url
    - it can raise an exception when the API returns a non-success status code
    - allow_redirects is False by default (since API usually don't use redirects)
    - `data` or `json` fields with value `None` can either be included or excluded from the
      request
    - boolean fields can be serialized to `'true'` or `'false'` instead of `'True'` and
      `'False'`

    Args:
      method: the HTTP method to use
      url: 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. Can also be 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.__init__`,
        but this will be applied for this request only.
      none_fields: like the parameter of the same name from `ApiClient.__init__`,
        but this will be applied for this request only.
      bool_fields: like the parameter of the same name from `ApiClient.__init__`,
        but this will be applied for this request only.

    Returns:
      a [requests.Response][] as returned by requests

    """
    url = self.to_absolute_url(url)

    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:
        try:
            true_value, false_value = bool_fields
        except ValueError:
            msg = "Invalid value for 'bool_fields'. Must be a 2 value tuple, with (true_value, false_value)."
            raise ValueError(msg) from None
        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,
        url,
        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(relative_url=None)

Convert a relative url to an absolute url.

Given a relative_url, 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
relative_url None | str | bytes | Iterable[str | bytes | int]

a relative url

None

Returns:

Type Description
str

the resulting absolute url

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

    Given a `relative_url`, 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:
      relative_url: a relative url

    Returns:
      the resulting absolute url

    """
    url = relative_url

    if self.base_url:
        if url is not None:
            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:
                    msg = (
                        "Unexpected url type, please pass a relative path as string or"
                        " bytes, or an iterable of string-able objects"
                    )
                    raise TypeError(
                        msg,
                        type(url),
                    ) from exc

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

            if "://" in url:
                msg = "url must be relative to root_url"
                raise ValueError(msg)

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

    if url is None or not isinstance(url, str):
        msg = "Unable to determine an absolute url."
        raise ValueError(msg)

    return url

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

Send a GET 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
url None | str | bytes | Iterable[str | bytes | int]

a url 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

Optional arguments that request() takes.

{}

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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
def get(
    self,
    url: None | str | bytes | Iterable[str | bytes | int] = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a GET 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:
        url: a url where the request will be sent.
        raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
        **kwargs: Optional arguments that [request()][requests.request] takes.

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

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

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

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

Send a POST 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
url str | bytes | Iterable[str | bytes] | None

an url 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

Optional arguments that request takes.

{}

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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def post(
    self,
    url: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a POST 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:
      url: an url where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: Optional arguments that ``request`` takes.

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

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

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

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

Send a PATCH 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
url str | bytes | Iterable[str | bytes] | None

an url 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

Optional arguments that request takes.

{}

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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
def patch(
    self,
    url: 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` 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:
      url: an url where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: Optional arguments that ``request`` takes.

    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", url, raise_for_status=raise_for_status, **kwargs)

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

Send a PUT 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
url str | bytes | Iterable[str | bytes] | None

a url 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
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
def put(
    self,
    url: 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` 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:
      url: a url 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", url, raise_for_status=raise_for_status, **kwargs)

delete(url=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
url str | bytes | Iterable[str | bytes] | None

a url 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
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def delete(
    self,
    url: 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:
      url: a url 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("DELETE", url, raise_for_status=raise_for_status, **kwargs)

BaseOAuth2RenewableTokenAuth

Bases: BearerAuth

Base class for BearerToken-based Auth Handlers, with an obtainable or renewable 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

an OAuth2Client

required
token None | BearerToken | str

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 include in token requests

{}
Source code in requests_oauth2client/auth.py
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
class BaseOAuth2RenewableTokenAuth(BearerAuth):
    """Base class for BearerToken-based Auth Handlers, with an obtainable or renewable 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: an OAuth2Client
        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 include in token requests

    """

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

    @override
    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        token = self.token
        if token is None or token.is_expired(self.leeway):
            self.renew_token()
        return super().__call__(request)

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

        Subclasses should implement this.

        """
        raise NotImplementedError

    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.

Subclasses should implement this.

Source code in requests_oauth2client/auth.py
139
140
141
142
143
144
145
def renew_token(self) -> None:
    """Obtain a new Bearer Token.

    Subclasses should implement this.

    """
    raise NotImplementedError

forget_token()

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

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

BearerAuth

Bases: AuthBase

An Auth Handler that includes a Bearer Token in API calls, as defined in RFC6750$2.1.

As a prerequisite to using this AuthBase, you have to obtain an access token manually. You most likely don't want to do that by yourself, but instead use an instance of OAuth2Client to do that for you. See the others Auth Handlers in this module, which will automatically obtain access tokens from an OAuth 2.x server.

Usage
1
2
auth = BearerAuth("my_access_token")
resp = requests.get("https://my.api.local/resource", auth=auth)

The HTTP request will look like:

1
2
3
GET /resource HTTP/1.1
Host: my.api.local
Authorization: Bearer my_access_token

Parameters:

Name Type Description Default
token str | BearerToken | None

a BearerToken or a string to use as token for this Auth Handler. If None, this Auth Handler is a no-op.

None
Source code in requests_oauth2client/auth.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 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
class BearerAuth(requests.auth.AuthBase):
    """An Auth Handler that includes a Bearer Token in API calls, as defined in [RFC6750$2.1].

    As a prerequisite to using this `AuthBase`, you have to obtain an access token manually.
    You most likely don't want to do that by yourself, but instead use an instance of
    [OAuth2Client][requests_oauth2client.client.OAuth2Client] to do that for you.
    See the others Auth Handlers in this module, which will automatically obtain
    access tokens from an OAuth 2.x server.

    [RFC6750$2.1]: https://datatracker.ietf.org/doc/html/rfc6750#section-2.1

    Usage:
        ```python
        auth = BearerAuth("my_access_token")
        resp = requests.get("https://my.api.local/resource", auth=auth)
        ```

        The HTTP request will look like:
        ```
        GET /resource HTTP/1.1
        Host: my.api.local
        Authorization: Bearer my_access_token
        ```

    Args:
        token: a [BearerToken][requests_oauth2client.tokens.BearerToken] or a string
            to use as token for this Auth Handler. If `None`, this Auth Handler is a no-op.

    """

    def __init__(self, token: str | BearerToken | None = None) -> None:
        self.token = token  # type: ignore[assignment] # until https://github.com/python/mypy/issues/3004 is fixed

    @property
    def token(self) -> BearerToken | None:
        """Return the [BearerToken] that is used for authorization against the API.

        Returns:
            the configured [BearerToken][requests_oauth2client.tokens.BearerToken] used with this
            AuthHandler.

        """
        return self._token

    @token.setter
    def token(self, token: str | BearerToken | None) -> None:
        """Change the access token used with this AuthHandler.

        Accepts a [BearerToken][requests_oauth2client.tokens.BearerToken] or an access token as
        `str`.

        Args:
            token: an access token to use for this Auth Handler

        """
        if token is not None and not isinstance(token, BearerToken):
            token = BearerToken(token)
        self._token = token

    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: a [PreparedRequest][requests.PreparedRequest]

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

        """
        if self.token is None:
            return request
        if self.token.is_expired():
            raise ExpiredAccessToken(self.token)
        request.headers["Authorization"] = self.token.authorization_header()
        return request

token: BearerToken | None property writable

Return the [BearerToken] that is used for authorization against the API.

Returns:

Type Description
BearerToken | None

the configured BearerToken used with this

BearerToken | None

AuthHandler.

OAuth2AccessTokenAuth

Bases: BaseOAuth2RenewableTokenAuth

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

This Requests Auth handler implementation uses an access token as Bearer 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. If a refresh_token and an expiration date are available, this Auth Handler will automatically refresh the access token once it is expired.

Parameters:

Name Type Description Default
client OAuth2Client

the OAuth2Client to use to refresh tokens.

required
token None | BearerToken | str

a access token that has been previously obtained

None
**token_kwargs Any

additional kwargs to pass to the token endpoint

{}
Usage

```python client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret")) token = BearerToken( access_token="access_token", expires_in=600, refresh_token="refresh_token" ) # obtain a BearerToken any way you see fit, including a refresh token oauth2at_auth = OAuth2ClientCredentialsAuth(client, token, scope="my_scope") resp = requests.post("https://my.api.local/resource", auth=oauth2at_auth) ````

Source code in requests_oauth2client/auth.py
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
class OAuth2AccessTokenAuth(BaseOAuth2RenewableTokenAuth):
    """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 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. If a
    refresh_token and an expiration date are available, this Auth Handler will automatically refresh
    the access token once it is expired.

    Args:
        client: the [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to refresh tokens.
        token: a access token that has been previously obtained
        **token_kwargs: additional kwargs to pass to the token endpoint

    Usage:
        ```python
        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        token = BearerToken(
            access_token="access_token", expires_in=600, refresh_token="refresh_token"
        )  # obtain a BearerToken any way you see fit, including a refresh token
        oauth2at_auth = OAuth2ClientCredentialsAuth(client, token, scope="my_scope")
        resp = requests.post("https://my.api.local/resource", auth=oauth2at_auth)
        ````

    """

    @override
    def renew_token(self) -> None:
        """Obtain a new token, using the Refresh Token, if available."""
        if self.token and self.token.refresh_token and self.client is not None:
            self.token = self.client.refresh_token(refresh_token=self.token.refresh_token, **self.token_kwargs)

renew_token()

Obtain a new token, using the Refresh Token, if available.

Source code in requests_oauth2client/auth.py
206
207
208
209
210
@override
def renew_token(self) -> None:
    """Obtain a new token, using the Refresh Token, if available."""
    if self.token and self.token.refresh_token and self.client is not None:
        self.token = self.client.refresh_token(refresh_token=self.token.refresh_token, **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 OAuth2Client to use to obtain Access Tokens.

required
code str | AuthorizationResponse

an Authorization Code that has been obtained from the AS.

required
**token_kwargs Any

additional kwargs to pass to the token endpoint

{}
Usage

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

Source code in requests_oauth2client/auth.py
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
class OAuth2AuthorizationCodeAuth(OAuth2AccessTokenAuth):
    """Authentication handler for the Authorization Code grant.

    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 [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to obtain Access Tokens.
        code: an Authorization Code that has been obtained from the AS.
        **token_kwargs: additional kwargs to pass to the token endpoint

    Usage:
        ```python
        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        code = "my_code"  # you must obtain this code yourself
        resp = requests.post("https://my.api.local/resource", auth=OAuth2AuthorizationCodeAuth(client, code))
        ````

    """

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

    @override
    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: a [PreparedRequest][requests.PreparedRequest]

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

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

    def exchange_code_for_token(self) -> None:
        """Obtain the initial access token with the authorization_code grant."""
        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()

Obtain the initial access token with the authorization_code grant.

Source code in requests_oauth2client/auth.py
262
263
264
265
266
def exchange_code_for_token(self) -> None:
    """Obtain the initial access token with the authorization_code grant."""
    if self.code:  # pragma: no branch
        self.token = self.client.authorization_code(code=self.code, **self.token_kwargs)
        self.code = None

OAuth2ClientCredentialsAuth

Bases: BaseOAuth2RenewableTokenAuth

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_kwargs Any

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

{}
Usage
1
2
3
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
class OAuth2ClientCredentialsAuth(BaseOAuth2RenewableTokenAuth):
    """An Auth Handler for the Client Credentials grant.

    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_kwargs: extra kw parameters to pass to the Token Endpoint. May include `scope`, `resource`, etc.

    Usage:
        ```python
        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)
        ```

    """

    @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
172
173
174
175
@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_kwargs Any

additional kwargs to pass to the token endpoint.

{}
Usage

```python 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
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
class OAuth2DeviceCodeAuth(OAuth2AccessTokenAuth):
    """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_kwargs: additional kwargs to pass to the token endpoint.

    Usage:
        ```python
        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)
        ````
    """

    def __init__(
        self,
        client: OAuth2Client,
        device_code: str | DeviceAuthorizationResponse,
        leeway: int = 20,
        interval: int = 5,
        expires_in: int = 360,
        **token_kwargs: Any,
    ) -> None:
        super().__init__(client=client, leeway=leeway, token=None, **token_kwargs)
        self.device_code: str | DeviceAuthorizationResponse | None = device_code
        self.interval = interval
        self.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

        """
        token = self.token
        if token is None or token.is_expired():
            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,
            )
            while self.token is None:
                self.token = pooling_job()
            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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
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,
        )
        while self.token is None:
            self.token = pooling_job()
        self.device_code = None

OAuth2ResourceOwnerPasswordAuth

Bases: BaseOAuth2RenewableTokenAuth

Authentication Handler for the Resource Owner Password Flow.

This Requests Auth handler implementation exchanges the user credentials for an Access Token, then automatically obtains a new one once it 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 nor MFA and depends on the user typing its credentials directly inside the application instead of on a dedicated login page, 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 OAuth2Client 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_kwargs Any

additional kwargs to pass to the token endpoint

{}
Source code in requests_oauth2client/auth.py
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
class OAuth2ResourceOwnerPasswordAuth(BaseOAuth2RenewableTokenAuth):
    """Authentication Handler for the [Resource Owner Password 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 obtains a new one once it 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 nor MFA and
    depends on the user typing its credentials directly inside the application instead of on a
    dedicated login page, 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 [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to obtain
            Access Tokens
        username: the username
        password: the user password
        leeway: an amount of time, in seconds
        **token_kwargs: additional kwargs to pass to the token endpoint

    """

    def __init__(
        self,
        client: OAuth2Client,
        username: str,
        password: str,
        leeway: int = 20,
        **token_kwargs: Any,
    ):
        super().__init__(client=client, leeway=leeway, **token_kwargs)
        self.username = username
        self.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
306
307
308
309
310
311
312
313
@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. 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, and authorization_response_iss_parameter_supported is False (default), then no issuer check is performed. Set authorization_response_iss_parameter_supported to True to enforce server identification: if no issuer is included in the Authorization Response, then an error will be raised instead.

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.

{}
Source code in requests_oauth2client/authorization_request.py
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
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
@frozen(init=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. 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, and
       `authorization_response_iss_parameter_supported` is `False` (default), then no issuer check
       is performed. Set `authorization_response_iss_parameter_supported`
       to `True` to enforce server identification: if no issuer is included in the Authorization
       Response, then an error will be raised instead.

    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.

    """

    authorization_endpoint: str

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

    code_verifier: str | None = None
    code_challenge: str | None = field(init=False, metadata={"query": True})
    authorization_response_iss_parameter_supported: bool = False
    issuer: str | None = None

    exception_classes: ClassVar[dict[str, type[Exception]]] = {
        "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 = "code",
        state: str | ellipsis | None = ...,  # noqa: F821
        nonce: str | ellipsis | None = ...,  # noqa: F821
        code_verifier: str | None = None,
        code_challenge_method: str | None = "S256",
        acr_values: str | Iterable[str] | None = None,
        max_age: int | None = None,
        issuer: str | None = None,
        authorization_response_iss_parameter_supported: bool = False,
        **kwargs: Any,
    ) -> None:
        if authorization_response_iss_parameter_supported and not issuer:
            msg = (
                "When 'authorization_response_iss_parameter_supported' is `True`, you must"
                " provide the expected `issuer` as parameter."
            )
            raise ValueError(msg)

        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:
            msg = "The `max_age` parameter is a number of seconds and cannot be negative."
            raise ValueError(msg)

        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)

        code_challenge: str | None = None
        if code_challenge_method:
            if not code_verifier:
                code_verifier = PkceUtils.generate_code_verifier()
            code_challenge = PkceUtils.derive_challenge(code_verifier, code_challenge_method)
        else:
            code_verifier = None

        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,
            kwargs=kwargs,
        )
        object.__setattr__(self, "code_challenge", code_challenge)

    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", {}))
        d.pop("code_challenge")
        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.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:
            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`.
            NotImplementedError: 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()
            if self.issuer and received_issuer != self.issuer:
                raise MismatchingIssuer(self.issuer, received_issuer)

        # 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)

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

        if "code" in self.response_type:
            code: str = response_url.args.get("code")
            if code is None:
                raise MissingAuthCode()
        else:
            raise NotImplementedError()

        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,
            **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.

        """
        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(error, error_description, 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

args: dict[str, Any] property

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

Returns:

Type Description
dict[str, Any]

a dict of parameters

furl: furl property

Return the Authorization Request URI, as a furl.

uri: str 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
279
280
281
282
@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
284
285
286
287
@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
371
372
373
374
375
376
377
378
379
380
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", {}))
    d.pop("code_challenge")
    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
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.

NotImplementedError

if response_type anything else than 'code'.

Source code in requests_oauth2client/authorization_request.py
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
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:
        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`.
        NotImplementedError: 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()
        if self.issuer and received_issuer != self.issuer:
            raise MismatchingIssuer(self.issuer, received_issuer)

    # 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)

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

    if "code" in self.response_type:
        code: str = response_url.args.get("code")
        if code is None:
            raise MissingAuthCode()
    else:
        raise NotImplementedError()

    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,
        **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
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
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
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
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
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
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
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
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.

Source code in requests_oauth2client/authorization_request.py
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
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.

    """
    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(error, error_description, 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
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
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,
    ):
        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)
        d.update(**d.pop("kwargs", {}))
        d.pop("code_challenge")
        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")
        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
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
@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)
    d.update(**d.pop("kwargs", {}))
    d.pop("code_challenge")
    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
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
@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")
    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
820
821
822
823
824
825
826
827
828
829
830
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
832
833
834
835
836
837
838
839
840
841
842
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, all accessible as 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 AuthorizationRequest

None
code_verifier str | None

the code_verifier matching the code_challenge that was passed as parameter in the AuthorizationRequest

None
state str | None

the state returned by the AS

None
**kwargs str

other parameters as returned by the AS

{}
Source code in requests_oauth2client/authorization_request.py
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
@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, all accessible as 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 AuthorizationRequest
        code_verifier: the code_verifier matching the code_challenge that was passed as
            parameter in the AuthorizationRequest
        state: the state returned by the AS
        **kwargs: other parameters as returned by the AS

    """

    code: str
    redirect_uri: str | None = None
    code_verifier: str | None = None
    state: str | None = None
    nonce: str | None = None
    acr_values: tuple[str, ...] | None = None
    max_age: int | None = None
    issuer: str | None = None
    kwargs: dict[str, Any] = Factory(dict)

    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,
        **kwargs: str,
    ):
        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,
            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

PKCE Code Challenge Methods.

Source code in requests_oauth2client/authorization_request.py
111
112
113
114
115
class CodeChallengeMethods(str, Enum):
    """PKCE Code Challenge Methods."""

    plain = "plain"
    S256 = "S256"

PkceUtils

Contains helper methods for PKCE, as described in RFC7636.

See RFC7636.

Source code in requests_oauth2client/authorization_request.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 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
class PkceUtils:
    """Contains helper methods for PKCE, as described in RFC7636.

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

    """

    code_verifier_re = 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 = "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

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

        if not cls.code_verifier_re.match(verifier):
            msg = f"Invalid code verifier, does not match {cls.code_verifier_re}"
            raise ValueError(
                msg,
                verifier,
            )

        if method == "S256":
            return BinaPy(verifier).to("sha256").to("b64u").ascii()
        elif method == "plain":
            return verifier
        else:
            msg = "Unsupported code_challenge_method"
            raise ValueError(msg, method)

    @classmethod
    def generate_code_verifier_and_challenge(cls, method: str = "S256") -> tuple[str, str]:
        """Generate a valid `code_verifier` and derive its `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 = "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_re.match(verifier) is not None and cls.derive_challenge(verifier, method) == challenge

code_verifier_re = 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
40
41
42
43
44
45
46
47
48
@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='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

Source code in requests_oauth2client/authorization_request.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@classmethod
def derive_challenge(cls, verifier: str | bytes, method: str = "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

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

    if not cls.code_verifier_re.match(verifier):
        msg = f"Invalid code verifier, does not match {cls.code_verifier_re}"
        raise ValueError(
            msg,
            verifier,
        )

    if method == "S256":
        return BinaPy(verifier).to("sha256").to("b64u").ascii()
    elif method == "plain":
        return verifier
    else:
        msg = "Unsupported code_challenge_method"
        raise ValueError(msg, method)

generate_code_verifier_and_challenge(method='S256') classmethod

Generate a valid code_verifier and derive its 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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@classmethod
def generate_code_verifier_and_challenge(cls, method: str = "S256") -> tuple[str, str]:
    """Generate a valid `code_verifier` and derive its `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='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
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@classmethod
def validate_code_verifier(cls, verifier: str, challenge: str, method: str = "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_re.match(verifier) is not None and cls.derive_challenge(verifier, method) == challenge

RequestParameterAuthorizationRequest

Represent an Authorization Request that includes a request JWT.

Parameters:

Name Type Description Default
authorization_endpoint str

the Authorization Endpoint uri

required
client_id str

the client_id

required
request 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
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
@frozen(init=False)
class RequestParameterAuthorizationRequest:
    """Represent an Authorization Request that includes a `request` JWT.

    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: str
    expires_at: datetime | None = None
    kwargs: dict[str, Any] = Factory(dict)

    @accepts_expires_in
    def __init__(
        self,
        authorization_endpoint: str,
        client_id: str,
        request: str,
        expires_at: datetime | None = None,
        **kwargs: Any,
    ):
        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            request=request,
            expires_at=expires_at,
            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": 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: furl property

Return the Authorization Request URI, as a furl instance.

uri: str 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 parameters to include in the request

{}
Source code in requests_oauth2client/authorization_request.py
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
@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 parameters to include in the request

    """

    authorization_endpoint: str
    client_id: str
    request_uri: str
    expires_at: datetime | None = None
    kwargs: dict[str, Any] = Factory(dict)

    @accepts_expires_in
    def __init__(
        self,
        authorization_endpoint: str,
        client_id: str,
        request_uri: str,
        expires_at: datetime | None = None,
        **kwargs: Any,
    ):
        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            request_uri=request_uri,
            expires_at=expires_at,
            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: furl property

Return the Authorization Request URI, as a furl instance.

uri: str property

Return the Authorization Request URI, as a str.

BackChannelAuthenticationPoolingJob

Bases: TokenEndpointPoolingJob

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

{}

auth=("client_id", "client_secret") ) pool_job = BackChannelAuthenticationPoolingJob( client=client, auth_req_id="my_auth_req_id" )

1
token = None while token is None: token = pool_job() ```
Source code in requests_oauth2client/backchannel_authentication.py
 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
class BackChannelAuthenticationPoolingJob(TokenEndpointPoolingJob):
    """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 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.

    Usage: ```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() ```

    """

    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,
    ):
        if isinstance(auth_req_id, BackChannelAuthenticationResponse) and interval is None:
            interval = auth_req_id.interval

        super().__init__(
            client=client,
            interval=interval,
            slow_down_interval=slow_down_interval,
            requests_kwargs=requests_kwargs,
            **token_kwargs,
        )
        self.auth_req_id = auth_req_id

    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
136
137
138
139
140
141
142
143
144
145
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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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,
    ):
        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

    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

        """
        if key == "expires_in":
            if self.expires_at is None:
                return None
            return int(self.expires_at.timestamp() - datetime.now(tz=timezone.utc).timestamp())
        return self.other.get(key) or super().__getattribute__(key)

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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

GrantType

Bases: str, Enum

An enum of standardized grant_type values.

Source code in requests_oauth2client/client.py
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
class GrantType(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
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.

{}
Usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)
Source code in requests_oauth2client/client.py
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  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
 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
@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.
        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.

    Usage:
        ```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)
        ```

    """

    auth: requests.auth.AuthBase = field(converter=client_auth_factory)
    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 = SignatureAlgs.RS256
    id_token_encrypted_response_alg: str | None = None
    id_token_decryption_key: Jwk | None = None
    code_challenge_method: str | None = "S256"
    authorization_response_iss_parameter_supported: bool = False
    session: requests.Session = field(factory=requests.Session)
    extra_metadata: dict[str, Any] = field(factory=dict)
    testing: bool = False

    bearer_token_class: type[BearerToken] = BearerToken

    exception_classes: ClassVar[dict[str, type[Exception]]] = {
        "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,
        "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 = "S256",
        authorization_response_iss_parameter_supported: bool = False,
        bearer_token_class: type[BearerToken] = BearerToken,
        session: requests.Session | None = None,
        testing: bool = False,
        **extra_metadata: Any,
    ):
        if authorization_response_iss_parameter_supported and not issuer:
            msg = (
                "If the Authorization Server supports Issuer Identification, as specified by"
                " `authorization_response_iss_parameter_supported=True`, then you must specify"
                " the expected `issuer` value with parameter `issuer`."
            )
            raise ValueError(msg)

        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:
                msg = (
                    "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."
                )
                raise ValueError(msg)

        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,
            bearer_token_class=bearer_token_class,
            extra_metadata=extra_metadata,
        )

    @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 ValueError as exc:
            msg = f"Invalid value '{uri}' for '{attribute.name}': {exc}"
            raise ValueError(msg) 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 ValueError as exc:
            msg = f"Invalid value '{uri}' for '{attribute.name}': {exc}"
            raise ValueError(msg) 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], T],
        on_failure: Callable[[requests.Response], T],
        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
            accept: the Accept header to include in the request
            method: the HTTP method to use
            **requests_kwargs: keyword arguments for the request

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

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

        return on_failure(response)

    def token_request(
        self,
        data: dict[str, Any],
        timeout: int = 10,
        **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
          **requests_kwargs: additional parameters for requests.post()

        Returns:
            the token endpoint response, as
            [`BearerToken`][requests_oauth2client.tokens.BearerToken] instance.

        """
        return self._request(
            "token_endpoint",
            auth=self.auth,
            data=data,
            timeout=timeout,
            on_success=self.parse_token_response,
            on_failure=self.on_token_error,
            **requests_kwargs,
        )

    def parse_token_response(self, response: requests.Response) -> 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][requests.Response] returned by the Token Endpoint.

        Returns:
            a [`BearerToken`][requests_oauth2client.tokens.BearerToken] based on the response
            contents.

        """
        try:
            token_response = self.bearer_token_class(**response.json())
        except Exception as response_class_exc:
            try:
                return self.on_token_error(response)
            except Exception as token_error_exc:
                raise token_error_exc from response_class_exc
        else:
            return token_response

    def on_token_error(self, response: requests.Response) -> 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][requests.Response] returned by the Token Endpoint.

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

        """
        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, error, error_description, error_uri)
        except Exception as exc:
            raise InvalidTokenResponse(response) from exc
        raise exception

    def client_credentials(
        self,
        scope: str | Iterable[str] | 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.
            requests_kwargs: additional parameters for the call to requests
            **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`. Common parameters

        Returns:
            a TokenResponse

        """
        requests_kwargs = requests_kwargs or {}

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

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

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

        Args:
             code: an authorization code or an `AuthorizationResponse` to exchange for tokens
             validate: if `True`, validate the received ID Token (this works only if `code` is an AuthorizationResponse)
             requests_kwargs: additional parameters for the call to requests
             **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `code`, etc.

        Returns:
            a `BearerToken`

        """
        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
            code = code.code

        requests_kwargs = requests_kwargs or {}

        data = dict(grant_type=GrantType.AUTHORIZATION_CODE, code=code, **token_kwargs)
        token = self.token_request(data, **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.

        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:
            a `BearerToken`

        """
        if isinstance(refresh_token, BearerToken):
            if refresh_token.refresh_token is None or not isinstance(refresh_token.refresh_token, str):
                msg = "This BearerToken doesn't have a refresh_token"
                raise ValueError(msg)
            refresh_token = refresh_token.refresh_token

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

    def device_code(
        self,
        device_code: str | DeviceAuthorizationResponse,
        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`
            requests_kwargs: additional parameters for the call to requests
            **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `device_code`, etc.

        Returns:
            a `BearerToken`

        """
        if isinstance(device_code, DeviceAuthorizationResponse):
            if device_code.device_code is None or not isinstance(device_code.device_code, str):
                msg = "This DeviceAuthorizationResponse doesn't have a device_code"
                raise ValueError(msg)
            device_code = device_code.device_code

        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantType.DEVICE_CODE,
            device_code=device_code,
            **token_kwargs,
        )
        return self.token_request(data, **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:
            a `BearerToken`

        """
        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):
                msg = "This `BackChannelAuthenticationResponse` doesn't have an `auth_req_id`"
                raise ValueError(msg)
            auth_req_id = auth_req_id.auth_req_id

        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantType.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,
        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.
            requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: additional parameters to include in the request body.

        Returns:
            a `BearerToken` as returned by the Authorization Server.

        """
        requests_kwargs = requests_kwargs or {}

        try:
            subject_token_type = self.get_token_type(subject_token_type, subject_token)
        except ValueError:
            msg = "Cannot determine the kind of 'subject_token' you provided. Please specify a 'subject_token_type'."
            raise TypeError(msg) from None
        if actor_token:  # pragma: no branch
            try:
                actor_token_type = self.get_token_type(actor_token_type, actor_token)
            except ValueError:
                msg = "Cannot determine the kind of 'actor_token' you provided. Please specify an 'actor_token_type'."
                raise TypeError(msg) from None

        data = dict(
            grant_type=GrantType.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, **requests_kwargs)

    def jwt_bearer(
        self,
        assertion: Jwt | str,
        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.
            requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: additional parameters to include in the request body.

        Returns:
            a `BearerToken` as returned by the Authorization Server.

        """
        requests_kwargs = requests_kwargs or {}

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

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

        return self.token_request(data, **requests_kwargs)

    def resource_owner_password(
        self,
        username: str,
        password: str,
        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
            requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: additional parameters to include in the request body.

        Returns:
            a `BearerToken` as returned by the Authorization Server

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

        return self.token_request(data, **requests_kwargs)

    def authorization_request(
        self,
        *,
        scope: None | str | Iterable[str] = "openid",
        response_type: str = "code",
        redirect_uri: str | None = None,
        state: str | ellipsis | None = ...,  # noqa: F821
        nonce: str | ellipsis | None = ...,  # noqa: F821
        code_verifier: 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.
            code_verifier: the PKCE `code_verifier` to use. Leave default to generate a random value.
            **kwargs: additional parameters to include in the auth request

        Returns:
            an AuthorizationRequest with the supplied parameters

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

        redirect_uri = redirect_uri or self.redirect_uri
        if not redirect_uri:
            msg = (
                "No 'redirect_uri' defined for this client. You must either pass a redirect_uri"
                " as parameter to this method, or include a redirect_uri when initializing your"
                " OAuth2Client."
            )
            raise AttributeError(msg)

        if response_type != "code":
            msg = "Only response_type=code is supported."
            raise ValueError(msg)

        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,
            **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(
            "pushed_authorization_request_endpoint",
            data=authorization_request.args,
            auth=self.auth,
            on_success=self.parse_pushed_authorization_response,
            on_failure=self.on_pushed_authorization_request_error,
            **requests_kwargs,
        )

    def parse_pushed_authorization_response(
        self, response: requests.Response
    ) -> RequestUriParameterAuthorizationRequest:
        """Parse the response obtained by `pushed_authorization_request()`.

        Args:
            response: the `requests.Response` returned by the PAR endpoint

        Returns:
            a RequestUriParameterAuthorizationRequest instance

        """
        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,
        )

    def on_pushed_authorization_request_error(
        self, response: requests.Response
    ) -> RequestUriParameterAuthorizationRequest:
        """Error Handler for Pushed Authorization Endpoint errors.

        Args:
            response: the HTTP response as returned by the AS PAR endpoint.

        Returns:
            a RequestUriParameterAuthorizationRequest, if the error is recoverable

        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, error, error_description, error_uri)
        except Exception as exc:
            raise InvalidPushedAuthorizationResponse(response) 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.

        """
        return self._request(
            "userinfo_endpoint",
            auth=BearerAuth(access_token),
            on_success=self.parse_userinfo_response,
            on_failure=self.on_userinfo_error,
        )

    def parse_userinfo_response(self, resp: requests.Response) -> Any:
        """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.

        Returns:
            the parsed JSON content from this response.

        """
        return resp.json()

    def on_userinfo_error(self, resp: requests.Response) -> Any:
        """Parse UserInfo error response.

        Args:
            resp: a [Response][requests.Response] returned from the UserInfo endpoint.

        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.

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

        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 ValueError(msg)
            elif isinstance(token, BearerToken):
                return "urn:ietf:params:oauth:token-type:access_token"
            elif isinstance(token, IdToken):
                return "urn:ietf:params:oauth:token-type:id_token"
            else:
                msg = "Unexpected type of token, please provide a string or a BearerToken or an IdToken."
                raise TypeError(
                    msg,
                    type(token),
                )
        elif token_type == TokenType.ACCESS_TOKEN:
            if token is not None and not isinstance(token, (str, BearerToken)):
                msg = "The supplied token is not a BearerToken or a string representation of it."
                raise TypeError(
                    msg,
                    type(token),
                )
            return "urn:ietf:params:oauth:token-type:access_token"
        elif token_type == TokenType.REFRESH_TOKEN:
            if token is not None and isinstance(token, BearerToken) and not token.refresh_token:
                msg = "The supplied BearerToken doesn't have a refresh_token."
                raise ValueError(msg)
            return "urn:ietf:params:oauth:token-type:refresh_token"
        elif token_type == "id_token":
            if token is not None and not isinstance(token, (str, IdToken)):
                msg = "The supplied token is not an IdToken or a string representation of it."
                raise TypeError(
                    msg,
                    type(token),
                )
            return "urn:ietf:params:oauth:token-type:id_token"
        else:
            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.

        """
        if isinstance(refresh_token, BearerToken):
            if refresh_token.refresh_token is None:
                msg = "The supplied BearerToken doesn't have a refresh token."
                raise ValueError(msg)
            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:
            `True` if the revocation succeeds, `False` if no revocation endpoint is present or a
            non-standardised error is returned.

        """
        requests_kwargs = requests_kwargs or {}

        if token_type_hint == TokenType.REFRESH_TOKEN and isinstance(token, BearerToken):
            if token.refresh_token is None:
                msg = "The supplied BearerToken doesn't have a refresh token."
                raise ValueError(msg)
            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(
            "revocation_endpoint",
            data=data,
            auth=self.auth,
            on_success=lambda resp: True,
            on_failure=self.on_revocation_error,
            **requests_kwargs,
        )

    def on_revocation_error(self, response: requests.Response) -> bool:
        """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 [Response][requests.Response] as returned by the Revocation Endpoint

        Returns:
            `False` to signal that an error occurred. May raise exceptions instead depending on the
            revocation 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, RevocationError)
            exception = exception_class(error, error_description, error_uri)
        except Exception:
            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.

        """
        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:
                    msg = "The supplied BearerToken doesn't have a refresh token."
                    raise ValueError(msg)
                else:
                    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 ValueError(msg)

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

        return self._request(
            "introspection_endpoint",
            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) -> 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.

        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) -> Any:
        """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.

        Returns:
            usually raises exceptions. A subclass can return a default response instead.

        """
        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(error, error_description, error_uri)
        except Exception as exc:
            raise UnknownIntrospectionError(response) 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

        """
        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 ValueError(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 ValueError(msg)

        requests_kwargs = requests_kwargs or {}

        if scope is not None and not isinstance(scope, str):
            try:
                scope = " ".join(scope)
            except Exception as exc:
                msg = "Unsupported `scope` value"
                raise ValueError(msg) from exc

        if acr_values is not None and not isinstance(acr_values, str):
            try:
                acr_values = " ".join(acr_values)
            except Exception as exc:
                msg = "Unsupported `acr_values`"
                raise ValueError(msg) 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(
            "backchannel_authentication_endpoint",
            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
    ) -> 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.

        Returns:
            a `BackChannelAuthenticationResponse`

        """
        try:
            return BackChannelAuthenticationResponse(**response.json())
        except TypeError as exc:
            raise InvalidBackChannelAuthenticationResponse(response) from exc

    def on_backchannel_authentication_error(self, response: requests.Response) -> 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.

        Returns:
            usually raises an exception. But a subclass can return a default response instead.

        """
        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(error, error_description, error_uri)
        except Exception as exc:
            raise InvalidBackChannelAuthenticationResponse(response) 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

        """
        requests_kwargs = requests_kwargs or {}

        return self._request(
            "device_authorization_endpoint",
            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) -> 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.

        Returns:
            a `DeviceAuthorizationResponse` as returned by AS

        """
        device_authorization_response = DeviceAuthorizationResponse(**response.json())
        return device_authorization_response

    def on_device_authorization_error(self, response: requests.Response) -> 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.

        Returns:
            usually raises an Exception. But a subclass may return a default response instead.

        """
        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, error, error_description, error_uri)
        except Exception as exc:
            raise InvalidDeviceAuthorizationResponse(response) 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 {}

        jwks = self._request(
            "jwks_uri",
            auth=None,
            method="GET",
            on_success=lambda resp: resp.json(),
            on_failure=lambda resp: resp.raise_for_status(),
            **requests_kwargs,
        )
        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:
        """Initialise an OAuth2Client based on Authorization Server Metadata.

        This will retrieve the standardised metadata document available at `url`, and will extract
        all Endpoint Uris from that document, will fetch the current public keys from its
        `jwks_uri`, then will initialise an OAuth2Client based on those endpoints.

        Args:
             url: the url where the server metadata 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
             issuer: if an issuer is given, check that it matches the one from the retrieved document
             testing: if True, don't try to validate the endpoint urls that are part of the document
             **kwargs: additional keyword parameters to pass to OAuth2Client

        Returns:
            an OAuth2Client with endpoint initialised based on the obtained metadata

        Raises:
            ValueError: if neither `url` nor `issuer` are suitable urls
            requests.HTTPError: if an error happens while fetching the documents

        """
        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 ValueError(msg)

        validate_endpoint_uri(url, path=False)

        session = session or requests.Session()
        discovery = session.get(url).json()

        jwks_uri = discovery.get("jwks_uri")
        if jwks_uri:
            jwks = JwkSet(session.get(jwks_uri).json())

        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(  # noqa: PLR0913
        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,
        session: requests.Session | None = None,
        https: bool = True,
        testing: bool = False,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialise an OAuth2Client, based on the server metadata from `discovery`.

        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
             session: a requests Session to use to retrieve the document and initialise the client with
             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`

        """
        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 = "Mismatching issuer value in discovery document: "
            raise ValueError(
                msg,
                issuer,
                discovery.get("issuer"),
            )
        elif issuer is None:
            issuer = discovery.get("issuer")

        token_endpoint = discovery.get("token_endpoint")
        if token_endpoint is None:
            msg = "token_endpoint not found in that discovery document"
            raise ValueError(msg)
        authorization_endpoint = discovery.get("authorization_endpoint")
        revocation_endpoint = discovery.get("revocation_endpoint")
        introspection_endpoint = discovery.get("introspection_endpoint")
        userinfo_endpoint = discovery.get("userinfo_endpoint")
        jwks_uri = discovery.get("jwks_uri")
        if jwks_uri is not None:
            validate_endpoint_uri(jwks_uri, https=https)
        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,
            jwks_uri=jwks_uri,
            authorization_server_jwks=authorization_server_jwks,
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            session=session,
            issuer=issuer,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            testing=testing,
            **kwargs,
        )

    def __enter__(self) -> OAuth2Client:
        """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: Any, exc_val: Any, exc_tb: Any) -> bool:  # noqa: D105
        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:
            msg = (
                f"No '{endpoint}' defined for this client. Please provide the URL for that"
                f" endpoint when initializing your {self.__class__.__name__} instance."
            )
            raise AttributeError(msg)

        return str(url)

client_id: str property

Client ID.

client_secret: str | None property

Client Secret.

client_jwks: JwkSet 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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
@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 ValueError as exc:
        msg = f"Invalid value '{uri}' for '{attribute.name}': {exc}"
        raise ValueError(msg) 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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
@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 ValueError as exc:
        msg = f"Invalid value '{uri}' for '{attribute.name}': {exc}"
        raise ValueError(msg) from exc

token_request(data, timeout=10, **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
**requests_kwargs Any

additional parameters for requests.post()

{}

Returns:

Type Description
BearerToken

the token endpoint response, as

BearerToken

BearerToken instance.

Source code in requests_oauth2client/client.py
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
def token_request(
    self,
    data: dict[str, Any],
    timeout: int = 10,
    **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
      **requests_kwargs: additional parameters for requests.post()

    Returns:
        the token endpoint response, as
        [`BearerToken`][requests_oauth2client.tokens.BearerToken] instance.

    """
    return self._request(
        "token_endpoint",
        auth=self.auth,
        data=data,
        timeout=timeout,
        on_success=self.parse_token_response,
        on_failure=self.on_token_error,
        **requests_kwargs,
    )

parse_token_response(response)

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

Returns:

Type Description
BearerToken

a BearerToken based on the response

BearerToken

contents.

Source code in requests_oauth2client/client.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def parse_token_response(self, response: requests.Response) -> 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][requests.Response] returned by the Token Endpoint.

    Returns:
        a [`BearerToken`][requests_oauth2client.tokens.BearerToken] based on the response
        contents.

    """
    try:
        token_response = self.bearer_token_class(**response.json())
    except Exception as response_class_exc:
        try:
            return self.on_token_error(response)
        except Exception as token_error_exc:
            raise token_error_exc from response_class_exc
    else:
        return token_response

on_token_error(response)

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

Returns:

Type Description
BearerToken

nothing, and raises an exception instead. But a subclass may return a

BearerToken

BearerToken to implement a default

BearerToken

behaviour if needed.

Source code in requests_oauth2client/client.py
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
def on_token_error(self, response: requests.Response) -> 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][requests.Response] returned by the Token Endpoint.

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

    """
    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, error, error_description, error_uri)
    except Exception as exc:
        raise InvalidTokenResponse(response) from exc
    raise exception

client_credentials(scope=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
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. Common parameters

{}

Returns:

Type Description
BearerToken

a TokenResponse

Source code in requests_oauth2client/client.py
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
def client_credentials(
    self,
    scope: str | Iterable[str] | 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.
        requests_kwargs: additional parameters for the call to requests
        **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`. Common parameters

    Returns:
        a TokenResponse

    """
    requests_kwargs = requests_kwargs or {}

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

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

authorization_code(code, *, validate=True, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint with the authorization_code grant.

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 received ID Token (this works only if code is an AuthorizationResponse)

True
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, code, etc.

{}

Returns:

Type Description
BearerToken

a BearerToken

Source code in requests_oauth2client/client.py
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
def authorization_code(
    self,
    code: str | AuthorizationResponse,
    *,
    validate: bool = True,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint with the `authorization_code` grant.

    Args:
         code: an authorization code or an `AuthorizationResponse` to exchange for tokens
         validate: if `True`, validate the received ID Token (this works only if `code` is an AuthorizationResponse)
         requests_kwargs: additional parameters for the call to requests
         **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `code`, etc.

    Returns:
        a `BearerToken`

    """
    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
        code = code.code

    requests_kwargs = requests_kwargs or {}

    data = dict(grant_type=GrantType.AUTHORIZATION_CODE, code=code, **token_kwargs)
    token = self.token_request(data, **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.

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

a BearerToken

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

    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:
        a `BearerToken`

    """
    if isinstance(refresh_token, BearerToken):
        if refresh_token.refresh_token is None or not isinstance(refresh_token.refresh_token, str):
            msg = "This BearerToken doesn't have a refresh_token"
            raise ValueError(msg)
        refresh_token = refresh_token.refresh_token

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

device_code(device_code, 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
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

a BearerToken

Source code in requests_oauth2client/client.py
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
def device_code(
    self,
    device_code: str | DeviceAuthorizationResponse,
    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`
        requests_kwargs: additional parameters for the call to requests
        **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `device_code`, etc.

    Returns:
        a `BearerToken`

    """
    if isinstance(device_code, DeviceAuthorizationResponse):
        if device_code.device_code is None or not isinstance(device_code.device_code, str):
            msg = "This DeviceAuthorizationResponse doesn't have a device_code"
            raise ValueError(msg)
        device_code = device_code.device_code

    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantType.DEVICE_CODE,
        device_code=device_code,
        **token_kwargs,
    )
    return self.token_request(data, **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

a BearerToken

Source code in requests_oauth2client/client.py
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
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:
        a `BearerToken`

    """
    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):
            msg = "This `BackChannelAuthenticationResponse` doesn't have an `auth_req_id`"
            raise ValueError(msg)
        auth_req_id = auth_req_id.auth_req_id

    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantType.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, 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
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

a BearerToken as returned by the Authorization Server.

Source code in requests_oauth2client/client.py
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
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,
    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.
        requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: additional parameters to include in the request body.

    Returns:
        a `BearerToken` as returned by the Authorization Server.

    """
    requests_kwargs = requests_kwargs or {}

    try:
        subject_token_type = self.get_token_type(subject_token_type, subject_token)
    except ValueError:
        msg = "Cannot determine the kind of 'subject_token' you provided. Please specify a 'subject_token_type'."
        raise TypeError(msg) from None
    if actor_token:  # pragma: no branch
        try:
            actor_token_type = self.get_token_type(actor_token_type, actor_token)
        except ValueError:
            msg = "Cannot determine the kind of 'actor_token' you provided. Please specify an 'actor_token_type'."
            raise TypeError(msg) from None

    data = dict(
        grant_type=GrantType.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, **requests_kwargs)

jwt_bearer(assertion, 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
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

a BearerToken as returned by the Authorization Server.

Source code in requests_oauth2client/client.py
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
def jwt_bearer(
    self,
    assertion: Jwt | str,
    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.
        requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: additional parameters to include in the request body.

    Returns:
        a `BearerToken` as returned by the Authorization Server.

    """
    requests_kwargs = requests_kwargs or {}

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

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

    return self.token_request(data, **requests_kwargs)

resource_owner_password(username, password, 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
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

a BearerToken as returned by the Authorization Server

Source code in requests_oauth2client/client.py
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
def resource_owner_password(
    self,
    username: str,
    password: str,
    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
        requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: additional parameters to include in the request body.

    Returns:
        a `BearerToken` as returned by the Authorization Server

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

    return self.token_request(data, **requests_kwargs)

authorization_request(*, scope='openid', response_type='code', redirect_uri=None, state=..., nonce=..., code_verifier=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.

...
code_verifier str | None

the PKCE code_verifier to use. Leave default to generate a random value.

None
**kwargs Any

additional parameters to include in the auth request

{}

Returns:

Type Description
AuthorizationRequest

an AuthorizationRequest with the supplied parameters

Source code in requests_oauth2client/client.py
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
def authorization_request(
    self,
    *,
    scope: None | str | Iterable[str] = "openid",
    response_type: str = "code",
    redirect_uri: str | None = None,
    state: str | ellipsis | None = ...,  # noqa: F821
    nonce: str | ellipsis | None = ...,  # noqa: F821
    code_verifier: 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.
        code_verifier: the PKCE `code_verifier` to use. Leave default to generate a random value.
        **kwargs: additional parameters to include in the auth request

    Returns:
        an AuthorizationRequest with the supplied parameters

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

    redirect_uri = redirect_uri or self.redirect_uri
    if not redirect_uri:
        msg = (
            "No 'redirect_uri' defined for this client. You must either pass a redirect_uri"
            " as parameter to this method, or include a redirect_uri when initializing your"
            " OAuth2Client."
        )
        raise AttributeError(msg)

    if response_type != "code":
        msg = "Only response_type=code is supported."
        raise ValueError(msg)

    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,
        **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
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
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(
        "pushed_authorization_request_endpoint",
        data=authorization_request.args,
        auth=self.auth,
        on_success=self.parse_pushed_authorization_response,
        on_failure=self.on_pushed_authorization_request_error,
        **requests_kwargs,
    )

parse_pushed_authorization_response(response)

Parse the response obtained by pushed_authorization_request().

Parameters:

Name Type Description Default
response Response

the requests.Response returned by the PAR endpoint

required

Returns:

Type Description
RequestUriParameterAuthorizationRequest

a RequestUriParameterAuthorizationRequest instance

Source code in requests_oauth2client/client.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
def parse_pushed_authorization_response(
    self, response: requests.Response
) -> RequestUriParameterAuthorizationRequest:
    """Parse the response obtained by `pushed_authorization_request()`.

    Args:
        response: the `requests.Response` returned by the PAR endpoint

    Returns:
        a RequestUriParameterAuthorizationRequest instance

    """
    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,
    )

on_pushed_authorization_request_error(response)

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

Returns:

Type Description
RequestUriParameterAuthorizationRequest

a RequestUriParameterAuthorizationRequest, if the error is recoverable

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
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
def on_pushed_authorization_request_error(
    self, response: requests.Response
) -> RequestUriParameterAuthorizationRequest:
    """Error Handler for Pushed Authorization Endpoint errors.

    Args:
        response: the HTTP response as returned by the AS PAR endpoint.

    Returns:
        a RequestUriParameterAuthorizationRequest, if the error is recoverable

    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, error, error_description, error_uri)
    except Exception as exc:
        raise InvalidPushedAuthorizationResponse(response) 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
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
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.

    """
    return self._request(
        "userinfo_endpoint",
        auth=BearerAuth(access_token),
        on_success=self.parse_userinfo_response,
        on_failure=self.on_userinfo_error,
    )

parse_userinfo_response(resp)

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

Returns:

Type Description
Any

the parsed JSON content from this response.

Source code in requests_oauth2client/client.py
886
887
888
889
890
891
892
893
894
895
896
897
898
899
def parse_userinfo_response(self, resp: requests.Response) -> Any:
    """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.

    Returns:
        the parsed JSON content from this response.

    """
    return resp.json()

on_userinfo_error(resp)

Parse UserInfo error response.

Parameters:

Name Type Description Default
resp Response

a Response returned from the UserInfo endpoint.

required

Returns:

Type Description
Any

nothing, raises exception instead.

Source code in requests_oauth2client/client.py
901
902
903
904
905
906
907
908
909
910
911
def on_userinfo_error(self, resp: requests.Response) -> Any:
    """Parse UserInfo error response.

    Args:
        resp: a [Response][requests.Response] returned from the UserInfo endpoint.

    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.

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

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

    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 ValueError(msg)
        elif isinstance(token, BearerToken):
            return "urn:ietf:params:oauth:token-type:access_token"
        elif isinstance(token, IdToken):
            return "urn:ietf:params:oauth:token-type:id_token"
        else:
            msg = "Unexpected type of token, please provide a string or a BearerToken or an IdToken."
            raise TypeError(
                msg,
                type(token),
            )
    elif token_type == TokenType.ACCESS_TOKEN:
        if token is not None and not isinstance(token, (str, BearerToken)):
            msg = "The supplied token is not a BearerToken or a string representation of it."
            raise TypeError(
                msg,
                type(token),
            )
        return "urn:ietf:params:oauth:token-type:access_token"
    elif token_type == TokenType.REFRESH_TOKEN:
        if token is not None and isinstance(token, BearerToken) and not token.refresh_token:
            msg = "The supplied BearerToken doesn't have a refresh_token."
            raise ValueError(msg)
        return "urn:ietf:params:oauth:token-type:refresh_token"
    elif token_type == "id_token":
        if token is not None and not isinstance(token, (str, IdToken)):
            msg = "The supplied token is not an IdToken or a string representation of it."
            raise TypeError(
                msg,
                type(token),
            )
        return "urn:ietf:params:oauth:token-type:id_token"
    else:
        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
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
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.

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

    """
    if isinstance(refresh_token, BearerToken):
        if refresh_token.refresh_token is None:
            msg = "The supplied BearerToken doesn't have a refresh token."
            raise ValueError(msg)
        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

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
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
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:
        `True` if the revocation succeeds, `False` if no revocation endpoint is present or a
        non-standardised error is returned.

    """
    requests_kwargs = requests_kwargs or {}

    if token_type_hint == TokenType.REFRESH_TOKEN and isinstance(token, BearerToken):
        if token.refresh_token is None:
            msg = "The supplied BearerToken doesn't have a refresh token."
            raise ValueError(msg)
        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(
        "revocation_endpoint",
        data=data,
        auth=self.auth,
        on_success=lambda resp: True,
        on_failure=self.on_revocation_error,
        **requests_kwargs,
    )

on_revocation_error(response)

Error handler for revoke_token().

Invoked by revoke_token() when the revocation endpoint returns an error.

Parameters:

Name Type Description Default
response Response

the Response as returned by the Revocation Endpoint

required

Returns:

Type Description
bool

False to signal that an error occurred. May raise exceptions instead depending on the

bool

revocation response.

Source code in requests_oauth2client/client.py
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
def on_revocation_error(self, response: requests.Response) -> bool:
    """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 [Response][requests.Response] as returned by the Revocation Endpoint

    Returns:
        `False` to signal that an error occurred. May raise exceptions instead depending on the
        revocation 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, RevocationError)
        exception = exception_class(error, error_description, error_uri)
    except Exception:
        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.

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

    """
    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:
                msg = "The supplied BearerToken doesn't have a refresh token."
                raise ValueError(msg)
            else:
                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 ValueError(msg)

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

    return self._request(
        "introspection_endpoint",
        data=data,
        auth=self.auth,
        on_success=self.parse_introspection_response,
        on_failure=self.on_introspection_error,
        **requests_kwargs,
    )

parse_introspection_response(response)

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

Returns:

Type Description
Any

the decoded JSON content, or a str with the content.

Source code in requests_oauth2client/client.py
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
def parse_introspection_response(self, response: requests.Response) -> 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.

    Returns:
        the decoded JSON content, or a `str` with the content.

    """
    try:
        return response.json()
    except ValueError:
        return response.text

on_introspection_error(response)

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

Returns:

Type Description
Any

usually raises exceptions. A subclass can return a default response instead.

Source code in requests_oauth2client/client.py
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
def on_introspection_error(self, response: requests.Response) -> Any:
    """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.

    Returns:
        usually raises exceptions. A subclass can return a default response instead.

    """
    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(error, error_description, error_uri)
    except Exception as exc:
        raise UnknownIntrospectionError(response) 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

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

    """
    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 ValueError(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 ValueError(msg)

    requests_kwargs = requests_kwargs or {}

    if scope is not None and not isinstance(scope, str):
        try:
            scope = " ".join(scope)
        except Exception as exc:
            msg = "Unsupported `scope` value"
            raise ValueError(msg) from exc

    if acr_values is not None and not isinstance(acr_values, str):
        try:
            acr_values = " ".join(acr_values)
        except Exception as exc:
            msg = "Unsupported `acr_values`"
            raise ValueError(msg) 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(
        "backchannel_authentication_endpoint",
        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)

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

Returns:

Type Description
BackChannelAuthenticationResponse

a BackChannelAuthenticationResponse

Source code in requests_oauth2client/client.py
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
def parse_backchannel_authentication_response(
    self, response: requests.Response
) -> 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.

    Returns:
        a `BackChannelAuthenticationResponse`

    """
    try:
        return BackChannelAuthenticationResponse(**response.json())
    except TypeError as exc:
        raise InvalidBackChannelAuthenticationResponse(response) from exc

on_backchannel_authentication_error(response)

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

Returns:

Type Description
BackChannelAuthenticationResponse

usually raises an exception. But a subclass can return a default response instead.

Source code in requests_oauth2client/client.py
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
def on_backchannel_authentication_error(self, response: requests.Response) -> 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.

    Returns:
        usually raises an exception. But a subclass can return a default response instead.

    """
    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(error, error_description, error_uri)
    except Exception as exc:
        raise InvalidBackChannelAuthenticationResponse(response) 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

Source code in requests_oauth2client/client.py
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
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

    """
    requests_kwargs = requests_kwargs or {}

    return self._request(
        "device_authorization_endpoint",
        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)

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

Returns:

Type Description
DeviceAuthorizationResponse

a DeviceAuthorizationResponse as returned by AS

Source code in requests_oauth2client/client.py
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
def parse_device_authorization_response(self, response: requests.Response) -> 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.

    Returns:
        a `DeviceAuthorizationResponse` as returned by AS

    """
    device_authorization_response = DeviceAuthorizationResponse(**response.json())
    return device_authorization_response

on_device_authorization_error(response)

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

Returns:

Type Description
DeviceAuthorizationResponse

usually raises an Exception. But a subclass may return a default response instead.

Source code in requests_oauth2client/client.py
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
def on_device_authorization_error(self, response: requests.Response) -> 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.

    Returns:
        usually raises an Exception. But a subclass may return a default response instead.

    """
    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, error, error_description, error_uri)
    except Exception as exc:
        raise InvalidDeviceAuthorizationResponse(response) 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
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
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 {}

    jwks = self._request(
        "jwks_uri",
        auth=None,
        method="GET",
        on_success=lambda resp: resp.json(),
        on_failure=lambda resp: resp.raise_for_status(),
        **requests_kwargs,
    )
    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

Initialise an OAuth2Client based on Authorization Server Metadata.

This will retrieve the standardised metadata document available at url, and will extract all Endpoint Uris from that document, will fetch the current public keys from its jwks_uri, then will initialise an OAuth2Client based on those endpoints.

Parameters:

Name Type Description Default
url str | None

the url where the server metadata 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
issuer str | None

if an issuer is given, check that it matches the one from the retrieved document

None
testing bool

if True, don't try to validate 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 endpoint initialised based on the obtained metadata

Raises:

Type Description
ValueError

if neither url nor issuer are suitable urls

HTTPError

if an error happens while fetching the documents

Source code in requests_oauth2client/client.py
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
@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:
    """Initialise an OAuth2Client based on Authorization Server Metadata.

    This will retrieve the standardised metadata document available at `url`, and will extract
    all Endpoint Uris from that document, will fetch the current public keys from its
    `jwks_uri`, then will initialise an OAuth2Client based on those endpoints.

    Args:
         url: the url where the server metadata 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
         issuer: if an issuer is given, check that it matches the one from the retrieved document
         testing: if True, don't try to validate the endpoint urls that are part of the document
         **kwargs: additional keyword parameters to pass to OAuth2Client

    Returns:
        an OAuth2Client with endpoint initialised based on the obtained metadata

    Raises:
        ValueError: if neither `url` nor `issuer` are suitable urls
        requests.HTTPError: if an error happens while fetching the documents

    """
    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 ValueError(msg)

    validate_endpoint_uri(url, path=False)

    session = session or requests.Session()
    discovery = session.get(url).json()

    jwks_uri = discovery.get("jwks_uri")
    if jwks_uri:
        jwks = JwkSet(session.get(jwks_uri).json())

    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, session=None, https=True, testing=False, **kwargs) classmethod

Initialise an OAuth2Client, based on the server metadata from discovery.

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
session Session | None

a requests Session to use to retrieve the document and initialise the client with

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

Source code in requests_oauth2client/client.py
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
@classmethod
def from_discovery_document(  # noqa: PLR0913
    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,
    session: requests.Session | None = None,
    https: bool = True,
    testing: bool = False,
    **kwargs: Any,
) -> OAuth2Client:
    """Initialise an OAuth2Client, based on the server metadata from `discovery`.

    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
         session: a requests Session to use to retrieve the document and initialise the client with
         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`

    """
    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 = "Mismatching issuer value in discovery document: "
        raise ValueError(
            msg,
            issuer,
            discovery.get("issuer"),
        )
    elif issuer is None:
        issuer = discovery.get("issuer")

    token_endpoint = discovery.get("token_endpoint")
    if token_endpoint is None:
        msg = "token_endpoint not found in that discovery document"
        raise ValueError(msg)
    authorization_endpoint = discovery.get("authorization_endpoint")
    revocation_endpoint = discovery.get("revocation_endpoint")
    introspection_endpoint = discovery.get("introspection_endpoint")
    userinfo_endpoint = discovery.get("userinfo_endpoint")
    jwks_uri = discovery.get("jwks_uri")
    if jwks_uri is not None:
        validate_endpoint_uri(jwks_uri, https=https)
    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,
        jwks_uri=jwks_uri,
        authorization_server_jwks=authorization_server_jwks,
        auth=auth,
        client_id=client_id,
        client_secret=client_secret,
        private_key=private_key,
        session=session,
        issuer=issuer,
        authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
        testing=testing,
        **kwargs,
    )

BaseClientAuthenticationMethod

Bases: AuthBase

Base class for all Client Authentication methods. This extends [requests.auth.AuthBase].

This base class only checks that requests are suitable to add Client Authentication parameters to, and doesn't modify the request.

Source code in requests_oauth2client/client_authentication.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class BaseClientAuthenticationMethod(requests.auth.AuthBase):
    """Base class for all Client Authentication methods. This extends [requests.auth.AuthBase].

    This base class only checks that requests are suitable to add Client Authentication parameters
    to, and doesn't modify the request.

    """

    def __init__(self, client_id: str):
        self.client_id = str(client_id)

    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,
        ):
            msg = "This request is not suitable for OAuth 2.0 Client Authentication"
            raise RuntimeError(msg)
        return request

ClientAssertionAuthenticationMethod

Bases: BaseClientAuthenticationMethod

Base class for assertion-based client authentication methods.

Parameters:

Name Type Description Default
client_id str

the client_id to use

required
alg str

the alg to use to sign generated Client Assertions.

required
lifetime int

the lifetime to use for generated Client Assertions.

required
jti_gen Callable[[], str]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

required
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.

None
Source code in requests_oauth2client/client_authentication.py
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
class ClientAssertionAuthenticationMethod(BaseClientAuthenticationMethod):
    """Base class for assertion-based client authentication methods.

    Args:
        client_id: the client_id to use
        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.

    """

    def __init__(
        self,
        client_id: str,
        alg: str,
        lifetime: int,
        jti_gen: Callable[[], str],
        aud: str | None = None,
    ) -> None:
        super().__init__(client_id)
        self.alg = alg
        self.lifetime = lifetime
        self.jti_gen = jti_gen
        self.aud = aud

    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()  # pragma: no cover

    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:
            msg = "No url defined for this request. This should never happen..."  # pragma: no cover
            raise ValueError(msg)  # 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
158
159
160
161
162
163
164
165
166
167
168
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()  # pragma: no cover

ClientSecretBasic

Bases: BaseClientAuthenticationMethod

Implement client_secret_basic authentication.

With this method, the client sends its Client ID and Secret, in the Authorization header, with the "Basic" scheme, in each authenticated request to the AS.

Parameters:

Name Type Description Default
client_id str

client_id to use.

required
client_secret str

client_secret to use.

required
Source code in requests_oauth2client/client_authentication.py
59
60
61
62
63
64
65
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
class ClientSecretBasic(BaseClientAuthenticationMethod):
    """Implement `client_secret_basic` authentication.

    With this method, the client sends its Client ID and Secret, in the Authorization header, with
    the "Basic" scheme, in each authenticated request to the AS.

    Args:
        client_id: `client_id` to use.
        client_secret: `client_secret` to use.

    """

    def __init__(self, client_id: str, client_secret: str):
        super().__init__(client_id)
        self.client_secret = str(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: a [requests.PreparedRequest][].

        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: ClientAssertionAuthenticationMethod

Implement client_secret_jwt client authentication method.

With this method, the client generates and signs a client assertion that is symmetrically signed 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[[], Any]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

lambda: uuid4()
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.

None
Source code in requests_oauth2client/client_authentication.py
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
class ClientSecretJwt(ClientAssertionAuthenticationMethod):
    """Implement `client_secret_jwt` client authentication method.

    With this method, the client generates and signs a client assertion that is symmetrically
    signed 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.

    """

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        alg: str = "HS256",
        lifetime: int = 60,
        jti_gen: Callable[[], Any] = lambda: uuid4(),
        aud: str | None = None,
    ) -> None:
        super().__init__(client_id, alg, lifetime, jti_gen, aud)
        self.client_secret = str(client_secret)

    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
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
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 to use.

required
client_secret str

client_secret to use.

required
Source code in requests_oauth2client/client_authentication.py
 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
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` to use.
        client_secret: `client_secret` to use.

    """

    def __init__(self, client_id: str, client_secret: str) -> None:
        super().__init__(client_id)
        self.client_secret = str(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

PrivateKeyJwt

Bases: ClientAssertionAuthenticationMethod

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.

Parameters:

Name Type Description Default
client_id str

the client_id to use.

required
private_jwk Jwk | dict[str, Any]

the private JWK to use to sign generated Client Assertions.

required
alg str

the alg to use to sign generated Client Assertions.

RS256
lifetime int

the lifetime to use for generated Client Assertions.

60
jti_gen Callable[[], Any]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

lambda: uuid4()
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.k

None
Source code in requests_oauth2client/client_authentication.py
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
class PrivateKeyJwt(ClientAssertionAuthenticationMethod):
    """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.

    Args:
        client_id: the `client_id` to use.
        private_jwk: the private JWK 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

    """

    def __init__(
        self,
        client_id: str,
        private_jwk: Jwk | dict[str, Any],
        alg: str = SignatureAlgs.RS256,
        lifetime: int = 60,
        jti_gen: Callable[[], Any] = lambda: uuid4(),
        aud: str | None = None,
    ) -> None:
        if not isinstance(private_jwk, Jwk):
            private_jwk = Jwk(private_jwk)

        if not private_jwk.is_private or private_jwk.is_symmetric:
            msg = "Private Key JWT client authentication method uses asymmetric signing thus requires a private key."
            raise ValueError(msg)

        alg = private_jwk.alg or alg
        if not alg:
            msg = "An asymmetric signing alg is required, either as part of the private JWK, or passed as parameter."
            raise ValueError(msg)
        kid = private_jwk.get("kid")
        if not kid:
            msg = "Asymmetric signing requires the private JWK to have a Key ID (kid)."
            raise ValueError(msg)

        super().__init__(client_id, alg, lifetime, jti_gen, aud)
        self.private_jwk = private_jwk

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

Parameters:

Name Type Description Default
client_id str

the client_id to use.

required
Source code in requests_oauth2client/client_authentication.py
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
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.

    Args:
        client_id: the client_id to use.

    """

    def __init__(self, client_id: str) -> None:
        self.client_id = client_id

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the `client_id` field in the request body.

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

        Returns:
            a [requests.PreparedRequest][] with the added `client_id` 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

DeviceAuthorizationPoolingJob

Bases: TokenEndpointPoolingJob

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.

{}

auth=("client_id", "client_secret") ) pool_job = DeviceAuthorizationPoolingJob(client=client, device_code="my_device_code")

1
token = None while token is None: token = pool_job() ```
Source code in requests_oauth2client/device_authorization.py
 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
class DeviceAuthorizationPoolingJob(TokenEndpointPoolingJob):
    """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.

    Usage: ```python client = OAuth2Client( token_endpoint="https://my.as.local/token",
    auth=("client_id", "client_secret") ) pool_job = DeviceAuthorizationPoolingJob(client=client,
    device_code="my_device_code")

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

    """

    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,
    ):
        super().__init__(
            client=client,
            interval=interval,
            slow_down_interval=slow_down_interval,
            requests_kwargs=requests_kwargs,
            **token_kwargs,
        )
        self.device_code = device_code

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

        This actually calls [OAuth2Client.device_code(device_code)] on `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 client.

Returns:

Type Description
BearerToken
Source code in requests_oauth2client/device_authorization.py
111
112
113
114
115
116
117
118
119
120
def token_request(self) -> BearerToken:
    """Implement the Device Code token request.

    This actually calls [OAuth2Client.device_code(device_code)] on `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
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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,
    ):
        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
56
57
58
59
60
61
62
63
64
65
66
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

AccessDenied

Bases: EndpointError

Raised when the Authorization Server returns error = access_denied.

Source code in requests_oauth2client/exceptions.py
97
98
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
172
173
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
125
126
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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, error: str, description: str | None = None, uri: str | None = None):
        self.error = error
        self.description = description
        self.uri = uri

BackChannelAuthenticationError

Bases: EndpointError

Base class for errors returned by the BackChannel Authentication endpoint.

Source code in requests_oauth2client/exceptions.py
269
270
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
180
181
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
121
122
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 requests.PreparedResponse 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 requests.PreparedResponse 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,
        error: str,
        description: str | None = None,
        uri: str | None = None,
    ):
        super().__init__(response)
        self.error = error
        self.description = description
        self.uri = uri

ExpiredAccessToken

Bases: RuntimeError

Raised when an expired access token is used.

Source code in requests_oauth2client/exceptions.py
61
62
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/exceptions.py
265
266
class ExpiredIdToken(InvalidIdToken):
    """Raised when the returned ID Token is expired."""

ExpiredToken

Bases: TokenEndpointError

Raised when the Token Endpoint returns error = expired_token.

Source code in requests_oauth2client/exceptions.py
133
134
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
164
165
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
113
114
class IntrospectionError(EndpointError):
    """Base class for Introspection Endpoint errors."""

InvalidAuthResponse

Bases: Exception

Raised when the Authorization Endpoint returns an invalid response.

Source code in requests_oauth2client/exceptions.py
184
185
class InvalidAuthResponse(Exception):
    """Raised when the Authorization Endpoint returns an invalid response."""

InvalidBackChannelAuthenticationResponse

Bases: OAuth2Error

Raised when the BackChannel Authentication endpoint returns a non-standard response.

Source code in requests_oauth2client/exceptions.py
273
274
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
81
82
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
137
138
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
93
94
class InvalidGrant(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_grant`."""

InvalidIdToken

Bases: InvalidJwt

Raised when trying to validate an invalid ID Token value.

Source code in requests_oauth2client/exceptions.py
141
142
class InvalidIdToken(InvalidJwt):
    """Raised when trying to validate an invalid ID Token value."""

InvalidPushedAuthorizationResponse

Bases: OAuth2Error

Raised when the Pushed Authorization Endpoint returns an error.

Source code in requests_oauth2client/exceptions.py
277
278
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
77
78
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
85
86
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
89
90
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
57
58
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
168
169
class LoginRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = login_required`."""

MismatchingAcr

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/exceptions.py
244
245
246
247
248
249
250
class MismatchingAcr(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.

    """

MismatchingAudience

Bases: InvalidIdToken

Raised when the ID Token audience does not include the requesting Client ID.

Source code in requests_oauth2client/exceptions.py
253
254
class MismatchingAudience(InvalidIdToken):
    """Raised when the ID Token audience does not include the requesting Client ID."""

MismatchingAzp

Bases: InvalidIdToken

Raised when the ID Token Authorized Presenter (azp) claim is not the Client ID.

Source code in requests_oauth2client/exceptions.py
257
258
class MismatchingAzp(InvalidIdToken):
    """Raised when the ID Token Authorized Presenter (azp) claim is not the Client ID."""

MismatchingIdTokenAlg

Bases: InvalidIdToken

Raised when the returned ID Token is signed with an unexpected alg.

Source code in requests_oauth2client/exceptions.py
261
262
class MismatchingIdTokenAlg(InvalidIdToken):
    """Raised when the returned ID Token is signed with an unexpected alg."""

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
226
227
228
229
230
231
232
class MismatchingIssuer(InvalidAuthResponse):
    """Raised on mismatching `iss` value.

    This happens when the Authorization Endpoints returns an 'iss' that doesn't match the expected
    value.

    """

MismatchingNonce

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/exceptions.py
235
236
237
238
239
240
241
class MismatchingNonce(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.

    """

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
217
218
219
220
221
222
223
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.

    """

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
188
189
190
191
192
193
194
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.

    """

MissingIdToken

Bases: InvalidAuthResponse

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/exceptions.py
208
209
210
211
212
213
214
class MissingIdToken(InvalidAuthResponse):
    """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.

    """

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
197
198
199
200
201
202
203
204
205
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.

    """

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
Source code in requests_oauth2client/exceptions.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class OAuth2Error(Exception):
    """Base class for Exceptions raised when a backend endpoint returns an error.

    Args:
        response: the HTTP response containing the error

    """

    def __init__(self, response: requests.Response):
        self.response = response

    @property
    def request(self) -> requests.PreparedRequest:
        """The request leading to the error."""
        return self.response.request

request: requests.PreparedRequest property

The request leading to the error.

RevocationError

Bases: EndpointError

Base class for Revocation Endpoint errors.

Source code in requests_oauth2client/exceptions.py
105
106
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
69
70
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
176
177
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
129
130
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
73
74
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
101
102
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
117
118
class UnknownIntrospectionError(OAuth2Error):
    """Raised when the Introspection Endpoint returns a non-standard error."""

UnknownTokenEndpointError

Bases: EndpointError

Raised when an otherwise unknown error is returned by the token endpoint.

Source code in requests_oauth2client/exceptions.py
65
66
class UnknownTokenEndpointError(EndpointError):
    """Raised when an otherwise unknown error is returned by the token endpoint."""

UnsupportedTokenType

Bases: RevocationError

Raised when the Revocation endpoint returns error = unsupported_token_type.

Source code in requests_oauth2client/exceptions.py
109
110
class UnsupportedTokenType(RevocationError):
    """Raised when the Revocation endpoint returns `error = unsupported_token_type`."""

TokenEndpointPoolingJob

Bases: ABC

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.

Parameters:

Name Type Description Default
client OAuth2Client

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

required
interval int | None

initial pooling interval, in seconds. If None, default to 5.

None
slow_down_interval int

when a SlowDown is received, this number of seconds will be added to the pooling interval.

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

{}
Source code in requests_oauth2client/pooling.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
class TokenEndpointPoolingJob(ABC):
    """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.

    Args:
        client: the [OAuth2Client][requests_oauth2client.client.OAuth2Client] that will be used
            to pool the token endpoint.
        interval: initial pooling interval, in seconds. If `None`, default to `5`.
        slow_down_interval: when a [SlowDown][requests_oauth2client.exceptions.SlowDown] is
            received, this number of seconds will be added to the pooling interval.
        requests_kwargs: additional parameters for the underlying calls to [requests.request][]
        **token_kwargs: additional parameters for the token request

    """

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

    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][requests_oauth2client.tokens.BearerToken] if the AS returns one, or
            `None` if the Authorization is still pending.

        """
        time.sleep(self.interval)
        try:
            return self.token_request()
        except SlowDown:
            self.interval += self.slow_down_interval
        except AuthorizationPending:
            pass
        return None

    @abstractmethod
    def token_request(self) -> BearerToken:
        """Abstract method for the token endpoint call.

        This must be implemented by subclasses. This method must 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  # pragma: no cover

token_request() abstractmethod

Abstract method for the token endpoint call.

This must be implemented by subclasses. This method must 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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@abstractmethod
def token_request(self) -> BearerToken:
    """Abstract method for the token endpoint call.

    This must be implemented by subclasses. This method must 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  # pragma: no cover

BearerToken

Bases: AccessToken

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
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
@frozen(init=False)
class BearerToken(AccessToken):
    """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] = AccessTokenType.BEARER.value

    access_token: str
    expires_at: datetime | None = None
    scope: str | None = None
    refresh_token: str | None = None
    token_type: str = TOKEN_TYPE
    id_token: IdToken | jwskate.JweCompact | None = None
    kwargs: dict[str, Any] = Factory(dict)

    @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,
    ):
        if token_type.title() != self.TOKEN_TYPE.title():
            msg = f"Token Type is not '{self.TOKEN_TYPE}'!"
            raise ValueError(msg, token_type)
        id_token_jwt: IdToken | jwskate.JweCompact | None = None
        if isinstance(id_token, (str, bytes)):
            try:
                id_token_jwt = IdToken(id_token)
            except jwskate.InvalidJwt:
                try:
                    id_token_jwt = jwskate.JweCompact(id_token)
                except jwskate.InvalidJwe:
                    msg = "ID Token is invalid because it is  neither a JWT or a JWE."
                    raise InvalidIdToken(msg) from None
        else:
            id_token_jwt = 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_jwt,
            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"Bearer {self.access_token}"

    def validate_id_token(self, client: OAuth2Client, azr: AuthorizationResponse) -> Self:  # noqa: C901, PLR0915
        """Validate that a token response is valid, and return the 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).

        If the ID Token is encrypted, this decrypts it and returns the clear-text ID Token.

        """
        if not self.id_token:
            raise MissingIdToken()

        raw_id_token = self.id_token

        if isinstance(raw_id_token, jwskate.JweCompact) and client.id_token_encrypted_response_alg is None:
            msg = "ID Token is encrypted while it should be clear-text"
            raise InvalidIdToken(msg, self)
        elif isinstance(raw_id_token, IdToken) and client.id_token_encrypted_response_alg is not None:
            msg = "ID Token is clear-text while it should be encrypted"
            raise InvalidIdToken(msg, self)

        if isinstance(raw_id_token, jwskate.JweCompact):
            enc_jwk = client.id_token_decryption_key
            if enc_jwk is None:
                msg = "ID Token is encrypted but client does not have a decryption key"
                raise InvalidIdToken(msg, self)
            nested_id_token = raw_id_token.decrypt(enc_jwk)
            id_token = IdToken(nested_id_token)
        else:
            id_token = raw_id_token

        if id_token.get_header("alg") is None and client.id_token_signed_response_alg is None:
            msg = (
                "ID 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)
        elif 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)

        id_token_alg = id_token.alg or client.id_token_signed_response_alg

        if azr.issuer and id_token.issuer != azr.issuer:
            raise MismatchingIssuer(id_token.issuer, azr.issuer, self)

        if id_token.audiences and client.client_id not in id_token.audiences:
            raise MismatchingAudience(id_token.audiences, client.client_id, self)

        if id_token.get_claim("azp") is not None and id_token.azp != client.client_id:
            raise MismatchingAzp(id_token.azp, client.client_id, self)

        if id_token.is_expired():
            raise ExpiredIdToken(id_token)

        if azr.nonce and id_token.nonce != azr.nonce:
            raise MismatchingNonce()

        if azr.acr_values and id_token.acr not in azr.acr_values:
            raise MismatchingAcr(id_token.acr, azr.acr_values)

        hash_function: Callable[[str], str]  # method used to calculate at_hash, s_hash, etc.

        if id_token_alg in jwskate.SignatureAlgs.ALL_SYMMETRIC:
            if not client.client_secret:
                msg = "ID Token is symmetrically signed but this client does not have a Client Secret."
                raise InvalidIdToken(msg)
            id_token.verify_signature(jwskate.SymmetricJwk.from_bytes(client.client_secret), alg=id_token_alg)
        elif id_token_alg in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
            if not client.authorization_server_jwks:
                msg = "ID Token is asymmetrically signed but the Authorization Server JWKS is not available."
                raise InvalidIdToken(msg)

            if id_token.get_header("kid") is None:
                msg = (
                    "ID Token does not contain a Key ID (kid) to specify the asymmetric key "
                    "to use for signature verification."
                )
                raise InvalidIdToken(msg)
            try:
                verification_jwk = client.authorization_server_jwks.get_jwk_by_kid(id_token.kid)
            except KeyError:
                msg = (
                    f"ID Token is asymmetrically signed but its Key ID '{id_token.kid}' "
                    "is not part of the Authorization Server JWKS."
                )
                raise InvalidIdToken(msg) from None

            if id_token_alg not in verification_jwk.supported_signing_algorithms():
                msg = "ID Token is asymmetrically signed but its algorithm is not supported by the verification key."
                raise InvalidIdToken(msg)

            id_token.verify_signature(verification_jwk, alg=id_token_alg)

            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)

        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)

        s_hash = id_token.get_claim("s_hash")
        if s_hash is not None:
            if azr.state is None:
                msg = "ID Token has a 's_hash' claim but no state was included in the request."
                raise InvalidIdToken(msg)
            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)

        if azr.max_age is not None:
            try:
                auth_time = id_token.auth_time
            except AttributeError:
                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) from None
            auth_age = datetime.now(tz=timezone.utc) - auth_time
            if auth_age.seconds > azr.max_age + 60:
                msg = (
                    "User authentication happened too long ago. The `auth_time` parameter from"
                    " the ID Token indicate that the last Authentication Time was at"
                    f" {auth_time} ({auth_age.seconds} sec ago), but the authorization request"
                    f" `max_age` parameter specified that it must be maximum {azr.max_age} sec"
                    " ago."
                )
                raise InvalidIdToken(msg)

        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 int(self.expires_at.timestamp() - datetime.now(tz=timezone.utc).timestamp())
        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)

expires_in: int | None property

Number of seconds until expiration.

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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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
192
193
194
195
196
197
198
199
200
201
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"Bearer {self.access_token}"

validate_id_token(client, azr)

Validate that a token response is valid, and return the ID Token.

This will validate the id_token as described in OIDC 1.0 $3.1.3.7.

If the ID Token is encrypted, this decrypts it and returns the clear-text ID Token.

Source code in requests_oauth2client/tokens.py
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
def validate_id_token(self, client: OAuth2Client, azr: AuthorizationResponse) -> Self:  # noqa: C901, PLR0915
    """Validate that a token response is valid, and return the 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).

    If the ID Token is encrypted, this decrypts it and returns the clear-text ID Token.

    """
    if not self.id_token:
        raise MissingIdToken()

    raw_id_token = self.id_token

    if isinstance(raw_id_token, jwskate.JweCompact) and client.id_token_encrypted_response_alg is None:
        msg = "ID Token is encrypted while it should be clear-text"
        raise InvalidIdToken(msg, self)
    elif isinstance(raw_id_token, IdToken) and client.id_token_encrypted_response_alg is not None:
        msg = "ID Token is clear-text while it should be encrypted"
        raise InvalidIdToken(msg, self)

    if isinstance(raw_id_token, jwskate.JweCompact):
        enc_jwk = client.id_token_decryption_key
        if enc_jwk is None:
            msg = "ID Token is encrypted but client does not have a decryption key"
            raise InvalidIdToken(msg, self)
        nested_id_token = raw_id_token.decrypt(enc_jwk)
        id_token = IdToken(nested_id_token)
    else:
        id_token = raw_id_token

    if id_token.get_header("alg") is None and client.id_token_signed_response_alg is None:
        msg = (
            "ID 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)
    elif 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)

    id_token_alg = id_token.alg or client.id_token_signed_response_alg

    if azr.issuer and id_token.issuer != azr.issuer:
        raise MismatchingIssuer(id_token.issuer, azr.issuer, self)

    if id_token.audiences and client.client_id not in id_token.audiences:
        raise MismatchingAudience(id_token.audiences, client.client_id, self)

    if id_token.get_claim("azp") is not None and id_token.azp != client.client_id:
        raise MismatchingAzp(id_token.azp, client.client_id, self)

    if id_token.is_expired():
        raise ExpiredIdToken(id_token)

    if azr.nonce and id_token.nonce != azr.nonce:
        raise MismatchingNonce()

    if azr.acr_values and id_token.acr not in azr.acr_values:
        raise MismatchingAcr(id_token.acr, azr.acr_values)

    hash_function: Callable[[str], str]  # method used to calculate at_hash, s_hash, etc.

    if id_token_alg in jwskate.SignatureAlgs.ALL_SYMMETRIC:
        if not client.client_secret:
            msg = "ID Token is symmetrically signed but this client does not have a Client Secret."
            raise InvalidIdToken(msg)
        id_token.verify_signature(jwskate.SymmetricJwk.from_bytes(client.client_secret), alg=id_token_alg)
    elif id_token_alg in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
        if not client.authorization_server_jwks:
            msg = "ID Token is asymmetrically signed but the Authorization Server JWKS is not available."
            raise InvalidIdToken(msg)

        if id_token.get_header("kid") is None:
            msg = (
                "ID Token does not contain a Key ID (kid) to specify the asymmetric key "
                "to use for signature verification."
            )
            raise InvalidIdToken(msg)
        try:
            verification_jwk = client.authorization_server_jwks.get_jwk_by_kid(id_token.kid)
        except KeyError:
            msg = (
                f"ID Token is asymmetrically signed but its Key ID '{id_token.kid}' "
                "is not part of the Authorization Server JWKS."
            )
            raise InvalidIdToken(msg) from None

        if id_token_alg not in verification_jwk.supported_signing_algorithms():
            msg = "ID Token is asymmetrically signed but its algorithm is not supported by the verification key."
            raise InvalidIdToken(msg)

        id_token.verify_signature(verification_jwk, alg=id_token_alg)

        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)

    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)

    s_hash = id_token.get_claim("s_hash")
    if s_hash is not None:
        if azr.state is None:
            msg = "ID Token has a 's_hash' claim but no state was included in the request."
            raise InvalidIdToken(msg)
        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)

    if azr.max_age is not None:
        try:
            auth_time = id_token.auth_time
        except AttributeError:
            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) from None
        auth_age = datetime.now(tz=timezone.utc) - auth_time
        if auth_age.seconds > azr.max_age + 60:
            msg = (
                "User authentication happened too long ago. The `auth_time` parameter from"
                " the ID Token indicate that the last Authentication Time was at"
                f" {auth_time} ({auth_age.seconds} sec ago), but the authorization request"
                f" `max_age` parameter specified that it must be maximum {azr.max_age} sec"
                " ago."
            )
            raise InvalidIdToken(msg)

    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
362
363
364
365
366
367
368
369
370
371
372
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
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
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,
    ):
        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

        """
        return BinaPy.serialize_to("json", token.as_dict()).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
420
421
422
423
424
425
426
427
428
429
430
431
@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

    """
    return BinaPy.serialize_to("json", token.as_dict()).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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
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
452
453
454
455
456
457
458
459
460
461
462
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
464
465
466
467
468
469
470
471
472
473
474
def loads(self, serialized: str) -> BearerToken:
    """Deserialize a serialized token.

    Args:
        serialized: the serialized token

    Returns:
        the deserialized token

    """
    return self.loader(serialized)

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
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 auth_time(self) -> datetime:
        """The last user authentication time."""
        auth_time = self.claims.get("auth_time")
        if auth_time:
            return self.timestamp_to_datetime(auth_time)
        msg = "This ID Token doesn't have an `auth_time` attribute."
        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, s_hash.

        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

auth_time: datetime property

The last user authentication time.

hash_method(key, alg=None) classmethod

Returns a callable that generates valid OIDC hashes, such as at_hash, c_hash, s_hash.

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
63
64
65
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
@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, s_hash.

    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

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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)

api_client

ApiClient 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".

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

An 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.

Usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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']

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. Note that this is the default behavior of requests.
  • 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 behaviour.

('true', 'false')
session Session | None

a preconfigured requests.Session to use with this ApiClient.

None
**session_kwargs Any

additional kwargs to configure the underlying requests.Session.

{}
Source code in requests_oauth2client/api_client.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 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
@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"`.

    `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.

    An `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].

    Usage:
        ```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: 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`. Note that this is
            the default behavior of `requests`.
            - 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 behaviour.
        session: a preconfigured `requests.Session` to use with this `ApiClient`.
        **session_kwargs: additional kwargs to configure the underlying `requests.Session`.

    """

    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"
    session: requests.Session = field(factory=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"),
        session: requests.Session | None = None,
        **session_kwargs: Any,
    ):
        session = session or requests.Session()
        for key, val in session_kwargs.items():
            setattr(session, key, val)

        if bool_fields is None:
            bool_fields = (True, False)

        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,
        url: 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:
        """Overridden `request` method with extra features.

        Features added compared to plain request():

        - takes a relative path instead of a full url, which will be appended to the
          base_url
        - it can raise an exception when the API returns a non-success status code
        - allow_redirects is False by default (since API usually don't use redirects)
        - `data` or `json` fields with value `None` can either be included or excluded from the
          request
        - boolean fields can be serialized to `'true'` or `'false'` instead of `'True'` and
          `'False'`

        Args:
          method: the HTTP method to use
          url: 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. Can also be 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.__init__`,
            but this will be applied for this request only.
          none_fields: like the parameter of the same name from `ApiClient.__init__`,
            but this will be applied for this request only.
          bool_fields: like the parameter of the same name from `ApiClient.__init__`,
            but this will be applied for this request only.

        Returns:
          a [requests.Response][] as returned by requests

        """
        url = self.to_absolute_url(url)

        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:
            try:
                true_value, false_value = bool_fields
            except ValueError:
                msg = "Invalid value for 'bool_fields'. Must be a 2 value tuple, with (true_value, false_value)."
                raise ValueError(msg) from None
            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,
            url,
            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, relative_url: None | str | bytes | Iterable[str | bytes | int] = None) -> str:
        """Convert a relative url to an absolute url.

        Given a `relative_url`, 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:
          relative_url: a relative url

        Returns:
          the resulting absolute url

        """
        url = relative_url

        if self.base_url:
            if url is not None:
                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:
                        msg = (
                            "Unexpected url type, please pass a relative path as string or"
                            " bytes, or an iterable of string-able objects"
                        )
                        raise TypeError(
                            msg,
                            type(url),
                        ) from exc

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

                if "://" in url:
                    msg = "url must be relative to root_url"
                    raise ValueError(msg)

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

        if url is None or not isinstance(url, str):
            msg = "Unable to determine an absolute url."
            raise ValueError(msg)

        return url

    def get(
        self,
        url: None | str | bytes | Iterable[str | bytes | int] = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a GET 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:
            url: a url where the request will be sent.
            raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
            **kwargs: Optional arguments that [request()][requests.request] takes.

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

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

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

    def post(
        self,
        url: str | bytes | Iterable[str | bytes] | None = None,
        raise_for_status: bool | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        """Send a POST 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:
          url: an url where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: Optional arguments that ``request`` takes.

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

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

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

    def patch(
        self,
        url: 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` 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:
          url: an url where the request will be sent.
          raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
          **kwargs: Optional arguments that ``request`` takes.

        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", url, raise_for_status=raise_for_status, **kwargs)

    def put(
        self,
        url: 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` 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:
          url: a url 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", url, raise_for_status=raise_for_status, **kwargs)

    def delete(
        self,
        url: 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:
          url: a url 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("DELETE", url, 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 initialised on the new base url

        Usage:
            ```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 initialised on the new base url

        Usage:
            ```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) -> ApiClient:
        """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.

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

        """
        return self

    def __exit__(self, *args: Any) -> None:
        """Close the underlying requests.Session on exit."""
        self.session.close()
request(method, url=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)

Overridden request method with extra features.

Features added compared to plain request():

  • takes a relative path instead of a full url, which will be appended to the base_url
  • it can raise an exception when the API returns a non-success status code
  • allow_redirects is False by default (since API usually don't use redirects)
  • data or json fields with value None can either be included or excluded from the request
  • boolean fields can be serialized to 'true' or 'false' instead of 'True' and 'False'

Parameters:

Name Type Description Default
method str

the HTTP method to use

required
url 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. Can also be 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.__init__, 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.__init__, 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.__init__, but this will be applied for this request only.

None

Returns:

Type Description
Response

a requests.Response as returned by requests

Source code in requests_oauth2client/api_client.py
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
def request(  # noqa: C901, PLR0913, D417
    self,
    method: str,
    url: 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:
    """Overridden `request` method with extra features.

    Features added compared to plain request():

    - takes a relative path instead of a full url, which will be appended to the
      base_url
    - it can raise an exception when the API returns a non-success status code
    - allow_redirects is False by default (since API usually don't use redirects)
    - `data` or `json` fields with value `None` can either be included or excluded from the
      request
    - boolean fields can be serialized to `'true'` or `'false'` instead of `'True'` and
      `'False'`

    Args:
      method: the HTTP method to use
      url: 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. Can also be 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.__init__`,
        but this will be applied for this request only.
      none_fields: like the parameter of the same name from `ApiClient.__init__`,
        but this will be applied for this request only.
      bool_fields: like the parameter of the same name from `ApiClient.__init__`,
        but this will be applied for this request only.

    Returns:
      a [requests.Response][] as returned by requests

    """
    url = self.to_absolute_url(url)

    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:
        try:
            true_value, false_value = bool_fields
        except ValueError:
            msg = "Invalid value for 'bool_fields'. Must be a 2 value tuple, with (true_value, false_value)."
            raise ValueError(msg) from None
        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,
        url,
        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(relative_url=None)

Convert a relative url to an absolute url.

Given a relative_url, 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
relative_url None | str | bytes | Iterable[str | bytes | int]

a relative url

None

Returns:

Type Description
str

the resulting absolute url

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

    Given a `relative_url`, 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:
      relative_url: a relative url

    Returns:
      the resulting absolute url

    """
    url = relative_url

    if self.base_url:
        if url is not None:
            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:
                    msg = (
                        "Unexpected url type, please pass a relative path as string or"
                        " bytes, or an iterable of string-able objects"
                    )
                    raise TypeError(
                        msg,
                        type(url),
                    ) from exc

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

            if "://" in url:
                msg = "url must be relative to root_url"
                raise ValueError(msg)

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

    if url is None or not isinstance(url, str):
        msg = "Unable to determine an absolute url."
        raise ValueError(msg)

    return url
get(url=None, raise_for_status=None, **kwargs)

Send a GET 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
url None | str | bytes | Iterable[str | bytes | int]

a url 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

Optional arguments that request() takes.

{}

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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
def get(
    self,
    url: None | str | bytes | Iterable[str | bytes | int] = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a GET 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:
        url: a url where the request will be sent.
        raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
        **kwargs: Optional arguments that [request()][requests.request] takes.

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

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

    """
    return self.request("GET", url, raise_for_status=raise_for_status, **kwargs)
post(url=None, raise_for_status=None, **kwargs)

Send a POST 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
url str | bytes | Iterable[str | bytes] | None

an url 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

Optional arguments that request takes.

{}

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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def post(
    self,
    url: str | bytes | Iterable[str | bytes] | None = None,
    raise_for_status: bool | None = None,
    **kwargs: Any,
) -> requests.Response:
    """Send a POST 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:
      url: an url where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: Optional arguments that ``request`` takes.

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

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

    """
    return self.request("POST", url, raise_for_status=raise_for_status, **kwargs)
patch(url=None, raise_for_status=None, **kwargs)

Send a PATCH 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
url str | bytes | Iterable[str | bytes] | None

an url 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

Optional arguments that request takes.

{}

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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
def patch(
    self,
    url: 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` 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:
      url: an url where the request will be sent.
      raise_for_status: overrides the `raises_for_status` parameter passed at initialization time.
      **kwargs: Optional arguments that ``request`` takes.

    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", url, raise_for_status=raise_for_status, **kwargs)
put(url=None, raise_for_status=None, **kwargs)

Send a PUT 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
url str | bytes | Iterable[str | bytes] | None

a url 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
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
def put(
    self,
    url: 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` 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:
      url: a url 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", url, raise_for_status=raise_for_status, **kwargs)
delete(url=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
url str | bytes | Iterable[str | bytes] | None

a url 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
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
def delete(
    self,
    url: 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:
      url: a url 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("DELETE", url, raise_for_status=raise_for_status, **kwargs)

auth

This module contains requests-compatible Auth Handlers that implement OAuth 2.0.

BearerAuth

Bases: AuthBase

An Auth Handler that includes a Bearer Token in API calls, as defined in RFC6750$2.1.

As a prerequisite to using this AuthBase, you have to obtain an access token manually. You most likely don't want to do that by yourself, but instead use an instance of OAuth2Client to do that for you. See the others Auth Handlers in this module, which will automatically obtain access tokens from an OAuth 2.x server.

Usage
1
2
auth = BearerAuth("my_access_token")
resp = requests.get("https://my.api.local/resource", auth=auth)

The HTTP request will look like:

1
2
3
GET /resource HTTP/1.1
Host: my.api.local
Authorization: Bearer my_access_token

Parameters:

Name Type Description Default
token str | BearerToken | None

a BearerToken or a string to use as token for this Auth Handler. If None, this Auth Handler is a no-op.

None
Source code in requests_oauth2client/auth.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 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
class BearerAuth(requests.auth.AuthBase):
    """An Auth Handler that includes a Bearer Token in API calls, as defined in [RFC6750$2.1].

    As a prerequisite to using this `AuthBase`, you have to obtain an access token manually.
    You most likely don't want to do that by yourself, but instead use an instance of
    [OAuth2Client][requests_oauth2client.client.OAuth2Client] to do that for you.
    See the others Auth Handlers in this module, which will automatically obtain
    access tokens from an OAuth 2.x server.

    [RFC6750$2.1]: https://datatracker.ietf.org/doc/html/rfc6750#section-2.1

    Usage:
        ```python
        auth = BearerAuth("my_access_token")
        resp = requests.get("https://my.api.local/resource", auth=auth)
        ```

        The HTTP request will look like:
        ```
        GET /resource HTTP/1.1
        Host: my.api.local
        Authorization: Bearer my_access_token
        ```

    Args:
        token: a [BearerToken][requests_oauth2client.tokens.BearerToken] or a string
            to use as token for this Auth Handler. If `None`, this Auth Handler is a no-op.

    """

    def __init__(self, token: str | BearerToken | None = None) -> None:
        self.token = token  # type: ignore[assignment] # until https://github.com/python/mypy/issues/3004 is fixed

    @property
    def token(self) -> BearerToken | None:
        """Return the [BearerToken] that is used for authorization against the API.

        Returns:
            the configured [BearerToken][requests_oauth2client.tokens.BearerToken] used with this
            AuthHandler.

        """
        return self._token

    @token.setter
    def token(self, token: str | BearerToken | None) -> None:
        """Change the access token used with this AuthHandler.

        Accepts a [BearerToken][requests_oauth2client.tokens.BearerToken] or an access token as
        `str`.

        Args:
            token: an access token to use for this Auth Handler

        """
        if token is not None and not isinstance(token, BearerToken):
            token = BearerToken(token)
        self._token = token

    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: a [PreparedRequest][requests.PreparedRequest]

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

        """
        if self.token is None:
            return request
        if self.token.is_expired():
            raise ExpiredAccessToken(self.token)
        request.headers["Authorization"] = self.token.authorization_header()
        return request
token: BearerToken | None property writable

Return the [BearerToken] that is used for authorization against the API.

Returns:

Type Description
BearerToken | None

the configured BearerToken used with this

BearerToken | None

AuthHandler.

BaseOAuth2RenewableTokenAuth

Bases: BearerAuth

Base class for BearerToken-based Auth Handlers, with an obtainable or renewable 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

an OAuth2Client

required
token None | BearerToken | str

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 include in token requests

{}
Source code in requests_oauth2client/auth.py
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
class BaseOAuth2RenewableTokenAuth(BearerAuth):
    """Base class for BearerToken-based Auth Handlers, with an obtainable or renewable 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: an OAuth2Client
        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 include in token requests

    """

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

    @override
    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        token = self.token
        if token is None or token.is_expired(self.leeway):
            self.renew_token()
        return super().__call__(request)

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

        Subclasses should implement this.

        """
        raise NotImplementedError

    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.

Subclasses should implement this.

Source code in requests_oauth2client/auth.py
139
140
141
142
143
144
145
def renew_token(self) -> None:
    """Obtain a new Bearer Token.

    Subclasses should implement this.

    """
    raise NotImplementedError
forget_token()

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

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

OAuth2ClientCredentialsAuth

Bases: BaseOAuth2RenewableTokenAuth

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_kwargs Any

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

{}
Usage
1
2
3
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
class OAuth2ClientCredentialsAuth(BaseOAuth2RenewableTokenAuth):
    """An Auth Handler for the Client Credentials grant.

    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_kwargs: extra kw parameters to pass to the Token Endpoint. May include `scope`, `resource`, etc.

    Usage:
        ```python
        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)
        ```

    """

    @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
172
173
174
175
@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)

OAuth2AccessTokenAuth

Bases: BaseOAuth2RenewableTokenAuth

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

This Requests Auth handler implementation uses an access token as Bearer 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. If a refresh_token and an expiration date are available, this Auth Handler will automatically refresh the access token once it is expired.

Parameters:

Name Type Description Default
client OAuth2Client

the OAuth2Client to use to refresh tokens.

required
token None | BearerToken | str

a access token that has been previously obtained

None
**token_kwargs Any

additional kwargs to pass to the token endpoint

{}
Usage

```python client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret")) token = BearerToken( access_token="access_token", expires_in=600, refresh_token="refresh_token" ) # obtain a BearerToken any way you see fit, including a refresh token oauth2at_auth = OAuth2ClientCredentialsAuth(client, token, scope="my_scope") resp = requests.post("https://my.api.local/resource", auth=oauth2at_auth) ````

Source code in requests_oauth2client/auth.py
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
class OAuth2AccessTokenAuth(BaseOAuth2RenewableTokenAuth):
    """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 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. If a
    refresh_token and an expiration date are available, this Auth Handler will automatically refresh
    the access token once it is expired.

    Args:
        client: the [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to refresh tokens.
        token: a access token that has been previously obtained
        **token_kwargs: additional kwargs to pass to the token endpoint

    Usage:
        ```python
        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        token = BearerToken(
            access_token="access_token", expires_in=600, refresh_token="refresh_token"
        )  # obtain a BearerToken any way you see fit, including a refresh token
        oauth2at_auth = OAuth2ClientCredentialsAuth(client, token, scope="my_scope")
        resp = requests.post("https://my.api.local/resource", auth=oauth2at_auth)
        ````

    """

    @override
    def renew_token(self) -> None:
        """Obtain a new token, using the Refresh Token, if available."""
        if self.token and self.token.refresh_token and self.client is not None:
            self.token = self.client.refresh_token(refresh_token=self.token.refresh_token, **self.token_kwargs)
renew_token()

Obtain a new token, using the Refresh Token, if available.

Source code in requests_oauth2client/auth.py
206
207
208
209
210
@override
def renew_token(self) -> None:
    """Obtain a new token, using the Refresh Token, if available."""
    if self.token and self.token.refresh_token and self.client is not None:
        self.token = self.client.refresh_token(refresh_token=self.token.refresh_token, **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 OAuth2Client to use to obtain Access Tokens.

required
code str | AuthorizationResponse

an Authorization Code that has been obtained from the AS.

required
**token_kwargs Any

additional kwargs to pass to the token endpoint

{}
Usage

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

Source code in requests_oauth2client/auth.py
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
class OAuth2AuthorizationCodeAuth(OAuth2AccessTokenAuth):
    """Authentication handler for the Authorization Code grant.

    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 [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to obtain Access Tokens.
        code: an Authorization Code that has been obtained from the AS.
        **token_kwargs: additional kwargs to pass to the token endpoint

    Usage:
        ```python
        client = OAuth2Client(token_endpoint="https://my.as.local/token", auth=("client_id", "client_secret"))
        code = "my_code"  # you must obtain this code yourself
        resp = requests.post("https://my.api.local/resource", auth=OAuth2AuthorizationCodeAuth(client, code))
        ````

    """

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

    @override
    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: a [PreparedRequest][requests.PreparedRequest]

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

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

    def exchange_code_for_token(self) -> None:
        """Obtain the initial access token with the authorization_code grant."""
        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()

Obtain the initial access token with the authorization_code grant.

Source code in requests_oauth2client/auth.py
262
263
264
265
266
def exchange_code_for_token(self) -> None:
    """Obtain the initial access token with the authorization_code grant."""
    if self.code:  # pragma: no branch
        self.token = self.client.authorization_code(code=self.code, **self.token_kwargs)
        self.code = None

OAuth2ResourceOwnerPasswordAuth

Bases: BaseOAuth2RenewableTokenAuth

Authentication Handler for the Resource Owner Password Flow.

This Requests Auth handler implementation exchanges the user credentials for an Access Token, then automatically obtains a new one once it 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 nor MFA and depends on the user typing its credentials directly inside the application instead of on a dedicated login page, 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 OAuth2Client 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_kwargs Any

additional kwargs to pass to the token endpoint

{}
Source code in requests_oauth2client/auth.py
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
class OAuth2ResourceOwnerPasswordAuth(BaseOAuth2RenewableTokenAuth):
    """Authentication Handler for the [Resource Owner Password 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 obtains a new one once it 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 nor MFA and
    depends on the user typing its credentials directly inside the application instead of on a
    dedicated login page, 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 [OAuth2Client][requests_oauth2client.client.OAuth2Client] to use to obtain
            Access Tokens
        username: the username
        password: the user password
        leeway: an amount of time, in seconds
        **token_kwargs: additional kwargs to pass to the token endpoint

    """

    def __init__(
        self,
        client: OAuth2Client,
        username: str,
        password: str,
        leeway: int = 20,
        **token_kwargs: Any,
    ):
        super().__init__(client=client, leeway=leeway, **token_kwargs)
        self.username = username
        self.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
306
307
308
309
310
311
312
313
@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_kwargs Any

additional kwargs to pass to the token endpoint.

{}
Usage

```python 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
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
class OAuth2DeviceCodeAuth(OAuth2AccessTokenAuth):
    """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_kwargs: additional kwargs to pass to the token endpoint.

    Usage:
        ```python
        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)
        ````
    """

    def __init__(
        self,
        client: OAuth2Client,
        device_code: str | DeviceAuthorizationResponse,
        leeway: int = 20,
        interval: int = 5,
        expires_in: int = 360,
        **token_kwargs: Any,
    ) -> None:
        super().__init__(client=client, leeway=leeway, token=None, **token_kwargs)
        self.device_code: str | DeviceAuthorizationResponse | None = device_code
        self.interval = interval
        self.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

        """
        token = self.token
        if token is None or token.is_expired():
            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,
            )
            while self.token is None:
                self.token = pooling_job()
            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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
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,
        )
        while self.token is None:
            self.token = pooling_job()
        self.device_code = None

authorization_request

Classes and utilities related to Authorization Requests and Responses.

PkceUtils

Contains helper methods for PKCE, as described in RFC7636.

See RFC7636.

Source code in requests_oauth2client/authorization_request.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 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
class PkceUtils:
    """Contains helper methods for PKCE, as described in RFC7636.

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

    """

    code_verifier_re = 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 = "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

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

        if not cls.code_verifier_re.match(verifier):
            msg = f"Invalid code verifier, does not match {cls.code_verifier_re}"
            raise ValueError(
                msg,
                verifier,
            )

        if method == "S256":
            return BinaPy(verifier).to("sha256").to("b64u").ascii()
        elif method == "plain":
            return verifier
        else:
            msg = "Unsupported code_challenge_method"
            raise ValueError(msg, method)

    @classmethod
    def generate_code_verifier_and_challenge(cls, method: str = "S256") -> tuple[str, str]:
        """Generate a valid `code_verifier` and derive its `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 = "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_re.match(verifier) is not None and cls.derive_challenge(verifier, method) == challenge
code_verifier_re = 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
40
41
42
43
44
45
46
47
48
@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='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

Source code in requests_oauth2client/authorization_request.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@classmethod
def derive_challenge(cls, verifier: str | bytes, method: str = "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

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

    if not cls.code_verifier_re.match(verifier):
        msg = f"Invalid code verifier, does not match {cls.code_verifier_re}"
        raise ValueError(
            msg,
            verifier,
        )

    if method == "S256":
        return BinaPy(verifier).to("sha256").to("b64u").ascii()
    elif method == "plain":
        return verifier
    else:
        msg = "Unsupported code_challenge_method"
        raise ValueError(msg, method)
generate_code_verifier_and_challenge(method='S256') classmethod

Generate a valid code_verifier and derive its 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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@classmethod
def generate_code_verifier_and_challenge(cls, method: str = "S256") -> tuple[str, str]:
    """Generate a valid `code_verifier` and derive its `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='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
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@classmethod
def validate_code_verifier(cls, verifier: str, challenge: str, method: str = "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_re.match(verifier) is not None and cls.derive_challenge(verifier, method) == challenge

CodeChallengeMethods

Bases: str, Enum

PKCE Code Challenge Methods.

Source code in requests_oauth2client/authorization_request.py
111
112
113
114
115
class CodeChallengeMethods(str, Enum):
    """PKCE Code Challenge Methods."""

    plain = "plain"
    S256 = "S256"

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, all accessible as 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 AuthorizationRequest

None
code_verifier str | None

the code_verifier matching the code_challenge that was passed as parameter in the AuthorizationRequest

None
state str | None

the state returned by the AS

None
**kwargs str

other parameters as returned by the AS

{}
Source code in requests_oauth2client/authorization_request.py
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
@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, all accessible as 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 AuthorizationRequest
        code_verifier: the code_verifier matching the code_challenge that was passed as
            parameter in the AuthorizationRequest
        state: the state returned by the AS
        **kwargs: other parameters as returned by the AS

    """

    code: str
    redirect_uri: str | None = None
    code_verifier: str | None = None
    state: str | None = None
    nonce: str | None = None
    acr_values: tuple[str, ...] | None = None
    max_age: int | None = None
    issuer: str | None = None
    kwargs: dict[str, Any] = Factory(dict)

    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,
        **kwargs: str,
    ):
        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,
            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. 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, and authorization_response_iss_parameter_supported is False (default), then no issuer check is performed. Set authorization_response_iss_parameter_supported to True to enforce server identification: if no issuer is included in the Authorization Response, then an error will be raised instead.

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.

{}
Source code in requests_oauth2client/authorization_request.py
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
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
@frozen(init=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. 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, and
       `authorization_response_iss_parameter_supported` is `False` (default), then no issuer check
       is performed. Set `authorization_response_iss_parameter_supported`
       to `True` to enforce server identification: if no issuer is included in the Authorization
       Response, then an error will be raised instead.

    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.

    """

    authorization_endpoint: str

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

    code_verifier: str | None = None
    code_challenge: str | None = field(init=False, metadata={"query": True})
    authorization_response_iss_parameter_supported: bool = False
    issuer: str | None = None

    exception_classes: ClassVar[dict[str, type[Exception]]] = {
        "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 = "code",
        state: str | ellipsis | None = ...,  # noqa: F821
        nonce: str | ellipsis | None = ...,  # noqa: F821
        code_verifier: str | None = None,
        code_challenge_method: str | None = "S256",
        acr_values: str | Iterable[str] | None = None,
        max_age: int | None = None,
        issuer: str | None = None,
        authorization_response_iss_parameter_supported: bool = False,
        **kwargs: Any,
    ) -> None:
        if authorization_response_iss_parameter_supported and not issuer:
            msg = (
                "When 'authorization_response_iss_parameter_supported' is `True`, you must"
                " provide the expected `issuer` as parameter."
            )
            raise ValueError(msg)

        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:
            msg = "The `max_age` parameter is a number of seconds and cannot be negative."
            raise ValueError(msg)

        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)

        code_challenge: str | None = None
        if code_challenge_method:
            if not code_verifier:
                code_verifier = PkceUtils.generate_code_verifier()
            code_challenge = PkceUtils.derive_challenge(code_verifier, code_challenge_method)
        else:
            code_verifier = None

        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,
            kwargs=kwargs,
        )
        object.__setattr__(self, "code_challenge", code_challenge)

    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", {}))
        d.pop("code_challenge")
        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.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:
            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`.
            NotImplementedError: 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()
            if self.issuer and received_issuer != self.issuer:
                raise MismatchingIssuer(self.issuer, received_issuer)

        # 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)

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

        if "code" in self.response_type:
            code: str = response_url.args.get("code")
            if code is None:
                raise MissingAuthCode()
        else:
            raise NotImplementedError()

        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,
            **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.

        """
        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(error, error_description, 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
args: dict[str, Any] property

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

Returns:

Type Description
dict[str, Any]

a dict of parameters

furl: furl property

Return the Authorization Request URI, as a furl.

uri: str 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
279
280
281
282
@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
284
285
286
287
@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
371
372
373
374
375
376
377
378
379
380
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", {}))
    d.pop("code_challenge")
    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
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.

NotImplementedError

if response_type anything else than 'code'.

Source code in requests_oauth2client/authorization_request.py
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
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:
        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`.
        NotImplementedError: 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()
        if self.issuer and received_issuer != self.issuer:
            raise MismatchingIssuer(self.issuer, received_issuer)

    # 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)

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

    if "code" in self.response_type:
        code: str = response_url.args.get("code")
        if code is None:
            raise MissingAuthCode()
    else:
        raise NotImplementedError()

    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,
        **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
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
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
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
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
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
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
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
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.

Source code in requests_oauth2client/authorization_request.py
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
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.

    """
    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(error, error_description, error_uri)

RequestParameterAuthorizationRequest

Represent an Authorization Request that includes a request JWT.

Parameters:

Name Type Description Default
authorization_endpoint str

the Authorization Endpoint uri

required
client_id str

the client_id

required
request 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
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
@frozen(init=False)
class RequestParameterAuthorizationRequest:
    """Represent an Authorization Request that includes a `request` JWT.

    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: str
    expires_at: datetime | None = None
    kwargs: dict[str, Any] = Factory(dict)

    @accepts_expires_in
    def __init__(
        self,
        authorization_endpoint: str,
        client_id: str,
        request: str,
        expires_at: datetime | None = None,
        **kwargs: Any,
    ):
        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            request=request,
            expires_at=expires_at,
            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": 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: furl property

Return the Authorization Request URI, as a furl instance.

uri: str 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 parameters to include in the request

{}
Source code in requests_oauth2client/authorization_request.py
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
@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 parameters to include in the request

    """

    authorization_endpoint: str
    client_id: str
    request_uri: str
    expires_at: datetime | None = None
    kwargs: dict[str, Any] = Factory(dict)

    @accepts_expires_in
    def __init__(
        self,
        authorization_endpoint: str,
        client_id: str,
        request_uri: str,
        expires_at: datetime | None = None,
        **kwargs: Any,
    ):
        self.__attrs_init__(
            authorization_endpoint=authorization_endpoint,
            client_id=client_id,
            request_uri=request_uri,
            expires_at=expires_at,
            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: furl property

Return the Authorization Request URI, as a furl instance.

uri: str 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
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
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,
    ):
        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)
        d.update(**d.pop("kwargs", {}))
        d.pop("code_challenge")
        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")
        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
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
@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)
    d.update(**d.pop("kwargs", {}))
    d.pop("code_challenge")
    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
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
@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")
    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
820
821
822
823
824
825
826
827
828
829
830
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
832
833
834
835
836
837
838
839
840
841
842
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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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,
    ):
        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

    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

        """
        if key == "expires_in":
            if self.expires_at is None:
                return None
            return int(self.expires_at.timestamp() - datetime.now(tz=timezone.utc).timestamp())
        return self.other.get(key) or super().__getattribute__(key)
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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: TokenEndpointPoolingJob

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

{}

auth=("client_id", "client_secret") ) pool_job = BackChannelAuthenticationPoolingJob( client=client, auth_req_id="my_auth_req_id" )

1
token = None while token is None: token = pool_job() ```
Source code in requests_oauth2client/backchannel_authentication.py
 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
class BackChannelAuthenticationPoolingJob(TokenEndpointPoolingJob):
    """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 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.

    Usage: ```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() ```

    """

    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,
    ):
        if isinstance(auth_req_id, BackChannelAuthenticationResponse) and interval is None:
            interval = auth_req_id.interval

        super().__init__(
            client=client,
            interval=interval,
            slow_down_interval=slow_down_interval,
            requests_kwargs=requests_kwargs,
            **token_kwargs,
        )
        self.auth_req_id = auth_req_id

    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
136
137
138
139
140
141
142
143
144
145
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.

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

{}
Usage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)
Source code in requests_oauth2client/client.py
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  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
 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
@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.
        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.

    Usage:
        ```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)
        ```

    """

    auth: requests.auth.AuthBase = field(converter=client_auth_factory)
    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 = SignatureAlgs.RS256
    id_token_encrypted_response_alg: str | None = None
    id_token_decryption_key: Jwk | None = None
    code_challenge_method: str | None = "S256"
    authorization_response_iss_parameter_supported: bool = False
    session: requests.Session = field(factory=requests.Session)
    extra_metadata: dict[str, Any] = field(factory=dict)
    testing: bool = False

    bearer_token_class: type[BearerToken] = BearerToken

    exception_classes: ClassVar[dict[str, type[Exception]]] = {
        "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,
        "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 = "S256",
        authorization_response_iss_parameter_supported: bool = False,
        bearer_token_class: type[BearerToken] = BearerToken,
        session: requests.Session | None = None,
        testing: bool = False,
        **extra_metadata: Any,
    ):
        if authorization_response_iss_parameter_supported and not issuer:
            msg = (
                "If the Authorization Server supports Issuer Identification, as specified by"
                " `authorization_response_iss_parameter_supported=True`, then you must specify"
                " the expected `issuer` value with parameter `issuer`."
            )
            raise ValueError(msg)

        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:
                msg = (
                    "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."
                )
                raise ValueError(msg)

        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,
            bearer_token_class=bearer_token_class,
            extra_metadata=extra_metadata,
        )

    @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 ValueError as exc:
            msg = f"Invalid value '{uri}' for '{attribute.name}': {exc}"
            raise ValueError(msg) 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 ValueError as exc:
            msg = f"Invalid value '{uri}' for '{attribute.name}': {exc}"
            raise ValueError(msg) 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], T],
        on_failure: Callable[[requests.Response], T],
        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
            accept: the Accept header to include in the request
            method: the HTTP method to use
            **requests_kwargs: keyword arguments for the request

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

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

        return on_failure(response)

    def token_request(
        self,
        data: dict[str, Any],
        timeout: int = 10,
        **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
          **requests_kwargs: additional parameters for requests.post()

        Returns:
            the token endpoint response, as
            [`BearerToken`][requests_oauth2client.tokens.BearerToken] instance.

        """
        return self._request(
            "token_endpoint",
            auth=self.auth,
            data=data,
            timeout=timeout,
            on_success=self.parse_token_response,
            on_failure=self.on_token_error,
            **requests_kwargs,
        )

    def parse_token_response(self, response: requests.Response) -> 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][requests.Response] returned by the Token Endpoint.

        Returns:
            a [`BearerToken`][requests_oauth2client.tokens.BearerToken] based on the response
            contents.

        """
        try:
            token_response = self.bearer_token_class(**response.json())
        except Exception as response_class_exc:
            try:
                return self.on_token_error(response)
            except Exception as token_error_exc:
                raise token_error_exc from response_class_exc
        else:
            return token_response

    def on_token_error(self, response: requests.Response) -> 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][requests.Response] returned by the Token Endpoint.

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

        """
        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, error, error_description, error_uri)
        except Exception as exc:
            raise InvalidTokenResponse(response) from exc
        raise exception

    def client_credentials(
        self,
        scope: str | Iterable[str] | 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.
            requests_kwargs: additional parameters for the call to requests
            **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`. Common parameters

        Returns:
            a TokenResponse

        """
        requests_kwargs = requests_kwargs or {}

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

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

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

        Args:
             code: an authorization code or an `AuthorizationResponse` to exchange for tokens
             validate: if `True`, validate the received ID Token (this works only if `code` is an AuthorizationResponse)
             requests_kwargs: additional parameters for the call to requests
             **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `code`, etc.

        Returns:
            a `BearerToken`

        """
        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
            code = code.code

        requests_kwargs = requests_kwargs or {}

        data = dict(grant_type=GrantType.AUTHORIZATION_CODE, code=code, **token_kwargs)
        token = self.token_request(data, **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.

        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:
            a `BearerToken`

        """
        if isinstance(refresh_token, BearerToken):
            if refresh_token.refresh_token is None or not isinstance(refresh_token.refresh_token, str):
                msg = "This BearerToken doesn't have a refresh_token"
                raise ValueError(msg)
            refresh_token = refresh_token.refresh_token

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

    def device_code(
        self,
        device_code: str | DeviceAuthorizationResponse,
        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`
            requests_kwargs: additional parameters for the call to requests
            **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `device_code`, etc.

        Returns:
            a `BearerToken`

        """
        if isinstance(device_code, DeviceAuthorizationResponse):
            if device_code.device_code is None or not isinstance(device_code.device_code, str):
                msg = "This DeviceAuthorizationResponse doesn't have a device_code"
                raise ValueError(msg)
            device_code = device_code.device_code

        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantType.DEVICE_CODE,
            device_code=device_code,
            **token_kwargs,
        )
        return self.token_request(data, **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:
            a `BearerToken`

        """
        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):
                msg = "This `BackChannelAuthenticationResponse` doesn't have an `auth_req_id`"
                raise ValueError(msg)
            auth_req_id = auth_req_id.auth_req_id

        requests_kwargs = requests_kwargs or {}
        data = dict(
            grant_type=GrantType.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,
        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.
            requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: additional parameters to include in the request body.

        Returns:
            a `BearerToken` as returned by the Authorization Server.

        """
        requests_kwargs = requests_kwargs or {}

        try:
            subject_token_type = self.get_token_type(subject_token_type, subject_token)
        except ValueError:
            msg = "Cannot determine the kind of 'subject_token' you provided. Please specify a 'subject_token_type'."
            raise TypeError(msg) from None
        if actor_token:  # pragma: no branch
            try:
                actor_token_type = self.get_token_type(actor_token_type, actor_token)
            except ValueError:
                msg = "Cannot determine the kind of 'actor_token' you provided. Please specify an 'actor_token_type'."
                raise TypeError(msg) from None

        data = dict(
            grant_type=GrantType.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, **requests_kwargs)

    def jwt_bearer(
        self,
        assertion: Jwt | str,
        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.
            requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: additional parameters to include in the request body.

        Returns:
            a `BearerToken` as returned by the Authorization Server.

        """
        requests_kwargs = requests_kwargs or {}

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

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

        return self.token_request(data, **requests_kwargs)

    def resource_owner_password(
        self,
        username: str,
        password: str,
        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
            requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
            **token_kwargs: additional parameters to include in the request body.

        Returns:
            a `BearerToken` as returned by the Authorization Server

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

        return self.token_request(data, **requests_kwargs)

    def authorization_request(
        self,
        *,
        scope: None | str | Iterable[str] = "openid",
        response_type: str = "code",
        redirect_uri: str | None = None,
        state: str | ellipsis | None = ...,  # noqa: F821
        nonce: str | ellipsis | None = ...,  # noqa: F821
        code_verifier: 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.
            code_verifier: the PKCE `code_verifier` to use. Leave default to generate a random value.
            **kwargs: additional parameters to include in the auth request

        Returns:
            an AuthorizationRequest with the supplied parameters

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

        redirect_uri = redirect_uri or self.redirect_uri
        if not redirect_uri:
            msg = (
                "No 'redirect_uri' defined for this client. You must either pass a redirect_uri"
                " as parameter to this method, or include a redirect_uri when initializing your"
                " OAuth2Client."
            )
            raise AttributeError(msg)

        if response_type != "code":
            msg = "Only response_type=code is supported."
            raise ValueError(msg)

        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,
            **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(
            "pushed_authorization_request_endpoint",
            data=authorization_request.args,
            auth=self.auth,
            on_success=self.parse_pushed_authorization_response,
            on_failure=self.on_pushed_authorization_request_error,
            **requests_kwargs,
        )

    def parse_pushed_authorization_response(
        self, response: requests.Response
    ) -> RequestUriParameterAuthorizationRequest:
        """Parse the response obtained by `pushed_authorization_request()`.

        Args:
            response: the `requests.Response` returned by the PAR endpoint

        Returns:
            a RequestUriParameterAuthorizationRequest instance

        """
        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,
        )

    def on_pushed_authorization_request_error(
        self, response: requests.Response
    ) -> RequestUriParameterAuthorizationRequest:
        """Error Handler for Pushed Authorization Endpoint errors.

        Args:
            response: the HTTP response as returned by the AS PAR endpoint.

        Returns:
            a RequestUriParameterAuthorizationRequest, if the error is recoverable

        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, error, error_description, error_uri)
        except Exception as exc:
            raise InvalidPushedAuthorizationResponse(response) 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.

        """
        return self._request(
            "userinfo_endpoint",
            auth=BearerAuth(access_token),
            on_success=self.parse_userinfo_response,
            on_failure=self.on_userinfo_error,
        )

    def parse_userinfo_response(self, resp: requests.Response) -> Any:
        """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.

        Returns:
            the parsed JSON content from this response.

        """
        return resp.json()

    def on_userinfo_error(self, resp: requests.Response) -> Any:
        """Parse UserInfo error response.

        Args:
            resp: a [Response][requests.Response] returned from the UserInfo endpoint.

        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.

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

        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 ValueError(msg)
            elif isinstance(token, BearerToken):
                return "urn:ietf:params:oauth:token-type:access_token"
            elif isinstance(token, IdToken):
                return "urn:ietf:params:oauth:token-type:id_token"
            else:
                msg = "Unexpected type of token, please provide a string or a BearerToken or an IdToken."
                raise TypeError(
                    msg,
                    type(token),
                )
        elif token_type == TokenType.ACCESS_TOKEN:
            if token is not None and not isinstance(token, (str, BearerToken)):
                msg = "The supplied token is not a BearerToken or a string representation of it."
                raise TypeError(
                    msg,
                    type(token),
                )
            return "urn:ietf:params:oauth:token-type:access_token"
        elif token_type == TokenType.REFRESH_TOKEN:
            if token is not None and isinstance(token, BearerToken) and not token.refresh_token:
                msg = "The supplied BearerToken doesn't have a refresh_token."
                raise ValueError(msg)
            return "urn:ietf:params:oauth:token-type:refresh_token"
        elif token_type == "id_token":
            if token is not None and not isinstance(token, (str, IdToken)):
                msg = "The supplied token is not an IdToken or a string representation of it."
                raise TypeError(
                    msg,
                    type(token),
                )
            return "urn:ietf:params:oauth:token-type:id_token"
        else:
            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.

        """
        if isinstance(refresh_token, BearerToken):
            if refresh_token.refresh_token is None:
                msg = "The supplied BearerToken doesn't have a refresh token."
                raise ValueError(msg)
            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:
            `True` if the revocation succeeds, `False` if no revocation endpoint is present or a
            non-standardised error is returned.

        """
        requests_kwargs = requests_kwargs or {}

        if token_type_hint == TokenType.REFRESH_TOKEN and isinstance(token, BearerToken):
            if token.refresh_token is None:
                msg = "The supplied BearerToken doesn't have a refresh token."
                raise ValueError(msg)
            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(
            "revocation_endpoint",
            data=data,
            auth=self.auth,
            on_success=lambda resp: True,
            on_failure=self.on_revocation_error,
            **requests_kwargs,
        )

    def on_revocation_error(self, response: requests.Response) -> bool:
        """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 [Response][requests.Response] as returned by the Revocation Endpoint

        Returns:
            `False` to signal that an error occurred. May raise exceptions instead depending on the
            revocation 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, RevocationError)
            exception = exception_class(error, error_description, error_uri)
        except Exception:
            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.

        """
        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:
                    msg = "The supplied BearerToken doesn't have a refresh token."
                    raise ValueError(msg)
                else:
                    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 ValueError(msg)

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

        return self._request(
            "introspection_endpoint",
            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) -> 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.

        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) -> Any:
        """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.

        Returns:
            usually raises exceptions. A subclass can return a default response instead.

        """
        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(error, error_description, error_uri)
        except Exception as exc:
            raise UnknownIntrospectionError(response) 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

        """
        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 ValueError(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 ValueError(msg)

        requests_kwargs = requests_kwargs or {}

        if scope is not None and not isinstance(scope, str):
            try:
                scope = " ".join(scope)
            except Exception as exc:
                msg = "Unsupported `scope` value"
                raise ValueError(msg) from exc

        if acr_values is not None and not isinstance(acr_values, str):
            try:
                acr_values = " ".join(acr_values)
            except Exception as exc:
                msg = "Unsupported `acr_values`"
                raise ValueError(msg) 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(
            "backchannel_authentication_endpoint",
            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
    ) -> 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.

        Returns:
            a `BackChannelAuthenticationResponse`

        """
        try:
            return BackChannelAuthenticationResponse(**response.json())
        except TypeError as exc:
            raise InvalidBackChannelAuthenticationResponse(response) from exc

    def on_backchannel_authentication_error(self, response: requests.Response) -> 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.

        Returns:
            usually raises an exception. But a subclass can return a default response instead.

        """
        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(error, error_description, error_uri)
        except Exception as exc:
            raise InvalidBackChannelAuthenticationResponse(response) 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

        """
        requests_kwargs = requests_kwargs or {}

        return self._request(
            "device_authorization_endpoint",
            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) -> 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.

        Returns:
            a `DeviceAuthorizationResponse` as returned by AS

        """
        device_authorization_response = DeviceAuthorizationResponse(**response.json())
        return device_authorization_response

    def on_device_authorization_error(self, response: requests.Response) -> 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.

        Returns:
            usually raises an Exception. But a subclass may return a default response instead.

        """
        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, error, error_description, error_uri)
        except Exception as exc:
            raise InvalidDeviceAuthorizationResponse(response) 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 {}

        jwks = self._request(
            "jwks_uri",
            auth=None,
            method="GET",
            on_success=lambda resp: resp.json(),
            on_failure=lambda resp: resp.raise_for_status(),
            **requests_kwargs,
        )
        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:
        """Initialise an OAuth2Client based on Authorization Server Metadata.

        This will retrieve the standardised metadata document available at `url`, and will extract
        all Endpoint Uris from that document, will fetch the current public keys from its
        `jwks_uri`, then will initialise an OAuth2Client based on those endpoints.

        Args:
             url: the url where the server metadata 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
             issuer: if an issuer is given, check that it matches the one from the retrieved document
             testing: if True, don't try to validate the endpoint urls that are part of the document
             **kwargs: additional keyword parameters to pass to OAuth2Client

        Returns:
            an OAuth2Client with endpoint initialised based on the obtained metadata

        Raises:
            ValueError: if neither `url` nor `issuer` are suitable urls
            requests.HTTPError: if an error happens while fetching the documents

        """
        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 ValueError(msg)

        validate_endpoint_uri(url, path=False)

        session = session or requests.Session()
        discovery = session.get(url).json()

        jwks_uri = discovery.get("jwks_uri")
        if jwks_uri:
            jwks = JwkSet(session.get(jwks_uri).json())

        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(  # noqa: PLR0913
        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,
        session: requests.Session | None = None,
        https: bool = True,
        testing: bool = False,
        **kwargs: Any,
    ) -> OAuth2Client:
        """Initialise an OAuth2Client, based on the server metadata from `discovery`.

        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
             session: a requests Session to use to retrieve the document and initialise the client with
             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`

        """
        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 = "Mismatching issuer value in discovery document: "
            raise ValueError(
                msg,
                issuer,
                discovery.get("issuer"),
            )
        elif issuer is None:
            issuer = discovery.get("issuer")

        token_endpoint = discovery.get("token_endpoint")
        if token_endpoint is None:
            msg = "token_endpoint not found in that discovery document"
            raise ValueError(msg)
        authorization_endpoint = discovery.get("authorization_endpoint")
        revocation_endpoint = discovery.get("revocation_endpoint")
        introspection_endpoint = discovery.get("introspection_endpoint")
        userinfo_endpoint = discovery.get("userinfo_endpoint")
        jwks_uri = discovery.get("jwks_uri")
        if jwks_uri is not None:
            validate_endpoint_uri(jwks_uri, https=https)
        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,
            jwks_uri=jwks_uri,
            authorization_server_jwks=authorization_server_jwks,
            auth=auth,
            client_id=client_id,
            client_secret=client_secret,
            private_key=private_key,
            session=session,
            issuer=issuer,
            authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
            testing=testing,
            **kwargs,
        )

    def __enter__(self) -> OAuth2Client:
        """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: Any, exc_val: Any, exc_tb: Any) -> bool:  # noqa: D105
        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:
            msg = (
                f"No '{endpoint}' defined for this client. Please provide the URL for that"
                f" endpoint when initializing your {self.__class__.__name__} instance."
            )
            raise AttributeError(msg)

        return str(url)
client_id: str property

Client ID.

client_secret: str | None property

Client Secret.

client_jwks: JwkSet 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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
@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 ValueError as exc:
        msg = f"Invalid value '{uri}' for '{attribute.name}': {exc}"
        raise ValueError(msg) 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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
@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 ValueError as exc:
        msg = f"Invalid value '{uri}' for '{attribute.name}': {exc}"
        raise ValueError(msg) from exc
token_request(data, timeout=10, **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
**requests_kwargs Any

additional parameters for requests.post()

{}

Returns:

Type Description
BearerToken

the token endpoint response, as

BearerToken

BearerToken instance.

Source code in requests_oauth2client/client.py
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
def token_request(
    self,
    data: dict[str, Any],
    timeout: int = 10,
    **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
      **requests_kwargs: additional parameters for requests.post()

    Returns:
        the token endpoint response, as
        [`BearerToken`][requests_oauth2client.tokens.BearerToken] instance.

    """
    return self._request(
        "token_endpoint",
        auth=self.auth,
        data=data,
        timeout=timeout,
        on_success=self.parse_token_response,
        on_failure=self.on_token_error,
        **requests_kwargs,
    )
parse_token_response(response)

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

Returns:

Type Description
BearerToken

a BearerToken based on the response

BearerToken

contents.

Source code in requests_oauth2client/client.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def parse_token_response(self, response: requests.Response) -> 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][requests.Response] returned by the Token Endpoint.

    Returns:
        a [`BearerToken`][requests_oauth2client.tokens.BearerToken] based on the response
        contents.

    """
    try:
        token_response = self.bearer_token_class(**response.json())
    except Exception as response_class_exc:
        try:
            return self.on_token_error(response)
        except Exception as token_error_exc:
            raise token_error_exc from response_class_exc
    else:
        return token_response
on_token_error(response)

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

Returns:

Type Description
BearerToken

nothing, and raises an exception instead. But a subclass may return a

BearerToken

BearerToken to implement a default

BearerToken

behaviour if needed.

Source code in requests_oauth2client/client.py
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
def on_token_error(self, response: requests.Response) -> 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][requests.Response] returned by the Token Endpoint.

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

    """
    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, error, error_description, error_uri)
    except Exception as exc:
        raise InvalidTokenResponse(response) from exc
    raise exception
client_credentials(scope=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
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. Common parameters

{}

Returns:

Type Description
BearerToken

a TokenResponse

Source code in requests_oauth2client/client.py
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
def client_credentials(
    self,
    scope: str | Iterable[str] | 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.
        requests_kwargs: additional parameters for the call to requests
        **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`. Common parameters

    Returns:
        a TokenResponse

    """
    requests_kwargs = requests_kwargs or {}

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

    data = dict(grant_type=GrantType.CLIENT_CREDENTIALS, scope=scope, **token_kwargs)
    return self.token_request(data, **requests_kwargs)
authorization_code(code, *, validate=True, requests_kwargs=None, **token_kwargs)

Send a request to the token endpoint with the authorization_code grant.

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 received ID Token (this works only if code is an AuthorizationResponse)

True
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, code, etc.

{}

Returns:

Type Description
BearerToken

a BearerToken

Source code in requests_oauth2client/client.py
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
def authorization_code(
    self,
    code: str | AuthorizationResponse,
    *,
    validate: bool = True,
    requests_kwargs: dict[str, Any] | None = None,
    **token_kwargs: Any,
) -> BearerToken:
    """Send a request to the token endpoint with the `authorization_code` grant.

    Args:
         code: an authorization code or an `AuthorizationResponse` to exchange for tokens
         validate: if `True`, validate the received ID Token (this works only if `code` is an AuthorizationResponse)
         requests_kwargs: additional parameters for the call to requests
         **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `code`, etc.

    Returns:
        a `BearerToken`

    """
    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
        code = code.code

    requests_kwargs = requests_kwargs or {}

    data = dict(grant_type=GrantType.AUTHORIZATION_CODE, code=code, **token_kwargs)
    token = self.token_request(data, **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.

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

a BearerToken

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

    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:
        a `BearerToken`

    """
    if isinstance(refresh_token, BearerToken):
        if refresh_token.refresh_token is None or not isinstance(refresh_token.refresh_token, str):
            msg = "This BearerToken doesn't have a refresh_token"
            raise ValueError(msg)
        refresh_token = refresh_token.refresh_token

    requests_kwargs = requests_kwargs or {}
    data = dict(grant_type=GrantType.REFRESH_TOKEN, refresh_token=refresh_token, **token_kwargs)
    return self.token_request(data, **requests_kwargs)
device_code(device_code, 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
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

a BearerToken

Source code in requests_oauth2client/client.py
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
def device_code(
    self,
    device_code: str | DeviceAuthorizationResponse,
    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`
        requests_kwargs: additional parameters for the call to requests
        **token_kwargs: additional parameters for the token endpoint, alongside `grant_type`, `device_code`, etc.

    Returns:
        a `BearerToken`

    """
    if isinstance(device_code, DeviceAuthorizationResponse):
        if device_code.device_code is None or not isinstance(device_code.device_code, str):
            msg = "This DeviceAuthorizationResponse doesn't have a device_code"
            raise ValueError(msg)
        device_code = device_code.device_code

    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantType.DEVICE_CODE,
        device_code=device_code,
        **token_kwargs,
    )
    return self.token_request(data, **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

a BearerToken

Source code in requests_oauth2client/client.py
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
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:
        a `BearerToken`

    """
    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):
            msg = "This `BackChannelAuthenticationResponse` doesn't have an `auth_req_id`"
            raise ValueError(msg)
        auth_req_id = auth_req_id.auth_req_id

    requests_kwargs = requests_kwargs or {}
    data = dict(
        grant_type=GrantType.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, 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
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

a BearerToken as returned by the Authorization Server.

Source code in requests_oauth2client/client.py
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
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,
    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.
        requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: additional parameters to include in the request body.

    Returns:
        a `BearerToken` as returned by the Authorization Server.

    """
    requests_kwargs = requests_kwargs or {}

    try:
        subject_token_type = self.get_token_type(subject_token_type, subject_token)
    except ValueError:
        msg = "Cannot determine the kind of 'subject_token' you provided. Please specify a 'subject_token_type'."
        raise TypeError(msg) from None
    if actor_token:  # pragma: no branch
        try:
            actor_token_type = self.get_token_type(actor_token_type, actor_token)
        except ValueError:
            msg = "Cannot determine the kind of 'actor_token' you provided. Please specify an 'actor_token_type'."
            raise TypeError(msg) from None

    data = dict(
        grant_type=GrantType.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, **requests_kwargs)
jwt_bearer(assertion, 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
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

a BearerToken as returned by the Authorization Server.

Source code in requests_oauth2client/client.py
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
def jwt_bearer(
    self,
    assertion: Jwt | str,
    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.
        requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: additional parameters to include in the request body.

    Returns:
        a `BearerToken` as returned by the Authorization Server.

    """
    requests_kwargs = requests_kwargs or {}

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

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

    return self.token_request(data, **requests_kwargs)
resource_owner_password(username, password, 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
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

a BearerToken as returned by the Authorization Server

Source code in requests_oauth2client/client.py
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
def resource_owner_password(
    self,
    username: str,
    password: str,
    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
        requests_kwargs: additional parameters to pass to the underlying `requests.post()` call.
        **token_kwargs: additional parameters to include in the request body.

    Returns:
        a `BearerToken` as returned by the Authorization Server

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

    return self.token_request(data, **requests_kwargs)
authorization_request(*, scope='openid', response_type='code', redirect_uri=None, state=..., nonce=..., code_verifier=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.

...
code_verifier str | None

the PKCE code_verifier to use. Leave default to generate a random value.

None
**kwargs Any

additional parameters to include in the auth request

{}

Returns:

Type Description
AuthorizationRequest

an AuthorizationRequest with the supplied parameters

Source code in requests_oauth2client/client.py
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
def authorization_request(
    self,
    *,
    scope: None | str | Iterable[str] = "openid",
    response_type: str = "code",
    redirect_uri: str | None = None,
    state: str | ellipsis | None = ...,  # noqa: F821
    nonce: str | ellipsis | None = ...,  # noqa: F821
    code_verifier: 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.
        code_verifier: the PKCE `code_verifier` to use. Leave default to generate a random value.
        **kwargs: additional parameters to include in the auth request

    Returns:
        an AuthorizationRequest with the supplied parameters

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

    redirect_uri = redirect_uri or self.redirect_uri
    if not redirect_uri:
        msg = (
            "No 'redirect_uri' defined for this client. You must either pass a redirect_uri"
            " as parameter to this method, or include a redirect_uri when initializing your"
            " OAuth2Client."
        )
        raise AttributeError(msg)

    if response_type != "code":
        msg = "Only response_type=code is supported."
        raise ValueError(msg)

    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,
        **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
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
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(
        "pushed_authorization_request_endpoint",
        data=authorization_request.args,
        auth=self.auth,
        on_success=self.parse_pushed_authorization_response,
        on_failure=self.on_pushed_authorization_request_error,
        **requests_kwargs,
    )
parse_pushed_authorization_response(response)

Parse the response obtained by pushed_authorization_request().

Parameters:

Name Type Description Default
response Response

the requests.Response returned by the PAR endpoint

required

Returns:

Type Description
RequestUriParameterAuthorizationRequest

a RequestUriParameterAuthorizationRequest instance

Source code in requests_oauth2client/client.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
def parse_pushed_authorization_response(
    self, response: requests.Response
) -> RequestUriParameterAuthorizationRequest:
    """Parse the response obtained by `pushed_authorization_request()`.

    Args:
        response: the `requests.Response` returned by the PAR endpoint

    Returns:
        a RequestUriParameterAuthorizationRequest instance

    """
    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,
    )
on_pushed_authorization_request_error(response)

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

Returns:

Type Description
RequestUriParameterAuthorizationRequest

a RequestUriParameterAuthorizationRequest, if the error is recoverable

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
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
def on_pushed_authorization_request_error(
    self, response: requests.Response
) -> RequestUriParameterAuthorizationRequest:
    """Error Handler for Pushed Authorization Endpoint errors.

    Args:
        response: the HTTP response as returned by the AS PAR endpoint.

    Returns:
        a RequestUriParameterAuthorizationRequest, if the error is recoverable

    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, error, error_description, error_uri)
    except Exception as exc:
        raise InvalidPushedAuthorizationResponse(response) 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
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
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.

    """
    return self._request(
        "userinfo_endpoint",
        auth=BearerAuth(access_token),
        on_success=self.parse_userinfo_response,
        on_failure=self.on_userinfo_error,
    )
parse_userinfo_response(resp)

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

Returns:

Type Description
Any

the parsed JSON content from this response.

Source code in requests_oauth2client/client.py
886
887
888
889
890
891
892
893
894
895
896
897
898
899
def parse_userinfo_response(self, resp: requests.Response) -> Any:
    """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.

    Returns:
        the parsed JSON content from this response.

    """
    return resp.json()
on_userinfo_error(resp)

Parse UserInfo error response.

Parameters:

Name Type Description Default
resp Response

a Response returned from the UserInfo endpoint.

required

Returns:

Type Description
Any

nothing, raises exception instead.

Source code in requests_oauth2client/client.py
901
902
903
904
905
906
907
908
909
910
911
def on_userinfo_error(self, resp: requests.Response) -> Any:
    """Parse UserInfo error response.

    Args:
        resp: a [Response][requests.Response] returned from the UserInfo endpoint.

    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.

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

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

    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 ValueError(msg)
        elif isinstance(token, BearerToken):
            return "urn:ietf:params:oauth:token-type:access_token"
        elif isinstance(token, IdToken):
            return "urn:ietf:params:oauth:token-type:id_token"
        else:
            msg = "Unexpected type of token, please provide a string or a BearerToken or an IdToken."
            raise TypeError(
                msg,
                type(token),
            )
    elif token_type == TokenType.ACCESS_TOKEN:
        if token is not None and not isinstance(token, (str, BearerToken)):
            msg = "The supplied token is not a BearerToken or a string representation of it."
            raise TypeError(
                msg,
                type(token),
            )
        return "urn:ietf:params:oauth:token-type:access_token"
    elif token_type == TokenType.REFRESH_TOKEN:
        if token is not None and isinstance(token, BearerToken) and not token.refresh_token:
            msg = "The supplied BearerToken doesn't have a refresh_token."
            raise ValueError(msg)
        return "urn:ietf:params:oauth:token-type:refresh_token"
    elif token_type == "id_token":
        if token is not None and not isinstance(token, (str, IdToken)):
            msg = "The supplied token is not an IdToken or a string representation of it."
            raise TypeError(
                msg,
                type(token),
            )
        return "urn:ietf:params:oauth:token-type:id_token"
    else:
        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
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
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.

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

    """
    if isinstance(refresh_token, BearerToken):
        if refresh_token.refresh_token is None:
            msg = "The supplied BearerToken doesn't have a refresh token."
            raise ValueError(msg)
        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

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
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
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:
        `True` if the revocation succeeds, `False` if no revocation endpoint is present or a
        non-standardised error is returned.

    """
    requests_kwargs = requests_kwargs or {}

    if token_type_hint == TokenType.REFRESH_TOKEN and isinstance(token, BearerToken):
        if token.refresh_token is None:
            msg = "The supplied BearerToken doesn't have a refresh token."
            raise ValueError(msg)
        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(
        "revocation_endpoint",
        data=data,
        auth=self.auth,
        on_success=lambda resp: True,
        on_failure=self.on_revocation_error,
        **requests_kwargs,
    )
on_revocation_error(response)

Error handler for revoke_token().

Invoked by revoke_token() when the revocation endpoint returns an error.

Parameters:

Name Type Description Default
response Response

the Response as returned by the Revocation Endpoint

required

Returns:

Type Description
bool

False to signal that an error occurred. May raise exceptions instead depending on the

bool

revocation response.

Source code in requests_oauth2client/client.py
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
def on_revocation_error(self, response: requests.Response) -> bool:
    """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 [Response][requests.Response] as returned by the Revocation Endpoint

    Returns:
        `False` to signal that an error occurred. May raise exceptions instead depending on the
        revocation 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, RevocationError)
        exception = exception_class(error, error_description, error_uri)
    except Exception:
        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.

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

    """
    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:
                msg = "The supplied BearerToken doesn't have a refresh token."
                raise ValueError(msg)
            else:
                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 ValueError(msg)

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

    return self._request(
        "introspection_endpoint",
        data=data,
        auth=self.auth,
        on_success=self.parse_introspection_response,
        on_failure=self.on_introspection_error,
        **requests_kwargs,
    )
parse_introspection_response(response)

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

Returns:

Type Description
Any

the decoded JSON content, or a str with the content.

Source code in requests_oauth2client/client.py
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
def parse_introspection_response(self, response: requests.Response) -> 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.

    Returns:
        the decoded JSON content, or a `str` with the content.

    """
    try:
        return response.json()
    except ValueError:
        return response.text
on_introspection_error(response)

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

Returns:

Type Description
Any

usually raises exceptions. A subclass can return a default response instead.

Source code in requests_oauth2client/client.py
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
def on_introspection_error(self, response: requests.Response) -> Any:
    """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.

    Returns:
        usually raises exceptions. A subclass can return a default response instead.

    """
    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(error, error_description, error_uri)
    except Exception as exc:
        raise UnknownIntrospectionError(response) 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

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

    """
    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 ValueError(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 ValueError(msg)

    requests_kwargs = requests_kwargs or {}

    if scope is not None and not isinstance(scope, str):
        try:
            scope = " ".join(scope)
        except Exception as exc:
            msg = "Unsupported `scope` value"
            raise ValueError(msg) from exc

    if acr_values is not None and not isinstance(acr_values, str):
        try:
            acr_values = " ".join(acr_values)
        except Exception as exc:
            msg = "Unsupported `acr_values`"
            raise ValueError(msg) 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(
        "backchannel_authentication_endpoint",
        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)

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

Returns:

Type Description
BackChannelAuthenticationResponse

a BackChannelAuthenticationResponse

Source code in requests_oauth2client/client.py
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
def parse_backchannel_authentication_response(
    self, response: requests.Response
) -> 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.

    Returns:
        a `BackChannelAuthenticationResponse`

    """
    try:
        return BackChannelAuthenticationResponse(**response.json())
    except TypeError as exc:
        raise InvalidBackChannelAuthenticationResponse(response) from exc
on_backchannel_authentication_error(response)

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

Returns:

Type Description
BackChannelAuthenticationResponse

usually raises an exception. But a subclass can return a default response instead.

Source code in requests_oauth2client/client.py
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
def on_backchannel_authentication_error(self, response: requests.Response) -> 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.

    Returns:
        usually raises an exception. But a subclass can return a default response instead.

    """
    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(error, error_description, error_uri)
    except Exception as exc:
        raise InvalidBackChannelAuthenticationResponse(response) 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

Source code in requests_oauth2client/client.py
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
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

    """
    requests_kwargs = requests_kwargs or {}

    return self._request(
        "device_authorization_endpoint",
        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)

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

Returns:

Type Description
DeviceAuthorizationResponse

a DeviceAuthorizationResponse as returned by AS

Source code in requests_oauth2client/client.py
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
def parse_device_authorization_response(self, response: requests.Response) -> 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.

    Returns:
        a `DeviceAuthorizationResponse` as returned by AS

    """
    device_authorization_response = DeviceAuthorizationResponse(**response.json())
    return device_authorization_response
on_device_authorization_error(response)

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

Returns:

Type Description
DeviceAuthorizationResponse

usually raises an Exception. But a subclass may return a default response instead.

Source code in requests_oauth2client/client.py
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
def on_device_authorization_error(self, response: requests.Response) -> 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.

    Returns:
        usually raises an Exception. But a subclass may return a default response instead.

    """
    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, error, error_description, error_uri)
    except Exception as exc:
        raise InvalidDeviceAuthorizationResponse(response) 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
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
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 {}

    jwks = self._request(
        "jwks_uri",
        auth=None,
        method="GET",
        on_success=lambda resp: resp.json(),
        on_failure=lambda resp: resp.raise_for_status(),
        **requests_kwargs,
    )
    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

Initialise an OAuth2Client based on Authorization Server Metadata.

This will retrieve the standardised metadata document available at url, and will extract all Endpoint Uris from that document, will fetch the current public keys from its jwks_uri, then will initialise an OAuth2Client based on those endpoints.

Parameters:

Name Type Description Default
url str | None

the url where the server metadata 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
issuer str | None

if an issuer is given, check that it matches the one from the retrieved document

None
testing bool

if True, don't try to validate 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 endpoint initialised based on the obtained metadata

Raises:

Type Description
ValueError

if neither url nor issuer are suitable urls

HTTPError

if an error happens while fetching the documents

Source code in requests_oauth2client/client.py
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
@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:
    """Initialise an OAuth2Client based on Authorization Server Metadata.

    This will retrieve the standardised metadata document available at `url`, and will extract
    all Endpoint Uris from that document, will fetch the current public keys from its
    `jwks_uri`, then will initialise an OAuth2Client based on those endpoints.

    Args:
         url: the url where the server metadata 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
         issuer: if an issuer is given, check that it matches the one from the retrieved document
         testing: if True, don't try to validate the endpoint urls that are part of the document
         **kwargs: additional keyword parameters to pass to OAuth2Client

    Returns:
        an OAuth2Client with endpoint initialised based on the obtained metadata

    Raises:
        ValueError: if neither `url` nor `issuer` are suitable urls
        requests.HTTPError: if an error happens while fetching the documents

    """
    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 ValueError(msg)

    validate_endpoint_uri(url, path=False)

    session = session or requests.Session()
    discovery = session.get(url).json()

    jwks_uri = discovery.get("jwks_uri")
    if jwks_uri:
        jwks = JwkSet(session.get(jwks_uri).json())

    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, session=None, https=True, testing=False, **kwargs) classmethod

Initialise an OAuth2Client, based on the server metadata from discovery.

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
session Session | None

a requests Session to use to retrieve the document and initialise the client with

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

Source code in requests_oauth2client/client.py
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
@classmethod
def from_discovery_document(  # noqa: PLR0913
    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,
    session: requests.Session | None = None,
    https: bool = True,
    testing: bool = False,
    **kwargs: Any,
) -> OAuth2Client:
    """Initialise an OAuth2Client, based on the server metadata from `discovery`.

    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
         session: a requests Session to use to retrieve the document and initialise the client with
         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`

    """
    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 = "Mismatching issuer value in discovery document: "
        raise ValueError(
            msg,
            issuer,
            discovery.get("issuer"),
        )
    elif issuer is None:
        issuer = discovery.get("issuer")

    token_endpoint = discovery.get("token_endpoint")
    if token_endpoint is None:
        msg = "token_endpoint not found in that discovery document"
        raise ValueError(msg)
    authorization_endpoint = discovery.get("authorization_endpoint")
    revocation_endpoint = discovery.get("revocation_endpoint")
    introspection_endpoint = discovery.get("introspection_endpoint")
    userinfo_endpoint = discovery.get("userinfo_endpoint")
    jwks_uri = discovery.get("jwks_uri")
    if jwks_uri is not None:
        validate_endpoint_uri(jwks_uri, https=https)
    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,
        jwks_uri=jwks_uri,
        authorization_server_jwks=authorization_server_jwks,
        auth=auth,
        client_id=client_id,
        client_secret=client_secret,
        private_key=private_key,
        session=session,
        issuer=issuer,
        authorization_response_iss_parameter_supported=authorization_response_iss_parameter_supported,
        testing=testing,
        **kwargs,
    )

GrantType

Bases: str, Enum

An enum of standardized grant_type values.

Source code in requests_oauth2client/client.py
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
class GrantType(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"

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.

BaseClientAuthenticationMethod

Bases: AuthBase

Base class for all Client Authentication methods. This extends [requests.auth.AuthBase].

This base class only checks that requests are suitable to add Client Authentication parameters to, and doesn't modify the request.

Source code in requests_oauth2client/client_authentication.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class BaseClientAuthenticationMethod(requests.auth.AuthBase):
    """Base class for all Client Authentication methods. This extends [requests.auth.AuthBase].

    This base class only checks that requests are suitable to add Client Authentication parameters
    to, and doesn't modify the request.

    """

    def __init__(self, client_id: str):
        self.client_id = str(client_id)

    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,
        ):
            msg = "This request is not suitable for OAuth 2.0 Client Authentication"
            raise RuntimeError(msg)
        return request

ClientSecretBasic

Bases: BaseClientAuthenticationMethod

Implement client_secret_basic authentication.

With this method, the client sends its Client ID and Secret, in the Authorization header, with the "Basic" scheme, in each authenticated request to the AS.

Parameters:

Name Type Description Default
client_id str

client_id to use.

required
client_secret str

client_secret to use.

required
Source code in requests_oauth2client/client_authentication.py
59
60
61
62
63
64
65
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
class ClientSecretBasic(BaseClientAuthenticationMethod):
    """Implement `client_secret_basic` authentication.

    With this method, the client sends its Client ID and Secret, in the Authorization header, with
    the "Basic" scheme, in each authenticated request to the AS.

    Args:
        client_id: `client_id` to use.
        client_secret: `client_secret` to use.

    """

    def __init__(self, client_id: str, client_secret: str):
        super().__init__(client_id)
        self.client_secret = str(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: a [requests.PreparedRequest][].

        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 to use.

required
client_secret str

client_secret to use.

required
Source code in requests_oauth2client/client_authentication.py
 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
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` to use.
        client_secret: `client_secret` to use.

    """

    def __init__(self, client_id: str, client_secret: str) -> None:
        super().__init__(client_id)
        self.client_secret = str(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

ClientAssertionAuthenticationMethod

Bases: BaseClientAuthenticationMethod

Base class for assertion-based client authentication methods.

Parameters:

Name Type Description Default
client_id str

the client_id to use

required
alg str

the alg to use to sign generated Client Assertions.

required
lifetime int

the lifetime to use for generated Client Assertions.

required
jti_gen Callable[[], str]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

required
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.

None
Source code in requests_oauth2client/client_authentication.py
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
class ClientAssertionAuthenticationMethod(BaseClientAuthenticationMethod):
    """Base class for assertion-based client authentication methods.

    Args:
        client_id: the client_id to use
        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.

    """

    def __init__(
        self,
        client_id: str,
        alg: str,
        lifetime: int,
        jti_gen: Callable[[], str],
        aud: str | None = None,
    ) -> None:
        super().__init__(client_id)
        self.alg = alg
        self.lifetime = lifetime
        self.jti_gen = jti_gen
        self.aud = aud

    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()  # pragma: no cover

    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:
            msg = "No url defined for this request. This should never happen..."  # pragma: no cover
            raise ValueError(msg)  # 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
158
159
160
161
162
163
164
165
166
167
168
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()  # pragma: no cover

ClientSecretJwt

Bases: ClientAssertionAuthenticationMethod

Implement client_secret_jwt client authentication method.

With this method, the client generates and signs a client assertion that is symmetrically signed 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[[], Any]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

lambda: uuid4()
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.

None
Source code in requests_oauth2client/client_authentication.py
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
class ClientSecretJwt(ClientAssertionAuthenticationMethod):
    """Implement `client_secret_jwt` client authentication method.

    With this method, the client generates and signs a client assertion that is symmetrically
    signed 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.

    """

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        alg: str = "HS256",
        lifetime: int = 60,
        jti_gen: Callable[[], Any] = lambda: uuid4(),
        aud: str | None = None,
    ) -> None:
        super().__init__(client_id, alg, lifetime, jti_gen, aud)
        self.client_secret = str(client_secret)

    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
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
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)

PrivateKeyJwt

Bases: ClientAssertionAuthenticationMethod

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.

Parameters:

Name Type Description Default
client_id str

the client_id to use.

required
private_jwk Jwk | dict[str, Any]

the private JWK to use to sign generated Client Assertions.

required
alg str

the alg to use to sign generated Client Assertions.

RS256
lifetime int

the lifetime to use for generated Client Assertions.

60
jti_gen Callable[[], Any]

a function to generate JWT Token Ids (jti) for generated Client Assertions.

lambda: uuid4()
aud str | None

the audience value to use. If None (default), the endpoint URL will be used.k

None
Source code in requests_oauth2client/client_authentication.py
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
class PrivateKeyJwt(ClientAssertionAuthenticationMethod):
    """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.

    Args:
        client_id: the `client_id` to use.
        private_jwk: the private JWK 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

    """

    def __init__(
        self,
        client_id: str,
        private_jwk: Jwk | dict[str, Any],
        alg: str = SignatureAlgs.RS256,
        lifetime: int = 60,
        jti_gen: Callable[[], Any] = lambda: uuid4(),
        aud: str | None = None,
    ) -> None:
        if not isinstance(private_jwk, Jwk):
            private_jwk = Jwk(private_jwk)

        if not private_jwk.is_private or private_jwk.is_symmetric:
            msg = "Private Key JWT client authentication method uses asymmetric signing thus requires a private key."
            raise ValueError(msg)

        alg = private_jwk.alg or alg
        if not alg:
            msg = "An asymmetric signing alg is required, either as part of the private JWK, or passed as parameter."
            raise ValueError(msg)
        kid = private_jwk.get("kid")
        if not kid:
            msg = "Asymmetric signing requires the private JWK to have a Key ID (kid)."
            raise ValueError(msg)

        super().__init__(client_id, alg, lifetime, jti_gen, aud)
        self.private_jwk = private_jwk

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

Parameters:

Name Type Description Default
client_id str

the client_id to use.

required
Source code in requests_oauth2client/client_authentication.py
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
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.

    Args:
        client_id: the client_id to use.

    """

    def __init__(self, client_id: str) -> None:
        self.client_id = client_id

    def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
        """Add the `client_id` field in the request body.

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

        Returns:
            a [requests.PreparedRequest][] with the added `client_id` 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

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] | type[ClientSecretBasic] | type[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.

Source code in requests_oauth2client/client_authentication.py
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
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] | type[ClientSecretBasic] | type[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.

    """
    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 ValueError(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 TypeError(msg, type(credential), credential)

    if client_id is None:
        msg = "A client_id must be provided."
        raise ValueError(msg)

    if private_key is not None:
        return PrivateKeyJwt(str(client_id), private_key)
    elif client_secret is None:
        return PublicApp(str(client_id))
    else:
        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
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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,
    ):
        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
56
57
58
59
60
61
62
63
64
65
66
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: TokenEndpointPoolingJob

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.

{}

auth=("client_id", "client_secret") ) pool_job = DeviceAuthorizationPoolingJob(client=client, device_code="my_device_code")

1
token = None while token is None: token = pool_job() ```
Source code in requests_oauth2client/device_authorization.py
 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
class DeviceAuthorizationPoolingJob(TokenEndpointPoolingJob):
    """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.

    Usage: ```python client = OAuth2Client( token_endpoint="https://my.as.local/token",
    auth=("client_id", "client_secret") ) pool_job = DeviceAuthorizationPoolingJob(client=client,
    device_code="my_device_code")

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

    """

    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,
    ):
        super().__init__(
            client=client,
            interval=interval,
            slow_down_interval=slow_down_interval,
            requests_kwargs=requests_kwargs,
            **token_kwargs,
        )
        self.device_code = device_code

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

        This actually calls [OAuth2Client.device_code(device_code)] on `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 client.

Returns:

Type Description
BearerToken
Source code in requests_oauth2client/device_authorization.py
111
112
113
114
115
116
117
118
119
120
def token_request(self) -> BearerToken:
    """Implement the Device Code token request.

    This actually calls [OAuth2Client.device_code(device_code)] on `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
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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)

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
Source code in requests_oauth2client/exceptions.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class OAuth2Error(Exception):
    """Base class for Exceptions raised when a backend endpoint returns an error.

    Args:
        response: the HTTP response containing the error

    """

    def __init__(self, response: requests.Response):
        self.response = response

    @property
    def request(self) -> requests.PreparedRequest:
        """The request leading to the error."""
        return self.response.request
request: requests.PreparedRequest 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 requests.PreparedResponse 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 requests.PreparedResponse 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,
        error: str,
        description: str | None = None,
        uri: str | None = None,
    ):
        super().__init__(response)
        self.error = error
        self.description = description
        self.uri = uri

InvalidTokenResponse

Bases: OAuth2Error

Raised when the Token Endpoint returns a non-standard response.

Source code in requests_oauth2client/exceptions.py
57
58
class InvalidTokenResponse(OAuth2Error):
    """Raised when the Token Endpoint returns a non-standard response."""

ExpiredAccessToken

Bases: RuntimeError

Raised when an expired access token is used.

Source code in requests_oauth2client/exceptions.py
61
62
class ExpiredAccessToken(RuntimeError):
    """Raised when an expired access token is used."""

UnknownTokenEndpointError

Bases: EndpointError

Raised when an otherwise unknown error is returned by the token endpoint.

Source code in requests_oauth2client/exceptions.py
65
66
class UnknownTokenEndpointError(EndpointError):
    """Raised when an otherwise unknown error is returned by the token endpoint."""

ServerError

Bases: EndpointError

Raised when the token endpoint returns error = server_error.

Source code in requests_oauth2client/exceptions.py
69
70
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
73
74
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
77
78
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
81
82
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
85
86
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
89
90
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
93
94
class InvalidGrant(TokenEndpointError):
    """Raised when the Token Endpoint returns `error = invalid_grant`."""

AccessDenied

Bases: EndpointError

Raised when the Authorization Server returns error = access_denied.

Source code in requests_oauth2client/exceptions.py
97
98
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
101
102
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
105
106
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
109
110
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
113
114
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
117
118
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
121
122
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
125
126
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
129
130
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
133
134
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
137
138
class InvalidDeviceAuthorizationResponse(OAuth2Error):
    """Raised when the Device Authorization Endpoint returns a non-standard error response."""

InvalidIdToken

Bases: InvalidJwt

Raised when trying to validate an invalid ID Token value.

Source code in requests_oauth2client/exceptions.py
141
142
class InvalidIdToken(InvalidJwt):
    """Raised when trying to validate an invalid ID Token value."""

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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, error: str, description: str | None = None, uri: str | None = None):
        self.error = error
        self.description = description
        self.uri = uri

InteractionRequired

Bases: AuthorizationResponseError

Raised when the Authorization Endpoint returns error = interaction_required.

Source code in requests_oauth2client/exceptions.py
164
165
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
168
169
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
172
173
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
176
177
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
180
181
class ConsentRequired(InteractionRequired):
    """Raised when the Authorization Endpoint returns `error = consent_required`."""

InvalidAuthResponse

Bases: Exception

Raised when the Authorization Endpoint returns an invalid response.

Source code in requests_oauth2client/exceptions.py
184
185
class InvalidAuthResponse(Exception):
    """Raised when the Authorization Endpoint returns an invalid 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
188
189
190
191
192
193
194
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.

    """

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
197
198
199
200
201
202
203
204
205
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.

    """

MissingIdToken

Bases: InvalidAuthResponse

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/exceptions.py
208
209
210
211
212
213
214
class MissingIdToken(InvalidAuthResponse):
    """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.

    """

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
217
218
219
220
221
222
223
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.

    """

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
226
227
228
229
230
231
232
class MismatchingIssuer(InvalidAuthResponse):
    """Raised on mismatching `iss` value.

    This happens when the Authorization Endpoints returns an 'iss' that doesn't match the expected
    value.

    """

MismatchingNonce

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/exceptions.py
235
236
237
238
239
240
241
class MismatchingNonce(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.

    """

MismatchingAcr

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/exceptions.py
244
245
246
247
248
249
250
class MismatchingAcr(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.

    """

MismatchingAudience

Bases: InvalidIdToken

Raised when the ID Token audience does not include the requesting Client ID.

Source code in requests_oauth2client/exceptions.py
253
254
class MismatchingAudience(InvalidIdToken):
    """Raised when the ID Token audience does not include the requesting Client ID."""

MismatchingAzp

Bases: InvalidIdToken

Raised when the ID Token Authorized Presenter (azp) claim is not the Client ID.

Source code in requests_oauth2client/exceptions.py
257
258
class MismatchingAzp(InvalidIdToken):
    """Raised when the ID Token Authorized Presenter (azp) claim is not the Client ID."""

MismatchingIdTokenAlg

Bases: InvalidIdToken

Raised when the returned ID Token is signed with an unexpected alg.

Source code in requests_oauth2client/exceptions.py
261
262
class MismatchingIdTokenAlg(InvalidIdToken):
    """Raised when the returned ID Token is signed with an unexpected alg."""

ExpiredIdToken

Bases: InvalidIdToken

Raised when the returned ID Token is expired.

Source code in requests_oauth2client/exceptions.py
265
266
class ExpiredIdToken(InvalidIdToken):
    """Raised when the returned ID Token is expired."""

BackChannelAuthenticationError

Bases: EndpointError

Base class for errors returned by the BackChannel Authentication endpoint.

Source code in requests_oauth2client/exceptions.py
269
270
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
273
274
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
277
278
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
67
68
69
70
71
72
73
74
class FlaskOAuth2ClientCredentialsAuth(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.

    """

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
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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:
        super().__init__(*args, **token_kwargs)
        self.serializer = serializer or BearerTokenSerializer()
        self.session_key = session_key

    @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: BearerToken | None 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
67
68
69
70
71
72
73
74
class FlaskOAuth2ClientCredentialsAuth(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.

    """

pooling

Contains base classes for pooling jobs.

TokenEndpointPoolingJob

Bases: ABC

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.

Parameters:

Name Type Description Default
client OAuth2Client

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

required
interval int | None

initial pooling interval, in seconds. If None, default to 5.

None
slow_down_interval int

when a SlowDown is received, this number of seconds will be added to the pooling interval.

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

{}
Source code in requests_oauth2client/pooling.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
class TokenEndpointPoolingJob(ABC):
    """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.

    Args:
        client: the [OAuth2Client][requests_oauth2client.client.OAuth2Client] that will be used
            to pool the token endpoint.
        interval: initial pooling interval, in seconds. If `None`, default to `5`.
        slow_down_interval: when a [SlowDown][requests_oauth2client.exceptions.SlowDown] is
            received, this number of seconds will be added to the pooling interval.
        requests_kwargs: additional parameters for the underlying calls to [requests.request][]
        **token_kwargs: additional parameters for the token request

    """

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

    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][requests_oauth2client.tokens.BearerToken] if the AS returns one, or
            `None` if the Authorization is still pending.

        """
        time.sleep(self.interval)
        try:
            return self.token_request()
        except SlowDown:
            self.interval += self.slow_down_interval
        except AuthorizationPending:
            pass
        return None

    @abstractmethod
    def token_request(self) -> BearerToken:
        """Abstract method for the token endpoint call.

        This must be implemented by subclasses. This method must 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  # pragma: no cover
token_request() abstractmethod

Abstract method for the token endpoint call.

This must be implemented by subclasses. This method must 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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@abstractmethod
def token_request(self) -> BearerToken:
    """Abstract method for the token endpoint call.

    This must be implemented by subclasses. This method must 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  # pragma: no cover

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
32
33
34
35
36
37
class TokenType(str, Enum):
    """An enum of standardised `token_type` values."""

    ACCESS_TOKEN = "access_token"
    REFRESH_TOKEN = "refresh_token"
    ID_TOKEN = "id_token"

AccessTokenType

Bases: str, Enum

An enum of standardised access_token types.

Source code in requests_oauth2client/tokens.py
40
41
42
43
class AccessTokenType(str, Enum):
    """An enum of standardised `access_token` types."""

    BEARER = "Bearer"

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
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 auth_time(self) -> datetime:
        """The last user authentication time."""
        auth_time = self.claims.get("auth_time")
        if auth_time:
            return self.timestamp_to_datetime(auth_time)
        msg = "This ID Token doesn't have an `auth_time` attribute."
        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, s_hash.

        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
auth_time: datetime property

The last user authentication time.

hash_method(key, alg=None) classmethod

Returns a callable that generates valid OIDC hashes, such as at_hash, c_hash, s_hash.

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
63
64
65
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
@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, s_hash.

    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

AccessToken

Base class for Access Tokens.

Source code in requests_oauth2client/tokens.py
 97
 98
 99
100
class AccessToken:
    """Base class for Access Tokens."""

    TOKEN_TYPE: ClassVar[str]

BearerToken

Bases: AccessToken

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
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
@frozen(init=False)
class BearerToken(AccessToken):
    """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] = AccessTokenType.BEARER.value

    access_token: str
    expires_at: datetime | None = None
    scope: str | None = None
    refresh_token: str | None = None
    token_type: str = TOKEN_TYPE
    id_token: IdToken | jwskate.JweCompact | None = None
    kwargs: dict[str, Any] = Factory(dict)

    @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,
    ):
        if token_type.title() != self.TOKEN_TYPE.title():
            msg = f"Token Type is not '{self.TOKEN_TYPE}'!"
            raise ValueError(msg, token_type)
        id_token_jwt: IdToken | jwskate.JweCompact | None = None
        if isinstance(id_token, (str, bytes)):
            try:
                id_token_jwt = IdToken(id_token)
            except jwskate.InvalidJwt:
                try:
                    id_token_jwt = jwskate.JweCompact(id_token)
                except jwskate.InvalidJwe:
                    msg = "ID Token is invalid because it is  neither a JWT or a JWE."
                    raise InvalidIdToken(msg) from None
        else:
            id_token_jwt = 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_jwt,
            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"Bearer {self.access_token}"

    def validate_id_token(self, client: OAuth2Client, azr: AuthorizationResponse) -> Self:  # noqa: C901, PLR0915
        """Validate that a token response is valid, and return the 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).

        If the ID Token is encrypted, this decrypts it and returns the clear-text ID Token.

        """
        if not self.id_token:
            raise MissingIdToken()

        raw_id_token = self.id_token

        if isinstance(raw_id_token, jwskate.JweCompact) and client.id_token_encrypted_response_alg is None:
            msg = "ID Token is encrypted while it should be clear-text"
            raise InvalidIdToken(msg, self)
        elif isinstance(raw_id_token, IdToken) and client.id_token_encrypted_response_alg is not None:
            msg = "ID Token is clear-text while it should be encrypted"
            raise InvalidIdToken(msg, self)

        if isinstance(raw_id_token, jwskate.JweCompact):
            enc_jwk = client.id_token_decryption_key
            if enc_jwk is None:
                msg = "ID Token is encrypted but client does not have a decryption key"
                raise InvalidIdToken(msg, self)
            nested_id_token = raw_id_token.decrypt(enc_jwk)
            id_token = IdToken(nested_id_token)
        else:
            id_token = raw_id_token

        if id_token.get_header("alg") is None and client.id_token_signed_response_alg is None:
            msg = (
                "ID 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)
        elif 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)

        id_token_alg = id_token.alg or client.id_token_signed_response_alg

        if azr.issuer and id_token.issuer != azr.issuer:
            raise MismatchingIssuer(id_token.issuer, azr.issuer, self)

        if id_token.audiences and client.client_id not in id_token.audiences:
            raise MismatchingAudience(id_token.audiences, client.client_id, self)

        if id_token.get_claim("azp") is not None and id_token.azp != client.client_id:
            raise MismatchingAzp(id_token.azp, client.client_id, self)

        if id_token.is_expired():
            raise ExpiredIdToken(id_token)

        if azr.nonce and id_token.nonce != azr.nonce:
            raise MismatchingNonce()

        if azr.acr_values and id_token.acr not in azr.acr_values:
            raise MismatchingAcr(id_token.acr, azr.acr_values)

        hash_function: Callable[[str], str]  # method used to calculate at_hash, s_hash, etc.

        if id_token_alg in jwskate.SignatureAlgs.ALL_SYMMETRIC:
            if not client.client_secret:
                msg = "ID Token is symmetrically signed but this client does not have a Client Secret."
                raise InvalidIdToken(msg)
            id_token.verify_signature(jwskate.SymmetricJwk.from_bytes(client.client_secret), alg=id_token_alg)
        elif id_token_alg in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
            if not client.authorization_server_jwks:
                msg = "ID Token is asymmetrically signed but the Authorization Server JWKS is not available."
                raise InvalidIdToken(msg)

            if id_token.get_header("kid") is None:
                msg = (
                    "ID Token does not contain a Key ID (kid) to specify the asymmetric key "
                    "to use for signature verification."
                )
                raise InvalidIdToken(msg)
            try:
                verification_jwk = client.authorization_server_jwks.get_jwk_by_kid(id_token.kid)
            except KeyError:
                msg = (
                    f"ID Token is asymmetrically signed but its Key ID '{id_token.kid}' "
                    "is not part of the Authorization Server JWKS."
                )
                raise InvalidIdToken(msg) from None

            if id_token_alg not in verification_jwk.supported_signing_algorithms():
                msg = "ID Token is asymmetrically signed but its algorithm is not supported by the verification key."
                raise InvalidIdToken(msg)

            id_token.verify_signature(verification_jwk, alg=id_token_alg)

            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)

        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)

        s_hash = id_token.get_claim("s_hash")
        if s_hash is not None:
            if azr.state is None:
                msg = "ID Token has a 's_hash' claim but no state was included in the request."
                raise InvalidIdToken(msg)
            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)

        if azr.max_age is not None:
            try:
                auth_time = id_token.auth_time
            except AttributeError:
                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) from None
            auth_age = datetime.now(tz=timezone.utc) - auth_time
            if auth_age.seconds > azr.max_age + 60:
                msg = (
                    "User authentication happened too long ago. The `auth_time` parameter from"
                    " the ID Token indicate that the last Authentication Time was at"
                    f" {auth_time} ({auth_age.seconds} sec ago), but the authorization request"
                    f" `max_age` parameter specified that it must be maximum {azr.max_age} sec"
                    " ago."
                )
                raise InvalidIdToken(msg)

        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 int(self.expires_at.timestamp() - datetime.now(tz=timezone.utc).timestamp())
        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)
expires_in: int | None property

Number of seconds until expiration.

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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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
192
193
194
195
196
197
198
199
200
201
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"Bearer {self.access_token}"
validate_id_token(client, azr)

Validate that a token response is valid, and return the ID Token.

This will validate the id_token as described in OIDC 1.0 $3.1.3.7.

If the ID Token is encrypted, this decrypts it and returns the clear-text ID Token.

Source code in requests_oauth2client/tokens.py
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
def validate_id_token(self, client: OAuth2Client, azr: AuthorizationResponse) -> Self:  # noqa: C901, PLR0915
    """Validate that a token response is valid, and return the 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).

    If the ID Token is encrypted, this decrypts it and returns the clear-text ID Token.

    """
    if not self.id_token:
        raise MissingIdToken()

    raw_id_token = self.id_token

    if isinstance(raw_id_token, jwskate.JweCompact) and client.id_token_encrypted_response_alg is None:
        msg = "ID Token is encrypted while it should be clear-text"
        raise InvalidIdToken(msg, self)
    elif isinstance(raw_id_token, IdToken) and client.id_token_encrypted_response_alg is not None:
        msg = "ID Token is clear-text while it should be encrypted"
        raise InvalidIdToken(msg, self)

    if isinstance(raw_id_token, jwskate.JweCompact):
        enc_jwk = client.id_token_decryption_key
        if enc_jwk is None:
            msg = "ID Token is encrypted but client does not have a decryption key"
            raise InvalidIdToken(msg, self)
        nested_id_token = raw_id_token.decrypt(enc_jwk)
        id_token = IdToken(nested_id_token)
    else:
        id_token = raw_id_token

    if id_token.get_header("alg") is None and client.id_token_signed_response_alg is None:
        msg = (
            "ID 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)
    elif 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)

    id_token_alg = id_token.alg or client.id_token_signed_response_alg

    if azr.issuer and id_token.issuer != azr.issuer:
        raise MismatchingIssuer(id_token.issuer, azr.issuer, self)

    if id_token.audiences and client.client_id not in id_token.audiences:
        raise MismatchingAudience(id_token.audiences, client.client_id, self)

    if id_token.get_claim("azp") is not None and id_token.azp != client.client_id:
        raise MismatchingAzp(id_token.azp, client.client_id, self)

    if id_token.is_expired():
        raise ExpiredIdToken(id_token)

    if azr.nonce and id_token.nonce != azr.nonce:
        raise MismatchingNonce()

    if azr.acr_values and id_token.acr not in azr.acr_values:
        raise MismatchingAcr(id_token.acr, azr.acr_values)

    hash_function: Callable[[str], str]  # method used to calculate at_hash, s_hash, etc.

    if id_token_alg in jwskate.SignatureAlgs.ALL_SYMMETRIC:
        if not client.client_secret:
            msg = "ID Token is symmetrically signed but this client does not have a Client Secret."
            raise InvalidIdToken(msg)
        id_token.verify_signature(jwskate.SymmetricJwk.from_bytes(client.client_secret), alg=id_token_alg)
    elif id_token_alg in jwskate.SignatureAlgs.ALL_ASYMMETRIC:
        if not client.authorization_server_jwks:
            msg = "ID Token is asymmetrically signed but the Authorization Server JWKS is not available."
            raise InvalidIdToken(msg)

        if id_token.get_header("kid") is None:
            msg = (
                "ID Token does not contain a Key ID (kid) to specify the asymmetric key "
                "to use for signature verification."
            )
            raise InvalidIdToken(msg)
        try:
            verification_jwk = client.authorization_server_jwks.get_jwk_by_kid(id_token.kid)
        except KeyError:
            msg = (
                f"ID Token is asymmetrically signed but its Key ID '{id_token.kid}' "
                "is not part of the Authorization Server JWKS."
            )
            raise InvalidIdToken(msg) from None

        if id_token_alg not in verification_jwk.supported_signing_algorithms():
            msg = "ID Token is asymmetrically signed but its algorithm is not supported by the verification key."
            raise InvalidIdToken(msg)

        id_token.verify_signature(verification_jwk, alg=id_token_alg)

        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)

    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)

    s_hash = id_token.get_claim("s_hash")
    if s_hash is not None:
        if azr.state is None:
            msg = "ID Token has a 's_hash' claim but no state was included in the request."
            raise InvalidIdToken(msg)
        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)

    if azr.max_age is not None:
        try:
            auth_time = id_token.auth_time
        except AttributeError:
            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) from None
        auth_age = datetime.now(tz=timezone.utc) - auth_time
        if auth_age.seconds > azr.max_age + 60:
            msg = (
                "User authentication happened too long ago. The `auth_time` parameter from"
                " the ID Token indicate that the last Authentication Time was at"
                f" {auth_time} ({auth_age.seconds} sec ago), but the authorization request"
                f" `max_age` parameter specified that it must be maximum {azr.max_age} sec"
                " ago."
            )
            raise InvalidIdToken(msg)

    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
362
363
364
365
366
367
368
369
370
371
372
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
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
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,
    ):
        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

        """
        return BinaPy.serialize_to("json", token.as_dict()).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
420
421
422
423
424
425
426
427
428
429
430
431
@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

    """
    return BinaPy.serialize_to("json", token.as_dict()).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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
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
452
453
454
455
456
457
458
459
460
461
462
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
464
465
466
467
468
469
470
471
472
473
474
def loads(self, serialized: str) -> BearerToken:
    """Deserialize a serialized token.

    Args:
        serialized: the serialized token

    Returns:
        the deserialized token

    """
    return self.loader(serialized)

DPoPToken

Bases: AccessToken

Represents a DPoP Token.

Source code in requests_oauth2client/tokens.py
477
478
class DPoPToken(AccessToken):
    """Represents a DPoP 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
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 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
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")
            or tenant.endswith(".us")
            or tenant.endswith(".au")
            or tenant.endswith(".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

        Usage:
            ```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
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@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")
        or tenant.endswith(".us")
        or tenant.endswith(".au")
        or tenant.endswith(".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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@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

{}
Usage
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
 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
@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

    Usage:
        ```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
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@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
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 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
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")
            or tenant.endswith(".us")
            or tenant.endswith(".au")
            or tenant.endswith(".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

        Usage:
            ```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
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@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")
        or tenant.endswith(".us")
        or tenant.endswith(".au")
        or tenant.endswith(".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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@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

{}
Usage
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
 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
@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

    Usage:
        ```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
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@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,
    )