New saml2 Plugin for Plone 6 based on python3-saml

Hi everybody

TL;DR

  1. Install wcs.samlauth plugin
  2. Add PAS plugin via ZMI (in acl_users)
  3. Go to http://localhost:8080/Plone/acl_users/saml/idp_metadata
  4. Fetch or upload IDP metadata -> Click "Get and store metadata"
  5. Go to http://localhost:8080/Plone/acl_users/saml/metadata and configure your IDP
  6. Use http://localhost:8080/Plone/acl_users/saml/sls to login via IDP

We still have many customers who require saml2 as preferred, respectively required, authentication method. There were some options for older Plone versions, but nothing really usable for Plone 6.
Further I always had issues configuring Plone as SP together with customers, since there was no automation and people (including me at that time) had issues debugging configuration problems on either site (SP and IDP).

I followed several discussions, newer and older:

Thus I wrote a new package, which implements most of the python3-saml module as PAS Plugin.
Important: It only implements Plone as SP (Service Provider)
The plugin is based on a similar architecture/concept as pas.plugins.oidc. It basically means that all endpoints are directly on the plugin and regular plone users are created upon first successful login (or not).

This also enables you to have multiple saml plugins at the same time. You just need implement your own login/logout views.

By default, the plugin does not interfere with plone login/logout endpoints. You need change those urls on your own and/or enable the Challenge plugin.

I released an alpha version of the package -- wcs.samlauth · PyPI
Please also check the documentation on GitHub - webcloud7/wcs.samlauth
It includes how to configure either azure, or keycloak as IDP.

I tested the plugin within a couple customer projects and will install it next week on the first production environment.

Hot it works

If you already have a IDP, it should be pretty easy to configure your plone site as SP.
There is a view which fetches the necessary configuration from federation metadata XML.

And the plugin also exposes the SP configuration as metadata.xml

Tests

The package is end to end tested with keycloak in various configurations, including SP signatures.
Also see https://app.circleci.com/pipelines/github/webcloud7/wcs.samlauth

Info

The package still has some rough edges:

I would really appreciate some feedback on how the package works for you and what needs to be improved. So if you have a test env with and IDP, give it a shot.

Cheers,
Mathias

8 Likes

This looks super useful @maethu what IDPs have you tested it with?

1 Like

@pigeonflight thanks! So far with Azure and Keycloak. But IMHO it should support everything that python3-saml supports. I can try help connection other IDP providers. Or try others if they are available to me.

1 Like

@maethu great work. If you see no problems with it, it would be helpful to move your project to Collective · GitHub. To increase its visibility and get more contributions from the community.

2 Likes

I forked the repo into the collective namespace. I'll also try to set up CircleCI ASAP next week.

1 Like

@ericof did a great job on pas.plugins.oidc to make the project testable with GHA using pytest against a keycloak instance. see Modernize pas.plugins.oidc by ericof · Pull Request #36 · collective/pas.plugins.oidc · GitHub, I think the work can be (almost) easily replicated to your package.

1 Like

Thanks, I have everything setup already on circleci, including e2e test with keycloak :smile: . Just need to hook it up.

2 Likes

@maethu This is great work!

I think your addon has the potential to improve our SSO story especially since pure LDAP/AD is becoming less common as either ADFS, Shibboleth, or whatever plugin can connect to the wider universe of credential stores (Apple, Micro$ft, Amazon, etc).

IMHO, we should maybe sprint on AD and SSO as Volto further matures?

==

OK, here's my issue at the moment, I am testing this addon in our environment
and I've managed to successfully install and configure. Were authenticating to an Oracle OAM IDp maintained by our CIS. I've successfully imported the metadata and configured the attributes needed to redirect Plone's login to a session with the OAM portal. At the OAM login form providing valid SSO credentials initiates successful authentication and once passing subsequent MFA the proper payload (I've checked the SAML data) the attributes we want are being returned.

However, I'm getting a "BadRequest" error in Plone. I've obfuscated the URLs.

The response was received at https://blah.blah.edu/VirtualHostBase/https/blah.blah.edu:443/saml2test/VirtualHostRoot//acl_users/saml/acs instead of https://blah.blah.edu/acl_users/saml/acs
2024-02-16 19:14:11,622 ERROR   [wcs.samlauth.views:87][waitress-1] ['invalid_response']
2024-02-16 19:14:11,622 ERROR   [wcs.samlauth.views:88][waitress-1] The response was received at https://blah.blah.edu/VirtualHostBase/https/blah.blah.edu:443/saml2test/VirtualHostRoot//acl_users/saml/acs instead of https://blah.blah.edu/acl_users/saml/acs
2024-02-16 19:14:11,652 ERROR   [Zope.SiteErrorLog:35][waitress-1] BadRequest: https://blah.blah.edu/acl_users/saml/acs

The error message befuddles me because virtualhostbase should be ok and handled by Nginx no? I do know that the attributes returned don't match those in the addon. I think I read in your documentation that currently the attributes for authenticating with Plone are hard coded correct? With pas.plugin.ldap we map the attributes in Plone to those in ldap. Can I do something like that in the code? I think I spotted what needs to be changed in plugin.py beginning at line 75?

Hopefully, this missive makes sense, I need to map two attributes I'm getting from the IDp to Plone for a successful login. Any ideas, pointers, suggestions, etc are greatly appreciated, and thanks for listening.

