Skip to content

Commit e77e8d4

Browse files
committed
Merge tag '0.10.0' into microsoft-main
2 parents 8345836 + 7b900d3 commit e77e8d4

12 files changed

Lines changed: 159 additions & 25 deletions

File tree

docs/django.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,17 @@ Configuration
3535
from identity.django import Auth
3636
load_dotenv()
3737
AUTH = Auth(
38+
# Instruction for these settings is available in this project's README file.
39+
# https://github.com/rayluo/identity?tab=readme-ov-file#scenarios-supported
3840
os.getenv('CLIENT_ID'),
3941
client_credential=os.getenv('CLIENT_SECRET'),
40-
redirect_uri=os.getenv('REDIRECT_URI'),
42+
redirect_uri=
43+
# Recommended to register and use a redirect_uri.
44+
# It looks like http://localhost:5000/redirect for local development,
45+
# or https://your_website.com/redirect for your production.
46+
# If absent, Identity library will fall back to a Device Code mode.
47+
os.getenv('REDIRECT_URI'),
48+
4149
..., # See below on how to feed in the authority url parameter
4250
)
4351

docs/flask.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,18 @@ Configuration
4040

4141
auth = Auth(
4242
app,
43+
44+
# Instruction for these settings is available in this project's README file.
45+
# https://github.com/rayluo/identity?tab=readme-ov-file#scenarios-supported
4346
os.getenv('CLIENT_ID'),
4447
client_credential=os.getenv('CLIENT_SECRET'),
45-
redirect_uri=os.getenv('REDIRECT_URI'),
48+
redirect_uri=
49+
# Recommended to register and use a redirect_uri.
50+
# It looks like http://localhost:5000/redirect for local development,
51+
# or https://your_website.com/redirect for your production.
52+
# If absent, Identity library will fall back to a Device Code mode.
53+
os.getenv('REDIRECT_URI'),
54+
4655
..., # See below on how to feed in the authority url parameter
4756
)
4857

docs/quart.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,18 @@ Configuration
2525

2626
auth = Auth(
2727
app,
28+
29+
# Instruction for these settings is available in this project's README file.
30+
# https://github.com/rayluo/identity?tab=readme-ov-file#scenarios-supported
2831
os.getenv('CLIENT_ID'),
2932
client_credential=os.getenv('CLIENT_SECRET'),
30-
redirect_uri=os.getenv('REDIRECT_URI'),
33+
redirect_uri=
34+
# Recommended to register and use a redirect_uri.
35+
# It looks like http://localhost:5000/redirect for local development,
36+
# or https://your_website.com/redirect for your production.
37+
# If absent, Identity library will fall back to a Device Code mode.
38+
os.getenv('REDIRECT_URI'),
39+
3140
..., # See below on how to feed in the authority url parameter
3241
)
3342

identity/django.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,30 @@ class Auth(WebFrameworkAuth):
3434
your project's ``urlpatterns`` list in ``your_project/urls.py``.
3535
"""
3636

37-
def __init__(self, *args, **kwargs):
37+
def __init__(
38+
self,
39+
*args,
40+
post_logout_view: Optional[callable] = None,
41+
**kwargs,
42+
):
43+
"""Initialize the Auth class for a Django web application.
44+
45+
:param callable post_logout_view:
46+
Optional.
47+
If not provided, the user will be redirected to the root URL of the app.
48+
49+
If provided, it shall be the view (which is a function)
50+
that will be redirected to, after the user has logged out.
51+
For example, you will typically use this parameter like this::
52+
53+
from . import public_views # This module shall NOT import settings.AUTH
54+
auth = Auth(
55+
...,
56+
post_logout_view=public_views.my_post_logout_view,
57+
)
58+
59+
where ``my_post_logout_view`` is a Django view function.
60+
"""
3861
super(Auth, self).__init__(*args, **kwargs)
3962
route, redirect_view = _parse_redirect_uri(self._redirect_uri)
4063
self.urlpattern = path(route, include([
@@ -46,6 +69,7 @@ def __init__(self, *args, **kwargs):
4669
self.auth_response,
4770
),
4871
]))
72+
self._post_logout_view = post_logout_view
4973

5074
def login(
5175
self,
@@ -109,8 +133,9 @@ def logout(self, request):
109133
So you can use ``{% url "identity.django.logout" %}`` to get the url
110134
from inside a template.
111135
"""
112-
return redirect(
113-
self._build_auth(request.session).log_out(request.build_absolute_uri("/")))
136+
return redirect(self._build_auth(request.session).log_out(request.build_absolute_uri(
137+
reverse(self._post_logout_view) if self._post_logout_view else "/"
138+
)))
114139

