-
Notifications
You must be signed in to change notification settings - Fork 12
Enable login_via_existing_session in local dev/test Synapse #5830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,6 +84,11 @@ suppress_key_server_warning: true | |
| ui_auth: | ||
| session_timeout: "300s" | ||
|
|
||
| login_via_existing_session: | ||
| enabled: true | ||
| require_ui_auth: false | ||
| token_timeout: "2m" | ||
|
Comment on lines
+87
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] The 2-minute Mechanism. In Synapse v1.126.0 (the version # Ratelimit aggressively to a maximum of 1 request per minute.
self._ratelimiter = Ratelimiter(
store=..., clock=...,
cfg=RatelimitSettings(key="<login token request>", per_second=1 / 60, burst_count=1),
)and invokes it as
Why it matters here. The new spec is safe purely by construction — every test calls Ask. Add a line to the block (in each copy) naming the constraint: login_via_existing_session:
enabled: true
require_ui_auth: false
token_timeout: "2m"
# Synapse rate-limits get_token to one request per minute per user id. That
# limit is hardcoded in the servlet — no rc_* setting and no admin
# override_ratelimit relaxes it — so anything minting more than one token
# per minute needs a distinct account per mint.Class: pre-existing upstream behavior that this change makes load-bearing. Non-blocking. Generated by Claude Code |
||
|
|
||
| email: | ||
| smtp_host: "boxel-smtp" | ||
| smtp_port: 25 | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] Most of what this file asserts is Synapse's behavior rather than ours — and the boxel-side path it was written to unblock is already testable today. Non-blocking, but it is the substantive question about this file: what regression does each test catch? Background. The config switch has exactly two observable consequences this repo owns: the endpoint exists, and Sorting the assertions against that:
The test that would guard this repo has both halves already in place. The host consumer exists: // illustrative — helper names/assertions to taste
let { username, credentials } = await createSubscribedUser('login-token');
let login_token = await mintLoginToken(credentials.accessToken);
await page.goto(`${appURL}?loginToken=${login_token}`);
await assertLoggedIn(page, { userId: `@${username}:localhost`, displayName: username });That covers what the three current tests do not: the query-param plumbing, the exchange through Ask. A decision, not necessarily a change: keep the upstream-behavior tests, or trade them for the end-to-end one. If they stay, dropping the second test loses no coverage. Class: coverage/scope. Generated by Claude Code |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { getSynapseURL } from '../support/environment-config.ts'; | ||
| import { createUser } from '../helpers/index.ts'; | ||
|
|
||
| // Exercises Synapse's login_via_existing_session feature (MSC3882), which the | ||
| // test homeserver config enables. A client holding an access token mints a | ||
| // short-lived, single-use login token and exchanges it for a fresh session — | ||
| // the mechanism that hands a pre-authenticated session off to the browser. | ||
| test.describe('login_via_existing_session', () => { | ||
| test('an access-token holder can mint a login token and exchange it for a session', async () => { | ||
| let { credentials } = await createUser('login-token'); | ||
|
|
||
| // Mint a login token with the existing session's access token. | ||
| let getTokenResponse = await fetch( | ||
| `${getSynapseURL()}/_matrix/client/v1/login/get_token`, | ||
| { | ||
| method: 'POST', | ||
| headers: { Authorization: `Bearer ${credentials.accessToken}` }, | ||
| body: JSON.stringify({}), | ||
| }, | ||
| ); | ||
| expect( | ||
| getTokenResponse.status, | ||
| 'get_token succeeds for an authenticated caller', | ||
| ).toBe(200); | ||
| let { login_token, expires_in_ms } = (await getTokenResponse.json()) as { | ||
| login_token: string; | ||
| expires_in_ms: number; | ||
| }; | ||
| expect(login_token, 'a login token is returned').toBeTruthy(); | ||
| // token_timeout is configured as "2m" in the test homeserver.yaml. | ||
| expect(expires_in_ms).toBe(120_000); | ||
|
|
||
| // Exchange the login token for a brand-new session belonging to the same user. | ||
| let loginResponse = await fetch( | ||
| `${getSynapseURL()}/_matrix/client/v3/login`, | ||
| { | ||
| method: 'POST', | ||
| body: JSON.stringify({ type: 'm.login.token', token: login_token }), | ||
| }, | ||
| ); | ||
| expect(loginResponse.status, 'login with the token succeeds').toBe(200); | ||
| let session = (await loginResponse.json()) as { | ||
| user_id: string; | ||
| access_token: string; | ||
| device_id: string; | ||
| }; | ||
| expect(session.user_id).toBe(credentials.userId); | ||
| expect(session.access_token, 'a fresh access token is issued').toBeTruthy(); | ||
| expect(session.device_id).toBeTruthy(); | ||
| // The handed-off session is independent of the caller's device. | ||
| expect(session.device_id).not.toBe(credentials.deviceId); | ||
| }); | ||
|
|
||
| test('the endpoint is recognized and requires authentication', async () => { | ||
| // Before the feature is enabled Synapse returns M_UNRECOGNIZED for this | ||
| // route; with it enabled an unauthenticated call is rejected as | ||
| // M_MISSING_TOKEN, proving the endpoint is wired up and enforcing auth. | ||
| let response = await fetch( | ||
| `${getSynapseURL()}/_matrix/client/v1/login/get_token`, | ||
| { method: 'POST', body: JSON.stringify({}) }, | ||
| ); | ||
| expect(response.status).toBe(401); | ||
| let body = (await response.json()) as { errcode: string }; | ||
| expect(body.errcode).toBe('M_MISSING_TOKEN'); | ||
| }); | ||
|
|
||
| test('a login token is single-use', async () => { | ||
| let { credentials } = await createUser('login-token-reuse'); | ||
|
|
||
| let { login_token } = (await ( | ||
| await fetch(`${getSynapseURL()}/_matrix/client/v1/login/get_token`, { | ||
| method: 'POST', | ||
| headers: { Authorization: `Bearer ${credentials.accessToken}` }, | ||
| body: JSON.stringify({}), | ||
| }) | ||
| ).json()) as { login_token: string }; | ||
|
|
||
| let first = await fetch(`${getSynapseURL()}/_matrix/client/v3/login`, { | ||
| method: 'POST', | ||
| body: JSON.stringify({ type: 'm.login.token', token: login_token }), | ||
| }); | ||
| expect(first.status, 'the first exchange succeeds').toBe(200); | ||
|
|
||
| let second = await fetch(`${getSynapseURL()}/_matrix/client/v3/login`, { | ||
| method: 'POST', | ||
| body: JSON.stringify({ type: 'm.login.token', token: login_token }), | ||
| }); | ||
| expect(second.status, 'the token cannot be reused').toBe(403); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code 🤖] Nothing in the repo selects this template, so this third of the diff is config that never loads. Not this change's doing — but worth knowing before treating "parity across all three templates" as a cost worth paying.
Verified.
synapseStartresolves a template viaopts?.template ?? 'test'(support/synapse/index.ts), and repo-wide there are exactly two call sites:scripts/synapse.tspassestemplate: 'dev', andtests/global.setup.tscallssynapseStart()with no options, taking the'test'default. A repo-wide grep forwithout-registration(excludingnode_modules, across.ts/.sh/.yml/.yaml/.json/.md) returns nothing outside the directory itself — no script, workflow, mise task, or spec names it. Its only distinguishing content isregistration_requires_token: false(plus a missingpresenceblock and the test OIDC block).The practical consequence is that an error in this copy is undetectable:
cfgDirFromTemplatenever renders it and Synapse never boots it, so a bad key here fails nowhere. That is the difference between this hunk and the identical hunk intest/(which the suite boots on every run, so the spec'sexpires_in_msassertion actually proves the block took effect).Ask. Either point out what selects it that I've missed, or file the follow-up to delete the directory — in which case this hunk goes with it. Class: pre-existing orphan; follow-up, not this change. Non-blocking.
Generated by Claude Code