matrix-nodejs-homeserver 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -9
- package/dist/api/account.js +109 -64
- package/dist/api/admin.js +9 -0
- package/dist/api/federation.js +1 -1
- package/dist/api/index.js +2 -0
- package/dist/api/misc.js +3 -3
- package/dist/api/oauthserver.js +620 -0
- package/dist/api/sync.js +1 -1
- package/dist/api/threepid.js +1 -1
- package/dist/api/uia.js +20 -8
- package/dist/api/user.js +6 -3
- package/dist/core/auth.d.ts +33 -2
- package/dist/core/auth.js +84 -21
- package/dist/core/oauth.js +2 -1
- package/dist/core/oauthserver.d.ts +141 -0
- package/dist/core/oauthserver.js +603 -0
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/ratelimit.js +4 -0
- package/dist/core/sso.d.ts +51 -15
- package/dist/core/sso.js +86 -26
- package/dist/homeserver.d.ts +12 -1
- package/dist/homeserver.js +17 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/storage/schema.js +20 -2
- package/dist/vendor/zod/v4/classic/errors.js +1 -1
- package/dist/vendor/zod/v4/classic/parse.js +1 -1
- package/dist/vendor/zod/v4/classic/schemas.js +1 -1
- package/dist/vendor/zod/v4/core/api.js +1 -1
- package/dist/vendor/zod/v4/core/checks.js +1 -1
- package/dist/vendor/zod/v4/core/core.js +1 -1
- package/dist/vendor/zod/v4/core/doc.js +1 -1
- package/dist/vendor/zod/v4/core/errors.js +1 -1
- package/dist/vendor/zod/v4/core/json-schema-processors.js +1 -1
- package/dist/vendor/zod/v4/core/memoizer.js +1 -1
- package/dist/vendor/zod/v4/core/parse.js +1 -1
- package/dist/vendor/zod/v4/core/regexes.js +1 -1
- package/dist/vendor/zod/v4/core/registries.js +1 -1
- package/dist/vendor/zod/v4/core/schemas.js +1 -1
- package/dist/vendor/zod/v4/core/to-json-schema.js +1 -1
- package/dist/vendor/zod/v4/core/util.js +1 -1
- package/dist/vendor/zod/v4/core/versions.js +1 -1
- package/dist/vendor/zod/v4/locales/en.js +1 -1
- package/package.json +12 -15
package/README.md
CHANGED
|
@@ -137,8 +137,9 @@ createHomeserver({
|
|
|
137
137
|
`await hs.auth.createLoginToken(userId)` and let the client exchange the token via
|
|
138
138
|
`m.login.token`. You can also call `hs.auth.createSession(userId)` to issue an
|
|
139
139
|
access token directly.
|
|
140
|
-
- **SSO with upstream OpenID Connect
|
|
141
|
-
|
|
140
|
+
- **SSO with upstream providers:** OpenID Connect (Google, Keycloak,
|
|
141
|
+
Authentik, Better Auth, …) or plain OAuth 2.0 such as GitHub
|
|
142
|
+
(`githubProvider({ clientId, clientSecret })`). Users can sign in with `m.login.sso`:
|
|
142
143
|
```ts
|
|
143
144
|
sso: { providers: [{ id: "google", name: "Google", issuer: "https://accounts.google.com",
|
|
144
145
|
clientId, clientSecret, mapUser: (claims) => ({ localpart: claims.email.split("@")[0] }) }] },
|
|
@@ -147,6 +148,37 @@ createHomeserver({
|
|
|
147
148
|
After signing in, users confirm the client the login token goes to, on a page
|
|
148
149
|
that names it. Clients you trust skip that page:
|
|
149
150
|
`sso: { providers, clientAllowlist: ["https://app.element.io/"] }`.
|
|
151
|
+
- **The OAuth 2.0 API, served by the homeserver** (spec v1.15–v1.18). Clients
|
|
152
|
+
such as Element X register themselves, and users sign in, register, approve
|
|
153
|
+
the client and manage their account on the server's own web pages. Those pages
|
|
154
|
+
offer the password and the providers of `sso`. Login only with GitHub:
|
|
155
|
+
```ts
|
|
156
|
+
import { createHomeserver, githubProvider } from "matrix-nodejs-homeserver";
|
|
157
|
+
|
|
158
|
+
createHomeserver({
|
|
159
|
+
homeserver: "example.com",
|
|
160
|
+
publicBaseUrl: "https://matrix.example.com",
|
|
161
|
+
oauthServer: { termsUrl: "https://example.com/terms", policyUrl: "https://example.com/privacy" },
|
|
162
|
+
userAuthAdapter: { passwordLoginEnabled: false },
|
|
163
|
+
sso: { providers: [githubProvider({ clientId, clientSecret })] },
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
The GitHub OAuth app's callback URL is
|
|
167
|
+
`https://matrix.example.com/_matrix/client/v3/login/sso/callback/github`. The first
|
|
168
|
+
login creates the account. Users are matched by their GitHub ID, so a renamed
|
|
169
|
+
GitHub account keeps its Matrix account.
|
|
170
|
+
|
|
171
|
+
What is served: dynamic client registration with the spec's redirect URI
|
|
172
|
+
rules, the authorization code grant with PKCE (S256), the device authorization
|
|
173
|
+
grant (for CLIs and TVs), refresh tokens and revocation. It also covers
|
|
174
|
+
`prompt=create`, the account management page with every action the spec
|
|
175
|
+
lists, `m.oauth` for cross-signing resets, and ID tokens for clients that ask
|
|
176
|
+
for `openid`. Access tokens live 5 minutes (`accessTokenLifetimeMs`). Refresh
|
|
177
|
+
tokens rotate: a client whose response got lost can retry, and a refresh
|
|
178
|
+
token used again after it was replaced logs the device out. The pages are
|
|
179
|
+
under `/_synapse/client/oauth2/`. The legacy API stays available
|
|
180
|
+
(`legacyApi: false` turns it off), and its `m.login.sso` goes through the same
|
|
181
|
+
pages.
|
|
150
182
|
- **Delegated OAuth 2.0** (the Matrix OAuth API, e.g. with
|
|
151
183
|
matrix-authentication-service, Keycloak or Authentik). The provider owns the
|
|
152
184
|
accounts, and the homeserver validates its access tokens by introspection:
|
|
@@ -342,8 +374,9 @@ paths (server admins only), so existing tools keep working.
|
|
|
342
374
|
| `cluster` | Cluster adapter for running several instances. |
|
|
343
375
|
| `adminRoom` | `true` or `{ firstUserIsAdmin, firstUserCanRegister, botLocalpart, … }`. Disabled by default. |
|
|
344
376
|
| `audit` | `{ enabled, database, sink, actions, retentionDays }`. Disabled by default. |
|
|
345
|
-
| `publicBaseUrl` | Advertised via `/.well-known/matrix/client`. Required for SSO
|
|
346
|
-
| `
|
|
377
|
+
| `publicBaseUrl` | Advertised via `/.well-known/matrix/client`. Required for SSO and `oauthServer`. |
|
|
378
|
+
| `oauthServer` | `true` or `{ legacyApi, accessTokenLifetimeMs, termsUrl, policyUrl, webSessionLifetimeMs }`: the OAuth 2.0 API, served by the homeserver. |
|
|
379
|
+
| `oauth`, `sso` | Delegated OAuth, and upstream identity providers (OIDC or OAuth 2.0, e.g. `githubProvider`). |
|
|
347
380
|
| `federation` | `{ enabled, verifyTls, allowedServers, blockedServers, fasterJoins }`. Off unless `enabled: true`. |
|
|
348
381
|
| `allowPrivateAddresses` | Lets requests to addresses that users or remote servers choose (federation, push gateways, identity servers, URL previews, remote media) reach private addresses: `true`, or ranges such as `["127.0.0.1/32"]`. Blocked by default. |
|
|
349
382
|
| `rtc` | `{ turn, transports }` |
|
|
@@ -377,13 +410,14 @@ available for integration and metrics.
|
|
|
377
410
|
- **Media:** legacy and authenticated endpoints, async uploads, thumbnails, the remote media proxy, URL previews
|
|
378
411
|
- **Federation, VoIP/MatrixRTC, admin, server notices**
|
|
379
412
|
|
|
380
|
-
Not implemented: third-party identifiers (email/phone), application services,
|
|
381
|
-
and a built-in OAuth authorization server. For OAuth, delegate to an existing provider.
|
|
382
|
-
|
|
383
413
|
## Development
|
|
384
414
|
|
|
415
|
+
The repository is an npm workspace: this package is `packages/core`, and `apps/` holds the
|
|
416
|
+
public server built on it (see the repository's README). Install from the repository root;
|
|
417
|
+
the commands below run in `packages/core`.
|
|
418
|
+
|
|
385
419
|
```sh
|
|
386
|
-
npm install
|
|
420
|
+
npm install # at the repository root
|
|
387
421
|
npm run typecheck # tsc only type-checks (noEmit)
|
|
388
422
|
npm run build # tsdown → dist/ (what the published package contains)
|
|
389
423
|
node examples/node-http.ts # development runs the TypeScript sources directly (type stripping)
|
|
@@ -493,7 +527,7 @@ CI runs the whole suite with the same script. `complement/passing.txt` lists the
|
|
|
493
527
|
|
|
494
528
|
### Load testing
|
|
495
529
|
|
|
496
|
-
Tuning and investigation runs are local; a [workflow](.github/workflows/loadtest.yml) runs the same scenarios on main for the badges. [loadtest/](loadtest/) holds a k6 scenario and a
|
|
530
|
+
Tuning and investigation runs are local; a [workflow](https://github.com/k18k/matrix-nodejs-homeserver/blob/main/.github/workflows/loadtest.yml) runs the same scenarios on main for the badges. [loadtest/](loadtest/) holds a k6 scenario and a
|
|
497
531
|
homeserver that reports, every 10 s, requests per second, memory, event-loop delay and
|
|
498
532
|
(with Postgres) how many queries wait for a pool connection and how long locks are held.
|
|
499
533
|
|
package/dist/api/account.js
CHANGED
|
@@ -9,8 +9,10 @@ import { canonicalAddress, isMedium } from "../core/threepid.js";
|
|
|
9
9
|
import { boolean, looseObject, string } from "../vendor/zod/v4/classic/schemas.js";
|
|
10
10
|
import { body, fail, lenient } from "../http/input.js";
|
|
11
11
|
import { procedure } from "../http/procedure.js";
|
|
12
|
+
import { OAUTH_PREFIX } from "../core/oauthserver.js";
|
|
12
13
|
import { requireUia } from "./uia.js";
|
|
13
|
-
import { checkRedirectUrl } from "../core/sso.js";
|
|
14
|
+
import { checkRedirectUrl, legacyClientRedirect } from "../core/sso.js";
|
|
15
|
+
import { readWebSession, setWebUser } from "./oauthserver.js";
|
|
14
16
|
import { createHmac, randomInt, timingSafeEqual } from "node:crypto";
|
|
15
17
|
//#region src/api/account.ts
|
|
16
18
|
const deviceFields = {
|
|
@@ -46,16 +48,18 @@ const sharedSecretRegisterBody = body({
|
|
|
46
48
|
});
|
|
47
49
|
function loginFlows(hs) {
|
|
48
50
|
if (hs.oauth) return [];
|
|
51
|
+
if (hs.oauthServer && !hs.oauthServer.legacyApi) throw errors.unrecognized("Log in with the OAuth 2.0 API");
|
|
49
52
|
const flows = [];
|
|
50
53
|
if (hs.userAuth.passwordLoginEnabled) flows.push({ type: M.LOGIN.PASSWORD });
|
|
51
|
-
if (hs.sso) flows.push({
|
|
54
|
+
if (hs.sso || hs.oauthServer) flows.push({
|
|
52
55
|
type: M.LOGIN.SSO,
|
|
53
|
-
identity_providers: hs.sso
|
|
56
|
+
identity_providers: (hs.sso?.providers ?? []).map((p) => ({
|
|
54
57
|
id: p.id,
|
|
55
58
|
name: p.name,
|
|
56
59
|
icon: p.icon,
|
|
57
60
|
brand: p.brand
|
|
58
|
-
}))
|
|
61
|
+
})),
|
|
62
|
+
...hs.oauthServer ? { oauth_aware_preferred: true } : {}
|
|
59
63
|
});
|
|
60
64
|
flows.push({
|
|
61
65
|
type: M.LOGIN.TOKEN,
|
|
@@ -65,6 +69,7 @@ function loginFlows(hs) {
|
|
|
65
69
|
}
|
|
66
70
|
/** Endpoints that manage passwords are unavailable when an OAuth provider owns the accounts. */
|
|
67
71
|
function rejectIfDelegated(hs) {
|
|
72
|
+
if (hs.oauthServer && !hs.oauthServer.legacyApi) throw errors.unrecognized("Use the OAuth 2.0 API");
|
|
68
73
|
if (hs.oauth) throw new MatrixError(403, "M_FORBIDDEN", "Accounts are managed by the OAuth provider", hs.oauth.options.accountManagementUrl ? { account_management_uri: hs.oauth.options.accountManagementUrl } : {});
|
|
69
74
|
}
|
|
70
75
|
async function validateNewUsername(hs, raw) {
|
|
@@ -105,6 +110,42 @@ async function createUser(hs, localpart, opts) {
|
|
|
105
110
|
await hs.adminRoom?.onUserCreated(userId).catch((err) => hs.logger.error("Admin room setup failed", err));
|
|
106
111
|
return userId;
|
|
107
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Check a password login, with the audit entries and automatic provisioning of `/login`; returns
|
|
115
|
+
* the user ID. Shared with the OAuth server's login page.
|
|
116
|
+
*/
|
|
117
|
+
async function checkPasswordLogin(hs, userId, password, request) {
|
|
118
|
+
let user = await hs.db.findOne("users", [{
|
|
119
|
+
field: "user_id",
|
|
120
|
+
value: userId
|
|
121
|
+
}]);
|
|
122
|
+
if (!await hs.userAuth.checkPassword({
|
|
123
|
+
userId,
|
|
124
|
+
localpart: userId.slice(1).split(":")[0],
|
|
125
|
+
password,
|
|
126
|
+
storedHash: user?.password_hash ?? null
|
|
127
|
+
})) {
|
|
128
|
+
await hs.audit.record({
|
|
129
|
+
action: "login_failed",
|
|
130
|
+
actor: userId,
|
|
131
|
+
ip: request.ip,
|
|
132
|
+
userAgent: request.userAgent,
|
|
133
|
+
outcome: "failure",
|
|
134
|
+
details: { type: M.LOGIN.PASSWORD }
|
|
135
|
+
});
|
|
136
|
+
throw errors.forbidden("Invalid username or password");
|
|
137
|
+
}
|
|
138
|
+
if (!user) {
|
|
139
|
+
if (!await hs.userAuth.autoProvision(userId)) throw errors.forbidden("Invalid username or password");
|
|
140
|
+
await createUser(hs, userId.slice(1).split(":")[0], {});
|
|
141
|
+
user = await hs.db.findOne("users", [{
|
|
142
|
+
field: "user_id",
|
|
143
|
+
value: userId
|
|
144
|
+
}]);
|
|
145
|
+
}
|
|
146
|
+
if (user.deactivated) throw errors.userDeactivated();
|
|
147
|
+
return userId;
|
|
148
|
+
}
|
|
108
149
|
async function resolveLoginUser(hs, body) {
|
|
109
150
|
let user;
|
|
110
151
|
const id = isObject(body.identifier) ? body.identifier : null;
|
|
@@ -138,45 +179,63 @@ async function resolveLoginUser(hs, body) {
|
|
|
138
179
|
}
|
|
139
180
|
return `@${user.toLowerCase()}:${hs.serverName}`;
|
|
140
181
|
}
|
|
182
|
+
const ssoConfirmPage = (serverName, result) => {
|
|
183
|
+
const client = escapeHtml(result.client);
|
|
184
|
+
const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Continue to ${client}</title>
|
|
185
|
+
<style>body{font:16px system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}a.button{display:inline-block;padding:.6rem 1.2rem;background:#0b7a55;color:#fff;border-radius:6px;text-decoration:none}</style>
|
|
186
|
+
<h1>Continue to ${client}?</h1>
|
|
187
|
+
<p>You signed in to <b>${escapeHtml(serverName)}</b>. <b>${client}</b> will get access to your account.</p>
|
|
188
|
+
<p>If you did not just sign in with this app, close this page.</p>
|
|
189
|
+
<p><a class="button" href="${escapeHtml(result.url)}">Continue to ${client}</a></p>`;
|
|
190
|
+
return new Response(html, { headers: {
|
|
191
|
+
"content-type": "text/html; charset=utf-8",
|
|
192
|
+
"cache-control": "no-store",
|
|
193
|
+
"referrer-policy": "no-referrer",
|
|
194
|
+
"x-frame-options": "DENY",
|
|
195
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'"
|
|
196
|
+
} });
|
|
197
|
+
};
|
|
198
|
+
const OAUTH_DISCOVERY_VERSIONS = ["v1", "unstable/org.matrix.msc2965"];
|
|
141
199
|
function registerAccountRoutes(r) {
|
|
142
200
|
r.client("GET", "/login", procedure({ handler: (ctx) => ({ flows: loginFlows(ctx.hs) }) }));
|
|
143
201
|
r.client("GET", "/auth_metadata", procedure({ handler: async (ctx) => {
|
|
202
|
+
if (ctx.hs.oauthServer) return ctx.hs.oauthServer.metadata();
|
|
144
203
|
if (!ctx.hs.oauth) throw errors.unrecognized("OAuth is not enabled");
|
|
145
204
|
return ctx.hs.oauth.serverMetadata();
|
|
146
|
-
} }),
|
|
205
|
+
} }), OAUTH_DISCOVERY_VERSIONS);
|
|
147
206
|
r.client("GET", "/auth_issuer", procedure({ handler: (ctx) => {
|
|
207
|
+
if (ctx.hs.oauthServer) return { issuer: ctx.hs.oauthServer.issuer };
|
|
148
208
|
if (!ctx.hs.oauth) throw errors.unrecognized("OAuth is not enabled");
|
|
149
209
|
return { issuer: ctx.hs.oauth.options.issuer };
|
|
150
|
-
} }),
|
|
151
|
-
const ssoConfirmPage = (serverName, result) => {
|
|
152
|
-
const client = escapeHtml(result.client);
|
|
153
|
-
const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Continue to ${client}</title>
|
|
154
|
-
<style>body{font:16px system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}a.button{display:inline-block;padding:.6rem 1.2rem;background:#0b7a55;color:#fff;border-radius:6px;text-decoration:none}</style>
|
|
155
|
-
<h1>Continue to ${client}?</h1>
|
|
156
|
-
<p>You signed in to <b>${escapeHtml(serverName)}</b>. <b>${client}</b> will get access to your account.</p>
|
|
157
|
-
<p>If you did not just sign in with this app, close this page.</p>
|
|
158
|
-
<p><a class="button" href="${escapeHtml(result.url)}">Continue to ${client}</a></p>`;
|
|
159
|
-
return new Response(html, { headers: {
|
|
160
|
-
"content-type": "text/html; charset=utf-8",
|
|
161
|
-
"cache-control": "no-store",
|
|
162
|
-
"referrer-policy": "no-referrer",
|
|
163
|
-
"x-frame-options": "DENY",
|
|
164
|
-
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'"
|
|
165
|
-
} });
|
|
166
|
-
};
|
|
210
|
+
} }), OAUTH_DISCOVERY_VERSIONS);
|
|
167
211
|
const ssoRedirect = async (ctx) => {
|
|
168
|
-
|
|
212
|
+
const hs = ctx.hs;
|
|
169
213
|
const redirectUrl = checkRedirectUrl(ctx.query.get("redirectUrl"));
|
|
214
|
+
if (hs.oauthServer) {
|
|
215
|
+
if (!hs.oauthServer.legacyApi) throw errors.unrecognized("Log in with the OAuth 2.0 API");
|
|
216
|
+
if (!redirectUrl) throw errors.missingParam("Missing or invalid redirectUrl");
|
|
217
|
+
const id = randomString(24);
|
|
218
|
+
await hs.cluster.set(`oauth:legacy:${id}`, redirectUrl, 18e5);
|
|
219
|
+
const next = `${OAUTH_PREFIX}/legacy/${id}`;
|
|
220
|
+
const idp = ctx.params.idpId;
|
|
221
|
+
if (idp && !hs.sso?.providers.some((p) => p.id === idp)) throw errors.notFound("Unknown identity provider");
|
|
222
|
+
const page = idp ? `/upstream/${encodeURIComponent(idp)}` : ctx.query.get("action") === "register" ? "/register-account" : "/login";
|
|
223
|
+
return Response.redirect(`${hs.oauthServer.url(page)}?next=${encodeURIComponent(next)}`, 302);
|
|
224
|
+
}
|
|
225
|
+
if (!hs.sso) throw errors.unrecognized("SSO is not enabled");
|
|
170
226
|
if (!redirectUrl) throw errors.missingParam("Missing or invalid redirectUrl");
|
|
171
|
-
return Response.redirect(await
|
|
227
|
+
return Response.redirect(await hs.sso.startLogin(ctx.params.idpId ?? null, {
|
|
228
|
+
kind: "client",
|
|
229
|
+
redirectUrl
|
|
230
|
+
}), 302);
|
|
172
231
|
};
|
|
173
232
|
r.client("POST", "/login/get_token", procedure({
|
|
174
233
|
auth: "user",
|
|
175
234
|
handler: async (ctx) => {
|
|
176
235
|
const s = ctx.session;
|
|
177
|
-
if (!ctx.hs.config.loginViaExistingSession || ctx.hs.oauth || s.isGuest) throw errors.unrecognized();
|
|
236
|
+
if (!ctx.hs.config.loginViaExistingSession || ctx.hs.oauth || s.oauth || s.isGuest) throw errors.unrecognized();
|
|
178
237
|
await ctx.rateLimit("login");
|
|
179
|
-
await requireUia(ctx.hs, await ctx.json(), [[M.LOGIN.PASSWORD]], s.userId);
|
|
238
|
+
await requireUia(ctx.hs, await ctx.json(), [[M.LOGIN.PASSWORD]], s.userId, { session: s });
|
|
180
239
|
const ttl = 12e4;
|
|
181
240
|
return {
|
|
182
241
|
login_token: await ctx.hs.auth.createLoginToken(s.userId, ttl),
|
|
@@ -187,9 +246,17 @@ function registerAccountRoutes(r) {
|
|
|
187
246
|
r.client("GET", "/login/sso/redirect", procedure({ handler: ssoRedirect }));
|
|
188
247
|
r.client("GET", "/login/sso/redirect/:idpId", procedure({ handler: ssoRedirect }));
|
|
189
248
|
r.client("GET", "/login/sso/callback/:idpId", procedure({ handler: async (ctx) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
249
|
+
const hs = ctx.hs;
|
|
250
|
+
if (!hs.sso) throw errors.unrecognized("SSO is not enabled");
|
|
251
|
+
const { userId, target } = await hs.sso.finishLogin(ctx.params.idpId, ctx.query);
|
|
252
|
+
if (target.kind === "web") {
|
|
253
|
+
const web = await readWebSession(ctx);
|
|
254
|
+
if (!web || web.sid !== target.sid) throw errors.forbidden("This login was started in another browser");
|
|
255
|
+
await setWebUser(hs, web, userId);
|
|
256
|
+
return Response.redirect(hs.oauthServer.url(target.next.slice(OAUTH_PREFIX.length)), 302);
|
|
257
|
+
}
|
|
258
|
+
const result = await legacyClientRedirect(hs, userId, target.redirectUrl);
|
|
259
|
+
return result.confirm ? ssoConfirmPage(hs.serverName, result) : Response.redirect(result.url, 302);
|
|
193
260
|
} }));
|
|
194
261
|
r.client("POST", "/login", procedure({
|
|
195
262
|
rateLimit: "login",
|
|
@@ -198,6 +265,7 @@ function registerAccountRoutes(r) {
|
|
|
198
265
|
const body = ctx.input;
|
|
199
266
|
const hs = ctx.hs;
|
|
200
267
|
if (hs.oauth) throw errors.unrecognized("Log in through the OAuth provider");
|
|
268
|
+
if (hs.oauthServer && !hs.oauthServer.legacyApi) throw errors.unrecognized("Log in with the OAuth 2.0 API");
|
|
201
269
|
if (body.type === M.LOGIN.APPLICATION_SERVICE) {
|
|
202
270
|
const reg = ctx.accessToken() ? hs.appservices.byToken(ctx.accessToken()) : null;
|
|
203
271
|
if (!reg) throw errors.unknownToken("An application service token is required");
|
|
@@ -254,37 +322,11 @@ function registerAccountRoutes(r) {
|
|
|
254
322
|
if (body.type !== M.LOGIN.PASSWORD || !hs.userAuth.passwordLoginEnabled) throw errors.unknown("Unsupported login type");
|
|
255
323
|
const userId = await resolveLoginUser(hs, body);
|
|
256
324
|
if (body.password === void 0) throw errors.missingParam("Missing password");
|
|
257
|
-
let user = await hs.db.findOne("users", [{
|
|
258
|
-
field: "user_id",
|
|
259
|
-
value: userId
|
|
260
|
-
}]);
|
|
261
|
-
const ok = await hs.userAuth.checkPassword({
|
|
262
|
-
userId,
|
|
263
|
-
localpart: userId.slice(1).split(":")[0],
|
|
264
|
-
password: body.password,
|
|
265
|
-
storedHash: user?.password_hash ?? null
|
|
266
|
-
});
|
|
267
325
|
const ua = ctx.request.headers.get("user-agent");
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
ip: ctx.ip,
|
|
273
|
-
userAgent: ua,
|
|
274
|
-
outcome: "failure",
|
|
275
|
-
details: { type: M.LOGIN.PASSWORD }
|
|
276
|
-
});
|
|
277
|
-
throw errors.forbidden("Invalid username or password");
|
|
278
|
-
}
|
|
279
|
-
if (!user) {
|
|
280
|
-
if (!await hs.userAuth.autoProvision(userId)) throw errors.forbidden("Invalid username or password");
|
|
281
|
-
await createUser(hs, userId.slice(1).split(":")[0], {});
|
|
282
|
-
user = await hs.db.findOne("users", [{
|
|
283
|
-
field: "user_id",
|
|
284
|
-
value: userId
|
|
285
|
-
}]);
|
|
286
|
-
}
|
|
287
|
-
if (user.deactivated) throw errors.userDeactivated();
|
|
326
|
+
await checkPasswordLogin(hs, userId, body.password, {
|
|
327
|
+
ip: ctx.ip,
|
|
328
|
+
userAgent: ua
|
|
329
|
+
});
|
|
288
330
|
const session = await hs.auth.createSession(userId, {
|
|
289
331
|
deviceId: body.device_id,
|
|
290
332
|
displayName: body.initial_device_display_name,
|
|
@@ -310,7 +352,7 @@ function registerAccountRoutes(r) {
|
|
|
310
352
|
handler: async (ctx) => {
|
|
311
353
|
const body = ctx.input;
|
|
312
354
|
if (body.refresh_token === void 0) throw errors.missingParam("Missing refresh_token");
|
|
313
|
-
const { access_token, refresh_token, expires_in_ms } = await ctx.hs.auth.refresh(body.refresh_token);
|
|
355
|
+
const { access_token, refresh_token, expires_in_ms } = await ctx.hs.auth.refresh(body.refresh_token, { clientId: null });
|
|
314
356
|
return {
|
|
315
357
|
access_token,
|
|
316
358
|
refresh_token,
|
|
@@ -464,7 +506,7 @@ function registerAccountRoutes(r) {
|
|
|
464
506
|
let s;
|
|
465
507
|
if (ctx.accessToken()) {
|
|
466
508
|
const session = await ctx.auth();
|
|
467
|
-
await requireUia(hs, body, [[M.LOGIN.PASSWORD]], session.userId);
|
|
509
|
+
await requireUia(hs, body, [[M.LOGIN.PASSWORD]], session.userId, { session });
|
|
468
510
|
s = {
|
|
469
511
|
userId: session.userId,
|
|
470
512
|
deviceId: session.deviceId
|
|
@@ -501,7 +543,7 @@ function registerAccountRoutes(r) {
|
|
|
501
543
|
const hs = ctx.hs;
|
|
502
544
|
rejectIfDelegated(hs);
|
|
503
545
|
const s = await ctx.auth();
|
|
504
|
-
await requireUia(hs, ctx.input, [[M.LOGIN.PASSWORD]], s.userId);
|
|
546
|
+
await requireUia(hs, ctx.input, [[M.LOGIN.PASSWORD]], s.userId, { session: s });
|
|
505
547
|
for (const membership of [
|
|
506
548
|
"join",
|
|
507
549
|
"invite",
|
|
@@ -593,7 +635,10 @@ async function registrationTokenValid(hs, token) {
|
|
|
593
635
|
}
|
|
594
636
|
async function consumeRegistrationToken(hs, body) {
|
|
595
637
|
if (body.auth?.type !== M.LOGIN.REGISTRATION_TOKEN) return;
|
|
596
|
-
|
|
638
|
+
await redeemRegistrationToken(hs, body.auth?.token);
|
|
639
|
+
}
|
|
640
|
+
/** Count one use of a registration token. */
|
|
641
|
+
async function redeemRegistrationToken(hs, token) {
|
|
597
642
|
const row = await hs.db.findOne("registration_tokens", [{
|
|
598
643
|
field: "token",
|
|
599
644
|
value: token
|
|
@@ -610,4 +655,4 @@ function wellKnown(hs) {
|
|
|
610
655
|
return wk;
|
|
611
656
|
}
|
|
612
657
|
//#endregion
|
|
613
|
-
export { createUser, registerAccountRoutes, registrationFlows, registrationTokenValid, wellKnown };
|
|
658
|
+
export { checkPasswordLogin, createUser, redeemRegistrationToken, registerAccountRoutes, registrationFlows, registrationTokenValid, ssoConfirmPage, validateNewUsername, wellKnown };
|
package/dist/api/admin.js
CHANGED
|
@@ -2,6 +2,7 @@ import { errors } from "../core/errors.js";
|
|
|
2
2
|
import { boolean, looseObject, number, string } from "../vendor/zod/v4/classic/schemas.js";
|
|
3
3
|
import { body, fail, lenient } from "../http/input.js";
|
|
4
4
|
import { procedure } from "../http/procedure.js";
|
|
5
|
+
import { allowCrossSigningReset } from "../core/oauthserver.js";
|
|
5
6
|
//#region src/api/admin.ts
|
|
6
7
|
const required = fail("M_BAD_JSON", "user_id and content are required");
|
|
7
8
|
const noticeBody = body({
|
|
@@ -132,6 +133,14 @@ function registerAdminRoutes(r) {
|
|
|
132
133
|
if (body.deactivated === true) await ctx.hs.admin.deactivateUser(userId);
|
|
133
134
|
return ctx.hs.admin.getUser(userId);
|
|
134
135
|
} }));
|
|
136
|
+
r.post("/_synapse/admin/v1/users/:userId/_allow_cross_signing_replacement_without_uia", procedure({
|
|
137
|
+
guard: adminGuard,
|
|
138
|
+
handler: async (ctx) => {
|
|
139
|
+
const userId = ctx.params.userId;
|
|
140
|
+
if (!ctx.hs.isLocalUser(userId) || !await ctx.hs.admin.getUser(userId)) throw errors.notFound("Unknown user");
|
|
141
|
+
return { updatable_without_uia_before_ms: await allowCrossSigningReset(ctx.hs, userId) };
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
135
144
|
r.post("/_synapse/admin/v1/deactivate/:userId", procedure({
|
|
136
145
|
guard: adminGuard,
|
|
137
146
|
input: body({ erase: lenient(boolean()) }),
|
package/dist/api/federation.js
CHANGED
|
@@ -11,8 +11,8 @@ import { domainOf, serverAllowedByAcl } from "../federation/index.js";
|
|
|
11
11
|
import { array, looseObject, record, string, unknown } from "../vendor/zod/v4/classic/schemas.js";
|
|
12
12
|
import { body, lenient } from "../http/input.js";
|
|
13
13
|
import { procedure } from "../http/procedure.js";
|
|
14
|
-
import { roomDetails } from "./spaces.js";
|
|
15
14
|
import { fullProfile } from "../core/profile.js";
|
|
15
|
+
import { roomDetails } from "./spaces.js";
|
|
16
16
|
import { relationshipsForServer } from "./relationships.js";
|
|
17
17
|
import { canonicalJsonResponse } from "../http/handler.js";
|
|
18
18
|
//#region src/api/federation.ts
|
package/dist/api/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerThreadSubscriptionRoutes } from "./threadsubs.js";
|
|
2
|
+
import { registerOAuthServerRoutes } from "./oauthserver.js";
|
|
2
3
|
import { registerAccountRoutes } from "./account.js";
|
|
3
4
|
import { registerBackupRoutes } from "./backup.js";
|
|
4
5
|
import { registerRoomRoutes } from "./rooms.js";
|
|
@@ -23,6 +24,7 @@ import { registerUserRoutes } from "./user.js";
|
|
|
23
24
|
function registerRoutes(r) {
|
|
24
25
|
registerMiscRoutes(r);
|
|
25
26
|
registerAccountRoutes(r);
|
|
27
|
+
registerOAuthServerRoutes(r);
|
|
26
28
|
registerProfileRoutes(r);
|
|
27
29
|
registerRoomRoutes(r);
|
|
28
30
|
registerLegacyRoutes(r);
|
package/dist/api/misc.js
CHANGED
|
@@ -60,7 +60,7 @@ function registerMiscRoutes(r) {
|
|
|
60
60
|
const available = {};
|
|
61
61
|
for (const v of SUPPORTED_ROOM_VERSIONS) available[v] = /^\d+$/.test(v) ? "stable" : "unstable";
|
|
62
62
|
return { capabilities: {
|
|
63
|
-
[M.CHANGE_PASSWORD]: { enabled: !ctx.hs.oauth && ctx.hs.userAuth.passwordLoginEnabled },
|
|
63
|
+
[M.CHANGE_PASSWORD]: { enabled: !ctx.hs.oauth && !ctx.session.oauth && ctx.hs.userAuth.passwordLoginEnabled },
|
|
64
64
|
[M.ROOM_VERSIONS]: {
|
|
65
65
|
default: ctx.hs.config.defaultRoomVersion,
|
|
66
66
|
available
|
|
@@ -68,8 +68,8 @@ function registerMiscRoutes(r) {
|
|
|
68
68
|
[M.SET_DISPLAYNAME]: { enabled: true },
|
|
69
69
|
[M.SET_AVATAR_URL]: { enabled: true },
|
|
70
70
|
[M.PROFILE_FIELDS]: { enabled: true },
|
|
71
|
-
[M.THREEPID_CHANGES]: { enabled: ctx.hs.threepids.enabled && !ctx.hs.oauth },
|
|
72
|
-
[M.GET_LOGIN_TOKEN]: { enabled: ctx.hs.config.loginViaExistingSession && !ctx.hs.oauth }
|
|
71
|
+
[M.THREEPID_CHANGES]: { enabled: ctx.hs.threepids.enabled && !ctx.hs.oauth && !ctx.session.oauth },
|
|
72
|
+
[M.GET_LOGIN_TOKEN]: { enabled: ctx.hs.config.loginViaExistingSession && !ctx.hs.oauth && !ctx.session.oauth }
|
|
73
73
|
} };
|
|
74
74
|
}
|
|
75
75
|
}));
|