Moving off cookie-based authentication surfaced several hard
issues, especially around loading virtual backgrounds: requests
used to be sent with cookies automatically, which trivially
authenticated those loads. With bearer tokens, those requests need
to be authenticated explicitly.
The situation is made harder by the fact that, when the custom
virtual background was introduced, some of the loading was done as
module-level, blocking imports that are not handled by React and
therefore live outside the normal auth flow.
Ship a functional patch to unblock third parties currently waiting
on this integration. The virtual background loading path should
definitely be refactored and simplified in a follow-up.
Wire the frontend to the backend's token exchange flow: when the
expected URL fragment is present, gate the app loading on exchanging
that fragment for a proper access token, which is then used to
interact with the API.
When no such fragment is present, the code path is a no-op and
should have minimal impact on load performance.
We now use `Authorization: Bearer <token>` to authenticate users
from the token exchange flow (used for iframe embeds).
Until now, the `Bearer` scheme was also reused for the alternative
LiveKit authentication, where a client presents its LiveKit token
issued by the backend to prove room membership on actions open to
any room participant. Sharing the scheme between the two flows is
not viable anymore.
Switch the LiveKit token authentication to a dedicated
`Authorization` scheme, so `Bearer` stays reserved for the iframe /
token-exchange flow.
Follow-up: a broader effort should look into harmonizing and
hardening the backend authentication stack of the app.
Some integrators render our videoconference inside an iframe, where
our cookie-based authentication does not work: our cookies are
SameSite=Lax/Strict, so the iframe drops them.
We looked at what Jitsi offers: a shared secret used to sign JWTs
that authenticate users coming from external services. Since we
already expose an external API where third parties authenticate as
a given user, it was simpler for us to add an exchange mechanism on
top of that.
Flow:
* Through the external API, mint a short-lived, single-use exchange
code for a user.
* The third party hands that code to the frontend as a URL fragment.
* The frontend exchanges the code for a longer-lived JWT that can be
used to query the regular API viewsets.
Known limitations and follow-ups:
* At some point it would be nice to shorten the JWT lifetime and
add a refresh mechanism. This will be handled in a follow-up PR
when actually needed.
* CSP rules to control which origins are allowed to embed the app
in an iframe still need to be added.
* This alternative authentication cannot easily be scoped to a
subset of endpoints without adding a lot of complexity, so it is
accepted globally on the API for now.
The client id and client secret are auto-generated with high entropy
when an application is created. Allowing them to be edited from the
Django admin let any administrator replace them with a short or
weak value, undermining that guarantee.
Make both fields read-only in the admin so they can only be
regenerated through the intended flow.
Application token authentication currently uses Django's default
password hasher. On our production hardware, token requests take
at least ~500 ms. Authentication happens per user of each
application, so this cost accumulates across frequently used
integrations.
Password hashers deliberately make guessing expensive to protect
human-chosen passwords after a database leak. Our application
secrets are generated server-side using a cryptographically secure
random generator, with a default length of 128 alphanumeric
characters. Guessing these secrets is already computationally
infeasible, making password stretching an unnecessary CPU cost.
Use salted SHA-256 for application secrets while retaining
constant-time comparison, and keep user password hashing
unchanged. Existing secrets migrate after successful verification
without requiring key rotation. Conditional updates prevent
migration from overwriting a concurrent rotation.
Store the new hash in `client_secret_sha256` while preserving the
original Django hash in `client_secret`. New applications populate
both fields, allowing the previous release to authenticate both
existing and newly created applications if we need to roll back.
Once every application has migrated and the rollback window has
closed, complete the migration by removing the legacy verification
code and `client_secret` field.
This assumes securely generated, high-entropy secrets. Deployments
that reduce `APPLICATION_CLIENT_SECRET_LENGTH` or supply
predictable secrets lose the offline guessing protection that the
previous slow hasher provided.
Explain the switch to django-lasuite marketing: removed BREVO_*
and MARKETING_SERVICE_CLASS settings, new LASUITE_MARKETING_BACKEND
and LASUITE_MARKETING_PARAMETERS variables, dummy backend default,
and the now-required Celery worker.
The `Unreleased` section of `UPGRADE.md` was not bumped when v1.33
was released, so follow-up changes landed under the wrong heading.
Reorganize the file so v1.33 is properly closed and a fresh
`Unreleased` section is opened on top.
`make run` no longer starts the metadata collector and the
multi-user transcriber. On my machine, this saves about 400 MB
of RAM and almost 2% CPU, out of the 11% the stack uses.
Start them with `make run-agents` when needed.
Disable `METADATA_COLLECTOR_ENABLED` by default in `common.dist` so
the backend does not dispatch jobs to an agent that is not running,
and document how to enable each agent in `developping_locally.md`.
The summary settings already default to the `redis` host, so
`redis-summary` was unused. Remove it and depend on `redis`.
Pin the summary Celery and task tracker URLs to DB 2 in
`summary.dist` to isolate them from LiveKit and the backend
Celery broker (DB 0) and the Django cache (DB 1).
Use the shared helm `dev-backend` chart to deploy the backend in
the Tilt dev stack, instead of maintaining our own copy.
The goal is to share more common pieces of the dev experience and
dev stack across projects, so we do not maintain N different
versions of similar setups.
The only adjustment needed was to backport Garage into the shared
chart.
Originally started by @rouja.
Improves PaginationControl: clearer structure, keyboard nav, a11y improved.
The main room and the picture-in-picture window share this control.
Ctrl+Shift+G focuses the pagination.
By default, `actions/checkout` saves the job's auth token
(`GITHUB_TOKEN` or the provided PAT) in the local git config so
later steps can run authenticated git commands. That token then
stays on disk for the rest of the job, where it can leak:
* If an artifact upload includes the checkout directory, the token
is packaged with it and anyone with artifact access can extract
it. On public repos that is anyone, and the token can be used
while the job is still running ("ArtiPACKED", flagged by
`zizmor` as `artipacked`).
* Any later step, third-party action, or build dependency can read
the token from the git config, which widens the impact of a
supply-chain compromise.
None of our workflows need authenticated git after checkout, so
disable credential persistence. If a step needs to push in
A separate vitest.config.ts keeps the tests off the build
plugins and the mediapipe version check in vite.config.ts.
make test now runs the frontend tests after the backend ones,
and test-front runs on Node 24, the current LTS.
Add vitest as a dev dependency, a test script that runs panda
codegen first, and a test-front job, so the frontend can carry
unit tests. One test covers normalizeRoomId, a plain function,
so no DOM library comes with it.
The hand button owns the lower-hand timer and the toast, and the
picture-in-picture window draws a second copy of that button, so two
offers go up and dismissing one leaves the other to lower the hand.
Move the watching into a component rendered once beside the room.
Surface the page's loading state to assistive technology, so screen
readers can announce that the page is still loading instead of
reading a partially rendered state as if it were complete.
When the user is updated, their lists on Brevo are overwritten with
the new value: this removes lists set by other products.
Switch to the common lib implementation from `django-lasuite`,
which manages this correctly.
This change was initially proposed by @qbey, but at the time our
deployment did not have a Celery worker running alongside the
backend. Since then, a Celery worker has been deployed, so the
switch to the common lib approach is now safe to adopt.
Highlight in the connection test when the user is connecting
through a TURN relay, especially over TLS or TCP. This usually
indicates that some network configuration is required on their
side, and gives them a concrete signal to pass to their IT team.
Suggested by a technical user, this is a first step toward making
users more autonomous when troubleshooting access to the tool.
Follow-up: show a similar warning in-product when we detect a
mid-meeting fallback to TURN/TLS. A one-time hint for first-time
users would likely be enough.
The home page only offers meeting creation to authenticated users, while the
backend already serves ephemeral rooms to anonymous visitors when
ALLOW_UNREGISTERED_ROOMS is enabled (the flag is checked in the room retrieve
view, not in the create one).
Expose the flag in the frontend configuration and, when it is on, show the
existing "Create a meeting" button to signed-out visitors. It navigates to a
freshly generated room id rather than calling POST /rooms/, which stays
reserved for registered rooms and for authenticated users.
The invite dialog opens for such a creator when the room is unregistered
(null id) and the navigation carries `create`. The `mode` computed in Room
is left untouched, so permissions and the join screen behave as before.
The home buttons row now wraps: signed-out visitors can see three controls
(create, join, login), and at the xsm breakpoint the fixed-width ProConnect
button leaves too little room for the other two.
A React Aria overlay is rendered at `top: 0; left: 0` until
`useOverlayPosition` computes its position, and `data-placement` is
only set once that succeeds. When the pointer moves quickly between
triggers, a closing tooltip can mount for its exit animation without
ever being positioned, React Aria does not retry, so it stays
stuck in the top-left corner.
Hide tooltips until they have a `data-placement` set, so unpositioned
tooltips never flash in the corner. Use `visibility` rather than
`display: none`, so the element stays measurable for the positioning
pass.
Replace the deprecated `enable_tracing=True` with `traces_sample_rate`,
read from a new `sentry_traces_sample_rate` setting (default 0.1,
validated to the 0.0–1.0 range).
Previously, tracing sampled 100% of transactions, which is costly and
unnecessary in production. The rate can now be tuned per environment
without a code change.
Sentry attaches stack frame locals to its events. When storing a
transcript in S3 failed, the full transcript held in `data` and
`transcript` was sent to Sentry.
Keep locals for debugging, but scrub variables and nested dict keys
known to hold transcripts, summaries, LLM prompts, participants'
personal data or pre-signed URLs. Share the Sentry init between the
API and the Celery worker, and never send request bodies.
Since boto3/botocore 1.36, the S3 client computes CRC32 checksums on
uploads by default (request_checksum_calculation="when_supported").
PutObject requests are then sent with aws-chunked encoding, a trailing
x-amz-checksum-crc32 header and a STREAMING-UNSIGNED-PAYLOAD-TRAILER
content hash.
Our production storage (S3NS, storage.s3nsapis.fr) is built on Google
Cloud Storage and exposes it through GCS's S3-compatible XML API, which
does not support these flexible checksums. It rejects the request with
a 403 SignatureDoesNotMatch ("Invalid argument"), so storing transcripts
failed in the Celery worker. Garage, used locally, supports them, which
is why the issue only appeared in production after switching the
client to boto3.
Add aws_request_checksum_calculation and
aws_response_checksum_validation settings, defaulting to
"when_required", and pass them to the botocore Config of the summary S3
client and the backend S3 client. Checksums are then only sent for
operations that require them, restoring the pre-1.36 behavior.
The nginx-unprivileged:1.30.4-alpine3.24 base image ships
pcre2 10.48-r0, which is affected by CVE-2026-103111 (HIGH,
out-of-bounds write via crafted regular expression). No newer
base image tag is available yet.
Upgrade pcre2 from the Alpine v3.24 repository with a minimum
version constraint (>=10.49-r0) so the build fails instead of
silently shipping a vulnerable version if the fix is unavailable.
Recordings were failing to download in the local stack because of
several small issues stacked together:
* nginx: add a `/media/recordings/` location that authorizes
against `recordings/media-auth/`. Before, every media request
went to `files/media-auth/`, which returned 403 for recording
paths.
* nginx: call `proxy_hide_header Content-Disposition` before
`add_header Content-Disposition "attachment"`. Garage stored the
header as `inline`, which combined with nginx's value into
`inline, attachment` and broke browser downloads.
* frontend: `mediaUrl()` now uses the frontend origin, so
recording links go through the Vite `/media` proxy instead of
hitting Django directly on `:8071`.
* frontend: include the file extension in the download filename.
co-author: cameldev
The devstack was pulling the full `node:22` image, around 1.6 GB.
Switch to `node:22-alpine`, which is much smaller, to speed up the
devstack bootstrap time and reduce disk usage.
The devstack was pulling two different versions of the Redis image.
Align everything on a single version to save a few MB of network
bandwidth when bootstrapping the stack.
Late-night minor optimization.
Add new options to query start-start recording API. A resolution
("540p", "720p", "1080p") and a profile ("talking_heads", "text", "mixed")
are resolved to provide a width, height, fps and bitrate which
are passed on to the encoder. Using profiles allows for some flexibility
on quality if necessary without changing front facing user config.
Co-authored-by: sarthakbahal <sarthakbahal.45@gmail.com>
The python:3.14.7-slim base image ships libpcre2-8-0
10.46-1~deb13u2, which is affected by CVE-2026-103111 (HIGH):
an out-of-bounds write triggered by a crafted regular expression.
Explicitly install libpcre2-8-0 in the base stage so apt pulls
the patched 10.46-1~deb13u3 from trixie-security. All stages
(builder, development, production) inherit the fix.
This line can be dropped once an upstream python slim image
ships the patched package.
Address the following HIGH severity CVEs reported by Trivy on the
backend image:
* Django 5.2.16 → 5.2.17
- CVE-2026-15307 — remote code execution via GeoDjango spatial
lookups.
* urllib3 2.7.0 → 2.8.0
- CVE-2026-97687 — traffic interception via HTTPS proxy TLS
configuration override.
- CVE-2026-97689 — denial of service via unbounded memory
allocation in the chunk parser.
The trivy scan fails on HIGH vulnerabilities found in the Debian 13
base images (util-linux, acl, ncurses, systemd and perl-base). None
of them has a fixed version available yet, so there is nothing we
can upgrade to clear them.
List these CVEs in a shared .github/.trivyignore and pass it to
every image scan, so the scan stays blocking for any new HIGH or
CRITICAL vulnerability. Remove the entries once Debian ships a fix.
Instead of referencing the shared CI repo on `main`, pin it to the
initial tagged version `v0.0.1`, so the CI behavior is stable and
does not silently change when the shared repo is updated.
Get the shellcheck CI job to pass again by addressing the issues it
flagged across our shell scripts.
Note: I am not 100% sure of every fix applied here. Reviewers should
feel free to challenge specific changes and suggest better ones
where relevant.
Get the spellcheck CI job to pass again by:
* Fixing the actual spelling issues it caught in the project.
* Excluding generated files from the scan, since they are not
written by us.
* Excluding translation files, whose content is not necessarily in
English and would trigger false positives.
Wire Menshen into the CI to scan the GitHub Actions we use and flag
vulnerable ones, following the same approach as other projects that
recently adopted it.
Note: I am not fully sure about the current setup. Reviewers should
feel free to adjust the configuration or the integration point as
they see fit.
The Crowdin workflows were not used by the project and had turned
into dead CI code.
Remove them to reduce noise and keep the CI configuration limited
to what is actually running.
First iteration of a migration toward a centralized repository
containing our shared CI logic.
Goals:
* Manage GitHub Actions version upgrades in one place.
* Make it easier to audit what actually runs in CI from a security
perspective.
* Avoid duplicating CI logic across projects and having each
repository slowly diverge over time.
* Centralize as many of our custom GitHub Actions as possible,
including some that still live in the old `numerique-gouv`
organization.
* Centralize the Renovate configuration alongside the workflows.
Inspired by the Accounts project, which recently simplified and
reorganized its CI setup.
This PR starts moving meet's CI to the shared repository so we can
validate the approach on a real project. For now, reusable
workflows are pinned to `main`; once we agree on the structure and
content of the central repository, they should be pinned to a
specific commit SHA instead.
Address the following CVEs in util-linux, reported by Cyberwatch on
the agents image. The python:3.14.6-slim tag is no longer rebuilt
and still ships util-linux 2.41-5. Bump the base image to
python:3.14.7-slim, which ships the patched 2.41.5-0+deb13u1
(DSA-6442-1), to cover them all:
* CVE-2026-53612 (7.0) — TOCTOU in mount post-mount ownership/mode
changes.
* CVE-2026-53613 (7.0) — TOCTOU in mount via ancestor directory swap.
* CVE-2026-53614 (7.0) — SUID mount(8) nosuid/noexec bypass via
`LIBMOUNT_FORCE_MOUNT2`.
* CVE-2026-13595 (5.3) — flaw in the libblkid library.
* CVE-2026-27456 (4.7) — TOCTOU in SUID mount(8) when setting up
loop devices.
Permissions on the recording panel checkboxes were not properly
enforced. A user with only partial access to some recording modes
could still tick a mode's checkbox and, for example, launch a
transcription from the recording panel even though they were not
authorized to.
Gate each checkbox on the user's actual permissions so that only
authorized modes can be started from the panel.
The per-minute room creation throttle absorbs bursts but does not stop
a compromised account from steadily creating rooms over hours or days.
Add RoomCreationDailyUserRateThrottle, a per-user throttle with its own
"room_creation_daily" scope, applied to room creation only alongside
the existing short-term throttle. It defaults to 1000 rooms per day and
is configurable via ROOM_CREATION_DAILY_THROTTLE_RATES.
Tests use a controllable clock and patch rates with monkeypatch so they
are restored after each test.
Add throttling on the endpoint used to generate meeting links, so a
compromised authenticated account cannot silently generate thousands
of links without hitting any suspicious errors or alerts.
The limits are set high enough not to affect legitimate usage, while
capping the damage a leaked account can do.