Commit Graph
7943 Commits
Author SHA1 Message Date
Devon HudsonandClaude Opus 5.5 a363eddb60 Rename the guest_invites config section to room_invites
The section only says where to send someone invited to a room, so it is
named after room invites. Nothing is released yet, so no alias for the
old key is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RkxQWj8RAKd7iZ1JQtXdxm
2026-10-13 19:50:33 +00:00
Devon HudsonandClaude Opus 5.5 1c8698dd76 Add an OpenRoom post-auth action
Guest invite links need to land the invitee on the room in their client
once they have registered or signed in. Post-auth actions can only name
MAS's own routes, because `go_next` has no access to config, so
`OpenRoom` goes to a new `GET /open-room`, which redirects to the
configured `client_room_url` with the room ID in it.

The action carries a room ID and never a URL: actions come from query
strings, so a URL in one would be an open redirect. The room ID is
percent-encoded with `urlencoding` rather than as a form value, because
clients decode the fragment with `decodeURIComponent`, which doesn't
turn `+` back into a space.

Without a client URL, or without a room ID that `ruma` parses and that
has no control or bidirectional characters, it goes where a missing
post-auth action goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:30:54 +02:00
Devon HudsonandClaude Opus 5.5 7ba3dea47c Add a guest_invites config section
Holds the settings for inviting guests to rooms by email, starting with
the client URL invitees are sent to once they are signed in. The value
goes on `SiteConfig`, which the admin handlers and the views already
take.

`client_room_url` is a string rather than a URL, because parsing it as
one would percent-encode the braces of a `{room_id}` placeholder in its
path, which would then never be replaced. Whenever it is set, it must
contain `{room_id}` and still parse as an absolute `http` or `https` URL
with a host once the placeholder is filled in, so that `/open-room`
never redirects relative to MAS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:30:54 +02:00
Devon HudsonandClaude Opus 5.5 059bc0bb0f Skip the registration rate limit for invite registrations
The registration rate limit is keyed on the client IP only, three per
hour by default, so a team invited together from behind one NAT could
only accept three invites an hour. An invite registers only the one
username it pins, and is minted by an admin client for one address, so
it can't be used to register in bulk. The email authentication rate
limits still apply to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZdGaj9bJcn5QeeUw8YXoF
2026-10-13 15:30:54 +02:00
Devon HudsonandClaude Opus 5.5 40c316ca29 Hide the upstream providers on an invite registration
A registration token which is passwordless and pins both a username and
an email address is an invite for one person. Its registration form
offered the upstream providers too, so an external invitee could pick
the company's single sign-on by mistake. The form now offers none for
an invite. The sign-in link stays, and the sign-in page keeps its own
providers, for an invitee who already has an account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZdGaj9bJcn5QeeUw8YXoF
2026-10-13 15:30:54 +02:00
Devon HudsonandClaude Opus 5.5 56f193dfbc Compare a registration token's pinned email address ignoring case
The rest of MAS compares email addresses ignoring case. Comparing the pin
exactly made a token pinning `Alice@Example.com` unusable by a registration
whose address came from an upstream provider as `alice@example.com`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpxhUvvbRYpQSxhidPnnP7
2026-10-13 15:30:54 +02:00
Devon HudsonandClaude Opus 5.5 6d30f3be66 Show the errors of pinned registration fields above the form
The registration form doesn't render a username pinned by the invite,
and shows a pinned email address as text, so errors the server reports
on either field weren't shown at all: the form just came back. The
invitee never chose these values, so "This username is already taken"
would mean nothing to them either. Each pinned field with an error now
gets its own message above the form, saying the invite can't be used
and to let whoever sent it know: a policy which refuses the address or
the username would refuse a new invite just the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZdGaj9bJcn5QeeUw8YXoF
2026-10-13 15:30:54 +02:00
Devon HudsonandClaude Opus 5.5 68aac69cbe Explain an invalid invite link when password registration is disabled
With password registration disabled, opening an invite link whose token
is unknown, revoked, expired or used redirected to the upstream
provider or to `/login`, so the invitee never learned that the link was
the problem. The page now stays, with the "invite invalid" alert above
the provider buttons and the sign-in link. A valid token which isn't
passwordless can't register there, so it still redirects.

Posting a token which has become invalid re-renders the page with only
the token's error, instead of validating the rest of the form, which
would tell whether a username exists even with registration closed. The
page submits a token only when it resolved, and the re-rendered form
drops it, so that the next submission is an ordinary registration
rather than the same error.

