Skip to content

Commit 6defd19

Browse files
[IMP] auth_saml: add option to display a specific error page when login fails
1 parent 7bf5379 commit 6defd19

File tree

8 files changed

+134
-24
lines changed

8 files changed

+134
-24
lines changed

auth_saml/__manifest__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Copyright (C) 2020 GlodoUK <https://www.glodo.uk/>
2-
# Copyright (C) 2010-2016, 2022 XCG Consulting <http://odoo.consulting>
2+
# Copyright (C) 2010-2016, 2022, 2026 XCG Consulting <https://orbeet.io/>
33
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
44

55
{
@@ -21,6 +21,7 @@
2121
"data": [
2222
"data/ir_config_parameter.xml",
2323
"security/ir.model.access.csv",
24+
"templates/webclient.xml",
2425
"views/auth_saml.xml",
2526
"views/res_config_settings.xml",
2627
"views/res_users.xml",

auth_saml/controllers/main.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88

99
import werkzeug.utils
10+
from saml2.validate import ResponseLifetimeExceed
1011
from werkzeug.exceptions import BadRequest
1112
from werkzeug.urls import url_quote_plus
1213

@@ -54,6 +55,18 @@ def wrapper(self, **kw):
5455
return wrapper
5556

5657

58+
def _error_message(error: str) -> str | None:
59+
if error == "no-signup":
60+
return _("Sign up is not allowed on this database.")
61+
if error == "access-denied":
62+
return _("Access Denied")
63+
if error == "response-lifetime-exceed":
64+
return _("Response Lifetime Exceeded")
65+
if error == "expired":
66+
return _("You do not have access to this database. Please contact support.")
67+
return None
68+
69+
5770
# ----------------------------------------------------------
5871
# Controller
5972
# ----------------------------------------------------------
@@ -131,19 +144,7 @@ def web_login(self, *args, **kw):
131144

132145
response = super().web_login(*args, **kw)
133146
if response.is_qweb:
134-
error = request.params.get("saml_error")
135-
if error == "no-signup":
136-
error = _("Sign up is not allowed on this database.")
137-
elif error == "access-denied":
138-
error = _("Access Denied")
139-
elif error == "expired":
140-
error = _(
141-
"You do not have access to this database. Please contact"
142-
" support."
143-
)
144-
else:
145-
error = None
146-
147+
error = _error_message(request.params.get("saml_error"))
147148
response.qcontext["saml_providers"] = providers
148149

149150
if error:
@@ -173,6 +174,17 @@ def _get_saml_extra_relaystate(self):
173174
}
174175
return state
175176

177+
def _get_saml_error_url(self, saml_error: str) -> str:
178+
"""Return the URL of the SAML error page.
179+
This module provides a configuration option to use another page.
180+
"""
181+
base = (
182+
request.env["ir.config_parameter"]
183+
.sudo()
184+
.get_param("auth_saml.saml_error_page", "/web/login")
185+
)
186+
return f"{base}?saml_error={saml_error}"
187+
176188
@http.route("/auth_saml/get_auth_request", type="http", auth="none")
177189
def get_auth_request(self, pid):
178190
provider_id = int(pid)
@@ -251,15 +263,17 @@ def signin(self, **kw):
251263
# user could be on a temporary session
252264
_logger.info("SAML2: access denied")
253265

254-
url = "/web/login?saml_error=expired"
255-
redirect = werkzeug.utils.redirect(url, 303)
266+
redirect = werkzeug.utils.redirect(self._get_saml_error_url("expired"), 303)
256267
redirect.autocorrect_location_header = False
257268
return redirect
269+
except ResponseLifetimeExceed as e:
270+
_logger.debug("Response Lifetime Exceed - %s", str(e))
271+
url = self._get_saml_error_url("response-lifetime-exceed")
258272

259273
except Exception as e:
260274
# signup error
261275
_logger.exception("SAML2: failure - %s", str(e))
262-
url = "/web/login?saml_error=access-denied"
276+
url = self._get_saml_error_url("access-denied")
263277

264278
redirect = request.redirect(url, 303)
265279
redirect.autocorrect_location_header = False
@@ -291,3 +305,15 @@ def saml_metadata(self, **kw):
291305
),
292306
[("Content-Type", "text/xml")],
293307
)
308+
309+
@http.route("/web/login/saml_error", type="http", auth="none", csrf=False)
310+
def saml_error(self, redirect=None, **kw):
311+
saml_error = request.params.get("saml_error")
312+
error = _error_message(saml_error)
313+
if not error:
314+
return request.redirect(redirect or "/")
315+
response = request.render("auth_saml.login_error", {"error": error})
316+
response.headers["Cache-Control"] = "no-cache"
317+
response.headers["X-Frame-Options"] = "SAMEORIGIN"
318+
response.headers["Content-Security-Policy"] = "frame-ancestors 'self'"
319+
return response