115140
def login_required( # Named after Django's login_required
116141
self,

identity/flask.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ class Auth(PalletAuth):
1313
_Session = Session
1414
_redirect = redirect
1515

16-
def __init__(self, app: Optional[Flask], *args, **kwargs):
17-
"""Create an identity helper for a web application.
16+
def __init__(
17+
self,
18+
app: Optional[Flask],
19+
*args,
20+
post_logout_view: Optional[callable] = None,
21+
**kwargs,
22+
):
23+
"""Initialize the Auth class for a Flask web application.
1824
1925
:param Flask app:
2026
It can be a Flask app instance, or ``None``.
@@ -56,10 +62,17 @@ def build_app():
5662
5763
app = build_app()
5864
65+
:param callable post_logout_view:
66+
Optional.
67+
If not provided, the user will be redirected to the root URL of the app.
68+
If provided, it shall be the view (which is a function)
69+
that will be redirected to, after the user has logged out.
70+
5971
It also passes extra parameters to :class:`identity.web.WebFrameworkAuth`.
6072
"""
6173
self._request = request # Not available during class definition
6274
self._session = session # Not available during class definition
75+
self._post_logout_view = post_logout_view
6376
super(Auth, self).__init__(app, *args, **kwargs)
6477

6578
def _render_auth_error( # type: ignore[override]
@@ -153,3 +166,8 @@ def call_an_api(*, context):
153166
"""
154167
return super(Auth, self).login_required(function, scopes=scopes)
155168

169+
def logout(self):
170+
return super(Auth, self).logout(url_for(
171+
self._post_logout_view.__name__, _external=True,
172+
) if self._post_logout_view else None)
173+

identity/pallet.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ def __getattribute__(self, name):
6262
"@auth.login_required() or auth.logout() etc.")
6363
return super(PalletAuth, self).__getattribute__(name)
6464

65-
def logout(self):
65+
def logout(self, post_logout_redirect_uri: Optional[str] = None):
6666
return self.__class__._redirect( # self._redirect(...) won't work
67-
self._auth.log_out(self._request.url_root))
67+
self._auth.log_out(post_logout_redirect_uri or self._request.url_root)
68+
)
6869

