django-auth-adfs 1.14.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.
- {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
|