From 3fd34c7fad0fd368d7ccb96dbb76e29b63ef760a Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Thu, 17 Oct 2024 19:42:54 -0500 Subject: [PATCH 1/2] Consolidate request handling logic into helper method. The _ms_request conslidates the error handling logic into a single method. This should make overriding methods easier for end users. --- django_auth_adfs/backend.py | 69 +++++++++++++++--------------- django_auth_adfs/rest_framework.py | 7 ++- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/django_auth_adfs/backend.py b/django_auth_adfs/backend.py index 4574cb42..3c58f66d 100644 --- a/django_auth_adfs/backend.py +++ b/django_auth_adfs/backend.py @@ -15,19 +15,22 @@ class AdfsBaseBackend(ModelBackend): - def exchange_auth_code(self, authorization_code, request): - logger.debug("Received authorization code: %s", authorization_code) - data = { - 'grant_type': 'authorization_code', - 'client_id': settings.CLIENT_ID, - 'redirect_uri': provider_config.redirect_uri(request), - 'code': authorization_code, - } - if settings.CLIENT_SECRET: - data['client_secret'] = settings.CLIENT_SECRET - logger.debug("Getting access token at: %s", provider_config.token_endpoint) - response = provider_config.session.post(provider_config.token_endpoint, data, timeout=settings.TIMEOUT) + def _ms_request(self, action, url, data=None, **kwargs): + """ + Make a Microsoft Entra/GraphQL request + + + Args: + action (callable): The callable for making a request. + url (str): The URL the request should be sent to. + data (dict): Optional dictionary of data to be sent in the request. + + Returns: + response: The response from the server. If it's not a 200, a + PermissionDenied is raised. + """ + response = action(url, data=data, timeout=settings.TIMEOUT, **kwargs) # 200 = valid token received # 400 = 'something' is wrong in our request if response.status_code == 400: @@ -39,7 +42,21 @@ def exchange_auth_code(self, authorization_code, request): if response.status_code != 200: logger.error("Unexpected ADFS response: %s", response.content.decode()) raise PermissionDenied + return response + + def exchange_auth_code(self, authorization_code, request): + logger.debug("Received authorization code: %s", authorization_code) + data = { + 'grant_type': 'authorization_code', + 'client_id': settings.CLIENT_ID, + 'redirect_uri': provider_config.redirect_uri(request), + 'code': authorization_code, + } + if settings.CLIENT_SECRET: + data['client_secret'] = settings.CLIENT_SECRET + logger.debug("Getting access token at: %s", provider_config.token_endpoint) + response = self._ms_request(provider_config.session.post, provider_config.token_endpoint, data) adfs_response = response.json() return adfs_response @@ -66,17 +83,7 @@ def get_obo_access_token(self, access_token): else: data["resource"] = 'https://graph.microsoft.com' - response = provider_config.session.get(provider_config.token_endpoint, data=data, timeout=settings.TIMEOUT) - # 200 = valid token received - # 400 = 'something' is wrong in our request - if response.status_code == 400: - logger.error("ADFS server returned an error: %s", response.json()["error_description"]) - raise PermissionDenied - - if response.status_code != 200: - logger.error("Unexpected ADFS response: %s", response.content.decode()) - raise PermissionDenied - + response = self._ms_request(provider_config.session.get, provider_config.token_endpoint, data) obo_access_token = response.json()["access_token"] logger.debug("Received OBO access token: %s", obo_access_token) return obo_access_token @@ -95,17 +102,11 @@ def get_group_memberships_from_ms_graph(self, obo_access_token): provider_config.msgraph_endpoint ) headers = {"Authorization": "Bearer {}".format(obo_access_token)} - response = provider_config.session.get(graph_url, headers=headers, timeout=settings.TIMEOUT) - # 200 = valid token received - # 400 = 'something' is wrong in our request - if response.status_code in [400, 401]: - logger.error("MS Graph server returned an error: %s", response.json()["message"]) - raise PermissionDenied - - if response.status_code != 200: - logger.error("Unexpected MS Graph response: %s", response.content.decode()) - raise PermissionDenied - + response = self._ms_request( + action=provider_config.session.get, + url=graph_url, + headers=headers, + ) claim_groups = [] for group_data in response.json()["value"]: if group_data["displayName"] is None: diff --git a/django_auth_adfs/rest_framework.py b/django_auth_adfs/rest_framework.py index 53a9b09c..0cce84da 100644 --- a/django_auth_adfs/rest_framework.py +++ b/django_auth_adfs/rest_framework.py @@ -6,6 +6,8 @@ BaseAuthentication, get_authorization_header ) +from django_auth_adfs.exceptions import MFARequired + class AdfsAccessTokenAuthentication(BaseAuthentication): """ @@ -33,7 +35,10 @@ def authenticate(self, request): # Authenticate the user # The AdfsAuthCodeBackend authentication backend will notice the "access_token" parameter # and skip the request for an access token using the authorization code - user = authenticate(access_token=auth[1]) + try: + user = authenticate(access_token=auth[1]) + except MFARequired as e: + raise exceptions.AuthenticationFailed('MFA auth is required.') from e if user is None: raise exceptions.AuthenticationFailed('Invalid access token.') From 9169f46d3e9e8804af6ac83282bb56166c02593a Mon Sep 17 00:00:00 2001 From: Tim Schilling Date: Thu, 17 Oct 2024 19:53:33 -0500 Subject: [PATCH 2/2] Support a querystring to filter groups. --- django_auth_adfs/backend.py | 20 ++++++++++++++++++++ docs/settings_ref.rst | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/django_auth_adfs/backend.py b/django_auth_adfs/backend.py index 3c58f66d..c3165cf3 100644 --- a/django_auth_adfs/backend.py +++ b/django_auth_adfs/backend.py @@ -88,6 +88,25 @@ def get_obo_access_token(self, access_token): logger.debug("Received OBO access token: %s", obo_access_token) return obo_access_token + def get_group_memberships_from_ms_graph_params(self): + """ + Return the parameters to be used in the querystring + when fetching the user's group memberships. + + Possible keys to be used: + - $count + - $expand + - $filter + - $orderby + - $search + - $select + - $top + + Docs: + https://learn.microsoft.com/en-us/graph/api/group-list-transitivememberof?view=graph-rest-1.0&tabs=python#http-request + """ + return {} + def get_group_memberships_from_ms_graph(self, obo_access_token): """ Looks up a users group membership from the MS Graph API @@ -105,6 +124,7 @@ def get_group_memberships_from_ms_graph(self, obo_access_token): response = self._ms_request( action=provider_config.session.get, url=graph_url, + data=self.get_group_memberships_from_ms_graph_params(), headers=headers, ) claim_groups = [] diff --git a/docs/settings_ref.rst b/docs/settings_ref.rst index 8d1c011a..313425b4 100644 --- a/docs/settings_ref.rst +++ b/docs/settings_ref.rst @@ -244,6 +244,12 @@ GROUPS_CLAIM Name of the claim in the JWT access token from ADFS that contains the groups the user is member of. If an entry in this claim matches a group configured in Django, the user will join it automatically. +If using Azure AD and there are too many groups to fit in the JWT access token, the application will +make a request to the Microsoft GraphQL API to find the groups. If you have many groups but only +need a specific few, you can customize the request by overriding +``AdfsBaseBackend.get_group_memberships_from_ms_graph_params`` and specifying the +`OData query parameters `_. + Set this setting to ``None`` to disable automatic group handling. The group memberships of the user will not be touched.