Description:
When using Nextcloud as an external identity provider, its JSON response from the userinfo endpoint is as follows:
{
"ocs":{
"meta":{
"status":"ok",
"statuscode":200,
"message":"OK"
},
"data":{
"enabled":true,
"storageLocation":"\\/var\\/www\\/nextcloud\\/data\\/[email protected]",
"id":"[email protected]",
"lastLogin":1602147919000,
"backend":"Database",
"subadmin":[],
"quota":{"free":78983929856,"used":17659078,"total":79001588934,"relative":0.02,"quota":-3},
"email":null,
"phone":"",
"address":"",
"website":"",
"twitter":"",
"groups":["admin"],
"language":"en",
"locale":"",
"backendCapabilities":{"setDisplayName":true,"setPassword":true},
"display-name":"[email protected]"
}
}
}
Which obviously isn't mapped properly.
I've manually hacked the OIDC mapping provider so it brings in the id, but it'd be great if someone who knew what they were doing on the Synapse side could make this connection work properly!
I've manually hacked the OIDC mapping provider so it brings in the id
What were the changes you had to make? This is one of the reasons that the mapping provider is pluggable, but if this is a standard setup we should potentially support it.
< return userinfo[self._config.subject_claim]
---
> return userinfo['ocs']['data']['id']
< localpart = self._config.localpart_template.render(user=userinfo).strip()
---
> localpart = userinfo['ocs']['data']['id']
I've no doubt I'm doing it wrong, but I tried all sorts of combinations for subject_claim in homeserver.yaml first:
homeserver.yaml:
oidc_config:
enabled: true
discover: false
issuer: "https://<>/"
client_id: "<>"
client_secret: "<>"
authorization_endpoint: "https://<>/apps/oauth2/authorize"
token_endpoint: "https://<>/apps/oauth2/api/v1/token"
userinfo_endpoint: "https://<>/ocs/v2.php/cloud/user?format=json"
skip_verification: false
scopes: ["nc doesn't implement"]
user_mapping_provider:
config:
subject_claim: "ocs, data, display-name"
localpart_template: "{{ user.login }}"
display_name_template: "{{ user.name }}"
It seems like their response includes stuff that is normally only sent in the HTTP header and the "real" data is under the data key. This seems to match their API docs, but only for that endpoint weirdly.
I checked around briefly but don't see anything about compatibility issues between Nextcloud and authlib (which is the underlying Python library we use for a lot of the OpenID code), so it might just be specific to how we're using the userinfo.
It could be reasonable to let the subject_claim take a dotted path?
I think you can set localpart_template: "{{ ocs.data.id }}", but I'm not 100% sure about that.
Alternately you can provide a completely separate mapping provider (something like) which knows how to deal with this nesting. See the oidc_config.user_mapping_provider.module setting in your config, this allows you to point it to any python object.
It could be reasonable to let the
subject_claimtake a dotted path?
That would be ace, and the "logical" approach for me - I'd tried that approach initially (having read that it was just using it as an array key), but it was just throwing me errors, so I hardcoded to prove to myself I wasn't going mad!
I think you can set
localpart_template: "{{ ocs.data.id }}", but I'm not 100% sure about that.
Sadly not, I tried that too
Oops! Something went wrong during authentication.
Try logging in again from your Matrix client and if the problem persists please contact the server's administrator.
Error: mapping_error
Could not extract user attributes from OIDC response: 'ocs' is undefined
Alternately you can provide a completely separate mapping provider (something like) which knows how to deal with this nesting. See the
oidc_config.user_mapping_provider.modulesetting in your config, this allows you to point it to any python object.
I've not written any python in a decade - can you point me to something to get me started, if I end up having to do this way?
Alternately you can provide a completely separate mapping provider (something like) which knows how to deal with this nesting. See the
oidc_config.user_mapping_provider.modulesetting in your config, this allows you to point it to any python object.I've not written any python in a decade - can you point me to something to get me started, if I end up having to do this way?
There's some (light) documentation, and then taking a look at the current implementation is probably useful:
https://github.com/matrix-org/synapse/blob/v1.20.1/synapse/handlers/oidc_handler.py#L1002-L1061
If you don't plain to make it flexible a lot of that code could go and you can essentially take your patches from above and just plop them in.
Thanks for your help, @clokep
Given the response from that endpoint isn't going to be changing any time soon, I'll write a mapper. Is it likely a pull request would be merged upstream? I may as well share the result if I'm going to the effort of dusting off my python to build it 馃槉
Thanks for your help, @clokep
No problem!
Given the response from that endpoint isn't going to be changing any time soon, I'll write a mapper. Is it likely a pull request would be merged upstream? I may as well share the result if I'm going to the effort of dusting off my python to build it 馃槉
I suspect that in core it might make more sense to add a configuration option which (given a response) lets you pull out the user info from it. I'm envisioning something like: path_to_userinfo: "ocs.data", while the default would be "empty" (meaning the userinfo is the root object returned)? This could be done as part of the mapping provider or outside of it, I haven't fully thought through pros/cons of that one. The change would probably be somewhere around https://github.com/matrix-org/synapse/blob/9789b1fba541a5ae01b946770416729e5b7e5b7e/synapse/handlers/oidc_handler.py#L440-L445?
Feel free to join #synapse-dev:matrix.org if you have any questions!
Thanks.
Obviously I've no real knowledge of the internals of the project (other than the FAQs, the sledgehammer hack-job I did of reverse-engineering the oidc mapper, & the bits of the API docs I've skimmed today) but I can't see a con in enabling the ability to change the root of the returned JSON to the bit which actually follows standards, other than it stops requiring e.g. Nextcloud from properly implementing the spec (and it doesn't look like they'll change their minds any time soon).
The pro is the ability for SSO from a Nextcloud/Owncloud/any other non-compliant backend 馃槉
Our use case is Element Web in Nextcloud doing SSO to anyone who can log into the instance.
Looks like authlib already supports this - could you expose it somehow?
https://docs.authlib.org/en/latest/client/oauth2.html#compliance-fix-oauth2
Looks like
authlibalready supports this - could you expose it somehow?docs.authlib.org/en/latest/client/oauth2.html#compliance-fix-oauth2
Good find! 馃憤 That's probably a more flexible way of handling this, although it doesn't seem that we actually use OAuth2Session (this is because Synapse uses Twisted to do the I/O here). I think adding similar hooks would be a reasonable way to add this support. To do that I would:
synapse.config.oidc_config) and ensure the function is importable (I think we have helpers to do this already, it would be similar to how oidc_config.user_mapping_provider.module is handled).synapse.handlers.oidc_handler if they're configured.The response function would be similar, but not identical since it would be receiving a Twisted response. Overall it should be pretty similar though.
Here is a simple Nextcloud OIDC mapping provider that is working on my nextcloud + synapse setup:
from synapse.handlers.oidc_handler import OidcMappingProvider
class NextcloudOidcMappingProvider(OidcMappingProvider):
def __init__(self, config):
self._config = config
@staticmethod
def parse_config(config):
return {}
def get_remote_user_id(self, userinfo):
return userinfo["ocs"]["data"]["id"]
async def map_user_attributes(self, userinfo, token):
localpart = userinfo["ocs"]["data"]["id"]
display_name = userinfo["ocs"]["data"]["display-name"]
return {"localpart": localpart, "display_name": display_name}
async def get_extra_attributes(self, userinfo, token):
extras = {}
return extras
I just dropped it into the site-packages dir on my python path, and use it with a config like so:
password_config:
enabled: false
sso:
client_whitelist:
- https://element.example.com/
- https://app.element.io/
oidc_config:
enabled: true
client_id: "changeme"
client_secret: "changeme"
scopes: ["profile", "email"]
issuer: "https://example.com"
discover: false
authorization_endpoint: "https://example.com/index.php/apps/oauth2/authorize"
userinfo_endpoint: "https://example.com/ocs/v2.php/cloud/user?format=json"
token_endpoint: "https://example.com/index.php/apps/oauth2/api/v1/token"
user_mapping_provider:
module: nextcloud_oicd_mapping_provider.NextcloudOidcMappingProvider
config: {}
Most helpful comment
Here is a simple Nextcloud OIDC mapping provider that is working on my nextcloud + synapse setup:
I just dropped it into the
site-packagesdir on my python path, and use it with a config like so: