Managing group membership via authentication

@david Continuing the discussion from

As you probably can guess, I’m also keen to see a stable solution to this problem, which is not specific to the openid-connect plugin. Let’s see if we can figure out a way forward

To pick up on the point you’ve raised.

How do you decide which groups to add and remove the user from? And how does that affect manually adding/removing users from groups in Discourse itself?

  1. a user authenticates via OIDC with groups ['group1', 'group2']
  2. In the discourse UI, the user is added to group3
  3. later, the same user authenticates via OIDC with groups ['group1']

Inspecting this as a human, we can see that the final state should be group1, group3 (group2 should be removed). But I don’t think there is enough state being tracked to make that decision programmatically. Similarly:

  1. a user authenticates via OIDC with groups ['group1', 'group2']
  2. In the discourse UI, the user is removed from group1
  3. later, the same user authenticates via OIDC with groups ['group1', 'group2']

Now what should happen? An admin has explicitely removed the user from group1 , but OIDC just added them back. This is very confusing UX for the admin. We may need some way to identify groups as ‘externally managed’, and hide all the add/remove UI :thinking:

I think there’s a few ways we could handle this issue (they’re non-exclusive)

Group and token attribute identification

In any implementation, explicit identification of

  1. which groups can have their membership managed via authentication; and
  2. which attributes in an auth token govern group membership

should be required, whether that be via site settings, group settings or otherwise, and default should be “off”.

Strict or permissive handling

Handling is “strict” if a user is removed when the group is absent from the identified token attribute. Handling is “permissive” if a user is not removed when the group is absent from the identified token attribute.

The strict/permissive state would be set on a per-group basis. There’s an example of how you could do this in a site setting here: Handle groups by mattcg · Pull Request #7 · discourse/discourse-openid-connect · GitHub. The same approach could be done via a group setting.

As you suggest, you could disable group membership addition / removal via the groups admin if the handling was “strict”.

Membership source identification

You could also possibly store the source of the group membership in the group_users table to allow for a “mixed” approach within a group, i.e. an admin can’t remove memberships created via auth token.

The more I think about this, the more I’m thinking this should be done via group settings.

5 Likes

Thanks @angus for getting this started! I know a lot of people are keen on this functionality!

Interesting! I had always imagined that groups would be auto-created during authentication, and that group name would be 1:1 with the group name on the identity provider. That’s how DiscourseConnect works at the moment.

But I actually like this more explicit option a lot! It means that people’s Discourse instances won’t be polluted by unneeded groups from their identity provider, and it means that admins can customize group names to their liking. It has a lot of parity with our existing email-domain-based membership

This sounds good from a technical point of view. My only worry is that it could be difficult to explain to users/admins. If we go with your idea of ‘explicit identification’ of auth-managed groups, I think we could just implement the ‘strict’ handling?

When a group is configured to be auth-managed, manually adding/removing is hidden behind a big warning, and it behaves like the ‘strict’ mode you described.

How does that sound?

Aside: we should also make sure that all automatic adding/removing of users is recorded in the group log. That will make it easier for everyone to understand what is happening and why.

4 Likes

Yeah, I feel it’s better to be explicit, for v1 of this feature set at least. I haven’t come across a use case yet where auto-creation has been really needed, i.e. the group couldn’t just be set up and configured by the site admin prior to any claims being handled.

Perhaps we could move to auto-creation as an option after we do the explicit version?

In terms of Group Settings, it would be something like

Settings Section: Membership (i.e. the existing section)
Settings Group Title: Authentication Management
Settings:

  • Service: List of authentication services. “All” would be an option. This setting would also function as an “enabled / disabled” state for this feature set. i.e. the default would be “None”.
  • Claim: text input to identify id token claim. The “description” for this feature (perhaps in a post here on meta) would explain the supported formats, e.g. boolean, comma-delineated string etc.
  • Mode: see below

Yes, if there was a strict / permissive setting we’d have to explain it concisely. I think the way to approach that would be to focus on “addition” and “removal”. You could perhaps describe it like this

Setting: “Mode”

Option 1 (permissive):

  • Label: “Add Members”
  • Description: “Allow members to be added to this group on authentication”

Option 2 (strict):

  • Label: “Add and Remove Members”
  • Description: “Allow members to be both added and removed on authentication. This will disable manual membership controls.”

The reason I’m keen to keep the “permissive” option is that when working with clients on this kind of thing before, that is the most common use case, i.e.

  • The main need is to allow for access to a group depending on a state in an external service

  • The need to remove access depending on the state in the external service is more marginal, i.e. people do lose access and should be removed, but this is relatively less important

  • There is often a desire to retain the ability to manually control membership in Discourse. When I’ve implemented the “strict” approach this has been “surprising” (i.e. “Why did person X lose membership?”) despite explaination(s) and it functioning (technically) as it should.

Yes this is important, as folks will often wonder “why” someone is added or removed, particularly in strict mode, and we (i.e. “Discourse”) won’t have control over the veracity of the claims the external auth service is making.

Perhaps a new type of “Remove User” and “Add User” action that includes the auth service responsible for the action, i.e. “Remove User ([service name])”.

2 Likes

One other “gotcha” here is that we’ll need to be clear that this isn’t a cure-all for the use case of “I want a group’s memberships to be based on external service X” as people don’t authenticate that frequently, or at least, according to the needs of the standard use case of that type.

This (auth management) is a necessary piece of handling that kind use case, but to properly serve it you also need to set up event-based integrations, e.g. a webhook receiver (we have a private webhook receiver plugin designed for group management that I hope to open source at some point soonish).

3 Likes

@david How do we feel about this? If I worked up a PR would you be open to it?

1 Like

This is valuable work you are doing here! :sunflower:

Just as a functionality comparison, thought I’d share what we do over at the Global Legal Empowerment Network, which uses wordpress and the discourse wp plugin. We have it set up so every time the user is updated in wordpress, it updates in discourse. This includes some special groups (e.g. core member, resource contributor, etc) as well as profile details. We added a hidden user field for “last updated” which helped with troubleshooting and making sure it was working properly.

We lock down those groups managed remotely so users cannot join or leave them when in discourse, but did not see the need to prevent group membership management by staff. I like the look of what you are trying above but it is a bit beyond me tbh.

We have some Discourse for Teams customers who use Okta actively for managing access to all their company apps. They set up roles there as well which are then supported for e.g. providing certain levels of accss to Microsoft Tableau, etc. Okta also is a directory so they manage user avatars, bio, location and other profile type info as well. They’d like to see this profile info updated in Teams externally from Okta as well.

3 Likes

We should try to keep the super-technical stuff out of the group settings if possible. Having to link to syntax documentation probably means it should be simplified, or relegated to the admin site settings. I think we should leave that part up to each auth plugin to implement, because it can vary so much.

Taking OIDC as an example, that plugin would add a new site setting openid_connect_roles_claim. If using Okta, the admin would configure that as groups. I think string array is a pretty standard format for OIDC, but we could explore more complex options here if really necessary.

To receive this information, Auth::Result would be given a new roles attribute, which accepts a simple array of strings. Core will then take this (in Auth::Result#apply_user_attributes!), and load it into a new UserAssociatedRoles table with columns (provider_name, user_id, role). This UserAssociatedRoles table gives us the ‘membership source identification’ you mentioned in the OP.

I imagine the group settings looking something like this:

Role names would be prefixed with the provider_name. Autocomplete would be based on all the existing values in the UserAssociatedRoles table, but we would also accept non-autocompleted values in case the role hasn’t been seen by Discourse yet.

The beauty of having a complete user_associated_roles table in the database is that admins can do whatever they like with Discourse groups, and memberships will be instantly updated without users having to log in again.

That’s fair. I think it would be best to keep this as simple as possible, at least for v1, so I’d rather not have multiple ‘modes’. How about we have this as the default behaviour:

  • Add members when they have a matching role from the auth provider
  • Remove members when that role disappears
  • Allow adding/removing in Discourse as well
  • If an admin tries to remove a user which was added via an auth provider, show a warning “this user may be added again next time they log in”

I think we should try to be secure-by-default here, and remove members when they lose the role on the identity provider. With the membership log improvements, I hope it should be less confusing than the current status-quo.

For sites that really don’t want that, we could have a site setting like remove_group_membership_when_auth_role_lost (default true).

Yeah for sure. We also have this problem with other user metadata like name, avatar, etc. I think at some point soon we need to look at making a sync_sso equivalent for other auth plugins. That could be passed all the info which is normally passed via OIDC / SAML / etc., including this new ‘role’ information’. Probably a separate project though.


How does that all sound @angus? It is a little less flexible than separate strict/permissive modes, but I think having just one mode will make troubleshooting / docs / support much easier.


With that plan, here’s a high-level view of what it looks like from an admin perspective:

Initial setup

  1. Set up okta auth, and enable the groups claim on Okta’s end

  2. In Discourse, set openid_connect_roles_claim to groups

Setting up a new group

  1. Create a discourse group as normal. Configure name / full-name / flair / etc. to whatever you like

  2. Go to the ‘membership’ preferences, put your cursor in the SSO roles dropdown, and choose a role from the dropdown. If Discourse hasn’t seen someone log in with the role yet, you’ll have to enter it manually

    You can specify multiple roles for a single Discourse group. e.g. a ‘team’ group in Discourse could be a combination of oidc:employees and oidc:contractors

  3. Press save, and group membership will be instantly updated with the role info Discourse has cached from previous logins.

  4. On future logins, any changes to identity provider roles will be reflected in the Discourse group

  5. You can still add / remove users to the group in the native Discourse UI

    • If you try to remove a user which was added via an identity provider, a warning will be shown to say that the user might be re-added on next login
  6. If you later decide you do not want that IDP role to be associated with the group, you can remove it, and all users which were members via that role will be removed