auth_saml/data/ir_config_parameter.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@
44
<field name="key">auth_saml.allow_saml_uid_and_internal_password</field>
55
<field name="value">True</field>
66
</record>
7+
<record id="saml_error_page_config_parameter" model="ir.config_parameter">
8+
<field name="key">auth_saml.saml_error_page</field>
9+
<field name="value">/web/login</field>
10+
</record>
711
</odoo>

auth_saml/models/res_config_settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
class ResConfigSettings(models.TransientModel):
1010
_inherit = "res.config.settings"
1111

12+
module_auth_saml = fields.Boolean("SAML Authentication")
1213
allow_saml_uid_and_internal_password = fields.Boolean(
1314
"Allow SAML users to possess an Odoo password (warning: decreases security)",
1415
config_parameter=ALLOW_SAML_UID_AND_PASSWORD,
1516
)
17+
saml_error_page = fields.Selection(
18+
[("/web/login", "Login Page"), ("/web/login/saml_error", "Error Page")],
19+
"SAML Error Page",
20+
config_parameter="auth_saml.saml_error_page",
21+
required=True,
22+
)

auth_saml/readme/CONFIGURE.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ automatic redirection in the provider settings. The autoredirection will
1515
only be done on the active provider with the highest priority. It is
1616
still possible to access the login without redirection by using the
1717
query parameter `disable_autoredirect`, as in
18-
`https://example.com/web/login?disable_autoredirect=` The login is also
18+
`https://example.com/web/login?disable_autoredirect=`.
19+
20+
The login is also
1921
displayed if there is an error with SAML login, in order to display any
2022
error message.
23+
There is an option to use a page that only displays the error message
24+
in a separate page instead of the login form.
25+
This is useful when displaying the login form is not desired.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add an option to use another page for displaying SAML error.

auth_saml/templates/webclient.xml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<odoo>
3+
<template id="auth_saml.login_error" name="SAML Login Error">
4+
<t t-call="web.layout">
5+
<t t-set="head">
6+
<t t-call-assets="web.assets_frontend" t-js="false" />
7+
</t>
8+
<t t-set="body_classname" t-value="'bg-100'" />
9+
10+
<div class="container py-5">
11+
<div
12+
t-attf-class="card border-0 mx-auto bg-100 {{login_card_classes}} o_database_list"
13+
style="max-width: 300px;"
14+
>
15+
<div class="card-body">
16+
<div
17+
t-attf-class="text-center pb-3 border-bottom {{'mb-3' if form_small else 'mb-4'}}"
18+
>
19+
<img
20+
t-attf-src="/web/binary/company_logo{{ '?dbname='+db if db else '' }}"
21+
alt="Logo"
22+
style="max-height:120px; max-width: 100%; width:auto"
23+
/>
24+
</div>
25+
<div class="oe_login_form">
26+
<p class="alert alert-danger" role="alert">
27+
<t t-out="error" />
28+
</p>
29+
<div
30+
t-attf-class="clearfix oe_login_buttons text-center mb-1 {{'pt-2' if form_small else 'pt-3'}}"
31+
>
32+
<a
33+
class="btn btn-primary btn-block"
34+
href="/web/login"
35+
>Log in</a>
36+
</div>
37+
</div>
38+
</div>
39+
</div>
40+
</div>
41+
</t>
42+
</template>
43+
</odoo>

auth_saml/views/res_config_settings.xml

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,35 @@
77
<field name="inherit_id" ref="base.res_config_settings_view_form" />
88
<field name="arch" type="xml">
99
<xpath expr="//block[@name='integration']" position="inside">
10-
<setting
11-
string="SAML"
12-
help="Allow SAML users to possess an Odoo password (warning: decreases security)"
13-
id="module_auth_saml"
14-
>
15-
<field name="allow_saml_uid_and_internal_password" />
10+
<setting string="SAML Authentication" id="module_auth_saml">
11+
<field name="module_auth_saml" />
12+
<div class="content-group" invisible="not module_auth_saml">
13+
<div class="content-group mt16">
14+
<div
15+
class="alert-danger"
16+
invisible="not allow_saml_uid_and_internal_password"
17+
>Allowing users with SAML to have a password decreases security!</div>
18+
<field name="allow_saml_uid_and_internal_password" />
19+
<label
20+
for="allow_saml_uid_and_internal_password"
21+
class="o_light_label mr8"
22+
string="Allow SAML users to possess an Odoo password"
23+
/>
24+
</div>
25+
<div class="content-group mt16">
26+
<label for="saml_error_page" class="o_light_label mr8" />
27+
<field name="saml_error_page" />
28+
</div>
29+
<div class="content-group mt16">
30+
<button
31+
type="action"
32+
name="%(auth_saml.action_saml_provider)d"
33+
string="SAML Providers"
34+
icon="oi-arrow-right"
35+
class="btn-link"
36+
/>
37+
</div>
38+
</div>
1639
</setting>
1740
</xpath>
1841
</field>

0 commit comments

Comments
 (0)