diff --git a/auth_oidc/controllers/main.py b/auth_oidc/controllers/main.py index f7d17dd790..9ef0ca9020 100644 --- a/auth_oidc/controllers/main.py +++ b/auth_oidc/controllers/main.py @@ -72,6 +72,8 @@ def logout(self, redirect="/web/login"): params = parse_qs(components.query) params["client_id"] = provider.client_id params["post_logout_redirect_uri"] = redirect_url + if provider.skip_logout_confirmation and user.oauth_id_token: + params["id_token_hint"] = user.oauth_id_token logout_url = components._replace(query=url_encode(params)).geturl() return super().logout(redirect=logout_url) # User has no account with any provider or no logout URL is configured for the provider diff --git a/auth_oidc/models/auth_oauth_provider.py b/auth_oidc/models/auth_oauth_provider.py index 2599eb3ece..87c5d2046b 100644 --- a/auth_oidc/models/auth_oauth_provider.py +++ b/auth_oidc/models/auth_oauth_provider.py @@ -52,6 +52,11 @@ class AuthOauthProvider(models.Model): "in the client, should be the value of end_session_endpoint specified by " "the authorization provider.", ) + skip_logout_confirmation = fields.Boolean( + default=False, + string="Skip Logout Confirmation", + help="If set to true, the logout confirmation is skipped in the authorization provider.", + ) @tools.ormcache("self.jwks_uri", "kid") def _get_keys(self, kid): diff --git a/auth_oidc/models/res_users.py b/auth_oidc/models/res_users.py index 4743619e00..50d07d4eef 100644 --- a/auth_oidc/models/res_users.py +++ b/auth_oidc/models/res_users.py @@ -6,7 +6,7 @@ import requests -from odoo import api, models +from odoo import api, fields, models from odoo.exceptions import AccessDenied from odoo.http import request @@ -16,6 +16,8 @@ class ResUsers(models.Model): _inherit = "res.users" + oauth_id_token = fields.Char(string="OAuth Id Token", readonly=True, copy=False) + def _auth_oauth_get_tokens_implicit_flow(self, oauth_provider, params): # https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse return params.get("access_token"), params.get("id_token") @@ -74,7 +76,12 @@ def auth_oauth(self, provider, params): raise AccessDenied() # retrieve and sign in user params["access_token"] = access_token + params["id_token"] = id_token login = self._auth_oauth_signin(provider, validation, params) + oauth_user = self.search( + [("login", "=", login), ("oauth_access_token", "=", access_token)] + ) + oauth_user.write({"oauth_id_token": params["id_token"]}) if not login: raise AccessDenied() # return user credentials diff --git a/auth_oidc/tests/test_auth_oidc_logout.py b/auth_oidc/tests/test_auth_oidc_logout.py index 5280ddcf54..4d4003f908 100644 --- a/auth_oidc/tests/test_auth_oidc_logout.py +++ b/auth_oidc/tests/test_auth_oidc_logout.py @@ -128,3 +128,58 @@ def test_oidc_logout_with_absolute_redirect_url(self): actual_params = dict(parse_qsl(actual_components.query)) self.assertEqual(CLIENT_ID, actual_params["client_id"]) self.assertEqual(BASE_URL, actual_params["post_logout_redirect_uri"]) + + def test_oidc_logout_skip_confirmation(self): + """Test that oidc logout skips confirmation""" + id_token = "test-id-token" + self._set_test_oidc_logout_url(urljoin(OIDC_BASE_LOGOUT_URL, OIDC_LOGOUT_PATH)) + self.provider.write({"skip_logout_confirmation": True}) + user = self._prepare_login_test_user(self.provider) + user.write({"oauth_id_token": id_token}) + with create_request(self.env, user.id, self.mock_logout_user): + resp = OpenIDLogout().logout() + self.assertTrue(resp.location.startswith(OIDC_BASE_LOGOUT_URL)) + actual_components = urlparse(resp.location) + self.assertEqual(OIDC_LOGOUT_PATH, actual_components.path) + actual_params = dict(parse_qsl(actual_components.query)) + self.assertEqual(CLIENT_ID, actual_params["client_id"]) + self.assertEqual(id_token, actual_params["id_token_hint"]) + self.assertEqual( + urljoin(BASE_URL, LOGIN_PATH), actual_params["post_logout_redirect_uri"] + ) + + def test_oidc_logout_not_skip_confirmation_if_no_id_token(self): + """Test that oidc logout does not skip confirmation if user has no oauth_id_token""" + self._set_test_oidc_logout_url(urljoin(OIDC_BASE_LOGOUT_URL, OIDC_LOGOUT_PATH)) + self.provider.write({"skip_logout_confirmation": True}) + user = self._prepare_login_test_user(self.provider) + with create_request(self.env, user.id, self.mock_logout_user): + resp = OpenIDLogout().logout() + self.assertTrue(resp.location.startswith(OIDC_BASE_LOGOUT_URL)) + actual_components = urlparse(resp.location) + self.assertEqual(OIDC_LOGOUT_PATH, actual_components.path) + actual_params = dict(parse_qsl(actual_components.query)) + self.assertEqual(CLIENT_ID, actual_params["client_id"]) + self.assertIsNone(actual_params.get("id_token_hint")) + self.assertEqual( + urljoin(BASE_URL, LOGIN_PATH), actual_params["post_logout_redirect_uri"] + ) + + def test_oidc_logout_not_skip_confirmation_if_not_enabled(self): + """Test that oidc logout skips confirmation""" + id_token = "test-id-token" + self._set_test_oidc_logout_url(urljoin(OIDC_BASE_LOGOUT_URL, OIDC_LOGOUT_PATH)) + self.provider.write({"skip_logout_confirmation": False}) + user = self._prepare_login_test_user(self.provider) + user.write({"oauth_id_token": id_token}) + with create_request(self.env, user.id, self.mock_logout_user): + resp = OpenIDLogout().logout() + self.assertTrue(resp.location.startswith(OIDC_BASE_LOGOUT_URL)) + actual_components = urlparse(resp.location) + self.assertEqual(OIDC_LOGOUT_PATH, actual_components.path) + actual_params = dict(parse_qsl(actual_components.query)) + self.assertEqual(CLIENT_ID, actual_params["client_id"]) + self.assertIsNone(actual_params.get("id_token_hint")) + self.assertEqual( + urljoin(BASE_URL, LOGIN_PATH), actual_params["post_logout_redirect_uri"] + ) diff --git a/auth_oidc/views/auth_oauth_provider.xml b/auth_oidc/views/auth_oauth_provider.xml index c890fb55a8..1cf85680b1 100644 --- a/auth_oidc/views/auth_oauth_provider.xml +++ b/auth_oidc/views/auth_oauth_provider.xml @@ -19,6 +19,7 @@ +