1
1
from functools import partial , wraps
2
2
from html import escape
3
+ import logging
4
+ import os
3
5
from typing import List # Needed in Python 3.7 & 3.8
6
+ from urllib .parse import urlparse
4
7
5
8
from django .shortcuts import redirect , render
6
- from django .urls import path , reverse
9
+ from django .urls import include , path , reverse
7
10
8
11
from .web import Auth as _Auth
9
12
10
13
14
+ logger = logging .getLogger (__name__ )
15
+
16
+
17
+ def _parse_redirect_uri (redirect_uri ):
18
+ """Parse the redirect_uri into a tuple of (django_route, view)"""
19
+ if redirect_uri :
20
+ prefix , view = os .path .split (urlparse (redirect_uri ).path )
21
+ if not view :
22
+ raise ValueError (
23
+ 'redirect_uri must contain a path which does not end with a "/"' )
24
+ route = prefix [1 :] if prefix and prefix [0 ] == '/' else prefix
25
+ if route :
26
+ route += "/"
27
+ return route , view
28
+ else :
29
+ return "" , None
30
+
11
31
class Auth (object ):
12
- _name_of_auth_response_view = f"{ __name__ } .auth_response" # Presumably unique
13
32
14
33
def __init__ (
15
34
self ,
16
35
client_id : str ,
17
36
* ,
18
37
client_credential = None ,
19
- redirect_view : str = None ,
38
+ redirect_uri : str = None ,
20
39
scopes : List [str ]= None ,
21
40
authority : str = None ,
22
41
@@ -38,20 +57,16 @@ def __init__(
38
57
It is somtimes a string.
39
58
The actual format is decided by the underlying auth library. TBD.
40
59
41
- :param str redirect_view :
42
- This will be used as the last segment to form your project's redirect_uri .
60
+ :param str redirect_uri :
61
+ This will be used to mount your project's auth views accordingly .
43
62
44
- For example, if you provide an input here as "auth_response",
45
- and your Django project mounts this ``Auth`` object's ``urlpatterns``
46
- by ``path("prefix/", include(auth.urlpatterns))``,
47
- then the actual redirect_uri will become ``.../prefix/auth_response``
48
- which MUST match what you have registered for your web application.
49
-
50
- Typically, if your application uses a flat redirect_uri as
51
- ``https://example.com/auth_response``,
52
- your shall use an redirect_view value as ``auth_response``,
53
- and then mount it by ``path("", include(auth.urlpatterns))``.
63
+ For example, if your input here is "https://example.com/x/y/z/redirect",
64
+ then your project's redirect page will be mounted at "/x/y/z/redirect",
65
+ login page will be at "/x/y/z/login",
66
+ and logout page will be at "/x/y/z/logout".
54
67
68
+ Afterwards, all you need to do is to insert ``auth.urlpattern`` into
69
+ your project's ``urlpatterns`` list in ``your_project/urls.py``.
55
70
56
71
:param list[str] scopes:
57
72
A list of strings representing the scopes used during login.
@@ -83,20 +98,20 @@ def __init__(
83
98
"""
84
99
self ._client_id = client_id
85
100
self ._client_credential = client_credential
86
- if redirect_view and "/" in redirect_view :
87
- raise ValueError ("redirect_view shall not contain slash" )
88
- self ._redirect_view = redirect_view
89
101
self ._scopes = scopes
90
- self .urlpatterns = [ # Note: path(..., view, ...) does not accept classmethod
102
+ self ._http_cache = {} # All subsequent _Auth instances will share this
103
+
104
+ self ._redirect_uri = redirect_uri
105
+ route , self ._redirect_view = _parse_redirect_uri (redirect_uri )
106
+ self .urlpattern = path (route , include ([
107
+ # Note: path(..., view, ...) does not accept classmethod
91
108
path ('login' , self .login ),
92
- path ('logout' , self .logout ),
109
+ path ('logout' , self .logout , name = f" { __name__ } .logout" ),
93
110
path (
94
- redirect_view or 'auth_response' , # The latter is used by device code flow
111
+ self . _redirect_view or 'auth_response' , # The latter is used by device code flow
95
112
self .auth_response ,
96
- name = self ._name_of_auth_response_view ,
97
- ),
98
- ]
99
- self ._http_cache = {} # All subsequent _Auth instances will share this
113
+ ),
114
+ ]))
100
115
101
116
# Note: We do not use overload, because we want to allow the caller to
102
117
# have only one code path that relay in all the optional parameters.
@@ -152,7 +167,11 @@ def get_edit_profile_url(self, request):
152
167
)["auth_uri" ] if self ._edit_profile_auth and self ._redirect_view else None
153
168
154
169
def login (self , request ):
155
- """The login view"""
170
+ """The login view.
171
+
172
+ You can redirect to the login page from inside a view, by calling
173
+ ``return redirect(auth.login)``.
174
+ """
156
175
if not self ._client_id :
157
176
return self ._render_auth_error (
158
177
request ,
@@ -161,6 +180,10 @@ def login(self, request):
161
180
)
162
181
redirect_uri = request .build_absolute_uri (
163
182
self ._redirect_view ) if self ._redirect_view else None
183
+ if redirect_uri != self ._redirect_uri :
184
+ logger .warning (
185
+ "redirect_uri mismatch: configured = %s, calculated = %s" ,
186
+ self ._redirect_uri , redirect_uri )
164
187
log_in_result = self ._build_auth (request ).log_in (
165
188
scopes = self ._scopes , # Have user consent to scopes during log-in
166
189
redirect_uri = redirect_uri , # Optional. If present, this absolute URL must match your app's redirect_uri registered in Azure Portal
@@ -175,7 +198,7 @@ def login(self, request):
175
198
return render (request , "identity/login.html" , dict (
176
199
log_in_result ,
177
200
reset_password_url = self ._get_reset_password_url (request ),
178
- auth_response_url = reverse (self ._name_of_auth_response_view ),
201
+ auth_response_url = reverse (self .auth_response ),
179
202
))
180
203
181
204
def _render_auth_error (self , request , error , error_description = None ):
@@ -187,7 +210,10 @@ def _render_auth_error(self, request, error, error_description=None):
187
210
))
188
211
189
212
def auth_response (self , request ):
190
- """The auth_response view"""
213
+ """The auth_response view.
214
+
215
+ You should not need to call this view directly.
216
+ """
191
217
result = self ._build_auth (request ).complete_log_in (request .GET )
192
218
if "error" in result :
193
219
return self ._render_auth_error (
@@ -198,7 +224,12 @@ def auth_response(self, request):
198
224
return redirect ("index" ) # TODO: Go back to a customizable url
199
225
200
226
def logout (self , request ):
201
- """The logout view"""
227
+ """The logout view.
228
+
229
+ The logout url is also available with the name "identity.django.logout".
230
+ So you can use ``{% url "identity.django.logout" %}`` to get the url
231
+ from inside a template.
232
+ """
202
233
return redirect (
203
234
self ._build_auth (request ).log_out (request .build_absolute_uri ("/" )))
204
235
0 commit comments