6970
def login_required( # Named after Django's login_required
7071
self,

identity/quart.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ class Auth(PalletAuth):
1313
_Session = Session
1414
_redirect = redirect
1515

16-
def __init__(self, app: Optional[Quart], *args, **kwargs):
16+
def __init__(
17+
self,
18+
app: Optional[Quart],
19+
*args,
20+
post_logout_view: Optional[callable] = None,
21+
**kwargs,
22+
):
1723
"""Create an identity helper for a web application.
1824
1925
:param Quart app:
@@ -56,10 +62,17 @@ def build_app():
5662
5763
app = build_app()
5864
65+
:param callable post_logout_view:
66+
Optional.
67+
If not provided, the user will be redirected to the root URL of the app.
68+
If provided, it shall be the view (which is a function)
69+
that will be redirected to, after the user has logged out.
70+
5971
It also passes extra parameters to :class:`identity.web.WebFrameworkAuth`.
6072
"""
6173
self._request = request # Not available during class definition
6274
self._session = session # Not available during class definition
75+
self._post_logout_view = post_logout_view
6376
super(Auth, self).__init__(app, *args, **kwargs)
6477

6578
async def _render_auth_error(self, *, error, error_description=None):
@@ -152,3 +165,8 @@ async def call_api(*, context):
152165
"""
153166
return super(Auth, self).login_required(function, scopes=scopes)
154167

168+
def logout(self):
169+
return super(Auth, self).logout(url_for(
170+
self._post_logout_view.__name__, _external=True,
171+
) if self._post_logout_view else None)
172+

identity/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.9.2" # Note: Perhaps update ReadTheDocs and README.md too?
1+
__version__ = "0.10.0" # Note: Perhaps update ReadTheDocs and README.md too?

identity/web.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,13 @@ def _get_oidc_config(self):
298298
"%s not found from OIDC config: %s", self._END_SESSION_ENDPOINT, conf)
299299
return conf
300300

301-
def log_out(self, homepage):
301+
def log_out(self, post_logout_redirect_uri: str) -> str:
302302
# The vocabulary is "log out" (rather than "sign out") in the specs
303303
# https://openid.net/specs/openid-connect-frontchannel-1_0.html
304304
"""Logs out the user from current app.
305305
306-
:param str homepage:
307-
The page to be redirected to, after the log-out.
306+
:param str post_logout_redirect_uri:
307+
The absolute uri of the page to be redirected to, after the log-out.
308308
In Flask, you can pass in ``url_for("index", _external=True)``.
309309
310310
:return:
@@ -318,11 +318,11 @@ def log_out(self, homepage):
318318
# but its default (i.e. v1.0) endpoint will sign out the (only?) account
319319
endpoint = self._get_oidc_config().get(self._END_SESSION_ENDPOINT)
320320
if endpoint:
321-
return f"{endpoint}?post_logout_redirect_uri={homepage}"
321+
return f"{endpoint}?post_logout_redirect_uri={post_logout_redirect_uri}"
322322
except requests.exceptions.RequestException:
323323
logger.exception("Failed to get OIDC config")
324-
logger.warning("No end_session_endpoint found. Fallback to %s", homepage)
325-
return homepage
324+
logger.warning("No end_session_endpoint found. Fallback to %s", post_logout_redirect_uri)
325+
return post_logout_redirect_uri # Fall back to this
326326

327327
def get_token_for_client(self, scopes):
328328
"""Get access token for the current app, with specified scopes.

tests/test_django.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
from unittest import mock
33

44
import pytest
5+
from django.conf import settings
56

67
from identity.django import _parse_redirect_uri, Auth
78

9+
urlpatterns = [] # This is required for Django to recognize the URL patterns
10+
settings.configure(
11+
ROOT_URLCONF='test_django', # Set the root URL configuration
12+
)
813

914
def test_parse_redirect_uri():
1015
with pytest.raises(ValueError):
@@ -35,3 +40,24 @@ def test_the_installed_package_contains_builtin_templates():
3540
templates_found.add(t)
3641
assert templates_needed == templates_found
3742

43+
def test_logout():
44+
request = mock.MagicMock(
45+
build_absolute_uri=lambda relative_uri: f"http://localhost{relative_uri}"
46+
)
47+
with mock.patch('identity.web.requests.get', new=mock.MagicMock(
48+
return_value=mock.MagicMock(
49+
json=mock.MagicMock(return_value={
50+
"end_session_endpoint": "https://example.com/end_session",
51+
}),
52+
status_code=200,
53+
)
54+
)):
55+
response = Auth("client_id").logout(request)
56+
assert response.status_code == 302
57+
assert response.url == "https://example.com/end_session?post_logout_redirect_uri=http://localhost/"
58+
59+
auth = Auth("client_id", post_logout_view=lambda r: "You have logged out")
60+
with mock.patch('identity.django.reverse', return_value="/post_logout"):
61+
response = auth.logout(request)
62+
assert response.status_code == 302
63+
assert response.url == "https://example.com/end_session?post_logout_redirect_uri=http://localhost/post_logout"

0 commit comments

Comments
 (0)