A blank token is no token, on the link and on the form alike, and
spaces around one are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZdGaj9bJcn5QeeUw8YXoF
2026-10-13 15:30:54 +02:00
Devon HudsonandClaude Opus 5.5 399b7bfe41 Enforce a registration token's pins when attaching it and when finishing
`POST /register` checks that a registration matches the username and
email address its token pins, but the token step could attach any valid
token to a registration afterwards. Registering as anyone, then
attaching a pinned token, registered a username the token doesn't pin.
Swapping a passwordless token for one that isn't left a registration
with no password at all.

The token step now refuses a token that doesn't fit the registration:
one that pins another username or another email address than the
registration's, or one that isn't passwordless for a registration
without a password or an upstream link. Finishing a registration checks
the same against the verified email address, and refuses a passwordless
registration without one. Only a tampered registration gets that far,
so these fail like the other last-minute checks there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZdGaj9bJcn5QeeUw8YXoF
2026-10-13 15:30:54 +02:00
Quentin Gliech 80e3304ef2 Add --username, --email and --passwordless to manage issue-user-registration-token
The three token properties were only reachable through the admin API.
2026-10-13 15:30:54 +02:00
Quentin Gliech fd43ca5ae7 Pre-fill the registration form from an invite link
`/register?token=…` now resolves the invite code server-side and hands what it
was issued for to the form island through a `data-invite` attribute: a pinned
email address is shown as the line announcing where the verification code goes
rather than as an input, a pinned username isn't shown at all, and a
`passwordless` code replaces the password fields with a notice and relabels the
submit button. A code the server refused only reports that, as an error above
the form.

Because a passwordless code is a way to register on its own, the shortcuts
which send the page to the upstream provider or to `/login` when password
registration is disabled are skipped when the link carries one.

The code itself travels back on submission through a hidden field, so a form
which comes back with errors keeps its shape, and so does the redirect the page
sends the user to when the CSRF token has expired.
2026-10-13 15:30:54 +02:00
Quentin Gliech be866612a5 Enforce invite code constraints server-side when registering
The registration form can now carry an invite code, in a hidden `token` field,
and the code is resolved before anything else, so that:

 - an unknown, revoked, expired or exhausted code is rejected with an error on
   the field, and re-rendering the form keeps the code
 - a code which pins a username or an email address only lets that identity
   register, an empty field meaning the user accepted what the code pins
 - a `passwordless` code skips the password entirely and requires an email
   address to verify, even when the server otherwise doesn't, and is a way to
   register on its own when password registration is disabled

The code is also consumed whenever one was used, not only when the server
requires one.
2026-10-13 15:30:54 +02:00
Quentin Gliech 426c3b2a33 Add a passwordless flag to registration tokens
A registration token can now be marked `passwordless`, which will let the
user register without picking a password: their identity is established by
the email verification code instead.

This only adds the column and plumbs it through the storage layer and the
admin API; the registration flow itself is unchanged for now.
2026-10-13 15:30:54 +02:00
Quentin Gliech 887116713e Expose the invite URL and the pinned username and email in the admin API
Registration tokens now report the `/register?token=…` deep link alongside the
`username` and `email` they were issued for, and the create endpoint accepts
both. All the token endpoints now take the `UrlBuilder` so they can build that
link, and `Register` carries the token in its query string.
2026-10-13 15:30:54 +02:00
Quentin Gliech d765af146d Add username and email columns to registration tokens
A registration token can now optionally carry the username and/or the email
address the account it is used for has to be registered with. This only adds
the columns and plumbs them through the storage layer and the CLI; nothing
enforces them yet.
2026-10-13 15:30:54 +02:00
Quentin Gliech 14ba102309 Check the username availability as it is typed in the registration form
Once the username input settles, the field queries `usernameAvailable`
and reports next to it whether the name is available, taken or rejected
by the registration policy. In the two-step layout, continuing is held
back while a settled verdict says the name can't be used. The island now
declares the GraphQL endpoint on its mount node and brings its own query
client.
2026-10-13 15:30:54 +02:00
Quentin Gliech a6d1122b1f Add a usernameAvailable GraphQL query for anonymous lookups
Exposes a new query telling whether a given username is available for
registration, and if not, why: already taken, or rejected by the
registration policy. A username reserved on the homeserver side is
deliberately reported as taken, mirroring the ambiguity of the
registration form, which doesn't tell the two apart either.

Because the query reveals whether a username exists, it is rate-limited
per requester, configurable through the new
`rate_limiting.username_availability` option.
2026-10-13 15:30:54 +02:00
Quentin Gliech 1c68fc986a Render the registration form with React
The server-rendered registration form is replaced by a React island mounted
on `/register`, which renders the CAPTCHA in-page, gives live password
complexity feedback, and lays out the upstream provider buttons and the
sign-in link the way `/login` does. The island owns the whole form, so every
variant of the page is rendered from the mount data alone, and the user fills
it in one step. Server-side validation errors are threaded back into it
through the mount node, so a rejected submission re-renders with the errors
attached to the fields they belong to, until the user edits them.

`RegisterContext` resolves the provider names and the sign-in link server-side
so that the island only has to render them. The sign-in link is derived from
the post-auth action in the query string instead of the `PostAuthContext`
loaded from the database, which drops a round trip from both the GET and the
invalid-form re-render.

Registering now requires JavaScript, including on the variant with upstream
providers only and on the one without a CAPTCHA: the <noscript> block shows a
notice instead of a duplicate form, which could not work anyway when a CAPTCHA
is configured. While the script loads, the page shows a skeleton of the form
to avoid layout shifts, and a hidden error notice is revealed if it fails to
boot. The server-rendered CAPTCHA component and its entrypoint are removed
with their only consumer.
2026-10-13 14:02:33 +02:00
Quentin Gliech 6857910db4 Reserve the space of the password confirmation message
The message appears when the confirmation field loses focus, which shifted the
button below it under the click that moved the focus away.
2026-10-13 14:02:33 +02:00
Quentin Gliech c78d038f85 Move the "or" separator styles into a CSS module
The class becomes `or-separator`, as the global `.separator` also matched the
footer's separator. The module is imported by `templates.css` for the SSR
`field.separator()` macro.
2026-10-13 14:02:33 +02:00
Quentin Gliech bdecd466bb Normalize the username server-side when registering
`POST /register` trims and lowercases the username before either the password
or the upstream provider path looks at it, since Matrix localparts are
lowercase.
2026-10-13 14:02:32 +02:00
Quentin Gliech 740e9551d8 Explain the /register/password redirect and reword a registration test comment 2026-10-13 11:32:18 +02:00
Quentin Gliech 1e58210141 Import the CAPTCHA component only from base.html on the registration page
The page imported it on its own as well, which is what broke the CAPTCHA rendering fixed by #5992 on the old password registration page. Also test that the page renders with a CAPTCHA configured.
2026-10-13 11:32:18 +02:00
Quentin Gliech 128d252dd6 Remove the old password registration template
`password.html` and the two "continue with" labels it needed have no caller
left.
2026-10-13 11:32:18 +02:00
Quentin Gliech ce347e0a7c Register with a password from POST /register
The registration page had two steps: `/register` picked between a password
and an upstream provider, then `/register/password` collected the username,
email and password. Both now live on `/register`, whose `POST` handler runs
the provider branch first and otherwise hands the form to the registration
body, which stays in `password.rs` as a helper.

The page renders the username, email, password and terms-of-service fields,
the captcha and the provider buttons in a single form. The provider buttons
submit that same form, so they are marked `formnovalidate` to skip the
validation of the fields they do not use, and come after the primary submit
button so that Enter still registers.

`/register/password` is gone; a `GET` on it redirects to `/register` with the
query string it was given, so bookmarks and links keep working. Its
`?username=` prefill has no producer left and is dropped.

`GET /register` now renders the page whenever password registration is
enabled, instead of redirecting to `/register/password` when there is no
upstream provider. Without password registration, a single provider still
gets redirected to directly, and no provider at all now redirects to
`/login`, where `/register/password` used to send that case.

An unknown or disabled provider gives a 404 through an inline `GenericError`
rather than the route-wide `RouteError`, and an expired CSRF token on the
password path now sends the user back to `/register` instead of failing the
request.
2026-10-13 11:32:18 +02:00
Quentin Gliech 8ad4fbec17 Absorb the registration form state and the captcha into RegisterContext
The registration page is about to render the password registration form
itself, so its context needs the form state and the captcha configuration
`PasswordRegisterContext` carries today.
2026-10-13 11:32:17 +02:00
Quentin Gliech 6057e83790 Upgrade ulid to 3.0 (#6039) 2026-10-12 13:32:44 +02:00
Quentin Gliech 6b37eb7d74 Upgrade ulid to 3.0
ulid 3 moves to `rand` 0.10, so it no longer pulls the `rand` 0.9
generation. Only `opentelemetry_sdk` and `sentry` still need it.

ULID generation keeps going through `UlidExt`, whose bit layout matches
ulid's own `from_datetime_with_source`. The only API change affecting
MAS is the rename of `Ulid::new` to `Ulid::generate`, which the clippy
ban list now targets.
2026-10-09 16:50:36 +02:00
Quentin Gliech 2d3bf03d63 Upgrade to rand 0.10 and the next RustCrypto generation
`rand` 0.10 and the new RustCrypto generation both build on `rand_core`
0.10, so they have to move together:

 - `rand`, `rand_chacha` and `rand_core` 0.10
 - `digest` 0.11, `sha2` 0.11, `hmac` 0.13, `const-oid` 0.10
 - `elliptic-curve` 0.14, `ecdsa` 0.17, `p256`, `p384` and `k256` 0.14,
   `signature` 3
 - `der`, `spki` and `sec1` 0.8, `pkcs8` 0.11, `pem-rfc7468` 1.0
 - `rsa` 0.10.0-rc.19 and `pkcs1` 0.8.0-rc.5, which have no stable
   release yet. They are pinned exactly, because release candidates
   break their API between releases.
 - `argon2` 0.6 and `pbkdf2` 0.13

On the `rand` side, `RngCore` becomes `Rng` and the old `Rng` becomes
`RngExt`. `gen_*` becomes `random_*`, `thread_rng` becomes `rng`,
`distributions` becomes `distr`, and `from_rng` can no longer fail. On
the crypto side, `mas-jose` and `mas-keystore` move to `BoxedUint` for
RSA, to the `pkcs1` and `pkcs8` reference types, and to the `Generate`
trait for key generation, and `mas-jose` no longer depends on
`generic-array`. Password hashing goes through `hash_password_with_rng`.

Stored data is unaffected. The password hashing snapshots are unchanged,
so bcrypt, Argon2 and PBKDF2 produce the same hashes for the same input
and salt, and `pbkdf2` 0.13 keeps the same defaults (SHA-256, 600 000
rounds). The keystore snapshots change only because keys generated from
a seeded RNG differ. The snapshots of keys loaded from files are
unchanged.

`opa-wasm` 0.3.3 already uses this generation, so most of the duplicate
crates it pulled in go away. The `deny.toml` skip list now names what
still pulls the older versions: cookie, sqlx, headers, wasmtime and pest
for RustCrypto, and opentelemetry_sdk, sentry and ulid for `rand` 0.9.
2026-10-09 15:42:26 +02:00
Quentin Gliech 9f40244eab Bump chacha20poly1305 from 0.10 to 0.11
It moves from `generic-array` to `hybrid-array`, so `Encrypter` builds
its key and nonce with the `Key` and `Nonce` aliases, and `mas-keystore`
no longer depends on `generic-array`. Cookie (via `aes-gcm`) is still on
`aead` 0.5, so the older `aead` and `universal-hash` go on the cargo-deny
skip list.
2026-10-09 15:38:53 +02:00
Quentin Gliech f8b7c5b879 Bump bcrypt from 0.18 to 0.19
The hashes it produces are unchanged. It pulls `blowfish` 0.10, which is
on `cipher` 0.5 while the rest of the tree is on 0.4, so the older
`cipher` and `inout` go on the cargo-deny skip list.
2026-10-09 15:38:53 +02:00
Quentin Gliech 9659701f7a Bump rand from 0.8.5 to 0.8.6 to address RUSTSEC-2026-0097
0.8.6 is the patched release for the `rand::thread_rng` unsoundness with
custom loggers, so the advisory ignore is no longer needed.
2026-10-09 15:38:52 +02:00
Quentin Gliech 186c592d61 Allow the duplicate crates pulled in by opa-wasm 0.3 and the icu bump
The `cargo deny` bans check fails on main because two dependency bumps
introduced duplicate crate versions:

 - `opa-wasm` 0.3.3 (#6029) uses `rand` 0.10, `base64` 0.23 and the
   `digest` 0.11 generation of the RustCrypto crates, while the rest of
   the tree is still on `rand` 0.9, `base64` 0.22 and `digest` 0.10.
 - The icu bump (#6015) pulls `zerovec-derive` 0.11.6, which uses `syn` 3,
   while everything else is on `syn` 2.

Add the older versions to the skip list until the rest of the tree
catches up.
2026-10-09 15:26:05 +02:00
Olivier 'reivilibre 2277a37c59 Remove the "Application Services login" warning page (#5962) 2026-10-09 11:18:08 +01:00
Eric Eastwood 1968992f16 Automatic merge back to main (#6031) 2026-10-06 11:44:51 -05:00
github-actions[bot] 912bcf2192 1.27.0-rc.0 v1.27.0-rc.0 2026-10-06 15:50:28 +00:00
Eric Eastwood 5a78ee96cd Translations updates for main (#6030) 2026-10-06 10:49:04 -05:00
github-actions[bot] 15e27bed51 Translations updates 2026-10-06 15:13:57 +00:00
Andrew Morgan eb834276cb Bump opa-wasm from 0.2.0 to 0.3.3, wasmtime from 43.0.2 to 49.0.2 (#6029) 2026-10-06 16:02:50 +01:00
Andrew Morgan f2006bb30c Bump wasmtime from 43.0.2 to 49.0.2
This silences
https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-32h6-97mm-8q3c,
even though MAS was not affected (as the `component-model-async` feature
was not enabled).

The command used to update was:

```
cargo update -p wasmtime --precise 49.0.2
```
2026-10-06 14:55:22 +01:00
Andrew Morgan e783c76030 Bump opa-wasm from 0.2.0 to 0.3.3 2026-10-06 14:41:22 +01:00
Olivier 'reivilibre 357cd68ad5 Allow password reset while signed in (#5985)
Users can have an ambient login whilst not knowing their password.

This is also an easily-hit path when upgrading an SSO account to have a password.
2026-10-06 11:04:19 +01:00
Olivier 'reivilibre 6cb3a49016 Properly limit number of jobs that can be fetched (#6006) 2026-10-05 16:39:03 +01:00
Olivier 'reivilibre 99ee247654 build(deps-dev): bump storybook from 10.5.3 to 10.6.0 in the storybook group across 1 directory (#6017) 2026-10-05 16:37:31 +01:00
dependabot[bot] c8fb006cfa build(deps-dev): bump storybook
Bumps the storybook group with 1 update in the / directory: [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core).


Updates `storybook` from 10.5.3 to 10.6.0
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.6.0/code/core)

---
updated-dependencies:
- dependency-name: storybook
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-05 11:44:30 +00:00
Olivier 'reivilibre 6ad02e32c3 build(deps-dev): bump the vite group across 1 directory with 2 updates (#6019) 2026-10-05 12:38:57 +01:00
dependabot[bot] d5a61a46e1 build(deps-dev): bump the vite group across 1 directory with 2 updates
Bumps the vite group with 2 updates in the / directory: [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `@vitejs/plugin-react` from 6.0.4 to 6.1.1
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.1/packages/plugin-react)

Updates `vite` from 8.1.5 to 8.3.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.3.0/packages/vite)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: vite
- dependency-name: vite
  dependency-version: 8.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: vite
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-05 11:14:48 +00:00
Olivier 'reivilibre 57f1314889 build(deps): bump the fontsource group across 1 directory with 2 updates (#6018) 2026-10-05 12:10:33 +01:00
Olivier 'reivilibre 83e8d41dab build(deps): bump the i18next group across 1 directory with 3 updates (#6020) 2026-10-05 12:10:03 +01:00
dependabot[bot] ab359fd4cf build(deps): bump the fontsource group across 1 directory with 2 updates
Bumps the fontsource group with 2 updates in the / directory: [@fontsource/inconsolata](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/inconsolata) and [@fontsource/inter](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/inter).


Updates `@fontsource/inconsolata` from 5.2.8 to 5.3.0
- [Changelog](https://github.com/fontsource/font-files/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/inconsolata)

Updates `@fontsource/inter` from 5.2.8 to 5.3.0
- [Changelog](https://github.com/fontsource/font-files/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/inter)

---
updated-dependencies:
- dependency-name: "@fontsource/inconsolata"
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: fontsource
- dependency-name: "@fontsource/inter"
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: fontsource
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-05 10:18:47 +00:00