OpenID Users from Keycloak don't work with the zopyx.usersascontent addon?

Hi @zopyx and anyone that can help me

I am working with the addon "zopyx.usersascontent" to create a user profile as a content type, this plugin has an adapter class called "RedirectAfterLoginAdapter", which redirects the user after login to create a content type" PloneUser" inside a folder content type called "PloneUserFolder".

My requirement is to enable this behavior before it works with an external user from a login with a "pas.plugins.oidc" connection from a Keycloak server with version 22.0.5.

Analyzing the "zopyx.usersascontent" addon

It works correctly with the following use cases:

Plone users with "Member" roles

  • When Plone users with "Member" role log in, the "Authenticated" role is granted, i.e. it creates a content type "PloneUserFolder" folder named "users" in the root directory of the website.

  • Create the "PloneUser" content type correctly.

It does not work correctly with the following use case:

External users connected with the "pas.plugins.oidc" addon

  • When external users connect using the "pas.plugins.oidc" addon it does not have the "Member" role when logging in, it only has the "Authenticated" role, i.e. it does not create a content type folder "PloneUserFolder" called " users" in the root directory of the website.

  • Does not create the "PloneUser" content type correctly.

My Versions are:

  • Plone (Classic UI) 6.0.7 (6018)

  • CMF 3.2

  • Zope 5.8.5

  • Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0]

  • PIL 9.5.0 (Pillow)

  • WSGI: On

  • Server: waitress 2.1.2

My Addons extra are:

  • pas.plugins.oidc = 1.0.0

  • zopyx.usersascontent = master branch

  • python-keycloak = 3.9.1

Sorry, really no clue. We integrated the package in an updated state in a 2022 Plone project...so the package is not under active maintenance..happy to accept pull requests..no idea about the interaction with other PAS plugins, sorry!

1 Like

Maybe it can be done with the approach I am programming here (I have not had the time to set up a proper URL to test it, it does not work proplery on localhost).

In other words: Use the 'first logged in' content rule to create the folder, should be quite straightforward ( ping me if you need to code)

Update: Content rule to redirect to page (Plone 6 Classic) - #4 by espenmn

1 Like

@zopyx

In my case I need that:

When the user logs in the first time, launch an event that creates a PloneUser content type included by the zopyx.usercontent addon.

For that create the "subscribers" directory with the following files:

/subscribers/
โ”œโ”€โ”€ configure.zcml
โ”œโ”€โ”€ __init__.py
โ””โ”€โ”€ postlogin.py

The content from /subscribers/configure.zcml is the following:

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:i18n="http://namespaces.zope.org/i18n"
    xmlns:plone="http://namespaces.plone.org/plone"
    i18n_domain="my.package">

  <!-- CREATE A 'PLONEUSER' OBJECT LATER OF THE FIRST LOGIN -->

  <subscriber
    for="Products.PlonePAS.interfaces.events.IUserInitialLoginInEvent"
    handler="my.package.subscribers.postlogin.logged_in_handler"
    />

</configure>

The content from /subscribers/postlogin.py is the following:

from plone import api

def logged_in_handler(event):
    """Create the ``PloneUser`` content type from the Authenticated Member data"""
    pass

In the /configure.zcml file at the root directory modified as the following:

...
  <include file="permissions.zcml" />
  <!-- <adapter
    factory=".adapters.RedirectAfterLoginAdapter"
    for="OFS.interfaces.ITraversable
         zope.publisher.interfaces.IRequest"
    /> -->
  ...
  <!-- -*- extra stuff goes here -*- -->
  ...
  <include package=".subscribers" />

This working very well when the user login from the Plone login form aka /login. That is, it creates the "PloneUser" content type by taking the session data of the authenticated user

But if the Keycloak external user login using the Keycloak login form aka /acl_users/oidc/login, it is omitted this event handler.

I tested with Products.PlonePAS.interfaces.events.IUserInitialLoginInEvent and Products.PluggableAuthService.interfaces.events.IUserLoggedInEvent events, and nothing happens, the event doesn't fire!

My question is, how do I force the pas.plugins.oidc addon to fire the event every time a user logs in for the first time so I can create this type of "PloneUser" content?

Maybe the plugin developers can help me @ericof @erral @mamico

Or anyone with Zope developer skills

All comments or ideas are welcome

Just to be sure: Is there a diffence between goint to /login and clicking login on the front page?

I ask, because I notice that if I add a 'Content rule' on 'login', it fires if I go to /login, but if I click on 'login' it will not fire

Espen via Plone Community wrote at 2024-3-21 16:44 +0000:

...

Just to be sure: Is there a diffence between goint to /login and clicking login on the front page?

I ask, because I notice that if I add a 'Content rule' on 'login', it fires if I go to /login, but if I click on 'login' it will not fire

There are two login aspects:

  1. presenting the login form
  2. the login action after the form is posted.

Those aspects can either be implemented by separate or a common view.

A login event will fire only after the login action has performed
the form data verification.

