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.
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/PKG-INFO +35 -11
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/README.rst +30 -7
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/__init__.py +1 -1
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/backend.py +55 -34
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/config.py +4 -1
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/rest_framework.py +6 -1
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/views.py +37 -0
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/pyproject.toml +5 -4
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/LICENSE +0 -0
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/drf-urls.py +0 -0
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/drf_urls.py +0 -0
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/exceptions.py +0 -0
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/middleware.py +0 -0
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/signals.py +0 -0
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/templates/django_auth_adfs/login_failed.html +0 -0
- {django_auth_adfs-1.14.0 → django_auth_adfs-1.15.0}/django_auth_adfs/urls.py +0 -0
@@ -1,19 +1,19 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: django-auth-adfs
|
3
|
-
Version: 1.
|
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.
|
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.
|
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
|
-
|
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
|
-
|
149
|
+
- Using GET requests:
|
147
150
|
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
------------
|
@@ -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
|
-
|
30
|
-
|
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
|
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 =
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
38
|
+
python = '^3.9'
|
38
39
|
django = [
|
39
|
-
{ version = '^4.2', python = '>=3.
|
40
|
+
{ version = '^4.2', python = '>=3.9 <3.10' },
|
40
41
|
{ version = '^4.2 || ^5', python = '>=3.10' },
|
41
42
|
]
|
42
43
|
requests = '*'
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|