Skip to content

Commit c53eadf

Browse files
committed
Replace Auth(..., redirect_view=...) with Auth(..., redirect_uri=...)
Also add a url name "identity.django.logout" for the logout view.
1 parent 5776615 commit c53eadf

File tree

4 files changed

+79
-30
lines changed

4 files changed

+79
-30
lines changed

identity/django.py

+60-29
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
from functools import partial, wraps
22
from html import escape
3+
import logging
4+
import os
35
from typing import List # Needed in Python 3.7 & 3.8
6+
from urllib.parse import urlparse
47

58
from django.shortcuts import redirect, render
6-
from django.urls import path, reverse
9+
from django.urls import include, path, reverse
710

811
from .web import Auth as _Auth
912

1013

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+
1131
class Auth(object):
12-
_name_of_auth_response_view = f"{__name__}.auth_response" # Presumably unique
1332

1433
def __init__(
1534
self,
1635
client_id: str,
1736
*,
1837
client_credential=None,
19-
redirect_view: str=None,
38+
redirect_uri: str=None,
2039
scopes: List[str]=None,
2140
authority: str=None,
2241

@@ -38,20 +57,16 @@ def __init__(
3857
It is somtimes a string.
3958
The actual format is decided by the underlying auth library. TBD.
4059
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.
4362
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".
5467
68+
Afterwards, all you need to do is to insert ``auth.urlpattern`` into
69+
your project's ``urlpatterns`` list in ``your_project/urls.py``.
5570
5671
:param list[str] scopes:
5772
A list of strings representing the scopes used during login.
@@ -83,20 +98,20 @@ def __init__(
8398
"""
8499
self._client_id = client_id
85100
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
89101
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
91108
path('login', self.login),
92-
path('logout', self.logout),
109+
path('logout', self.logout, name=f"{__name__}.logout"),
93110
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
95112
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+
]))
100115

101116
# Note: We do not use overload, because we want to allow the caller to
102117
# have only one code path that relay in all the optional parameters.
@@ -152,7 +167,11 @@ def get_edit_profile_url(self, request):
152167
)["auth_uri"] if self._edit_profile_auth and self._redirect_view else None
153168

154169
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+
"""
156175
if not self._client_id:
157176
return self._render_auth_error(
158177
request,
@@ -161,6 +180,10 @@ def login(self, request):
161180
)
162181
redirect_uri = request.build_absolute_uri(
163182
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)
164187
log_in_result = self._build_auth(request).log_in(
165188
scopes=self._scopes, # Have user consent to scopes during log-in
166189
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):
175198
return render(request, "identity/login.html", dict(
176199
log_in_result,
177200
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),
179202
))
180203

181204
def _render_auth_error(self, request, error, error_description=None):
@@ -187,7 +210,10 @@ def _render_auth_error(self, request, error, error_description=None):
187210
))
188211

189212
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+
"""
191217
result = self._build_auth(request).complete_log_in(request.GET)
192218
if "error" in result:
193219
return self._render_auth_error(
@@ -198,7 +224,12 @@ def auth_response(self, request):
198224
return redirect("index") # TODO: Go back to a customizable url
199225

200226
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+
"""
202233
return redirect(
203234
self._build_auth(request).log_out(request.build_absolute_uri("/")))
204235

identity/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.4.0" # Note: Perhaps update ReadTheDocs and README.md too?
1+
__version__ = "0.5.0" # Note: Perhaps update ReadTheDocs and README.md too?

requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
.
2+
3+
# Since we currently still supports Python 3.7, we choose Django version accordingly.
4+
# See https://docs.djangoproject.com/en/5.0/faq/install/#what-python-version-can-i-use-with-django
5+
django>=3.2,<6

tests/test_django.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# pytest requires at least one test case to run
2+
import pytest
3+
from identity.django import _parse_redirect_uri
4+
5+
def test_parse_redirect_uri():
6+
with pytest.raises(ValueError):
7+
_parse_redirect_uri("https://example.com")
8+
with pytest.raises(ValueError):
9+
_parse_redirect_uri("https://example.com/")
10+
assert _parse_redirect_uri("https://example.com/x") == ("", "x")
11+
with pytest.raises(ValueError):
12+
_parse_redirect_uri("https://example.com/x/")
13+
assert _parse_redirect_uri("https://example.com/x/y") == ("x/", "y")
14+
assert _parse_redirect_uri("https://example.com/x/y/z") == ("x/y/", "z")

0 commit comments

Comments
 (0)