django-auth-adfs 1.14.0__tar.gz → 1.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,19 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: django-auth-adfs
3
- Version: 1.14.0
3
+ Version: 1.15.0
4
4
  Summary: A Django authentication backend for Microsoft ADFS and AzureAD
5
- Home-page: https://github.com/snok/django-auth-adfs
6
5
  License: BSD-1-Clause
7
6
  Keywords: django,authentication,adfs,azure,ad,oauth2
8
7
  Author: Joris Beckers
9
8
  Author-email: joris.beckers@gmail.com
10
9
  Maintainer: Jonas Krüger Svensson
11
10
  Maintainer-email: jonas-ks@hotmail.com
12
- Requires-Python: >=3.8,<4.0
11
+ Requires-Python: >=3.9,<4.0
13
12
  Classifier: Development Status :: 5 - Production/Stable
14
13
  Classifier: Environment :: Web Environment
15
14
  Classifier: Framework :: Django :: 4.2
16
15
  Classifier: Framework :: Django :: 5.0
16
+ Classifier: Framework :: Django :: 5.1
17
17
  Classifier: Intended Audience :: Developers
18
18
  Classifier: Intended Audience :: End Users/Desktop
19
19
  Classifier: License :: OSI Approved
@@ -21,11 +21,11 @@ Classifier: License :: OSI Approved :: BSD License
21
21
  Classifier: Operating System :: OS Independent
22
22
  Classifier: Programming Language :: Python
23
23
  Classifier: Programming Language :: Python :: 3
24
- Classifier: Programming Language :: Python :: 3.8
25
24
  Classifier: Programming Language :: Python :: 3.9
26
25
  Classifier: Programming Language :: Python :: 3.10
27
26
  Classifier: Programming Language :: Python :: 3.11
28
27
  Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
29
  Classifier: Topic :: Internet :: WWW/HTTP
30
30
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
31
31
  Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
@@ -33,11 +33,12 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
33
33
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
34
  Requires-Dist: PyJWT
35
35
  Requires-Dist: cryptography
36
- Requires-Dist: django (>=4.2,<5.0) ; python_version >= "3.8" and python_version < "3.10"
36
+ Requires-Dist: django (>=4.2,<5.0) ; python_version >= "3.9" and python_version < "3.10"
37
37
  Requires-Dist: django (>=4.2,<6) ; python_version >= "3.10"
38
38
  Requires-Dist: requests
39
39
  Requires-Dist: urllib3
40
40
  Project-URL: Documentation, https://django-auth-adfs.readthedocs.io/en/latest
41
+ Project-URL: Homepage, https://github.com/snok/django-auth-adfs
41
42
  Project-URL: Repository, https://github.com/snok/django-auth-adfs
42
43
  Description-Content-Type: text/x-rst
43
44
 
@@ -141,13 +142,36 @@ This will add these paths to Django:
141
142
  * ``/oauth2/callback`` where ADFS redirects back to after login. So make sure you set the redirect URI on ADFS to this.
142
143
  * ``/oauth2/logout`` which logs out the user from both Django and ADFS.
143
144
 
144
- You can use them like this in your django templates:
145
+ Below is sample Django template code to use these paths depending if
146
+ you'd like to use GET or POST requests. Logging out was deprecated in
147
+ `Django 4.1 <https://docs.djangoproject.com/en/5.1/releases/4.1/#features-deprecated-in-4-1>`_.
145
148
 
146
- .. code-block:: html
149
+ - Using GET requests:
147
150
 
148
- <a href="{% url 'django_auth_adfs:logout' %}">Logout</a>
149
- <a href="{% url 'django_auth_adfs:login' %}">Login</a>
150
- <a href="{% url 'django_auth_adfs:login-no-sso' %}">Login (no SSO)</a>
151
+ .. code-block:: html
152
+
153
+ <a href="{% url 'django_auth_adfs:logout' %}">Logout</a>
154
+ <a href="{% url 'django_auth_adfs:login' %}">Login</a>
155
+ <a href="{% url 'django_auth_adfs:login-no-sso' %}">Login (no SSO)</a>
156
+
157
+ - Using POST requests:
158
+
159
+ .. code-block:: html+django
160
+
161
+ <form method="post" action="{% url 'django_auth_adfs:logout' %}">
162
+ {% csrf_token %}
163
+ <button type="submit">Logout</button>
164
+ </form>
165
+ <form method="post" action="{% url 'django_auth_adfs:login' %}">
166
+ {% csrf_token %}
167
+ <input type="hidden" name="next" value="{{ next }}">
168
+ <button type="submit">Login</button>
169
+ </form>
170
+ <form method="post" action="{% url 'django_auth_adfs:login-no-sso' %}">
171
+ {% csrf_token %}
172
+ <input type="hidden" name="next" value="{{ next }}">
173
+ <button type="submit">Login (no SSO)</button>
174
+ </form>
151
175
 
152
176
  Contributing
153
177
  ------------
@@ -98,13 +98,36 @@ This will add these paths to Django:
98
98
  * ``/oauth2/callback`` where ADFS redirects back to after login. So make sure you set the redirect URI on ADFS to this.
99
99
  * ``/oauth2/logout`` which logs out the user from both Django and ADFS.
100
100
 
101
- You can use them like this in your django templates:
102
-
103
- .. code-block:: html
104
-
105
- <a href="{% url 'django_auth_adfs:logout' %}">Logout</a>
106
- <a href="{% url 'django_auth_adfs:login' %}">Login</a>
107
- <a href="{% url 'django_auth_adfs:login-no-sso' %}">Login (no SSO)</a>
101
+ Below is sample Django template code to use these paths depending if
102
+ you'd like to use GET or POST requests. Logging out was deprecated in
103
+ `Django 4.1 <https://docs.djangoproject.com/en/5.1/releases/4.1/#features-deprecated-in-4-1>`_.
104
+
105
+ - Using GET requests:
106
+
107
+ .. code-block:: html
108
+
109
+ <a href="{% url 'django_auth_adfs:logout' %}">Logout</a>
110
+ <a href="{% url 'django_auth_adfs:login' %}">Login</a>
111
+ <a href="{% url 'django_auth_adfs:login-no-sso' %}">Login (no SSO)</a>
112
+
113
+ - Using POST requests:
114
+
115
+ .. code-block:: html+django
116
+
117
+ <form method="post" action="{% url 'django_auth_adfs:logout' %}">
118
+ {% csrf_token %}
119
+ <button type="submit">Logout</button>
120
+ </form>
121
+ <form method="post" action="{% url 'django_auth_adfs:login' %}">
122
+ {% csrf_token %}
123
+ <input type="hidden" name="next" value="{{ next }}">
124
+ <button type="submit">Login</button>
125
+ </form>
126
+ <form method="post" action="{% url 'django_auth_adfs:login-no-sso' %}">
127
+ {% csrf_token %}
128
+ <input type="hidden" name="next" value="{{ next }}">
129
+ <button type="submit">Login (no SSO)</button>
130
+ </form>
108
131
 
109
132
  Contributing
110
133
  ------------
@@ -4,4 +4,4 @@ This file is imported by setup.py
4
4
  Adding imports here will break setup.py
5
5
  """
6
6
 
7
- __version__ = '1.14.0'
7
+ __version__ = '1.15.0'
@@ -15,19 +15,22 @@ logger = logging.getLogger("django_auth_adfs")
15
15
 
16
16
 
17
17
  class AdfsBaseBackend(ModelBackend):
18
- def exchange_auth_code(self, authorization_code, request):
19
- logger.debug("Received authorization code: %s", authorization_code)
20
- data = {
21
- 'grant_type': 'authorization_code',
22
- 'client_id': settings.CLIENT_ID,
23
- 'redirect_uri': provider_config.redirect_uri(request),
24
- 'code': authorization_code,
25
- }
26
- if settings.CLIENT_SECRET:
27
- data['client_secret'] = settings.CLIENT_SECRET
28
18
 
29
- logger.debug("Getting access token at: %s", provider_config.token_endpoint)
30
- response = provider_config.session.post(provider_config.token_endpoint, data, timeout=settings.TIMEOUT)
19
+ def _ms_request(self, action, url, data=None, **kwargs):
20
+ """
21
+ Make a Microsoft Entra/GraphQL request
22
+
23
+
24
+ Args:
25
+ action (callable): The callable for making a request.
26
+ url (str): The URL the request should be sent to.
27
+ data (dict): Optional dictionary of data to be sent in the request.
28
+
29
+ Returns:
30
+ response: The response from the server. If it's not a 200, a
31
+ PermissionDenied is raised.
32
+ """
33
+ response = action(url, data=data, timeout=settings.TIMEOUT, **kwargs)
31
34
  # 200 = valid token received
32
35
  # 400 = 'something' is wrong in our request
33
36
  if response.status_code == 400:
@@ -39,7 +42,21 @@ class AdfsBaseBackend(ModelBackend):
39
42
  if response.status_code != 200:
40
43
  logger.error("Unexpected ADFS response: %s", response.content.decode())
41
44
  raise PermissionDenied
45
+ return response
42
46
 
47
+ def exchange_auth_code(self, authorization_code, request):
48
+ logger.debug("Received authorization code: %s", authorization_code)
49
+ data = {
50
+ 'grant_type': 'authorization_code',
51
+ 'client_id': settings.CLIENT_ID,
52
+ 'redirect_uri': provider_config.redirect_uri(request),
53
+ 'code': authorization_code,
54
+ }
55
+ if settings.CLIENT_SECRET:
56
+ data['client_secret'] = settings.CLIENT_SECRET
57
+
58
+ logger.debug("Getting access token at: %s", provider_config.token_endpoint)
59
+ response = self._ms_request(provider_config.session.post, provider_config.token_endpoint, data)
43
60
  adfs_response = response.json()
44
61
  return adfs_response
45
62
 
@@ -66,21 +83,30 @@ class AdfsBaseBackend(ModelBackend):
66
83
  else:
67
84
  data["resource"] = 'https://graph.microsoft.com'
68
85
 
69
- response = provider_config.session.get(provider_config.token_endpoint, data=data, timeout=settings.TIMEOUT)
70
- # 200 = valid token received
71
- # 400 = 'something' is wrong in our request
72
- if response.status_code == 400:
73
- logger.error("ADFS server returned an error: %s", response.json()["error_description"])
74
- raise PermissionDenied
75
-
76
- if response.status_code != 200:
77
- logger.error("Unexpected ADFS response: %s", response.content.decode())
78
- raise PermissionDenied
79
-
86
+ response = self._ms_request(provider_config.session.get, provider_config.token_endpoint, data)
80
87
  obo_access_token = response.json()["access_token"]
81
88
  logger.debug("Received OBO access token: %s", obo_access_token)
82
89
  return obo_access_token
83
90
 
91
+ def get_group_memberships_from_ms_graph_params(self):
92
+ """
93
+ Return the parameters to be used in the querystring
94
+ when fetching the user's group memberships.
95
+
96
+ Possible keys to be used:
97
+ - $count
98
+ - $expand
99
+ - $filter
100
+ - $orderby
101
+ - $search
102
+ - $select
103
+ - $top
104
+
105
+ Docs:
106
+ https://learn.microsoft.com/en-us/graph/api/group-list-transitivememberof?view=graph-rest-1.0&tabs=python#http-request
107
+ """
108
+ return {}
109
+
84
110
  def get_group_memberships_from_ms_graph(self, obo_access_token):
85
111
  """
86
112
  Looks up a users group membership from the MS Graph API
@@ -95,17 +121,12 @@ class AdfsBaseBackend(ModelBackend):
95
121
  provider_config.msgraph_endpoint
96
122
  )
97
123
  headers = {"Authorization": "Bearer {}".format(obo_access_token)}
98
- response = provider_config.session.get(graph_url, headers=headers, timeout=settings.TIMEOUT)
99
- # 200 = valid token received
100
- # 400 = 'something' is wrong in our request
101
- if response.status_code in [400, 401]:
102
- logger.error("MS Graph server returned an error: %s", response.json()["message"])
103
- raise PermissionDenied
104
-
105
- if response.status_code != 200:
106
- logger.error("Unexpected MS Graph response: %s", response.content.decode())
107
- raise PermissionDenied
108
-
124
+ response = self._ms_request(
125
+ action=provider_config.session.get,
126
+ url=graph_url,
127
+ data=self.get_group_memberships_from_ms_graph_params(),
128
+ headers=headers,
129
+ )
109
130
  claim_groups = []
110
131
  for group_data in response.json()["value"]:
111
132
  if group_data["displayName"] is None:
@@ -337,7 +337,10 @@ class ProviderConfig(object):
337
337
 
338
338
  """
339
339
  self.load_config()
340
- redirect_to = request.GET.get(REDIRECT_FIELD_NAME, None)
340
+ if request.method == 'POST':
341
+ redirect_to = request.POST.get(REDIRECT_FIELD_NAME, None)
342
+ else:
343
+ redirect_to = request.GET.get(REDIRECT_FIELD_NAME, None)
341
344
  if not redirect_to:
342
345
  redirect_to = django_settings.LOGIN_REDIRECT_URL
343
346
  redirect_to = base64.urlsafe_b64encode(redirect_to.encode()).decode()
@@ -6,6 +6,8 @@ from rest_framework.authentication import (
6
6
  BaseAuthentication, get_authorization_header
7
7
  )
8
8
 
9
+ from django_auth_adfs.exceptions import MFARequired
10
+
9
11
 
10
12
  class AdfsAccessTokenAuthentication(BaseAuthentication):
11
13
  """
@@ -33,7 +35,10 @@ class AdfsAccessTokenAuthentication(BaseAuthentication):
33
35
  # Authenticate the user
34
36
  # The AdfsAuthCodeBackend authentication backend will notice the "access_token" parameter
35
37
  # and skip the request for an access token using the authorization code
36
- user = authenticate(access_token=auth[1])
38
+ try:
39
+ user = authenticate(access_token=auth[1])
40
+ except MFARequired as e:
41
+ raise exceptions.AuthenticationFailed('MFA auth is required.') from e
37
42
 
38
43
  if user is None:
39
44
  raise exceptions.AuthenticationFailed('Invalid access token.')
@@ -84,6 +84,15 @@ class OAuth2LoginView(View):
84
84
  """
85
85
  return redirect(provider_config.build_authorization_endpoint(request))
86
86
 
87
+ def post(self, request):
88
+ """
89
+ Initiates the OAuth2 flow and redirect the user agent to ADFS
90
+
91
+ Args:
92
+ request (django.http.request.HttpRequest): A Django Request object
93
+ """
94
+ return redirect(provider_config.build_authorization_endpoint(request))
95
+
87
96
 
88
97
  class OAuth2LoginNoSSOView(View):
89
98
  def get(self, request):
@@ -95,6 +104,15 @@ class OAuth2LoginNoSSOView(View):
95
104
  """
96
105
  return redirect(provider_config.build_authorization_endpoint(request, disable_sso=True))
97
106
 
107
+ def post(self, request):
108
+ """
109
+ Initiates the OAuth2 flow and redirect the user agent to ADFS
110
+
111
+ Args:
112
+ request (django.http.request.HttpRequest): A Django Request object
113
+ """
114
+ return redirect(provider_config.build_authorization_endpoint(request, disable_sso=True))
115
+
98
116
 
99
117
  class OAuth2LoginForceMFA(View):
100
118
  def get(self, request):
@@ -106,6 +124,15 @@ class OAuth2LoginForceMFA(View):
106
124
  """
107
125
  return redirect(provider_config.build_authorization_endpoint(request, force_mfa=True))
108
126
 
127
+ def post(self, request):
128
+ """
129
+ Initiates the OAuth2 flow and redirect the user agent to ADFS
130
+
131
+ Args:
132
+ request (django.http.request.HttpRequest): A Django Request object
133
+ """
134
+ return redirect(provider_config.build_authorization_endpoint(request, force_mfa=True))
135
+
109
136
 
110
137
  class OAuth2LogoutView(View):
111
138
  def get(self, request):
@@ -117,3 +144,13 @@ class OAuth2LogoutView(View):
117
144
  """
118
145
  logout(request)
119
146
  return redirect(provider_config.build_end_session_endpoint())
147
+
148
+ def post(self, request):
149
+ """
150
+ Logs out the user from both Django and ADFS
151
+
152
+ Args:
153
+ request (django.http.request.HttpRequest): A Django Request object
154
+ """
155
+ logout(request)
156
+ return redirect(provider_config.build_end_session_endpoint())
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = 'django-auth-adfs'
3
- version = "1.14.0" # Remember to also change __init__.py version
3
+ version = "1.15.0" # Remember to also change __init__.py version
4
4
  description = 'A Django authentication backend for Microsoft ADFS and AzureAD'
5
5
  authors = ['Joris Beckers <joris.beckers@gmail.com>']
6
6
  maintainers = ['Jonas Krüger Svensson <jonas-ks@hotmail.com>', 'Sondre Lillebø Gundersen <sondrelg@live.no>']
@@ -14,17 +14,18 @@ classifiers = [
14
14
  'Environment :: Web Environment',
15
15
  'Framework :: Django :: 4.2',
16
16
  'Framework :: Django :: 5.0',
17
+ 'Framework :: Django :: 5.1',
17
18
  'Intended Audience :: Developers',
18
19
  'Intended Audience :: End Users/Desktop',
19
20
  'Operating System :: OS Independent',
20
21
  'License :: OSI Approved :: BSD License',
21
22
  'Programming Language :: Python',
22
23
  'Programming Language :: Python :: 3',
23
- 'Programming Language :: Python :: 3.8',
24
24
  'Programming Language :: Python :: 3.9',
25
25
  'Programming Language :: Python :: 3.10',
26
26
  'Programming Language :: Python :: 3.11',
27
27
  'Programming Language :: Python :: 3.12',
28
+ 'Programming Language :: Python :: 3.13',
28
29
  'Topic :: Internet :: WWW/HTTP',
29
30
  'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
30
31
  'Topic :: Internet :: WWW/HTTP :: WSGI',
@@ -34,9 +35,9 @@ classifiers = [
34
35
  ]
35
36
 
36
37
  [tool.poetry.dependencies]
37
- python = '^3.8'
38
+ python = '^3.9'
38
39
  django = [
39
- { version = '^4.2', python = '>=3.8 <3.10' },
40
+ { version = '^4.2', python = '>=3.9 <3.10' },
40
41
  { version = '^4.2 || ^5', python = '>=3.10' },
41
42
  ]
42
43
  requests = '*'