feat(matrix): add OIDC authentication via Matrix Authentication Service#730
feat(matrix): add OIDC authentication via Matrix Authentication Service#730
Conversation
…ce (#711) Modern Matrix homeservers (including matrix.org since April 2025) use Matrix Authentication Service (MAS) which implements OAuth 2.0/OIDC via MSC3861. Users on these homeservers cannot connect Moltis with password or access_token auth alone. This adds a full two-phase browser-based OIDC flow using matrix-sdk's built-in OAuth API: - MatrixAuthMode enum (password/access_token/oidc) with backward-compat auto-detection when auth_mode is absent - New oidc.rs module: OIDC discovery, dynamic client registration, PKCE authorization code flow, session persistence, token refresh - channels.oauth_start / channels.oauth_complete RPC methods - OAuth callback handler extended with channel OIDC as third fallback - Web UI: OIDC as default auth mode in AddMatrixModal, EditMatrixModal, and onboarding MatrixForm with browser-based flow and polling - Config template, schema validation, and docs updated Closes #711 Entire-Checkpoint: c8dfc16f805a
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThis PR adds OIDC / OAuth 2.0 authentication for Matrix homeservers (MSC3861), targeting the matrix.org migration away from password auth. A two-phase browser-based flow is wired end-to-end: Two remaining issues warrant attention before merge: the Confidence Score: 4/5Safe to merge after addressing the zombie AccountState issue; the onboarding poll cleanup is a minor resource leak. All previous P0/P1 findings (token exposure, blocking I/O, URL encoding, file permissions, panic, polling correctness) have been resolved. Two P2 issues remain: a ghost AccountState entry created by oidc_start that surfaces as a permanently-disconnected account in channels.status when OIDC is abandoned, and a missing unmount cleanup for the OIDC poll interval in onboarding-view.js. The zombie state issue produces visibly wrong UI output on the feature's primary code path, which bumps the score to 4 rather than 5. crates/matrix/src/plugin.rs (oidc_start zombie entry) and crates/web/src/assets/js/onboarding-view.js (missing poll cleanup)
|
| Filename | Overview |
|---|---|
| crates/matrix/src/plugin.rs | Adds oidc_start/oidc_complete methods. oidc_start creates a ghost AccountState entry (bot_user_id="", no sync loop) that persists in the accounts map and appears in channels.status if the OIDC flow is abandoned. |
| crates/matrix/src/oidc.rs | New OIDC module implementing the two-phase MSC3861 flow. Manual Debug impl on PersistedOidcSession properly redacts tokens; write_session_file applies 0o600 permissions via post-write chmod; async file I/O uses tokio::fs throughout. |
| crates/web/src/assets/js/onboarding-view.js | OIDC flow added to onboarding MatrixForm but oidcPollRef is not cleared when the component unmounts (no useEffect cleanup); page-channels.js was fixed via resetForm in onClose but equivalent not applied here. |
| crates/gateway/src/channel.rs | oauth_start and oauth_complete properly implemented; callback URL uses url::Url query_pairs_mut to safely re-encode code and state; channel persisted via store.upsert after successful OIDC completion. |
| crates/web/src/oauth.rs | Three-stage OAuth callback routing (provider_setup → mcp → channel) is structurally correct; completion_params is cloned for each stage; error tuple extended cleanly. |
| crates/web/src/assets/js/page-channels.js | OIDC flow added to AddMatrixModal with correct resetForm cleanup on modal close; payload.auth_url / payload.channels field access is consistent; oidcWaiting signal properly gates the submit button. |
| crates/matrix/src/client.rs | Auth mode dispatch correctly extended for OIDC; handle_refresh_tokens() added to builder for OIDC clients; backward-compatible auto-detection preserved; new tests cover key scenarios. |
| crates/matrix/src/config.rs | MatrixAuthMode enum added with correct serde rename_all=snake_case; auth_mode field is skip_serializing_if Option; RedactedConfig serializer updated; round-trip tests cover all three modes. |
| crates/matrix/src/state.rs | OidcPendingState added as pub(crate) struct; oidc_pending: Mutex<Option> guards real state; lock ordering (outer RwLock then inner Mutex) is consistent throughout plugin.rs. |
| crates/service-traits/src/interfaces.rs | oauth_start/oauth_complete added as default trait methods returning not-supported errors, keeping all other ChannelService impls working without change. |
| crates/web/src/assets/js/channel-utils.js | normalizeMatrixAuthMode, matrixAuthModeGuidance, and validateChannelFields updated correctly for the new oidc mode; OIDC short-circuits credential validation as intended. |
Sequence Diagram
sequenceDiagram
participant UI as Web UI
participant RPC as RPC (channels.*)
participant GW as LiveChannelService
participant MP as MatrixPlugin
participant SDK as matrix-sdk OAuth
participant IDP as Identity Provider
UI->>RPC: channels.oauth_start {account_id, homeserver, redirect_uri}
RPC->>GW: oauth_start(params)
GW->>MP: oidc_start(account_id, config, redirect_uri)
MP->>SDK: build_client() + start_oidc_login()
SDK->>IDP: OIDC discovery + dynamic client registration
IDP-->>SDK: auth_url + PKCE state
Note over MP: AccountState inserted (oidc_pending = Some)
MP-->>GW: {auth_url, state}
GW-->>UI: {auth_url, state}
UI->>IDP: window.open(auth_url)
IDP-->>UI: Redirect to /api/oauth/callback?code=&state=
UI->>GW: GET /api/oauth/callback
GW->>GW: oauth_callback_handler (tries provider → mcp → channel)
GW->>MP: oidc_complete(csrf_state, callback_url)
MP->>SDK: finish_oidc_login(callback_url)
SDK->>IDP: Token exchange (code + PKCE verifier)
IDP-->>SDK: access_token + refresh_token
MP->>MP: save_oidc_session() + spawn_session_persistence_task()
MP->>SDK: sync_once_and_spawn_loop()
MP-->>GW: {ok, account_id, user_id}
GW->>GW: store.upsert(StoredChannel)
GW-->>UI: 200 Authentication successful
loop Every 1s (UI poll)
UI->>RPC: channels.status
RPC-->>UI: payload.channels[connected=true]
UI->>UI: clearInterval + onConnected()
end
Reviews (5): Last reviewed commit: "fix(matrix): remove unused oidc_issuer c..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
- Use secrecy::Secret<String> for access_token and refresh_token in PersistedOidcSession with manual Debug impl that emits [REDACTED] - Replace std::fs blocking I/O with tokio::fs async equivalents in save_oidc_session and load_oidc_session - Use url::Url builder for callback URL construction to prevent parameter injection from unescaped code/state values - Make build_client_metadata return ChannelResult to eliminate panic! in production code path - Store OIDC poll interval in useRef and clear on modal close / form reset to prevent leaked timers on unmount Entire-Checkpoint: ffb8b1be9243
|
@greptile review |
- Fix OIDC polling response field: res.result -> res.payload (matches sendRpc response shape used everywhere else in the codebase) - Restrict OIDC session file permissions to 0o600 on Unix to prevent local credential exposure on shared systems Entire-Checkpoint: 630609f8d7f7
|
@greptile review |
The previous replace_all only caught res.result?.auth_url (optional chaining) but missed the non-optional res.result.auth_url on the window.open line, causing a TypeError at runtime. Entire-Checkpoint: c46461bf916b
|
@greptile review |
The field was documented but never consumed — matrix-sdk auto-discovers the OIDC issuer from the homeserver URL. Remove to avoid dead config. Can be added back if a custom issuer override becomes needed. Entire-Checkpoint: 4daa4040f4c4
|
@greptile review |
Summary
channels.oauth_startreturns an auth URL, the user authenticates in the browser, andchannels.oauth_completeexchanges the code for tokensValidation
Completed
cargo check— full workspace compiles cleanlycargo test -p moltis-matrix— all 107 tests pass (including 12 new tests for OIDC config, auth mode dispatch, session persistence, client metadata)cargo test -p moltis-gateway -p moltis-channels -p moltis-config— all tests passcargo clippyon all modified crates — cleancargo +nightly-2025-11-30 fmt --all -- --check— cleanbiome check --writeon JS files — cleanauth_modefield auto-detect from credentials exactly as beforeRemaining
./scripts/local-validate.sh— full CI validationcrates/web/ui/e2e/specs/channels-matrix.spec.js)Manual QA
https://matrix.org) and click Authenticate with OIDCCloses #711
🤖 Generated with Claude Code