Skip to content

Commit a1a0d5d

Browse files
committed
logout refactor: deprecate old validate_logout_request
1 parent 293bcea commit a1a0d5d

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

oauth2_provider/views/oidc.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import warnings
23
from urllib.parse import urlparse
34

45
from django.contrib.auth import logout
@@ -209,6 +210,76 @@ def _validate_claims(request, claims):
209210
return True
210211

211212

213+
def validate_logout_request(request, id_token_hint, client_id, post_logout_redirect_uri):
214+
"""
215+
Validate an OIDC RP-Initiated Logout Request.
216+
`(prompt_logout, (post_logout_redirect_uri, application), token_user)` is returned.
217+
218+
`prompt_logout` indicates whether the logout has to be confirmed by the user. This happens if the
219+
specifications force a confirmation, or it is enabled by `OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT`.
220+
`post_logout_redirect_uri` is the validated URI where the User should be redirected to after the
221+
logout. Can be None. None will redirect to "/" of this app. If it is set `application` will also
222+
be set to the Application that is requesting the logout. `token_user` is the id_token user, which will
223+
used to revoke the tokens if found.
224+
225+
The `id_token_hint` will be validated if given. If both `client_id` and `id_token_hint` are given they
226+
will be validated against each other.
227+
"""
228+
229+
warnings.warn("This method is deprecated and will be removed in version 2.5.0.", DeprecationWarning)
230+
231+
id_token = None
232+
must_prompt_logout = True
233+
token_user = None
234+
if id_token_hint:
235+
# Only basic validation has been done on the IDToken at this point.
236+
id_token, claims = _load_id_token(id_token_hint)
237+
238+
if not id_token or not _validate_claims(request, claims):
239+
raise InvalidIDTokenError()
240+
241+
token_user = id_token.user
242+
243+
if id_token.user == request.user:
244+
# A logout without user interaction (i.e. no prompt) is only allowed
245+
# if an ID Token is provided that matches the current user.
246+
must_prompt_logout = False
247+
248+
# If both id_token_hint and client_id are given it must be verified that they match.
249+
if client_id:
250+
if id_token.application.client_id != client_id:
251+
raise ClientIdMissmatch()
252+
253+
# The standard states that a prompt should always be shown.
254+
# This behaviour can be configured with OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT.
255+
prompt_logout = must_prompt_logout or oauth2_settings.OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT
256+
257+
application = None
258+
# Determine the application that is requesting the logout.
259+
if client_id:
260+
application = get_application_model().objects.get(client_id=client_id)
261+
elif id_token:
262+
application = id_token.application
263+
264+
# Validate `post_logout_redirect_uri`
265+
if post_logout_redirect_uri:
266+
if not application:
267+
raise InvalidOIDCClientError()
268+
scheme = urlparse(post_logout_redirect_uri)[0]
269+
if not scheme:
270+
raise InvalidOIDCRedirectURIError("A Scheme is required for the redirect URI.")
271+
if oauth2_settings.OIDC_RP_INITIATED_LOGOUT_STRICT_REDIRECT_URIS and (
272+
scheme == "http" and application.client_type != "confidential"
273+
):
274+
raise InvalidOIDCRedirectURIError("http is only allowed with confidential clients.")
275+
if scheme not in application.get_allowed_schemes():
276+
raise InvalidOIDCRedirectURIError(f'Redirect to scheme "{scheme}" is not permitted.')
277+
if not application.post_logout_redirect_uri_allowed(post_logout_redirect_uri):
278+
raise InvalidOIDCRedirectURIError("This client does not have this redirect uri registered.")
279+
280+
return prompt_logout, (post_logout_redirect_uri, application), token_user
281+
282+
212283
class RPInitiatedLogoutView(OIDCLogoutOnlyMixin, FormView):
213284
template_name = "oauth2_provider/logout_confirm.html"
214285
form_class = ConfirmLogoutForm

tests/test_oidc_views.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
RPInitiatedLogoutView,
1515
_load_id_token,
1616
_validate_claims,
17+
validate_logout_request,
1718
)
1819

1920
from . import presets
@@ -189,6 +190,89 @@ def mock_request_for(user):
189190
return request
190191

191192

193+
@pytest.mark.django_db
194+
@pytest.mark.parametrize("ALWAYS_PROMPT", [True, False])
195+
def test_deprecated_validate_logout_request(
196+
oidc_tokens, public_application, other_user, rp_settings, ALWAYS_PROMPT
197+
):
198+
rp_settings.OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT = ALWAYS_PROMPT
199+
oidc_tokens = oidc_tokens
200+
application = oidc_tokens.application
201+
client_id = application.client_id
202+
id_token = oidc_tokens.id_token
203+
assert validate_logout_request(
204+
request=mock_request_for(oidc_tokens.user),
205+
id_token_hint=None,
206+
client_id=None,
207+
post_logout_redirect_uri=None,
208+
) == (True, (None, None), None)
209+
assert validate_logout_request(
210+
request=mock_request_for(oidc_tokens.user),
211+
id_token_hint=None,
212+
client_id=client_id,
213+
post_logout_redirect_uri=None,
214+
) == (True, (None, application), None)
215+
assert validate_logout_request(
216+
request=mock_request_for(oidc_tokens.user),
217+
id_token_hint=None,
218+
client_id=client_id,
219+
post_logout_redirect_uri="http://example.org",
220+
) == (True, ("http://example.org", application), None)
221+
assert validate_logout_request(
222+
request=mock_request_for(oidc_tokens.user),
223+
id_token_hint=id_token,
224+
client_id=None,
225+
post_logout_redirect_uri="http://example.org",
226+
) == (ALWAYS_PROMPT, ("http://example.org", application), oidc_tokens.user)
227+
assert validate_logout_request(
228+
request=mock_request_for(other_user),
229+
id_token_hint=id_token,
230+
client_id=None,
231+
post_logout_redirect_uri="http://example.org",
232+
) == (True, ("http://example.org", application), oidc_tokens.user)
233+
assert validate_logout_request(
234+
request=mock_request_for(oidc_tokens.user),
235+
id_token_hint=id_token,
236+
client_id=client_id,
237+
post_logout_redirect_uri="http://example.org",
238+
) == (ALWAYS_PROMPT, ("http://example.org", application), oidc_tokens.user)
239+
with pytest.raises(ClientIdMissmatch):
240+
validate_logout_request(
241+
request=mock_request_for(oidc_tokens.user),
242+
id_token_hint=id_token,
243+
client_id=public_application.client_id,
244+
post_logout_redirect_uri="http://other.org",
245+
)
246+
with pytest.raises(InvalidOIDCClientError):
247+
validate_logout_request(
248+
request=mock_request_for(oidc_tokens.user),
249+
id_token_hint=None,
250+
client_id=None,
251+
post_logout_redirect_uri="http://example.org",
252+
)
253+
with pytest.raises(InvalidOIDCRedirectURIError):
254+
validate_logout_request(
255+
request=mock_request_for(oidc_tokens.user),
256+
id_token_hint=None,
257+
client_id=client_id,
258+
post_logout_redirect_uri="example.org",
259+
)
260+
with pytest.raises(InvalidOIDCRedirectURIError):
261+
validate_logout_request(
262+
request=mock_request_for(oidc_tokens.user),
263+
id_token_hint=None,
264+
client_id=client_id,
265+
post_logout_redirect_uri="imap://example.org",
266+
)
267+
with pytest.raises(InvalidOIDCRedirectURIError):
268+
validate_logout_request(
269+
request=mock_request_for(oidc_tokens.user),
270+
id_token_hint=None,
271+
client_id=client_id,
272+
post_logout_redirect_uri="http://other.org",
273+
)
274+
275+
192276
@pytest.mark.django_db
193277
def test_validate_logout_request(oidc_tokens, public_application):
194278
oidc_tokens = oidc_tokens

0 commit comments

Comments
 (0)