Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support computation and validation of OIDC at_hash value #295

Closed
sirosen opened this issue Sep 28, 2017 · 1 comment
Closed

Support computation and validation of OIDC at_hash value #295

sirosen opened this issue Sep 28, 2017 · 1 comment
Labels
stale Issues without activity for more than 60 days

Comments

@sirosen
Copy link
Contributor

sirosen commented Sep 28, 2017

Open ID Connect specifies an optional additional claim for its ID token JWTs: the at_hash.
It's just a hash of the access token issued alongside the ID token.

Although OIDC lists it as optional, it also states that it's required when performing the "Authorization Code Grant" (3-legged OAuth), which is confusing.

It's very easy to compute. Just

token_hash = hashlib.sha256(access_token).digest()
base64.urlsafe_b64encode(
    token_hash[:(len(token_hash) / 2)]
    ).rstrip('=')

replacing hashlib.sha256 with whatever alg is specified.

For those implementing OIDC servers, and clients of those servers, computing and validating this at_hash value, respectively, makes sense as PyJWT functionality.

sirosen added a commit to sirosen/pyjwt that referenced this issue Oct 2, 2017
Use PyJWT to compute the at_hash value for OpenID Connect:
http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken

This makes more sense in PyJWT than its client code because of the tight
coupling between the chosen signing algorithm and the computation of the
at_hash. Any client code would have to jump through hoops to get this to
work nicely based on the algorithm being fed to PyJWT.

Closes jpadilla#295

Primary changes:

Add support for access_token=... as a param to PyJWT.encode and
PyJWT.decode . On encode, the at_hash claim is computed and added to the
payload. On decode, unpacks the at_hash value, raising a missing claim
error if its missing, and compares it to a freshly computed at_hash.
Raises a new error type if they don't match.
Does not use the verification options dict, as it's redundant with the
caller supplying access_token in this case.

Supporting changes:
- Add tests for the above
- Let PyJWT and PyJWS get an algorithm object from a string as a method
- Add a method, compute_at_hash, to PyJWT objects
- PyJWT._validate_claims now takes the header as an arg (needed to get
  algo)
sirosen added a commit to sirosen/pyjwt that referenced this issue Oct 2, 2017
Use PyJWT to compute the at_hash value for OpenID Connect:
http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken

This makes more sense in PyJWT than its client code because of the tight
coupling between the chosen signing algorithm and the computation of the
at_hash. Any client code would have to jump through hoops to get this to
work nicely based on the algorithm being fed to PyJWT.

Closes jpadilla#295

Primary changes:

Add support for access_token=... as a param to PyJWT.encode and
PyJWT.decode . On encode, the at_hash claim is computed and added to the
payload. On decode, unpacks the at_hash value, raising a missing claim
error if its missing, and compares it to a freshly computed at_hash.
Raises a new error type if they don't match.
Does not use the verification options dict, as it's redundant with the
caller supplying access_token in this case.

Supporting changes:
- Add tests for the above
- Let PyJWT and PyJWS get an algorithm object from a string as a method
- Add a method, compute_at_hash, to PyJWT objects
- PyJWT._validate_claims now takes the header as an arg (needed to get
  algo)
sirosen added a commit to sirosen/pyjwt that referenced this issue Oct 2, 2017
Use PyJWT to compute the at_hash value for OpenID Connect:
http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken

This makes more sense in PyJWT than its client code because of the tight
coupling between the chosen signing algorithm and the computation of the
at_hash. Any client code would have to jump through hoops to get this to
work nicely based on the algorithm being fed to PyJWT.

Closes jpadilla#295

Primary changes:

Add support for access_token=... as a param to PyJWT.encode and
PyJWT.decode . On encode, the at_hash claim is computed and added to the
payload. On decode, unpacks the at_hash value, raising a missing claim
error if its missing, and compares it to a freshly computed at_hash.
Raises a new error type if they don't match.
Does not use the verification options dict, as it's redundant with the
caller supplying access_token in this case.

Supporting changes:
- Add tests for the above
- Let PyJWT and PyJWS get an algorithm object from a string as a method
- Add a method, compute_at_hash, to PyJWT objects
- PyJWT._validate_claims now takes the header as an arg (needed to get
  algo)
sirosen added a commit to sirosen/pyjwt that referenced this issue Oct 19, 2017
Use PyJWT to compute the at_hash value for OpenID Connect:
http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken

This makes more sense in PyJWT than its client code because of the tight
coupling between the chosen signing algorithm and the computation of the
at_hash. Any client code would have to jump through hoops to get this to
work nicely based on the algorithm being fed to PyJWT.

Closes jpadilla#295

Primary changes:

Add support for access_token=... as a param to PyJWT.encode and
PyJWT.decode . On encode, the at_hash claim is computed and added to the
payload. On decode, unpacks the at_hash value, raising a missing claim
error if its missing, and compares it to a freshly computed at_hash.
Raises a new error type if they don't match.
Does not use the verification options dict, as it's redundant with the
caller supplying access_token in this case.

Supporting changes:
- Add tests for the above
- Let PyJWT and PyJWS get an algorithm object from a string as a method
- Add a method, compute_at_hash, to PyJWT objects
- PyJWT._validate_claims now takes the header as an arg (needed to get
  algo)
sirosen added a commit to sirosen/pyjwt that referenced this issue Jan 10, 2018
The JWTPayload class allows PyJWT.decode() to expose header, signature,
signing_input, and compute_hash_digest() (based on header) without
changing the pyjwt API in a breaking way.
Merely making this info accessible to the client (without specify an
additional verification callback scheme) is simpler for everyone.