Regards

Eric

DM.zope.saml2 has code for creating users with attributes brought in from the Idp. You can get inspiration from that code

Thanks @riker11451

I'm happy to take a look at both. The issue with the virtualhostmonster and the attribute mapping feature. It might take a week or two, since I'm currently super bussy with other projects. I'll keep you posted!

Mathias

@riker11451 Can you print or debug the following line: wcs.samlauth/wcs/samlauth/views.py at main · collective/wcs.samlauth · GitHub in your project? To see what's in url there? I just want to make sure there is no issue with the Nginx config.

Is this any help?

I've not modified any code under the hood other than inserting a breakpoint to figure out what's happening.

/opt/plone6/project-title/backend/lib/python3.11/site-packages/IPython/core/interactiveshell.py:915: UserWarning: Attempting to work in a virtualenv. If you encounter problems, please install IPython inside the virtualenv.
  warn(
> /opt/plone6/project-title/backend/lib/python3.11/site-packages/wcs/samlauth/views.py(27)_prepare_request()
     26         ipdb.set_trace()
---> 27         url = urlparse(self.request.URL)
     28         request = {

ipdb> c
> /opt/plone6/project-title/backend/lib/python3.11/site-packages/wcs/samlauth/views.py(27)_prepare_request()
     26         ipdb.set_trace()
---> 27         url = urlparse(self.request.URL)
     28         request = {

ipdb> c


@riker11451
Thanks, now instead of typing c run print(self.request.URL)

My bad, it doesn't look like Nginx is messing anything up here and I don't have any paths specified in Zope's VHM.

> /opt/plone6/project-title/backend/lib/python3.11/site-packages/wcs/samlauth/views.py(27)_prepare_request()
     26         ipdb.set_trace()
---> 27         url = urlparse(self.request.URL)
     28         request = {

ipdb> print(self.request.URL)
https://blah.blah.edu/acl_users/saml/sls
ipdb> c
> /opt/plone6/project-title/backend/lib/python3.11/site-packages/wcs/samlauth/views.py(27)_prepare_request()
     26         ipdb.set_trace()
---> 27         url = urlparse(self.request.URL)
     28         request = {

ipdb> print(self.request.URL)
https://blah.blah.edu/acl_users/saml/acs
ipdb> c
The response was received at https://blah.blah.edu/VirtualHostBase/https/blah.blah.edu:443/saml2test/VirtualHostRoot//acl_users/saml/acs instead of https://blah.blah.edu/acl_users/saml/acs
2024-02-20 14:46:01,179 ERROR   [wcs.samlauth.views:88][waitress-0] ['invalid_response']
2024-02-20 14:46:01,179 ERROR   [wcs.samlauth.views:89][waitress-0] The response was received at https://blah.blah.edu/VirtualHostBase/https/blah.blah.edu:443/saml2test/VirtualHostRoot//acl_users/saml/acs instead of https://blah.blah.edu/acl_users/saml/acs

@riker11451

I was able to reproduce the issue with nginx. On my K8s cluster with nginx as ingress, the problem is not there. I changed how I put together the request for the saml processing. Now, it will work with nginx (Assuming a config like here)

Issue is fixed here: Do not use SCRIPT_NAME for the callback URL by maethu · Pull Request #5 · webcloud7/wcs.samlauth · GitHub
New release is out: wcs.samlauth · PyPI

I hope it works now for you as well!!

Cheers,
Mathias

2 Likes

Mathias,

It indeed works! Thank you for your efforts.

1 Like

Are you able to change the attributes on your end? With keycloak and azure it's no problem to define/transform the attributes on the IDP end to match what wcs.samlauth needs. I can implement a mapper, but it will take some time :slight_smile:

Mathias,

Again thanks for your quick assistance, I'm sorry I've been too busy to reply sooner.

It would be great to have a mapper to match attributes that aren't standard. I'm looking to use the University ID and login email which are different from what is hard coded.

As is, upon successful login, my user is being created from the ID generated from the SAML2 Assertion which looks like

# id-vrBpcyPwqvBRo5Kk60A3TQUksifwnttsmeXdJgPa

SAML 2.0 Assertion
ID	id-CB36x8kO48GQpQZx2LG37Ttkd1--JFbUcaUPHxze
Version	2.0
IssueInstant	2024-02-29T18:24:15Z
Subject	id-vrBpcyPwqvBRo5Kk60A3TQUksifwnttsmeXdJgPa
SAML 2.0 AttributeStatement

what I need to figure out is how to get the data from the SAML2 AttibuteStatement which contains the attributes we want to use. do you think I could monkey-patch the addon to just get things working as a POC?

@riker11451 I'm sure you can patch something together to match your use-case.

So, the ID is determined by your IDP setting, respectively, your NameID Format.

The attributes from your IDP are getting updates here: wcs.samlauth/wcs/samlauth/plugin.py at 433ebdca301d76aad88edc97b4a6ea0f0338802c · webcloud7/wcs.samlauth · GitHub

If they are correctly in the SAML payload you can check them here wcs.samlauth/wcs/samlauth/plugin.py at 433ebdca301d76aad88edc97b4a6ea0f0338802c · webcloud7/wcs.samlauth · GitHub

You can patch _updateUserProperties to your needs :slight_smile:

1 Like

thanks!

are you using Volto with this add-on or just classic?