diff options
author | Stephen Rosen <sirosen@globus.org> | 2022-11-02 06:01:52 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-02 17:01:52 +0600 |
commit | 00cd759d86aae24176ead7bdbed273a07532443e (patch) | |
tree | 34f5fd7e5d0c55299fc5c8685fb558aeebaf9f4a /docs/usage.rst | |
parent | 345549567dbb58fd7bf901392cf6b1a626f36e24 (diff) | |
download | pyjwt-00cd759d86aae24176ead7bdbed273a07532443e.tar.gz |
Add `Algorithm.compute_hash_digest` and use it to implement at_hash validation example (#775)
* Add compute_hash_digest to Algorithm objects
`Algorithm.compute_hash_digest` is defined as a method which inspects
the object to see that it has the requisite attributes, `hash_alg`.
If `hash_alg` is not set, then the method raises a
NotImplementedError. This applies to classes like NoneAlgorithm.
If `hash_alg` is set, then it is checked for
```
has_crypto # is cryptography available?
and isinstance(hash_alg, type)
and issubclass(hash_alg, hashes.HashAlgorithm)
```
to see which API for computing a digest is appropriate --
`hashlib` vs `cryptography.hazmat.primitives.hashes`.
These checks could be avoided at runtime if it were necessary to
optimize further (e.g. attach compute_hash_digest methods to classes
with a class decorator) but this is not clearly a worthwhile
optimization. Such perf tuning is intentionally omitted for now.
* Add doc example of OIDC login flow
The goal of this doc example is to demonstrate usage of
`get_algorithm_by_name` and `compute_hash_digest` for the purpose of
`at_hash` validation. It is not meant to be a "guaranteed correct" and
spec-compliant example.
closes #314
Diffstat (limited to 'docs/usage.rst')
-rw-r--r-- | docs/usage.rst | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/docs/usage.rst b/docs/usage.rst index 91d9679..a85fa18 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -297,3 +297,74 @@ Retrieve RSA signing keys from a JWKS endpoint ... ) >>> print(data) {'iss': 'https://dev-87evx9ru.auth0.com/', 'sub': 'aW4Cca79xReLWUz0aE2H6kD0O3cXBVtC@clients', 'aud': 'https://expenses-api', 'iat': 1572006954, 'exp': 1572006964, 'azp': 'aW4Cca79xReLWUz0aE2H6kD0O3cXBVtC', 'gty': 'client-credentials'} + +OIDC Login Flow +--------------- + +The following usage demonstrates an OIDC login flow using pyjwt. Further +reading about the OIDC spec is recommended for implementers. + +In particular, this demonstrates validation of the ``at_hash`` claim. +This claim relies on data from outside of the the JWT for validation. Methods +are provided which support computation and validation of this claim, but it +is not built into pyjwt. + +.. code-block:: python + + import base64 + import jwt + import requests + + + # Part 1: setup + # get the OIDC config and JWKs to use + + # in OIDC, you must know your client_id (this is the OAuth 2.0 client_id) + client_id = ... + + # example of fetching data from your OIDC server + # see: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig + oidc_server = ... + oidc_config = requests.get( + f"https://{oidc_server}/.well-known/openid-configuration" + ).json() + signing_algos = oidc_config["id_token_signing_alg_values_supported"] + + # setup a PyJWKClient to get the appropriate signing key + jwks_client = jwt.PyJWKClient(oidc_config["jwks_uri"]) + + + # Part 2: login / authorization + # when a user completes an OIDC login flow, there will be a well-formed + # response object to parse/handle + + # data from the login flow + # see: https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse + token_response = ... + id_token = token_response["id_token"] + access_token = token_response["access_token"] + + + # Part 3: decode and validate at_hash + # after the login is complete, the id_token needs to be decoded + # this is the stage at which an OIDC client must verify the at_hash + + # get signing_key from id_token + signing_key = jwks_client.get_signing_key_from_jwt(id_token) + + # now, decode_complete to get payload + header + data = jwt.decode_complete( + id_token, + key=signing_key.key, + algorithms=signing_algos, + audience=client_id, + ) + payload, header = data["payload"], data["header"] + + # get the pyjwt algorithm object + alg_obj = jwt.get_algorithm_by_name(header["alg"]) + + # compute at_hash, then validate / assert + digest = alg_obj.compute_hash_digest(access_token) + at_hash = base64.urlsafe_b64encode(digest[: (len(digest) // 2)]).rstrip("=") + assert at_hash == payload["at_hash"] |