Page MenuHomePhabricator

Write OAuth auth plugin for Synapse (Matrix)
Closed, ResolvedPublic

Assigned To
Authored By
Tgr
Aug 15 2019, 10:16 AM
Referenced Files
F41442786: Screenshot_20231103-144513.png
Nov 3 2023, 9:52 PM
F41442784: Screenshot_20231103-144503.png
Nov 3 2023, 9:52 PM
F41442782: Screenshot_20231103-144449.png
Nov 3 2023, 9:52 PM
F41442757: Screenshot Capture - 2023-11-03 - 14-41-38.png
Nov 3 2023, 9:52 PM
F35440363: non_ascii_username2.png
Aug 13 2022, 8:24 PM
F35440361: non_ascii_username1.png
Aug 13 2022, 8:24 PM
F35440175: account_disable3.png
Aug 13 2022, 7:09 PM
F35440162: account_disable2.png
Aug 13 2022, 7:09 PM

Description

This would allow using SUL for T230531: Run Matrix trial using the modular.im-hosted instance (and Matrix in general).
Upstream task: matrix-org/synapse#2376 (for OAuth 2)

Event Timeline

I am still working on setting up a development environment for Synapse, but would like to work on this. I also got some help on the

The suggestion was to look at these two pull requests:

OpenID: https://github.com/matrix-org/synapse/pull/765

SAML: https://github.com/matrix-org/synapse/commit/6eecb6e500776bb6b72536458a9529985a819bfd

I now have Riot running in the browser connected to a local matrix instance. In order for people to use OAuth the clients also need a way of initializing the OAuth registrations or login. I will try to integrate that into Riot first.

Riot OAuth ticket: https://github.com/vector-im/riot-web/issues/4610

After a couple hours of figuring out how Riot is stitched together (and how React works) I have a login screen with an OAuth button.

Bildschirmfoto von 2019-11-08 13-03-43.png (381×490 px, 82 KB)

Next is a page where I can configure OAuth providers and their endpoints. When that is complete I will publish the branch somewhere so other people can join in for the server-side (Synapse) patches.

Synapse supports OpenID Connect now. We don't (filed as T254063: OAuth extension should support OpenID Connect), but that also seems like a realistic path towards identity integration with Synapse; maybe a more sustainable one.

(OTOH there might be customization options in a Wikimedia-specific synapse plugin which make it a more valuable approach than the OIDC one.)

Tgr renamed this task from Write OAuth 1.0 auth plugin for Synapse (Matrix) to Write OAuth auth plugin for Synapse (Matrix).Apr 13 2022, 1:18 PM
Tgr updated the task description. (Show Details)

Synapse uses authlib, one of the two major OAuth client frameworks for Python (the other being python-social-auth which already supports MediaWiki: T155945), which is fairly flexible: it supports OpenID Connect proper, but also custom OIDC-like OAuth-based APIs (the Synapse docs have an example for GitHub auth, which is just OAuth + a UserInfo API). The path of the least resistance for us is the latter, as the OAuth MediaWiki extension doesn't support OIDC currently, but like GitHub has an API very similar to the OIDC UserInfo API (see my comments in T254063).

@Tgr Thanks for the assessment. I think I also started from the Github example weeks ago. This config allows me to use the provider and start Synapse:

oidc_providers:
  # Generic example
  - idp_id: Wikipedia
    idp_name: "Connect with Mediawiki"
    idp_icon: "mxc://example.com/mediaid"
    discover: false
    issuer: "https://meta.wikimedia.org"
    client_id: ***********
    client_secret: **********
    client_auth_method: client_secret_post
    scopes: ["profile"]
    authorization_endpoint: "https://meta.wikimedia.org/w/rest.php/oauth2/authorize"
    token_endpoint: "https://meta.wikimedia.org/w/rest.php/oauth2/access_token"
    userinfo_endpoint: "https://meta.wikimedia.org/w/rest.php/oauth2/resource/profile"
    # jwks_uri: "false" # Is not needed if "scopes" does not inlude openid
    skip_verification: true
    user_mapping_provider:
      config:
        subject_claim: "id"
        localpart_template: "{{ user.login }}"
        display_name_template: "{{ user.name }}"
        email_template: "{{ user.email }}"
    attribute_requirements:
      - attribute: userGroup
        value: "synapseUsers"

I am planing to look at the handshake this week with the debugger to see how it works and how we can achieve registering of the user.

p.s.: If anyone wants to join, I have gotten quite good at setting up the dev environment for both Synapse and Element and help setting it up.

I am planing to look at the handshake this week with the debugger to see how it works and how we can achieve registering of the user.

Awesome!

(note for self: the config fields are documented here, and used here; the m object referenced there seems to be authlib.)

Is skip_verification set because you are testing it with a local Synapse instance, or will that be needed in production? Maybe we can fix those issues in the OAuth extension.
(Seems like the verification mostly depends on authlib, so it can be tested without involving Synapse. I can probably test it some time this week.)

