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 @@
+