-
-
Notifications
You must be signed in to change notification settings - Fork 400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[14.0][IMP] auth_oidc: Logout the user from the auth provider #666
base: 14.0
Are you sure you want to change the base?
Conversation
Hi @sbidoul, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your efforts to make this module better. Could you run the OCA pre-commit or incoperate the suggestions from the log output: https://github.com/OCA/server-auth/actions/runs/9479497854/job/26118147534?pr=666
auth_oidc/controllers/main.py
Outdated
p = request.env["auth.oauth.provider"].sudo().browse(user.oauth_provider_id.id) | ||
if p.logout_endpoint: | ||
r = request.env['ir.config_parameter'].sudo().get_param('web.base.url') + redirect | ||
logout_url = p.logout_endpoint + "?client_id=" + p.client_id + "&post_logout_redirect_uri=" + r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the logout_endpoint may already contain query parameters:
end_session_endpoint
REQUIRED. URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP. This URL MUST use the https scheme and MAY contain port, path, and query parameter components.
https://openid.net/specs/openid-connect-rpinitiated-1_0.html
So it would be better to not add a hardcocded questionmark here but use a more dynamic solution to adding parameters.
Also it seems suspicious that the redirect url r
is not encoded here. (But i have not tested it yet)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes r
should be encoded when added as post_logout_redirect_uri. (e.g. when redirect is /web/login?db=devel&something=different
it would possibly end up as parameter something on the logout_endpoint)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I will update the PR
@@ -46,6 +46,10 @@ class AuthOauthProvider(models.Model): | |||
string="Token URL", help="Required for OpenID Connect authorization code flow." | |||
) | |||
jwks_uri = fields.Char(string="JWKS URL", help="Required for OpenID Connect.") | |||
logout_endpoint = fields.Char( | |||
string="Logout URL", | |||
help="Required for OpenID Connect to logout the user in the authorization provider upon logout in the client." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this is expected to be the same value as end_session_endpoint
from the provider discovery metadata, can you add a remark to the documentation that this is the value to be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I could also rename the property to end_session_endpoint
auth_oidc/controllers/main.py
Outdated
if user.oauth_provider_id.id: | ||
p = request.env["auth.oauth.provider"].sudo().browse(user.oauth_provider_id.id) | ||
if p.logout_endpoint: | ||
r = request.env['ir.config_parameter'].sudo().get_param('web.base.url') + redirect |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ap-wtioit is redirect expected to always be relative to the base URL? If full URLs are supported then the line above would need to be updated to be optional if redirect starts with http
or https
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think only relative redirects are used in odoo but there is no technical limit for that and the redirect paramter is sent directly to werkzeug: https://github.com/odoo/odoo/blob/14.0/addons/web/controllers/main.py#L1319
so yes that's a good catch and web.base.url should only be used when the url is not starting with a protocol (i guess http(s) will suffice), also there is the possibility that the redirect is relative to /web/session/logout (e.g. ../login
, or destroy
) but i think this is also not used currently (and i tested it only locally for checking security)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mentioned destroy
, I see it also logs out the user, do we need to override it too to logout the user in the IDP?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i can only see destroy
being used in one Odoo Enterprise application "project_timesheet_synchro / Synchronization with the external timesheet application" and that code upon login does not support the things added by auth_oidc so i would skip this usecase here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So is this good to go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to test it again (most likely tomorrow) but yes this looks good to me now.
auth_oidc/controllers/main.py
Outdated
p = request.env["auth.oauth.provider"].sudo().browse(user.oauth_provider_id.id) | ||
if p.logout_endpoint: | ||
r = redirect | ||
if r.find('http') == -1 and r.find('https') == -1: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have sneaked in support for full URLs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use not r.startswith("http:") and not r.startswith("https:")
here. the check you used would skip adding the base url for all redirects containing http.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Reviewed again, did you see my comment regarding oca-precommit? The check is still failing. |
@ap-wtioit when do you plan to have this merged? |
Apply formatting changes Apply formatting changes
to me it looks good now. to get it merged we need a second approval, ideally from a PSC (Project Steering Committee member) or the maintainer of the module. then it can be merged (by the maintainer or PSC). |
@sbidoul do you mind becoming the second reviewer/approver on this PR? |
I will look into it. Not had time yet. |
I see that this was added to the 14.0 milestone, it would be great to forward port the changes to later milestones too i.e. 15, 16 and 17 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi,
Thanks for this contrib.
I made a few comments, the most important being the URL parsing logic that should use urllib.parse for robustness. I think this is important is such a security sensitive module.
Could you also add a test? The existing tests should give inspiration.
Head up about forward porting: I tested this on 16.0 and it will require some work to adapt, because Odoo now forces a local redirect (requests.redirect has a local=True argument):
@@ -46,6 +46,12 @@ class AuthOauthProvider(models.Model): | |||
string="Token URL", help="Required for OpenID Connect authorization code flow." | |||
) | |||
jwks_uri = fields.Char(string="JWKS URL", help="Required for OpenID Connect.") | |||
logout_endpoint = fields.Char( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest naming this field end_session_endpoint
to match the field in the openid configuration document. This will make the code easier to understand.
auth_oidc/controllers/main.py
Outdated
request.env["ir.config_parameter"] | ||
.sudo() | ||
.get_param("web.base.url") | ||
+ r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use urllib.parse.urljoin
instead of +
here. It will be better in case web.base.url
ends with a /
.
auth_oidc/controllers/main.py
Outdated
logout_base_url = p.logout_endpoint | ||
params = {} | ||
if "?" in p.logout_endpoint: | ||
url_parts = p.logout_endpoint.split("?") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too comfortable with parsing a URL this way. What if, for instance, there is more than 1 ?
in the url. It should not happen of course, but... It would be more robust to use urllib.parse.urlparse
and parse_qs
.
auth_oidc/controllers/main.py
Outdated
params = url_decode(url_parts[1]) | ||
params["client_id"] = p.client_id | ||
params["post_logout_redirect_uri"] = r | ||
logout_url = f"{logout_base_url}?{url_encode(params)}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to extract this url manipulation logic in a little class method that could be unit tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there documentation on how to run the test suite?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wluyima This part of the odoo documentation should get you started. Let me know if you hit any issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @sbidoul is there a way I can find documentation to run Odoo 14.0 locally from source? I was following the 15.0 documentation and I can't seem to install the packages in the requirements.txt file, I get this error when I try to install the packages, do you have any clues? I'm using Python 3.12.4. I'm not sure if is not compatible, is there documentation on Odoo and Python versions compatibility?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Odoo 14 I generally use python 3.8. It will for sure not work with 3.12.
I recommend installing the Odoo dependencies in a virtual environment:
python3.8 -m venv ./venv
source ./venv/bin/activate
pip install -r https://raw.githubusercontent.com/odoo/odoo/14.0/requirements.txt
But if you don't use Odoo 14, why do you do this PR to the 14.0 branch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @sbidoul! I did say we're using Odoo 14 which is why this PR is issued against the 14.0 branch and I'm installing the dependencies in a virtual environment but I was using python 3.12, I'll try with python 3.8 and see if it works. I was actually able to later find the documentation for Odoo 14 here.
auth_oidc/controllers/main.py
Outdated
def logout(self, redirect="/web/login"): | ||
user = request.env["res.users"].sudo().browse(request.session.uid) | ||
if user.oauth_provider_id.id: | ||
p = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little detail: let's use explicit variable names?
string="Logout URL", | ||
help="Required for OpenID Connect to logout the user in the authorization provider " | ||
"upon logout in the client, should be the value of end_session_endpoint specified by " | ||
"the authorization provider", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall we mention that when this field is set, logging out of Odoo also causes a complete logout of the identify provider?
|
||
class OpenIDLogout(Session): | ||
@http.route("/web/session/logout", type="http", auth="none") | ||
def logout(self, redirect="/web/login"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment or docstring referencing the spec that this implements?
https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
auth_oidc/controllers/main.py
Outdated
) | ||
if provider.end_session_endpoint: | ||
red_url = redirect | ||
if not red_url.startswith("http:") and not red_url.startswith("https:"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is now unnecessary, I think, because urljoin will do the right thing.
auth_oidc/controllers/main.py
Outdated
.browse(user.oauth_provider_id.id) | ||
) | ||
if provider.end_session_endpoint: | ||
red_url = redirect |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
red_url = redirect | |
post_logout_redirect_uri = redirect |
@sbidoul tests have been added |
@sbidoul @ap-wtioit tests were added, did you get a chance to take a look? |
Added a mechanism to logout the user from the auth provider when