subject_claim: "id"
localpart_template: "{{ user.login }}"
display_name_template: "{{ user.name }}"
email_template: "{{ user.email }}"

Are you sure this is correct? I think it should be

localpart_template: "{{ user.username }}"
display_name_template: "{{ user.realname or user.username }}"
email_template: "{{ user.email if user.confirmed_email else None }}"

(subject_claim will default to sub which works), per UserStatementProvider.

All of these fields might be missing in the UserInfo response, not sure how that gets handled. Looking at the mapping provider code, user is an authlib.oidc.core.UserInfo object, and looking at that code, it will ignore email not being present because that's a standard OIDC field, but will throw for the others. So it should probably be {{ user.get('username') }} etc. (and that still leaves the question of whether Synapse can handle getting None for these fields).

Also we should get T283456: OAuth identfy endpoint should not expose unconfirmed email address fixed to avoid having to do manual email verification checks in the long term.

Also, it would be nice to propagate MediaWiki bans, but that too will require some changes to the API code.

p.s.: If anyone wants to join, I have gotten quite good at setting up the dev environment for both Synapse and Element and help setting it up.

It would be great to have it set up on WMCS if you are up to it. I can add you to the projectadmins for the matrix project if you provide your LDAP username (or even better, connect it to your Phabricator account).

Here is a short summary that might enable us to close this task, as Synapse now includes an OAuth / OIDC plugin that seems to work well with Mediawiki:

To test the configuration I only had so set this in homeserver.yaml, as Mediwiki allows OAuth2-Redirects to non-https consumers:

yaml
server_name: "localhost:14123"
public_baseurl: http://localhost:14123

The configuration of the provider in homeserver.yaml should look something like this (some selected parameters are described below):

yaml
oidc_providers:
  - idp_id: Wikipedia
    idp_name: "Connect with Mediawiki"
    idp_icon: "mxc://example.com/mediaid"
    discover: false
    issuer: "https://meta.wikimedia.org"
    client_id: "***"
    client_secret: "***"
    scopes: ["mwoauth-authonly"]
    authorization_endpoint: "https://meta.wikimedia.org/w/rest.php/oauth2/authorize"
    token_endpoint: "https://meta.wikimedia.org/w/rest.php/oauth2/access_token"
    userinfo_endpoint: "https://meta.wikimedia.org/w/rest.php/oauth2/resource/profile"
    skip_verification: false
    user_mapping_provider:
      config:
        localpart_template: "{{ user.username }}"
        display_name_template: "{{ user.username }}"
        email_template: "{{ user.email if user.confirmed_email else None }}"

client_id: This is the ID you receive from the OAuth extension (https://www.mediawiki.org/wiki/Extension:OAuth)
client_secret: This is the secret you receive from the OAuth extension
discover: This was set to false, because Wikimedia OAuth2 does not support OIDC
scopes: The scope "User identity verification only, no ability to read pages or act on a user's behalf." is used. No further permissions are needed.

Scopes

When proposing a consumer, you can choose 3 scopes:

  • User identity verification only, no ability to read pages or act on a user's behalf.
  • User identity verification only with access to real name and email address, no ability to read pages or act on a user's behalf.
  • Request authorization for specific permissions.

The first two options correspond to:

  • mwoauth-authonly
  • mwoauth-authonlyprivate

Registering a user at synapse works well with the 1st option mwoauth-authonly.

OAuth consumer registration

When proposing a consumer the callback-URL has to be set to <public_baseurl>/_synapse/client/oidc/callback . In my case http://localhost:14123/_synapse/client/oidc/callback worked well.

Result

The result is a registered user in Synapse and a redirect to Element (which I used for the front-end):

element_synapse_login.png (1×1 px, 189 KB)

I no other questions about the integration appear I think we can close this tickets and continue in T193961

Great work, thanks @Tobias1984!

A few questions that aren't blockers for using this but should probably be checked eventually:

  • What happens if the user gets renamed?
    • Scenario 1: User A logs into Synapse, gets renamed on the wiki to B, tries to log in again.
    • Scenario 1: User A logs into Synapse, gets renamed on the wiki to B, another user takes the username A and tries to log into Synapse (where A is already reserved for the first user).
  • What happens if the user's account gets hidden? I'm not sure if MediaWiki would refuse authentication entirely, or just return a mostly-empty object from the oauth2/resource/profile API, in which case things like user.username would probably raise a KeyError, and we'd need user.get('username') instead.
  • Will there be a problem with exotic MediaWiki usernames (some or all characters non-ASCII, probably-reserved characters like :, very long username etc)?
  • For non-Wikimedia sites, would realname be preferable over username for display name (assuming the mwoauth-authonlyprivate scope is used)?

Also the sample config should probably be moved to mediawiki.org for discoverability.

Renaming

I finally had time to test renaming. This is what I did:

  1. Created a user "Tobiastesting2"
  2. Logged into Matrix and got the display-name "Tobiastesting2"
  3. Renamed it with a 2nd user to "Tobiastesting3"
  4. Logged into Matrix again

The OAuth flow recognizes that I have an account with the display-name "Tobiastesting2" and allows me to continue:

matrix_log_in_after_rename.png (500×447 px, 30 KB)

The display-name is not changed after renaming the Mediawiki account. The account name also stays as "tobiastesting2" (Note that users are free to change their display-name. Perhaps a bot could be programmed that syncs the Mediawiki username with the display-name on Matrix).

element_shows_display_name.png (283×736 px, 30 KB)

Here is a discussion about renaming an account on the Matrix side: https://github.com/vector-im/element-web/issues/8927

Renaming to an existing account

Renaming a user on Mediawiki to an existing name on Matrix was tested like this:

  1. Created a user "tobiasfirst"
  2. Logged into matrix and got account name: "@tobiasfirst:localhost:14123"
  3. Renamed the user on Mediawiki
  4. Created a 2nd user and renamed it to "tobiasfirst"
  5. After logging into Matrix the user gets the account name: "@tobiasfirst1:localhost:14123"

So Matrix prevents a user from impersonating another user with the same username on Mediawiki. As the Github threads describes the usernames are "immutable and unique".

Disabling

I also tested disabled the account on Matrix. This requires a repeated OAuth verification:

account_disable1.png (708×893 px, 94 KB)

account_disable2.png (452×616 px, 69 KB)

After that the account can't be reactivated by the user. Even if the OAuth access is revoked on Mediawiki and granted again. This is the only thing that the user still sees after granting OAuth:

account_disable3.png (370×539 px, 32 KB)

Non ASCII usernames

To test non-ascii username I registered an account with the username: "മലയാളം"

This shows the following when connecting to Matrix:

non_ascii_username1.png (524×496 px, 32 KB)

The display-name looks fine, but the Matrix account name is quite long:

non_ascii_username2.png (311×903 px, 32 KB)

It would be worth to investigate if Matrix allows having account names with the same characters supported by Mediawiki.

Per https://spec.matrix.org/v1.4/appendices/#user-identifiers:

The user ID ... has the form: @localpart:domain
The localpart of a user ID is an opaque identifier for that user. It MUST NOT be empty, and MUST contain only the characters a-z, 0-9, ., _, =, -, and /.
...
The length of a user ID, including the @ sigil and the domain, MUST NOT exceed 255 characters.

A test homeserver (based on hte configuration from @Tobias1984): https://element.wikimedia.onmatrix.chat/

Some issues apart from what's already mentioned:


Although the registration form can be disabled, Element shows username/password fields on login even if you use OIDC, which makes finding the right button more confusing than it should be:

Screenshot Capture - 2023-11-03 - 14-41-38.png (567×718 px, 142 KB)

The same is true for the mobile app as well.


The process of share encryption keys between devices (needed so you can see on one device the encrypted messages you received on another device) is broken at least on the Android app:

Screenshot_20231103-144449.png (2×1 px, 129 KB)
Screenshot_20231103-144503.png (2×1 px, 93 KB)
Screenshot_20231103-144513.png (2×1 px, 157 KB)

If you tap on the "Verify..." bar, a loading screen comes up and never finishes; if you then click Skip, you get a scary-sounding message (even though it just cancels the stuck verification process).


It would be worth to investigate if Matrix allows having account names with the same characters supported by Mediawiki.

While it doesn't, what we could do is converting to a sensible romanization. The user_mapping_provider configuration field uses some kind of templating (Jinja, I think?) which allows embedded Python snippets.

BTW Matrix is planning to replace its own authentication protocols with OIDC (WIP spec proposal, blog post, status). This is mainly about how the Matrix client and homeserver communicate but I imagine it will improve support for third-party OIDC providers as well.

To test non-ascii username I registered an account with the username: "മലയാളം"
This shows the following when connecting to Matrix: [``]

Yeah this is not great. I think we should at least transliterate such usernames to a reasonable ASCII approximation. Matrix handles username conflicts (it just appends a number to the end of the new account) so I don't see much disadvantage. We couldn't really rely on account names to easily and 100% surely identify users, but we can deal with that by setting enable_set_displayname: false so users can't change their human-readable nickname, and then that could always match their Wikimedia username.

oidc_providers.user_mapping_provider supports a custom Python class handling data conversion so this seems relatively simple.

So Matrix prevents a user from impersonating another user with the same username on Mediawiki.

That is good. If we don't let users change the displayname, we need to update that as well after a rename. Maybe that happens automatically when the displayname in the OIDC userinfo changes?

Synapse allows account data callback plugins for reacting to user account data changes, but I suspect that's about changes in Matrix, not in the OIDC provider.


It would also be nice (though not super important) to automatically ban users who are blocked on-wiki. This seems doable with account validity callbacks.