@espenmn In my case, the Plone Classic UI theme used on the project disables the modal window from the default behavior of Plone and clicking on "Login" link redirect to URL /login

@dieter This is the reason why don't fire the event when the user login using the "pas.plugin.oidc" addon because it uses a custom view for login aka /acl_users/oidc/login for redirect to login with Keycloak service, where the user fills the form with user and password the form data verification is on Keycloak not in Plone, until where I understand.

If the user authentication is ok, it grants the authorization to access to Plone as a User with the "Authenticated" role.

Will there be another event that can be used to fire my function so that when the user logs in for the first time the event will fire?

Because I understand that, in the case of Content Rules, it will not trigger for external Plone users either, since these users use the URL /acl_users/oidc/login instead of the URL /login for login.

Leonardo Caballero via Plone Community wrote at 2024-3-22 08:53 +0000:

...
@dieter This is the reason why don't fire the event when the user login using the "pas.plugin.oidc" addon because it uses a custom view for login aka /acl_users/oidc/login for redirect to login with Keycloak service, where the user fills the form with user and password the form data verification is on Keycloak not in Plone, until where I understand.

If the user authentication is ok, it grants the authorization to access to Plone as a User with the "Authenticated" role.

I do not know Keycloak.
But after the verification, it must come back to Plone.
You likely can customize the return path to notify a "login event"
if the verification has been successful.

1 Like

@dieter Keycloak is an Identity and Access Management https://www.keycloak.org

I use the pas.plugins.oidc addon to integrate Keycloak with Plone

Yes, after the verification is correct, it back to Plone.

Using the zope.event.notify() [0] right?

[0] Events โ€” Plone Documentation v5.2

Leonardo Caballero via Plone Community wrote at 2024-3-22 10:09 +0000:

...

Using the zope.event.notify() [0] right?

Right.

1 Like

I started a PR but abandoned it later, to signal that the user had logged in, in order to have those login events fired.

I tried to do it calling the loginUser method in portal_membership but it didn't work: login user in portal_membership by erral ยท Pull Request #41 ยท collective/pas.plugins.oidc ยท GitHub

I finally overrode the callback function and call the relevant notify action there.

1 Like

My override is like the following example:

The browser/view.py file includes:

from Products.PlonePAS.interfaces.events import IUserInitialLoginInEvent
from pas.plugin.oidc.browser.view import CallbackView
from zope.event import notify

class CallbackView(CallbackView):

    def __call__(self):
        # Do something different from the default Callback view
        return super(CallbackView, self).__call__()

The browser/configure.zcml file includes:

    <browser:page
        for="pas.plugin.oidc.browser.view.CallbackView"
        name="callback"
        class="my.package.browser.view.CallbackView"
        layer="my.package.interfaces.ILayer" />

@erral something like the above code?

the for should be the plugin itself (check the callback view registration on pas.plugins.oidc.views), and yes to the evet firing in the __call__

1 Like

@erral Hi :slight_smile:

I registered a Browser view as the callback view registration on the "pas.plugins.oidc" views like the following code:

  <browser:page
      for="pas.plugins.oidc.plugins.IOIDCPlugin"
      name="callback"
      class="my.package.browser.view.CustomCallbackView"
      layer="my.package.interfaces.IMarkerLayer"
      permission="zope2.View"
      />

Saved it and reload it, using "plone.reload" addon.

Then when I login via the /acl_users/oidc/login URL and only load the original callback view and not my custom callback view.

Any idea what's wrong with my configuration?

you've to override it. Otherwise the plugin has 2 views and maybe it is picking the first one?

Try to comment out the original view registration and restart, and see if your view is pick up.

1 Like

@yurj I thought I was overriding it, could you please give me a code example in some public addon or some technical documentation which I indicated how to do it.

Leonardo Caballero via Plone Community wrote at 2024-3-26 08:07 +0000:

I registered a Browser view as the callback view registration on the "pas.plugins.oidc" views like the following code:

 <browser:page
     for="pas.plugins.oidc.plugins.IOIDCPlugin"
     name="callback"
     class="my.package.browser.view.CustomCallbackView"
     layer="my.package.interfaces.IMarkerLayer"
     permission="zope2.View"
     />

Saved it and reload it, using "plone.reload" addon.

Then when I login via the /acl_users/oidc/login URL and only load the original callback view and not my custom callback view.

Any idea what's wrong with my configuration?

I see several potential causes:

  • plone.reload might not have worked as you expect.

    I like plone.reload a lot, but if after the reload things
    do not work as I expect, I always restart and check again.

  • Your layer might not be active

  • Your IMarkerLayer might not compare with the
    layer used for the original registration -- only if your
    layer interface derives from the other one, it is guaranteed that
    your registration wins

  • Your class does not implement the right interface.

2 Likes

here you can see what @dieter said, and also a suggestion to use override.zcml.

1 Like

Make sure you're overriding the correct layer, the original registration is here:

1 Like