Skip to content

feat: OIDC account management page (MSC2965) + UIA bypass for SSO/OIDC users#407

Open
shaba wants to merge 3 commits intomatrix-construct:mainfrom
shaba:oidc-account-management
Open

feat: OIDC account management page (MSC2965) + UIA bypass for SSO/OIDC users#407
shaba wants to merge 3 commits intomatrix-construct:mainfrom
shaba:oidc-account-management

Conversation

@shaba
Copy link
Copy Markdown

@shaba shaba commented Apr 6, 2026

Summary

This PR contains three changes related to native OIDC authentication:

1. fix: url-encode idp_id in OIDC authorize SSO redirect path

Identity provider IDs may contain characters invalid in URL path segments
(e.g. colons, slashes). The idp_id is now percent-encoded before being
interpolated into /_matrix/client/v3/login/sso/redirect/{idp_id}.

A url_encode helper implementing the RFC 3986 unreserved character set
is added to src/api/oidc/mod.rs.

2. feat: self-hosted OIDC account management page (MSC2965)

Replaces the simple IdP redirect in account_route with a fully
server-hosted account management UI, so account_management_uri in the
OIDC discovery document points to a functional page.

New endpoints:

Route Description
GET /_tuwunel/oidc/account Dispatches via SSO to the requested action
GET /_tuwunel/oidc/account_callback Renders the action page after SSO authentication
POST /_tuwunel/oidc/account_callback Executes the confirmed action
GET /_tuwunel/oidc/account.js Inline JS for client-side timestamp localisation
GET /_tuwunel/oidc/account.css Shared stylesheet (enables style-src 'self' in CSP)

Supported actions (advertised in account_management_actions_supported):

  • org.matrix.sessions_list — list all active sessions, sorted newest-first
  • org.matrix.session_view — view details of a single session
  • org.matrix.session_end — sign out a session (CSRF-safe POST confirmation)
  • org.matrix.profile — view and edit display name

Security notes:

  • The global CSP has form-action 'none' and sandbox. Account pages set a
    per-route CSP (form-action 'self', no sandbox, style-src 'self') that
    takes precedence via SetResponseHeaderLayer::if_not_present.
  • Session deletion requires a POST with the original SSO login token (CSRF
    protection). GET handlers peek at the token without consuming it so it can
    be embedded in the confirmation form; the token is consumed once on POST.
    This avoids creating a secondary token on every GET and prevents accumulation
    of orphaned tokens if the user navigates back.
  • A peek_login_token() method is added to the users service alongside the
    existing destructive find_from_login_token().
  • All user-supplied strings in HTML output are passed through html_escape().

3. fix: bypass UIA for SSO/OIDC users without a local password

SSO and native OIDC users are created with PASSWORD_SENTINEL ("*") as
their password hash. has_password() returns false for them, so no UIA
flows were advertised, and Matrix clients (e.g. FluffyChat) showed
"no permissions" when attempting to delete devices via the standard Matrix API.

Per MSC3861,
possession of a valid OIDC access token is itself sufficient proof of identity.
auth_uiaa() now returns early when has_password is false: the access token
has already been verified by the middleware, and there is no additional
credential to present. This covers both traditional SSO users and native OIDC
users (who may not have an OAuth session entry from exists_for_user()).

Test plan

  • Open /_tuwunel/oidc/account in a browser — redirects through SSO and shows the sessions list
  • Verify sessions are sorted newest-first
  • View a session, sign it out (confirm → POST → success page)
  • Edit display name via the Profile page
  • Verify Content-Security-Policy header on account pages allows form submission
  • Confirm that a Matrix client (e.g. FluffyChat) can delete devices without "no permissions" error for SSO/OIDC users
  • Confirm that IDP IDs with special characters (e.g. containing :) do not break the SSO redirect

🤖 Generated with Claude Code

shaba and others added 3 commits April 6, 2026 16:04
Identity provider IDs may contain characters that are invalid in URL
path segments (e.g. colons, slashes). Percent-encode the idp_id before
interpolating it into the SSO redirect URL to avoid a 404 or broken
redirect when the IDP ID is not a plain alphanumeric string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the simple IdP redirect in account_route with a self-hosted
account management UI:

- GET /_tuwunel/oidc/account: redirects through SSO to authenticate the
  user, then dispatches to the requested action page.
- GET/POST /_tuwunel/oidc/account_callback: handles the login-token
  callback and renders HTML pages for sessions list, session detail,
  session sign-out (with CSRF-safe POST confirmation), and profile
  editing.
- GET /_tuwunel/oidc/account.js: tiny inline JS to localise timestamps
  client-side; served separately for CSP compatibility.
- GET /_tuwunel/oidc/account.css: shared stylesheet for all pages,
  enabling style-src 'self' in the Content-Security-Policy.

For actions that require a POST confirmation (session_end, profile),
the GET handler peeks at the SSO login token without consuming it and
embeds it directly in the form. The token is consumed once on POST,
so no secondary token is ever created and repeated GETs do not
accumulate orphaned tokens.

Per-route CSP overrides the global form-action 'none' and sandbox
directives so HTML forms can submit, using style-src 'self' instead
of 'unsafe-inline' (styles are in account.css).

Advertises org.matrix.sessions_list, org.matrix.session_view,
org.matrix.session_end, and org.matrix.profile actions in the OIDC
discovery document (account_management_actions_supported).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SSO and native OIDC users are created with PASSWORD_SENTINEL ("*") as
their password hash. has_password() returns false for them, so no UIA
flows were advertised, and Matrix clients (e.g. FluffyChat) showed
"no permissions" when trying to delete devices via the direct Matrix API.

Per MSC3861 the possession of a valid OIDC access token is itself
sufficient proof of identity, so UIA should not be required for these
users. Short-circuit auth_uiaa() early when has_password is false:
the caller has already verified the access token, and there is no
additional credential to present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jevolk jevolk closed this Apr 6, 2026
@jevolk jevolk reopened this Apr 6, 2026
@jevolk
Copy link
Copy Markdown
Member

jevolk commented Apr 6, 2026

Sorry, I didn't mean to hit close. I think we just have to decide whether to go ahead with 1.6.0 or delay until we can work this in. The delay will be significant and I'm leaning toward going ahead and working this into the main branch afterward unless there's reason otherwise.

@shaba
Copy link
Copy Markdown
Author

shaba commented Apr 6, 2026

No worries, please go ahead with 1.6.0 — this can be worked in afterward without any rush.

For context: all three changes in this PR have been running in production on our ALT Linux packaging of Tuwunel for a few days without issues. The UIA bypass in particular fixed a real user-facing problem (FluffyChat showing "no permissions" when deleting devices for OIDC users), and the account management page has been tested with both Fractal and FluffyChat.

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.

2 participants