Include doc on why JWTPayload is a good idea (in a docstring), since
it's a little unusual to subclass `dict`. The intent is to make the JWT
payload change as little as possible while still making it easy to add
more verification after the fact.

Add a simple test for `JWTPayload.compute_hash_digest()`

Closes jpadilla#314, jpadilla#295
sirosen added a commit to sirosen/pyjwt that referenced this issue Jan 10, 2018
The JWTPayload class allows PyJWT.decode() to expose header, signature,
signing_input, and compute_hash_digest() (based on header) without
changing the pyjwt API in a breaking way.
Merely making this info accessible to the client (without specify an
additional verification callback scheme) is simpler for everyone.

Include doc on why JWTPayload is a good idea (in a docstring), since
it's a little unusual to subclass `dict`. The intent is to make the JWT
payload change as little as possible while still making it easy to add
more verification after the fact.

Add a simple test for `JWTPayload.compute_hash_digest()`

Closes jpadilla#314, jpadilla#295
sirosen added a commit to sirosen/pyjwt that referenced this issue Jan 10, 2018
The JWTPayload class allows PyJWT.decode() to expose header, signature,
signing_input, and compute_hash_digest() (based on header) without
changing the pyjwt API in a breaking way.
Merely making this info accessible to the client (without specify an
additional verification callback scheme) is simpler for everyone.

Include doc on why JWTPayload is a good idea (in a docstring), since
it's a little unusual to subclass `dict`. The intent is to make the JWT
payload change as little as possible while still making it easy to add
more verification after the fact.

Add a simple test for `JWTPayload.compute_hash_digest()`

Closes jpadilla#314, jpadilla#295
sirosen added a commit to sirosen/pyjwt that referenced this issue May 14, 2020
The JWTPayload class allows PyJWT.decode() to expose header, signature,
signing_input, and compute_hash_digest() (based on header) without
changing the pyjwt API in a breaking way.
Merely making this info accessible to the client without specifying an
additional verification callback scheme is simpler for everyone.

Include doc on why JWTPayload is a good idea in a module docstring,
since it's a little unusual to subclass `dict`. The intent is to make
the JWT payload change as little as possible while still making it easy
to add more verification after the fact.

Add a simple test for `JWTPayload.compute_hash_digest()` and a test
for compute_hash_digest with cryptography (which is compared against a
manual hashlib usage).

Closes jpadilla#314, jpadilla#295
@github-actions
Copy link

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days

@github-actions github-actions bot added the stale Issues without activity for more than 60 days label Jun 22, 2022
sirosen added a commit to sirosen/pyjwt that referenced this issue Jun 29, 2022
Looking up an algorithm by name is used internally for signature
generation. This encapsulates that functionality in a dedicated method
and adds it to the public API. No new tests are needed to exercise the
functionality.

Rationale:

1. Inside of PyJWS, this improves the code. The KeyError handler is
   better scoped and the signing code reads more directly.

2. This is part of the path to supporting OIDC at_hash validation as a
   use-case (see: jpadilla#295, jpadilla#296, jpadilla#314).

This is arguably sufficient to consider that use-case supported and
close it. However, it is an improvement and step in the right
direction in either case.
sirosen added a commit to sirosen/pyjwt that referenced this issue Jun 29, 2022
Looking up an algorithm by name is used internally for signature
generation. This encapsulates that functionality in a dedicated method
and adds it to the public API. No new tests are needed to exercise the
functionality.

Rationale:

1. Inside of PyJWS, this improves the code. The KeyError handler is
   better scoped and the signing code reads more directly.

2. This is part of the path to supporting OIDC at_hash validation as a
   use-case (see: jpadilla#295, jpadilla#296, jpadilla#314).

This is arguably sufficient to consider that use-case supported and
close it. However, it is an improvement and step in the right
direction in either case.
sirosen added a commit to sirosen/pyjwt that referenced this issue Jun 29, 2022
Looking up an algorithm by name is used internally for signature
generation. This encapsulates that functionality in a dedicated method
and adds it to the public API. No new tests are needed to exercise the
functionality.

Rationale:

1. Inside of PyJWS, this improves the code. The KeyError handler is
   better scoped and the signing code reads more directly.

2. This is part of the path to supporting OIDC at_hash validation as a
   use-case (see: jpadilla#295, jpadilla#296, jpadilla#314).

This is arguably sufficient to consider that use-case supported and
close it. However, it is an improvement and step in the right
direction in either case.

A minor change was needed to satisfy mypy, as a union-typed variable
does not narrow its type based on assignments. The easiest resolution
is to use a new name, in this case, simply `algorithm -> algorithm_`.
jpadilla pushed a commit that referenced this issue Jul 3, 2022
* Expose get_algorithm_by_name as new method

Looking up an algorithm by name is used internally for signature
generation. This encapsulates that functionality in a dedicated method
and adds it to the public API. No new tests are needed to exercise the
functionality.

Rationale:

1. Inside of PyJWS, this improves the code. The KeyError handler is
   better scoped and the signing code reads more directly.

2. This is part of the path to supporting OIDC at_hash validation as a
   use-case (see: #295, #296, #314).

This is arguably sufficient to consider that use-case supported and
close it. However, it is an improvement and step in the right
direction in either case.

A minor change was needed to satisfy mypy, as a union-typed variable
does not narrow its type based on assignments. The easiest resolution
is to use a new name, in this case, simply `algorithm -> algorithm_`.

* Use get_algorithm_by_name in _verify_signature

Rather than catching the KeyError from a dict lookup, catch the
NotImplementedError raised by get_algorithm_by_name. This changes the
exception seen in the cause under exception chaining but otherwise has
no public-facing impact.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale Issues without activity for more than 60 days
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant