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

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,22 +1,19 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: django-auth-adfs
3
- Version: 1.13.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
- Classifier: Framework :: Django :: 3.2
16
- Classifier: Framework :: Django :: 4.0
17
- Classifier: Framework :: Django :: 4.1
18
14
  Classifier: Framework :: Django :: 4.2
19
15
  Classifier: Framework :: Django :: 5.0
16
+ Classifier: Framework :: Django :: 5.1
20
17
  Classifier: Intended Audience :: Developers
21
18
  Classifier: Intended Audience :: End Users/Desktop
22
19
  Classifier: License :: OSI Approved
@@ -24,11 +21,11 @@ Classifier: License :: OSI Approved :: BSD License
24
21
  Classifier: Operating System :: OS Independent
25
22
  Classifier: Programming Language :: Python
26
23
  Classifier: Programming Language :: Python :: 3
27
- Classifier: Programming Language :: Python :: 3.8
28
24
  Classifier: Programming Language :: Python :: 3.9
29
25
  Classifier: Programming Language :: Python :: 3.10
30
26
  Classifier: Programming Language :: Python :: 3.11
31
27
  Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
32
29
  Classifier: Topic :: Internet :: WWW/HTTP
33
30
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
34
31
  Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
@@ -36,11 +33,12 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
36
33
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
34
  Requires-Dist: PyJWT
38
35
  Requires-Dist: cryptography
39
- Requires-Dist: django (>=3,<5) ; python_version >= "3.8" and python_version < "3.10"
40
- Requires-Dist: django (>=4,<6) ; python_version >= "3.10"
36
+ Requires-Dist: django (>=4.2,<5.0) ; python_version >= "3.9" and python_version < "3.10"
37
+ Requires-Dist: django (>=4.2,<6) ; python_version >= "3.10"
41
38
  Requires-Dist: requests
42
39
  Requires-Dist: urllib3
43
40
  Project-URL: Documentation, https://django-auth-adfs.readthedocs.io/en/latest
41
+ Project-URL: Homepage, https://github.com/snok/django-auth-adfs
44
42
  Project-URL: Repository, https://github.com/snok/django-auth-adfs
45
43
  Description-Content-Type: text/x-rst
46
44
 
@@ -144,13 +142,36 @@ This will add these paths to Django:
144
142
  * ``/oauth2/callback`` where ADFS redirects back to after login. So make sure you set the redirect URI on ADFS to this.
145
143
  * ``/oauth2/logout`` which logs out the user from both Django and ADFS.
146
144
 
147
- 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>`_.
148
148
 
149
- .. code-block:: html
149
+ - Using GET requests:
150
150
 
151
- <a href="{% url 'django_auth_adfs:logout' %}">Logout</a>
152
- <a href="{% url 'django_auth_adfs:login' %}">Login</a>
153
- <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>
154
175
 
155
176
  Contributing
156
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.13.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.13.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>']
@@ -12,22 +12,20 @@ keywords = ['django', 'authentication', 'adfs', 'azure', 'ad', 'oauth2']
12
12
  readme = 'README.rst'
13
13
  classifiers = [
14
14
  'Environment :: Web Environment',
15
- 'Framework :: Django :: 3.2',
16
- 'Framework :: Django :: 4.0',
17
- 'Framework :: Django :: 4.1',
18
15
  'Framework :: Django :: 4.2',
19
16
  'Framework :: Django :: 5.0',
17
+ 'Framework :: Django :: 5.1',
20
18
  'Intended Audience :: Developers',
21
19
  'Intended Audience :: End Users/Desktop',
22
20
  'Operating System :: OS Independent',
23
21
  'License :: OSI Approved :: BSD License',
24
22
  'Programming Language :: Python',
25
23
  'Programming Language :: Python :: 3',
26
- 'Programming Language :: Python :: 3.8',
27
24
  'Programming Language :: Python :: 3.9',
28
25
  'Programming Language :: Python :: 3.10',
29
26
  'Programming Language :: Python :: 3.11',
30
27
  'Programming Language :: Python :: 3.12',
28
+ 'Programming Language :: Python :: 3.13',
31
29
  'Topic :: Internet :: WWW/HTTP',
32
30
  'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
33
31
  'Topic :: Internet :: WWW/HTTP :: WSGI',
@@ -37,10 +35,10 @@ classifiers = [
37
35
  ]
38
36
 
39
37
  [tool.poetry.dependencies]
40
- python = '^3.8'
38
+ python = '^3.9'
41
39
  django = [
42
- { version = '^3 || ^4', python = '>=3.8 <3.10' },
43
- { version = '^4 || ^5', python = '>=3.10' },
40
+ { version = '^4.2', python = '>=3.9 <3.10' },
41
+ { version = '^4.2 || ^5', python = '>=3.10' },
44
42
  ]
45
43
  requests = '*'
46
44
  urllib3 = '*'