Skip to content

Add reqval.fill_id_token with technicals OIDC fields into id_token #660

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

Merged
merged 15 commits into from
May 6, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add technicals fields of id_token in oauthlib OIDC support
A new RequestValidator `fill_id_token` has been introduced to replace `get_id_token`. It aims to have the bare minimum amount of fields to complete a full OIDC id_token support. `get_id_token` is still valid but optional, and if it is implemented, `fill_id_token` will not be called. The current `fill_id_token` came with full support of `aud`, `iat`, `nonce`, `at_hash` and `c_hash`. More could come in the future e.g. `auth_time`, ...
  • Loading branch information
JonathanHuot committed Feb 28, 2019
commit 7c570c763725fdaa40778d6cd6689b09b3971f50
17 changes: 9 additions & 8 deletions docs/oauth2/oidc/id_tokens.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
ID Tokens
=========

The creation of `ID Tokens`_ is ultimately done not by OAuthLib but by your ``RequestValidator`` subclass. This is because their
The creation of `ID Tokens`_ is ultimately not done by OAuthLib but by your ``RequestValidator`` subclass. This is because their
content is dependent on your implementation of users, their attributes, any claims you may wish to support, as well as the
details of how you model the notion of a Client Application. As such OAuthLib simply calls your validator's ``get_id_token``
details of how you model the notion of a Client Application. As such OAuthLib simply calls your validator's ``fill_id_token``
method at the appropriate times during the authorization flow, depending on the grant type requested (Authorization Code, Implicit,
Hybrid, etc.).

Expand All @@ -12,7 +12,7 @@ See examples below.
.. _`ID Tokens`: http://openid.net/specs/openid-connect-core-1_0.html#IDToken

.. autoclass:: oauthlib.oauth2.RequestValidator
:members: get_id_token
:members: fill_id_token


JWT/JWS example with pyjwt library
Expand All @@ -38,12 +38,13 @@ You can switch to jwcrypto library if you want to return JWE instead.

super().__init__(self, **kwargs)

def get_id_token(self, token, token_handler, request):
def fill_id_token(self, id_token, token, token_handler, request):
import jwt

data = {"nonce": request.nonce} if request.nonce is not None else {}

id_token["iss"] = "https://my.cool.app.com"
id_token["sub"] = request.user.id
id_token["exp"] = id_token["iat"] + 3600 * 24 # keep it valid for 24hours
for claim_key in request.claims:
data[claim_key] = request.userattributes[claim_key] # this must be set in another callback
id_token[claim_key] = request.userattributes[claim_key] # this must be set in another callback

return jwt.encode(data, self.private_pem, 'RS256')
return jwt.encode(id_token, self.private_pem, 'RS256')
3 changes: 3 additions & 0 deletions oauthlib/oauth2/rfc6749/request_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs):
- Code Challenge (``request.code_challenge``) and
- Code Challenge Method (``request.code_challenge_method``)

To support OIDC, you MUST associate the code with:
- nonce, if present (``code["nonce"]``)

The ``code`` argument is actually a dictionary, containing at least a
``code`` key with the actual authorization code:

Expand Down
20 changes: 20 additions & 0 deletions oauthlib/openid/connect/core/grant_types/authorization_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,23 @@ def __init__(self, request_validator=None, **kwargs):
self.custom_validators.post_auth.append(
self.openid_authorization_validator)
self.register_token_modifier(self.add_id_token)

def add_id_token(self, token, token_handler, request):
"""
Construct an initial version of id_token, and let the
request_validator sign or encrypt it.

The authorization_code version of this method is used to
retrieve the nonce accordingly to the code storage.
"""
# Treat it as normal OAuth 2 auth code request if openid is not present
if not request.scopes or 'openid' not in request.scopes:
return token

nonce = self.request_validator.get_authorization_code_nonce(
request.client_id,
request.code,
request.redirect_uri,
request
)
return super(AuthorizationCodeGrant, self).add_id_token(token, token_handler, request, nonce=nonce)
64 changes: 58 additions & 6 deletions oauthlib/openid/connect/core/grant_types/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from .exceptions import OIDCNoPrompt

import base64
import datetime
import hashlib
import logging
from json import loads

