BookStack
BookStack copied to clipboard
OIDC Group Sync
This issue has been opened to get feedback in relation to supporting group sync for OIDC usage. We'd look to implement group sync in a similar manner to LDAP and SAML2 for consistency.
In regards to feedback, I primarily want to know:
- What do groups look like when provided by your IDP in the OIDC ID Token?
Knowing the above helps us plan a stable and well supported implementation.
Hello there,
One popular solution for self-hosted OIDC, which may come in pair with a self-hosted BookStack would be Keycloak, which will usually return a list of groups as part of the token. So something like:
[...]
"groups": [
"administrators",
"group-1",
"group-2",
],
[...]
Hope it helps,
Best,
Would groups be synced to roles or to its own new(?) "group" concept? I would vote for the latter. Basically the concept of groups that is separated from roles. It could then serve as a container for multiple roles and also be able to "own" books and bookshelves.
@hellerbarde Roles. As per our LDAP/SAML implementations. I wouldn't want to add another layer of grouping into this system as we'd likely be adding complexity with little benefit.
Ok. Its just that the permission system seems rather opaque to non-technical end users. So far i had to fix permissions on every new book one of my users created so the correct group of people had access in the correct way to it. And i figured if I could assign a book to a "group" which then applies a pre-defined set of permissions, it would fix that problem.
But i understand if you dont want to load that added data model complexity on top.
Also, this is quickly going down a topic that doesnt belong in this issue, so i shall excuse myself. My question has been answered :)
Cheers, Phil
As you know by helping me before, I am using django-oauth-toolkit to extend django as an ID Provider.
When having Abstract User Model, as always recomended in django, I had to get the step of writting the file oauth_validator.py, providing extra claims.
I've tested it adding groups generated by wagtail (a very good cms for django).
# oauth_validator.py
from oauth2_provider.oauth2_validators import OAuth2Validator
class CustomOAuth2Validator(OAuth2Validator):
def get_additional_claims(self, request):
groups = [
group for group in request.user.groups.all()
] # get all groups of the user
return {
"email": request.user.email,
"first_name": request.user.first_name,
"last_name": request.user.last_name,
"groups": groups,
}
The resulting OIDC_DUMP_USER_DETAILS=true is:
{
"aud": "W****************************************N7m51caT",
"iat": "*****************",
"at_hash": "**********************",
"sub": "1",
"email": "[email protected]",
"first_name": "Jesús",
"last_name": "López de Leyva",
"groups": [
"Moderators",
"Editors"
],
"iss": "https://******************.ngrok.io",
"exp": 1639077321,
"auth_time": 1639041314,
"jti": "b8102c32-****-4051-*****-******************"
}
With OKTA, Groups can be retrieved and added as a group claim in a token - https://developer.okta.com/docs/guides/customize-tokens-groups-claim/main/#request-an-id-token-that-contains-the-groups-claim
I'd prefer to use Role Bindings instead of Groups: Roles define what someone is allowed to do. Groups are just grouping Users.
That allows for a rather natural way of defining authorization infos (and is the way that e.g. KeyCloak supports). Furthermore, in KeyCloak you define Roles and Users on Realm-Level. Roles can be defined on Client-Level as well.
So, for managing Authorization in Bookstack, I'd define the Bookstack-specific Roles (Admin, Editor, Viewer, etc.) in the Bookstack-specific KeyCloak Client and would then assign these Roles to my Users and Groups I whish to grant access to.
That would result in an Access Token similar to this one:
{
"exp": 1659004398,
"iat": 1659004098,
"jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"iss": "https://keycloak.instance",
"sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"typ": "Bearer",
"azp": "bookstack",
"session_state": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"resource_access": {
"bookstack": { <=== this is the client id
"roles": [
"Editor" <=== this is a (maybe client specific) role name
]
}
},
"scope": "openid uid groups profile email",
"sid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"uid": "someuid",
"email_verified": true,
"name": "Michael Schaefers",
"preferred_username": "mschaefers",
"given_name": "Michael",
"family_name": "Schaefers",
"email": "[email protected]"
}
Thanks everyone for your input, Looks like we can safely assume a nice flat array of simple group name strings then based upon the above, but they could be within a nested object structure. Should be pretty straightforward to handle, can have an option like OIDC_GROUPS_ATTRIBUTE=resource_access.bookstack.roles using the above as an example.
I've assigned this to the next feature release.
Some last ideas :)
- It might be necessary to define a group name mapping if matching group names can not be provided by OIDC provider. (
bookstackname=oidcname) - The user trying to login should be unauthorized when there is no matching role provided
@mschaefers We already have a scheme for custom mapping BookStack roles to auth system groups (Via a text field in the BookStack role configuration). We'll just use the same system here.
In regards to 2, I'm not going to do anything specifically to enforce that but you'd have the option to not assign a default role so they'd have access to the system but be without any roles assigned.
Functionality added in #3616, ready to be part of the next feature release.
Now merged, will be part of the next feature release