Skip to content

Commit

Permalink
✨ [mozilla#517] Implement content-type negotation in userinfo endpoint
Browse files Browse the repository at this point in the history
TODO: address broken tests
TODO: add new tests
  • Loading branch information
sergei-maertens committed Feb 14, 2024
1 parent a8cd537 commit 6bfe752
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 3 deletions.
43 changes: 40 additions & 3 deletions mozilla_django_oidc/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
from requests.auth import HTTPBasicAuth
from requests.exceptions import HTTPError

from mozilla_django_oidc.utils import absolutify, import_from_settings
from ._jose import verify_jws_and_decode
from mozilla_django_oidc._jose import verify_jws_and_decode
from mozilla_django_oidc.utils import (
absolutify,
extract_content_type,
import_from_settings,
)

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -258,7 +262,40 @@ def get_userinfo(self, access_token, id_token, payload):
proxies=self.get_settings("OIDC_PROXY", None),
)
user_response.raise_for_status()
return user_response.json()

# From https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
#
# > The UserInfo Endpoint MUST return a content-type header to indicate which
# > format is being returned.
#
# XXX: should we .lower() this value to be sure?
content_type = extract_content_type(user_response.headers["Content-Type"])
if content_type == "application/json":
return user_response.json()
elif content_type == "application/jwt":
token = user_response.content
# get the key from the configured keys endpoint
# XXX: tested with asymmetric encryption. algorithms like HS256 rely on
# out-of-band key exchange and are currently untested. Re-using
# self.OIDC_RP_IDP_SIGN_KEY seems like a bad idea since the endpoint may
# use a separate key alltogether
key = self.retrieve_matching_jwk(token)
payload = verify_jws_and_decode(
token,
key,
# Providers typically control the algorithm which may differ from
# self.OIDC_RP_SIGN_ALGO, e.g. Keycloak allows setting the algorithm
# specfically for the userinfo endpoint.
signing_algorithm="",
decode_json=True,
)
return payload
else:
raise ValueError(
f"Got an invalid Content-Type header value ({content_type}) "
"according to OpenID Connect Core 1.0 standard. Contact your "
"vendor."
)

def authenticate(self, request, **kwargs):
"""Authenticates a user based on the OIDC code flow."""
Expand Down
14 changes: 14 additions & 0 deletions mozilla_django_oidc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import josepy.b64
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from requests.utils import _parse_content_type_header # type: ignore

LOGGER = logging.getLogger(__name__)

Expand All @@ -21,6 +22,19 @@ def parse_www_authenticate_header(header):
return parse_keqv_list(items)


def extract_content_type(ct_header: str) -> str:
"""
Get the content type + parameters from content type header.
This is internal API since we use a requests internal utility, which may be
removed/modified at any time. However, this is a deliberate choices since I trust
requests to have a correct implementation more than coming up with one myself.
"""
content_type, _ = _parse_content_type_header(ct_header)
# discard the params, we only want the content type itself
return content_type


def import_from_settings(attr, *args):
"""
Load an attribute from the django settings.
Expand Down

0 comments on commit 6bfe752

Please sign in to comment.