Expand Down Expand Up @@ -49,7 +51,45 @@ def _inflate_claims(self, request):
raise InvalidRequestError(description="Malformed claims parameter",
uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter")

def add_id_token(self, token, token_handler, request):
def hash_id_token(self, value, hashfunc=hashlib.sha256):
"""
Its value is the base64url encoding of the left-most half of the
hash of the octets of the ASCII representation of the access_token
value, where the hash algorithm used is the hash algorithm used in
the alg Header Parameter of the ID Token's JOSE Header.

For instance, if the alg is RS256, hash the access_token value
with SHA-256, then take the left-most 128 bits and
base64url-encode them.
For instance, if the alg is HS512, hash the code value with
SHA-512, then take the left-most 256 bits and base64url-encode
them. The c_hash value is a case-sensitive string.

Example of hash from OIDC specification (bound to a JWS using RS256):

code:
Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk

c_hash:
LDktKdoQak3Pk0cnXxCltA
"""
digest = hashfunc(value.encode()).digest()
left_most = int(len(digest) / 2)
return base64.urlsafe_b64encode(digest[:left_most]).decode().rstrip("=")

def add_id_token(self, token, token_handler, request, nonce=None):
"""
Construct an initial version of id_token, and let the
request_validator sign or encrypt it.

The initial version can contain the fields below, accordingly
to the spec:
- aud
- iat
- nonce
- at_hash
- c_hash
"""
# Treat it as normal OAuth 2 auth code request if openid is not present
if not request.scopes or 'openid' not in request.scopes:
return token
Expand All @@ -58,13 +98,25 @@ def add_id_token(self, token, token_handler, request):
if request.response_type and 'id_token' not in request.response_type:
return token

if request.max_age:
d = datetime.datetime.utcnow()
token['auth_time'] = d.isoformat("T") + "Z"
# Implementation mint its own id_token without help.
id_token = self.request_validator.get_id_token(token, token_handler, request)
if id_token:
token['id_token'] = id_token
return token

# Fallback for asking some help from oauthlib framework.
# Start with technicals fields bound to the specification.
id_token = {}
id_token['aud'] = request.client_id
id_token['iat'] = int(datetime.datetime.now().timestamp())
if nonce is not None:
id_token["nonce"] = nonce

# TODO: acr claims (probably better handled by server code using oauthlib in get_id_token)
if "access_token" in token:
id_token["at_hash"] = self.hash_id_token(token["access_token"])

token['id_token'] = self.request_validator.get_id_token(token, token_handler, request)
# Call request_validator to complete/sign/encrypt id_token
token['id_token'] = self.request_validator.fill_id_token(id_token, token, token_handler, request)

return token

Expand Down
4 changes: 2 additions & 2 deletions oauthlib/openid/connect/core/grant_types/implicit.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def __init__(self, request_validator=None, **kwargs):
self.register_token_modifier(self.add_id_token)

def add_id_token(self, token, token_handler, request):
if 'state' not in token:
if 'state' not in token and request.state:
token['state'] = request.state
return super(ImplicitGrant, self).add_id_token(token, token_handler, request)
return super(ImplicitGrant, self).add_id_token(token, token_handler, request, nonce=request.nonce)

def openid_authorization_validator(self, request):
"""Additional validation when following the implicit flow.
Expand Down
75 changes: 74 additions & 1 deletion oauthlib/openid/connect/core/request_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,31 @@ def get_authorization_code_scopes(self, client_id, code, redirect_uri, request):
"""
raise NotImplementedError('Subclasses must implement this method.')

def get_authorization_code_nonce(self, client_id, code, redirect_uri, request):
""" Extracts nonce from saved authorization code.

If present in the Authentication Request, Authorization
Servers MUST include a nonce Claim in the ID Token with the
Claim Value being the nonce value sent in the Authentication
Request. Authorization Servers SHOULD perform no other
processing on nonce values used. The nonce value is a
case-sensitive string.

Only code param should be sufficient to retrieve grant code from
any storage you are using, `client_id` and `redirect_uri` can gave a
blank value `""` don't forget to check it before using those values
in a select query if a database is used.

:param client_id: Unicode client identifier
:param code: Unicode authorization code grant
:param redirect_uri: Unicode absolute URI
:return: A list of scope

Method is used by:
- Authorization Token Grant Dispatcher
"""
raise NotImplementedError('Subclasses must implement this method.')

def get_jwt_bearer_token(self, token, token_handler, request):
"""Get JWT Bearer token or OpenID Connect ID token

Expand All @@ -57,6 +82,12 @@ def get_jwt_bearer_token(self, token, token_handler, request):
def get_id_token(self, token, token_handler, request):
"""Get OpenID Connect ID token

This method is OPTIONAL and is NOT RECOMMENDED.
`fill_id_token` SHOULD be implemented instead. However, if you
want a full control over the minting of the `id_token`, you
MAY want to override `get_id_token` instead of using
`fill_id_token`.

In the OpenID Connect workflows when an ID Token is requested this method is called.
Subclasses should implement the construction, signing and optional encryption of the
ID Token as described in the OpenID Connect spec.
Expand Down Expand Up @@ -85,7 +116,49 @@ def get_id_token(self, token, token_handler, request):
:type request: oauthlib.common.Request
:return: The ID Token (a JWS signed JWT)
"""
# the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token
return None

def fill_id_token(self, id_token, token, token_handler, request):
"""Fill OpenID Connect ID token & Sign or Encrypt.

In the OpenID Connect workflows when an ID Token is requested
this method is called. Subclasses should implement the
construction, signing and optional encryption of the ID Token
as described in the OpenID Connect spec.

The `id_token` parameter is a dict containing a couple of OIDC
technical fields related to the specification. Prepopulated
attributes are:

- `aud`, equals to `request.client_id`.
- `iat`, equals to current time.
- `nonce`, if present, is equals to the `nonce` from the
authorization request.
- `at_hash`, hash of `access_token`, if relevant.
- `c_hash`, hash of `code`, if relevant.

This method MUST provide required fields as below:

- `iss`, REQUIRED. Issuer Identifier for the Issuer of the response.
- `sub`, REQUIRED. Subject Identifier
- `exp`, REQUIRED. Expiration time on or after which the ID
Token MUST NOT be accepted by the RP when performing
authentication with the OP.

Additionals claims must be added, note that `request.scope`
should be used to determine the list of claims.

More information can be found at `OpenID Connect Core#Claims`_

.. _`OpenID Connect Core#Claims`: https://openid.net/specs/openid-connect-core-1_0.html#Claims

:param id_token: A dict containing technical fields of id_token
:param token: A Bearer token dict
:param token_handler: the token handler (BearerToken class)
:param request: OAuthlib request.
:type request: oauthlib.common.Request
:return: The ID Token (a JWS signed JWT or JWE encrypted JWT)
"""
raise NotImplementedError('Subclasses must implement this method.')

def validate_jwt_bearer_token(self, token, scopes, request):
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy