|
| 1 | +try: |
| 2 | + from urllib import urlencode |
| 3 | +except ImportError: |
| 4 | + from urllib.parse import urlencode |
| 5 | + |
1 | 6 | from django.core.management import call_command |
2 | 7 | try: |
3 | 8 | from django.urls import reverse |
@@ -26,55 +31,139 @@ class EndSessionTestCase(TestCase): |
26 | 31 | def setUp(self): |
27 | 32 | call_command('creatersakey') |
28 | 33 | self.user = create_fake_user() |
| 34 | + self.client.force_login(self.user) |
29 | 35 |
|
| 36 | + # Create a client with a custom logout URL. |
30 | 37 | self.oidc_client = create_fake_client('id_token') |
31 | | - self.LOGOUT_URL = 'http://example.com/logged-out/' |
32 | | - self.oidc_client.post_logout_redirect_uris = [self.LOGOUT_URL] |
| 38 | + self.url_logout = 'http://example.com/logged-out/' |
| 39 | + self.oidc_client.post_logout_redirect_uris = [self.url_logout] |
33 | 40 | self.oidc_client.save() |
34 | 41 |
|
35 | | - self.url = reverse('oidc_provider:end-session') |
36 | | - |
37 | | - def test_redirects_when_aud_is_str(self): |
38 | | - query_params = { |
39 | | - 'post_logout_redirect_uri': self.LOGOUT_URL, |
40 | | - } |
41 | | - response = self.client.get(self.url, query_params) |
42 | | - # With no id_token the OP MUST NOT redirect to the requested |
43 | | - # redirect_uri. |
44 | | - self.assertRedirects( |
45 | | - response, settings.get('OIDC_LOGIN_URL'), |
46 | | - fetch_redirect_response=False) |
47 | | - |
| 42 | + # Create a valid ID Token for the user. |
48 | 43 | token = create_token(self.user, self.oidc_client, []) |
49 | 44 | id_token_dic = create_id_token( |
50 | 45 | token=token, user=self.user, aud=self.oidc_client.client_id) |
51 | | - id_token = encode_id_token(id_token_dic, self.oidc_client) |
| 46 | + self.id_token = encode_id_token(id_token_dic, self.oidc_client) |
52 | 47 |
|
53 | | - query_params['id_token_hint'] = id_token |
| 48 | + self.url = reverse('oidc_provider:end-session') |
| 49 | + self.url_prompt = reverse('oidc_provider:end-session-prompt') |
54 | 50 |
|
| 51 | + def test_id_token_hint_not_present_user_prompted(self): |
| 52 | + response = self.client.get(self.url) |
| 53 | + # We should display a logout consent prompt if id_token_hint parameter is not present. |
| 54 | + self.assertEqual(response.status_code, 302) |
| 55 | + self.assertEqual(response.headers['Location'], self.url_prompt) |
| 56 | + |
| 57 | + def test_id_token_hint_is_present_user_redirected_to_client_logout_url(self): |
| 58 | + query_params = { |
| 59 | + 'id_token_hint': self.id_token, |
| 60 | + } |
55 | 61 | response = self.client.get(self.url, query_params) |
56 | | - self.assertRedirects( |
57 | | - response, self.LOGOUT_URL, fetch_redirect_response=False) |
| 62 | + # ID Token is valid so user was |
| 63 | + self.assertEqual(response.status_code, 302) |
| 64 | + self.assertEqual(response.headers['Location'], self.url_logout) |
| 65 | + |
| 66 | + def test_id_token_hint_is_present_user_redirected_to_client_logout_url_with_post(self): |
| 67 | + data = { |
| 68 | + 'id_token_hint': self.id_token, |
| 69 | + } |
| 70 | + response = self.client.post(self.url, data) |
| 71 | + # ID Token is valid so user was |
| 72 | + self.assertEqual(response.status_code, 302) |
| 73 | + self.assertEqual(response.headers['Location'], self.url_logout) |
58 | 74 |
|
59 | | - def test_redirects_when_aud_is_list(self): |
60 | | - """Check with 'aud' containing a list of str.""" |
| 75 | + def test_state_is_present_and_being_passed_to_logout_url(self): |
61 | 76 | query_params = { |
62 | | - 'post_logout_redirect_uri': self.LOGOUT_URL, |
| 77 | + 'id_token_hint': self.id_token, |
| 78 | + 'state': 'ABCDE', |
63 | 79 | } |
64 | | - token = create_token(self.user, self.oidc_client, []) |
65 | | - id_token_dic = create_id_token( |
66 | | - token=token, user=self.user, aud=self.oidc_client.client_id) |
67 | | - id_token_dic['aud'] = [id_token_dic['aud']] |
68 | | - id_token = encode_id_token(id_token_dic, self.oidc_client) |
69 | | - query_params['id_token_hint'] = id_token |
70 | 80 | response = self.client.get(self.url, query_params) |
71 | | - self.assertRedirects( |
72 | | - response, self.LOGOUT_URL, fetch_redirect_response=False) |
| 81 | + # Let's ensure state is being passed to the logout url. |
| 82 | + self.assertEqual(response.status_code, 302) |
| 83 | + self.assertEqual(response.headers['Location'], '{0}?state={1}'.format(self.url_logout, 'ABCDE')) |
| 84 | + |
| 85 | + def test_post_logout_uri_not_in_client_urls(self): |
| 86 | + query_params = { |
| 87 | + 'id_token_hint': self.id_token, |
| 88 | + 'post_logout_redirect_uri': 'http://other.com/bye/', |
| 89 | + } |
| 90 | + response = self.client.get(self.url, query_params) |
| 91 | + # We prompt the user since the post logout url is not from client urls. |
| 92 | + # Also ensure client_id is present since we could validate id_token_hint. |
| 93 | + self.assertEqual(response.status_code, 302) |
| 94 | + self.assertEqual(response.headers['Location'], '{0}?client_id={1}'.format(self.url_prompt, self.oidc_client.client_id)) |
| 95 | + |
| 96 | + def test_prompt_view_redirecting_to_client_post_logout_since_user_unauthenticated(self): |
| 97 | + self.client.logout() |
| 98 | + query_params = { |
| 99 | + 'client_id': self.oidc_client.client_id, |
| 100 | + } |
| 101 | + response = self.client.get(self.url_prompt, query_params) |
| 102 | + # Since user is unauthenticated on the backend, we send it back to client post logout |
| 103 | + # registered uri. |
| 104 | + self.assertEqual(response.status_code, 302) |
| 105 | + self.assertEqual(response.headers['Location'], self.url_logout) |
| 106 | + |
| 107 | + def test_prompt_view_raising_404_since_user_unauthenticated_and_no_client(self): |
| 108 | + self.client.logout() |
| 109 | + response = self.client.get(self.url_prompt) |
| 110 | + # Since user is unauthenticated and no client information is present, we just show |
| 111 | + # not found page. |
| 112 | + self.assertEqual(response.status_code, 404) |
| 113 | + |
| 114 | + def test_prompt_view_displaying_logout_decision_form_to_user(self): |
| 115 | + query_params = { |
| 116 | + 'client_id': self.oidc_client.client_id, |
| 117 | + } |
| 118 | + response = self.client.get(self.url_prompt, query_params) |
| 119 | + # User is prompted to logout with client information displayed. |
| 120 | + self.assertContains(response, '<p>Hi <strong>johndoe@example.com</strong>, are you sure you want to log out from <strong>Some Client</strong> app?</p>', status_code=200, html=True) |
73 | 121 |
|
| 122 | + def test_prompt_view_displaying_logout_decision_form_to_user_no_client(self): |
| 123 | + response = self.client.get(self.url_prompt) |
| 124 | + # User is prompted to logout without client information displayed. |
| 125 | + self.assertContains(response, '<p>Hi <strong>johndoe@example.com</strong>, are you sure you want to log out?</p>', status_code=200, html=True) |
| 126 | + |
| 127 | + @mock.patch(settings.get('OIDC_AFTER_END_SESSION_HOOK')) |
| 128 | + def test_prompt_view_user_logged_out_after_form_allowed(self, end_session_hook): |
| 129 | + self.assertIn('_auth_user_id', self.client.session) |
| 130 | + # We want to POST to /end-session-prompt/?client_id=ABC endpoint. |
| 131 | + url_prompt_with_client = self.url_prompt + '?' + urlencode({ |
| 132 | + 'client_id': self.oidc_client.client_id, |
| 133 | + }) |
| 134 | + data = { |
| 135 | + 'allow': 'Anything', # This means user allowed being logged out. |
| 136 | + } |
| 137 | + response = self.client.post(url_prompt_with_client, data) |
| 138 | + # Ensure user is now logged out and redirected to client post logout uri. |
| 139 | + self.assertNotIn('_auth_user_id', self.client.session) |
| 140 | + self.assertEqual(response.status_code, 302) |
| 141 | + self.assertEqual(response.headers['Location'], self.url_logout) |
| 142 | + # End session hook should be called. |
| 143 | + self.assertTrue(end_session_hook.called) |
| 144 | + self.assertTrue(end_session_hook.call_count == 1) |
| 145 | + |
| 146 | + @mock.patch(settings.get('OIDC_AFTER_END_SESSION_HOOK')) |
| 147 | + def test_prompt_view_user_logged_out_after_form_not_allowed(self, end_session_hook): |
| 148 | + self.assertIn('_auth_user_id', self.client.session) |
| 149 | + # We want to POST to /end-session-prompt/?client_id=ABC endpoint. |
| 150 | + url_prompt_with_client = self.url_prompt + '?' + urlencode({ |
| 151 | + 'client_id': self.oidc_client.client_id, |
| 152 | + }) |
| 153 | + response = self.client.post(url_prompt_with_client) # No data. |
| 154 | + # Ensure user is still logged in and redirected to client post logout uri. |
| 155 | + self.assertIn('_auth_user_id', self.client.session) |
| 156 | + self.assertEqual(response.status_code, 302) |
| 157 | + self.assertEqual(response.headers['Location'], self.url_logout) |
| 158 | + # End session hook should not be called. |
| 159 | + self.assertFalse(end_session_hook.called) |
| 160 | + |
74 | 161 | @mock.patch(settings.get('OIDC_AFTER_END_SESSION_HOOK')) |
75 | | - def test_call_post_end_session_hook(self, hook_function): |
76 | | - self.client.get(self.url) |
77 | | - self.assertTrue(hook_function.called, 'OIDC_AFTER_END_SESSION_HOOK should be called') |
78 | | - self.assertTrue( |
79 | | - hook_function.call_count == 1, |
80 | | - 'OIDC_AFTER_END_SESSION_HOOK should be called once') |
| 162 | + def test_prompt_view_user_not_logged_out_after_form_not_allowed_no_client(self, end_session_hook): |
| 163 | + self.assertIn('_auth_user_id', self.client.session) |
| 164 | + response = self.client.post(self.url_prompt) # No data. |
| 165 | + # Ensure user is still logged in and 404 NOT FOUND was raised. |
| 166 | + self.assertIn('_auth_user_id', self.client.session) |
| 167 | + self.assertEqual(response.status_code, 404) |
| 168 | + # End session hook should not be called. |
| 169 | + self.assertFalse(end_session_hook.called) |
0 commit comments