Skip to content

feat(matrix): add OIDC authentication via Matrix Authentication Service#730

Open
penso wants to merge 5 commits intomainfrom
fork-piper
Open

feat(matrix): add OIDC authentication via Matrix Authentication Service#730
penso wants to merge 5 commits intomainfrom
fork-piper

Conversation

@penso
Copy link
Copy Markdown
Collaborator

@penso penso commented Apr 15, 2026

Summary

  • Adds OAuth 2.0 / OIDC authentication for Matrix homeservers using Matrix Authentication Service (MSC3861), resolving the blocker for modern homeservers like matrix.org (which dropped password auth in April 2025)
  • Implements a two-phase browser-based flow: channels.oauth_start returns an auth URL, the user authenticates in the browser, and channels.oauth_complete exchanges the code for tokens
  • OIDC is now the default auth mode in the web UI, with password and access token still available for older homeservers

Validation

Completed

  • cargo check — full workspace compiles cleanly
  • cargo 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 pass
  • cargo clippy on all modified crates — clean
  • cargo +nightly-2025-11-30 fmt --all -- --check — clean
  • biome check --write on JS files — clean
  • Backward compatibility: configs without auth_mode field auto-detect from credentials exactly as before

Remaining

  • ./scripts/local-validate.sh — full CI validation
  • Manual QA: start Moltis, go to Channels, add Matrix with OIDC, verify browser opens auth page
  • E2E Playwright tests for OIDC UI flow (crates/web/ui/e2e/specs/channels-matrix.spec.js)

Manual QA

  1. Start Moltis and open the web UI
  2. Go to Settings → Channels and click Connect Matrix
  3. Verify OIDC is the default authentication mode
  4. Enter a homeserver URL (e.g. https://matrix.org) and click Authenticate with OIDC
  5. Verify a browser window opens to the homeserver's identity provider
  6. Complete authentication in the browser
  7. Verify the channel appears as connected in the channel list
  8. Restart Moltis and verify the OIDC session is restored automatically
  9. Verify password and access token modes still work (regression check)

Closes #711

🤖 Generated with Claude Code

…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
@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Apr 15, 2026

Merging this PR will not alter performance

✅ 39 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing fork-piper (bbbad83) with main (be0964e)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 15, 2026

Greptile Summary

This 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: channels.oauth_start builds an authorization URL via the matrix-sdk's built-in PKCE/dynamic-registration support, and channels.oauth_complete is triggered by the OAuth callback handler in oauth.rs. Previous round of review feedback (token exposure, blocking I/O, URL-encoding, file permissions, panic, polling bugs) has been properly addressed.

Two remaining issues warrant attention before merge: the MatrixForm OIDC poll in onboarding-view.js has no unmount cleanup (the page-channels.js version was fixed via resetForm/onClose but the equivalent was not applied to the onboarding path), and oidc_start inserts a live AccountState entry into the accounts map that persists if the user abandons the OIDC flow — causing the phantom account to appear as "disconnected" in every channels.status response until the server restarts.

Confidence Score: 4/5

Safe 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)

Important Files Changed

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
Loading

Reviews (5): Last reviewed commit: "fix(matrix): remove unused oidc_issuer c..." | Re-trigger Greptile

Comment thread crates/matrix/src/oidc.rs
Comment thread crates/matrix/src/oidc.rs
Comment thread crates/gateway/src/channel.rs Outdated
Comment thread crates/web/src/assets/js/page-channels.js
Comment thread crates/matrix/src/oidc.rs Outdated
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 15, 2026

- 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
@penso
Copy link
Copy Markdown
Collaborator Author

penso commented Apr 15, 2026

@greptile review

Comment thread crates/web/src/assets/js/page-channels.js Outdated
Comment thread crates/matrix/src/oidc.rs
- 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
@penso
Copy link
Copy Markdown
Collaborator Author

penso commented Apr 15, 2026

@greptile review

Comment thread crates/web/src/assets/js/page-channels.js Outdated
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
@penso
Copy link
Copy Markdown
Collaborator Author

penso commented Apr 15, 2026

@greptile review

Comment thread crates/matrix/src/config.rs Outdated
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
@penso
Copy link
Copy Markdown
Collaborator Author

penso commented Apr 15, 2026

@greptile review

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: OpenID authentication in matrix

1 participant