Skip to content

Add support for OAuth 2.0 (next-gen auth) login - #16

Open
Deniel9204 wants to merge 1 commit into
mautrix:mainfrom
Deniel9204:feat/oauth-login
Open

Deniel9204 wants to merge 1 commit into
mautrix:mainfrom
Deniel9204:feat/oauth-login

Conversation

@Deniel9204

@Deniel9204 Deniel9204 commented Aug 29, 2026

Copy link
Copy Markdown

Problem

Homeservers using next-generation authentication (MSC3861) disable interactive login. On such a server — e.g. Continuwuity with oauth.compatibility_mode = exclusive — the client-server API responds:

GET /_matrix/client/v3/login
→ {"errcode":"M_UNRECOGNIZED","error":"User-interactive authentication is not available on this server."}

The login screen is driven entirely by /v3/login, so it renders neither the password form nor the SSO button, and there is no way to log in. Everything the client needs is instead advertised at /_matrix/client/v1/auth_metadata.

Change

When getLoginFlows() fails, the login screen falls back to GET /v1/auth_metadata. If the auth server advertises the device authorization grant, it offers an OAuth login button.

The device-code grant (RFC 8628) is used deliberately: it needs no redirect URI, so it reuses the existing openInBrowser + doClientHTTP plumbing and requires no changes to the custom-protocol handling. The flow:

  1. Dynamically register a public native client at registration_endpoint (RFC 7591); the client_id is cached in localStorage per issuer.
  2. Request a device code (device_authorization_endpoint) with scope urn:matrix:client:api:* urn:matrix:client:device:<id>.
  3. Open the verification URL in the browser and show the user code.
  4. Poll token_endpoint (handling authorization_pending / slow_down) until approval.
  5. Use the returned access token as the Matrix access token; /whoami resolves the user and device ID.

All OAuth requests to the auth server go through the main-process doClientHTTP (the endpoints live on the auth server's origin, not the homeserver's). The legacy password/SSO paths are untouched — the OAuth UI only appears when interactive login is unavailable.

Files: src/api/oauth.ts (new), src/api/matrixclient.ts (getAuthMetadata), src/app/MatrixLogin.tsx, src/types/matrix.ts, src/app/MatrixLogin.css.

Testing

  • npm run lint (tsc --noEmit && eslint) passes clean.
  • The three network steps were validated against a live Continuwuity server in OAuth-exclusive mode:
    • Client registration201, the client metadata above accepted verbatim.
    • Device authorization200, the stable urn:matrix:client:api:* scope accepted; device/user codes returned.
    • Token poll400 {"error":"authorization_pending"} — the exact shape the poll loop handles.
  • Not yet exercised: the interactive approval + final token issuance (needs a real browser sign-in), and the flow against a full Electron build. The renderer-side wiring is typechecked but not click-tested.

One environment note from that server: its device-authorization response omits interval (defaults to 5s per RFC 8628) and returns a short expires_in (60s); both are handled.

Homeservers using next-gen authentication (MSC3861), such as Continuwuity
in OAuth-exclusive mode, disable interactive login entirely: /v3/login
returns M_UNRECOGNIZED and only /v1/auth_metadata is available. The login
screen had no way to authenticate against those servers.

When getLoginFlows fails, the login screen now falls back to fetching
/v1/auth_metadata and, if the auth server supports the device
authorization grant, offers an OAuth login. The device-code grant is used
so no custom redirect handling is needed: the app dynamically registers a
public client, requests a device code, opens the verification URL in the
browser, and polls the token endpoint until the user approves. The
resulting access token is used as the Matrix access token.
@Deniel9204

Copy link
Copy Markdown
Author

Validated the network side against a live Continuwuity (OAuth-exclusive) instance: dynamic registration → 201, device authorization with the stable urn:matrix:client:api:* scope → 200, and the token poll returns authorization_pending in the handled shape. npm run lint is clean. The interactive approval + final token exchange and a full Electron build are still to be click-tested — happy to adjust anything you'd prefer (e.g. auth-code+PKCE instead of device-code).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant