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/dist/api/uia.js
CHANGED
|
@@ -2,15 +2,26 @@ import { isObject } from "../core/json.js";
|
|
|
2
2
|
import { randomString } from "../core/crypto.js";
|
|
3
3
|
import { M, UNSTABLE } from "../core/m.js";
|
|
4
4
|
import { MatrixError, errors } from "../core/errors.js";
|
|
5
|
+
import { crossSigningResetAllowed } from "../core/oauthserver.js";
|
|
5
6
|
//#region src/api/uia.ts
|
|
6
7
|
const UIA_TTL_MS = 6e5;
|
|
7
8
|
/**
|
|
8
9
|
* User-interactive authentication. Resolves when one of `flows` has been
|
|
9
10
|
* completed; otherwise throws the 401 response describing what is required.
|
|
10
|
-
* `userId` binds password stages to an already-authenticated user.
|
|
11
|
+
* `userId` binds password stages to an already-authenticated user. `session` is the caller's,
|
|
12
|
+
* and `crossSigningReset` marks `/keys/device_signing/upload`, the one endpoint where tokens of
|
|
13
|
+
* the OAuth 2.0 API may use UIA (the `m.oauth` stage).
|
|
11
14
|
*/
|
|
12
|
-
async function requireUia(hs, body, flows, userId) {
|
|
13
|
-
|
|
15
|
+
async function requireUia(hs, body, flows, userId, opts = {}) {
|
|
16
|
+
const viaOAuth = !!opts.session?.oauth || !!hs.oauth && flows.some((f) => f.includes(M.LOGIN.PASSWORD));
|
|
17
|
+
let oauthUrl;
|
|
18
|
+
if (viaOAuth) {
|
|
19
|
+
const accountUri = hs.oauthServer?.accountManagementUri ?? hs.oauth?.options.accountManagementUrl;
|
|
20
|
+
if (!opts.crossSigningReset) throw new MatrixError(403, "M_FORBIDDEN", "Do this on the account management page", accountUri ? { account_management_uri: accountUri } : {});
|
|
21
|
+
if (userId && await crossSigningResetAllowed(hs, userId)) return { threepids: [] };
|
|
22
|
+
flows = [[M.OAUTH], [UNSTABLE.ORG.MATRIX.CROSS_SIGNING_RESET]];
|
|
23
|
+
oauthUrl = hs.oauthServer ? `${hs.oauthServer.accountManagementUri}?action=org.matrix.cross_signing_reset` : hs.oauth.options.accountManagementUrl ?? hs.oauth.options.issuer;
|
|
24
|
+
}
|
|
14
25
|
const auth = isObject(body.auth) ? body.auth : null;
|
|
15
26
|
let sessionId = typeof auth?.session === "string" ? auth.session : void 0;
|
|
16
27
|
const stored = sessionId ? await hs.cluster.get(`uia:${sessionId}`) : null;
|
|
@@ -25,8 +36,7 @@ async function requireUia(hs, body, flows, userId) {
|
|
|
25
36
|
userId: session.userId,
|
|
26
37
|
threepids: session.threepids
|
|
27
38
|
}, UIA_TTL_MS);
|
|
28
|
-
const
|
|
29
|
-
const params = hs.oauth ? {
|
|
39
|
+
const params = oauthUrl ? {
|
|
30
40
|
[M.OAUTH]: { url: oauthUrl },
|
|
31
41
|
[UNSTABLE.ORG.MATRIX.CROSS_SIGNING_RESET]: { url: oauthUrl }
|
|
32
42
|
} : {};
|
|
@@ -49,12 +59,14 @@ async function requireUia(hs, body, flows, userId) {
|
|
|
49
59
|
break;
|
|
50
60
|
case M.OAUTH:
|
|
51
61
|
case UNSTABLE.ORG.MATRIX.CROSS_SIGNING_RESET:
|
|
52
|
-
if (!
|
|
62
|
+
if (!viaOAuth) throw challenge({
|
|
53
63
|
errcode: "M_UNRECOGNIZED",
|
|
54
64
|
error: "OAuth is not enabled"
|
|
55
65
|
});
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
throw challenge({
|
|
67
|
+
errcode: "M_FORBIDDEN",
|
|
68
|
+
error: "The reset has not been approved on the account page"
|
|
69
|
+
});
|
|
58
70
|
case M.LOGIN.REGISTRATION_TOKEN: {
|
|
59
71
|
const { registrationTokenValid } = await import("./account.js");
|
|
60
72
|
if (typeof auth.token !== "string" || !await registrationTokenValid(hs, auth.token)) throw challenge({
|
package/dist/api/user.js
CHANGED
|
@@ -185,7 +185,7 @@ function registerUserRoutes(r) {
|
|
|
185
185
|
value: ctx.params.deviceId
|
|
186
186
|
}];
|
|
187
187
|
if (!await ctx.hs.db.findOne("devices", where)) throw errors.notFound("Device not found");
|
|
188
|
-
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
|
|
188
|
+
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId, { session: s });
|
|
189
189
|
await ctx.hs.auth.deleteDevice(s.userId, ctx.params.deviceId);
|
|
190
190
|
return {};
|
|
191
191
|
}
|
|
@@ -196,7 +196,7 @@ function registerUserRoutes(r) {
|
|
|
196
196
|
handler: async (ctx) => {
|
|
197
197
|
const s = ctx.session;
|
|
198
198
|
const body = ctx.input;
|
|
199
|
-
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
|
|
199
|
+
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId, { session: s });
|
|
200
200
|
for (const d of body.devices) if (typeof d === "string") await ctx.hs.auth.deleteDevice(s.userId, d);
|
|
201
201
|
return {};
|
|
202
202
|
}
|
|
@@ -275,7 +275,10 @@ function registerUserRoutes(r) {
|
|
|
275
275
|
"self_signing",
|
|
276
276
|
"user_signing"
|
|
277
277
|
].filter((type) => body[`${type}_key`] !== void 0).every((type) => stored.get(type) === canonicalJson(body[`${type}_key`]))) return {};
|
|
278
|
-
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId
|
|
278
|
+
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId, {
|
|
279
|
+
session: s,
|
|
280
|
+
crossSigningReset: true
|
|
281
|
+
});
|
|
279
282
|
}
|
|
280
283
|
await ctx.hs.keys.uploadCrossSigning(s.userId, body);
|
|
281
284
|
return {};
|
package/dist/core/auth.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface Session {
|
|
|
13
13
|
suspended?: boolean;
|
|
14
14
|
/** Set when the request was made with an application service token. */
|
|
15
15
|
appservice?: AppServiceRegistration;
|
|
16
|
+
/**
|
|
17
|
+
* The token comes from the OAuth 2.0 API (this server's or a delegated one), so endpoints that
|
|
18
|
+
* need User-Interactive Authentication send the user to account management instead.
|
|
19
|
+
*/
|
|
20
|
+
oauth?: boolean;
|
|
16
21
|
}
|
|
17
22
|
export interface CreatedSession {
|
|
18
23
|
user_id: string;
|
|
@@ -20,6 +25,8 @@ export interface CreatedSession {
|
|
|
20
25
|
access_token: string;
|
|
21
26
|
refresh_token?: string;
|
|
22
27
|
expires_in_ms?: number;
|
|
28
|
+
/** OAuth 2.0 API: the scope the tokens were issued for. */
|
|
29
|
+
scope?: string;
|
|
23
30
|
}
|
|
24
31
|
export declare class AuthService {
|
|
25
32
|
private hs;
|
|
@@ -31,9 +38,33 @@ export declare class AuthService {
|
|
|
31
38
|
deviceId?: string;
|
|
32
39
|
displayName?: string;
|
|
33
40
|
refreshToken?: boolean;
|
|
41
|
+
/** OAuth 2.0 API: the client the tokens are issued to. */
|
|
42
|
+
clientId?: string;
|
|
43
|
+
/** Access token lifetime; default `accessTokenLifetimeMs` (for tokens with a refresh token). */
|
|
44
|
+
lifetimeMs?: number;
|
|
45
|
+
/** OAuth 2.0 API: the scope granted. */
|
|
46
|
+
scope?: string;
|
|
47
|
+
}): Promise<CreatedSession>;
|
|
48
|
+
issueToken(userId: string, deviceId: string, withRefresh: boolean, opts?: {
|
|
49
|
+
clientId?: string | null;
|
|
50
|
+
lifetimeMs?: number;
|
|
51
|
+
parent?: string;
|
|
52
|
+
scope?: string | null;
|
|
53
|
+
}): Promise<CreatedSession>;
|
|
54
|
+
/**
|
|
55
|
+
* Exchange a refresh token for new tokens.
|
|
56
|
+
*
|
|
57
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/66: the old row used to be deleted
|
|
58
|
+
* at once, so a client whose response was lost could not retry and was logged out, and a
|
|
59
|
+
* refresh token used twice went unnoticed. As the OAuth 2.0 API asks (and the legacy API
|
|
60
|
+
* allows): the old refresh token keeps working until the new access token is first used; a
|
|
61
|
+
* retry before that replaces the unused tokens. Once the new token was used, the old refresh
|
|
62
|
+
* token is spent, and presenting it again means it leaked, so the device is logged out.
|
|
63
|
+
*/
|
|
64
|
+
refresh(refreshToken: string, opts?: {
|
|
65
|
+
clientId?: string | null;
|
|
66
|
+
lifetimeMs?: number;
|
|
34
67
|
}): Promise<CreatedSession>;
|
|
35
|
-
issueToken(userId: string, deviceId: string, withRefresh: boolean): Promise<CreatedSession>;
|
|
36
|
-
refresh(refreshToken: string): Promise<CreatedSession>;
|
|
37
68
|
/** Drop cached sessions of a user (after admin/deactivation changes). */
|
|
38
69
|
invalidateUser(userId: string): Promise<void>;
|
|
39
70
|
/** Issue a short-lived single-use `m.login.token` for a user (SSO, or host-app sign-in). */
|
package/dist/core/auth.js
CHANGED
|
@@ -11,13 +11,13 @@ var AuthService = class {
|
|
|
11
11
|
if (msg?.from === hs.instanceId) return;
|
|
12
12
|
if (msg.token) this.cache.delete(msg.token);
|
|
13
13
|
if (msg.userId) {
|
|
14
|
-
for (const [t,
|
|
14
|
+
for (const [t, e] of this.cache) if (e.session.userId === msg.userId) this.cache.delete(t);
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
async authenticate(token, ip) {
|
|
19
19
|
const cached = this.cache.get(token);
|
|
20
|
-
if (cached) return cached;
|
|
20
|
+
if (cached && cached.expires > Date.now()) return cached.session;
|
|
21
21
|
const row = await this.hs.db.findOne("access_tokens", [{
|
|
22
22
|
field: "token",
|
|
23
23
|
value: token
|
|
@@ -39,9 +39,24 @@ var AuthService = class {
|
|
|
39
39
|
isGuest: !!user.is_guest,
|
|
40
40
|
isAdmin: !!user.is_admin,
|
|
41
41
|
locked: !!user.locked,
|
|
42
|
-
suspended: !!user.suspended
|
|
42
|
+
suspended: !!user.suspended,
|
|
43
|
+
...row.client_id ? { oauth: true } : {}
|
|
43
44
|
};
|
|
44
|
-
if (row.
|
|
45
|
+
if (row.refresh_parent) {
|
|
46
|
+
await this.hs.db.update("access_tokens", [{
|
|
47
|
+
field: "token",
|
|
48
|
+
value: row.refresh_parent
|
|
49
|
+
}], { refresh_used: true });
|
|
50
|
+
await this.hs.db.update("access_tokens", [{
|
|
51
|
+
field: "token",
|
|
52
|
+
value: token
|
|
53
|
+
}], { refresh_parent: null });
|
|
54
|
+
}
|
|
55
|
+
if (this.cache.size > 1e5) this.cache.clear();
|
|
56
|
+
this.cache.set(token, {
|
|
57
|
+
session,
|
|
58
|
+
expires: row.expires_ts ?? Infinity
|
|
59
|
+
});
|
|
45
60
|
await this.hs.db.update("devices", [{
|
|
46
61
|
field: "user_id",
|
|
47
62
|
value: row.user_id
|
|
@@ -91,46 +106,94 @@ var AuthService = class {
|
|
|
91
106
|
target: deviceId
|
|
92
107
|
});
|
|
93
108
|
}
|
|
94
|
-
return this.issueToken(userId, deviceId, !!opts.refreshToken
|
|
109
|
+
return this.issueToken(userId, deviceId, !!opts.refreshToken, {
|
|
110
|
+
clientId: opts.clientId,
|
|
111
|
+
lifetimeMs: opts.lifetimeMs,
|
|
112
|
+
scope: opts.scope
|
|
113
|
+
});
|
|
95
114
|
}
|
|
96
|
-
async issueToken(userId, deviceId, withRefresh) {
|
|
115
|
+
async issueToken(userId, deviceId, withRefresh, opts = {}) {
|
|
97
116
|
const token = `mat_${randomToken()}`;
|
|
98
|
-
const lifetime = this.hs.config.accessTokenLifetimeMs;
|
|
117
|
+
const lifetime = opts.lifetimeMs ?? this.hs.config.accessTokenLifetimeMs;
|
|
99
118
|
const refresh = withRefresh ? `mar_${randomToken()}` : null;
|
|
100
119
|
await this.hs.db.create("access_tokens", {
|
|
101
120
|
token,
|
|
102
121
|
user_id: userId,
|
|
103
122
|
device_id: deviceId,
|
|
104
123
|
refresh_token: refresh,
|
|
105
|
-
expires_ts: refresh && lifetime ? Date.now() + lifetime : null
|
|
124
|
+
expires_ts: refresh && lifetime ? Date.now() + lifetime : null,
|
|
125
|
+
client_id: opts.clientId ?? null,
|
|
126
|
+
scope: opts.scope ?? null,
|
|
127
|
+
refresh_parent: opts.parent ?? null,
|
|
128
|
+
refresh_used: false
|
|
106
129
|
});
|
|
107
130
|
const out = {
|
|
108
131
|
user_id: userId,
|
|
109
132
|
device_id: deviceId,
|
|
110
133
|
access_token: token
|
|
111
134
|
};
|
|
135
|
+
if (opts.scope) out.scope = opts.scope;
|
|
112
136
|
if (refresh) {
|
|
113
137
|
out.refresh_token = refresh;
|
|
114
138
|
if (lifetime) out.expires_in_ms = lifetime;
|
|
115
139
|
}
|
|
116
140
|
return out;
|
|
117
141
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return this.
|
|
142
|
+
/**
|
|
143
|
+
* Exchange a refresh token for new tokens.
|
|
144
|
+
*
|
|
145
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/66: the old row used to be deleted
|
|
146
|
+
* at once, so a client whose response was lost could not retry and was logged out, and a
|
|
147
|
+
* refresh token used twice went unnoticed. As the OAuth 2.0 API asks (and the legacy API
|
|
148
|
+
* allows): the old refresh token keeps working until the new access token is first used; a
|
|
149
|
+
* retry before that replaces the unused tokens. Once the new token was used, the old refresh
|
|
150
|
+
* token is spent, and presenting it again means it leaked, so the device is logged out.
|
|
151
|
+
*/
|
|
152
|
+
async refresh(refreshToken, opts = {}) {
|
|
153
|
+
return this.hs.cluster.lock(`refresh:${refreshToken}`, async () => {
|
|
154
|
+
const row = await this.hs.db.findOne("access_tokens", [{
|
|
155
|
+
field: "refresh_token",
|
|
156
|
+
value: refreshToken
|
|
157
|
+
}]);
|
|
158
|
+
if (!row) throw errors.unknownToken("Unknown refresh token");
|
|
159
|
+
if (opts.clientId !== void 0 && (row.client_id ?? null) !== opts.clientId) throw errors.unknownToken("Unknown refresh token");
|
|
160
|
+
if (row.refresh_used) {
|
|
161
|
+
this.hs.logger.warn("A spent refresh token was used again; logging out", row.user_id, row.device_id);
|
|
162
|
+
await this.hs.audit.record({
|
|
163
|
+
action: "logout",
|
|
164
|
+
actor: row.user_id,
|
|
165
|
+
target: row.device_id,
|
|
166
|
+
details: { refresh_token_reuse: true }
|
|
167
|
+
});
|
|
168
|
+
await this.deleteDevice(row.user_id, row.device_id);
|
|
169
|
+
throw errors.unknownToken("Refresh token was already used");
|
|
170
|
+
}
|
|
171
|
+
for (const unused of await this.hs.db.findMany("access_tokens", { where: [{
|
|
172
|
+
field: "refresh_parent",
|
|
173
|
+
value: row.token
|
|
174
|
+
}] })) await this.revokeToken(unused.token);
|
|
175
|
+
if (row.expires_ts === null || row.expires_ts > Date.now()) {
|
|
176
|
+
await this.hs.db.update("access_tokens", [{
|
|
177
|
+
field: "token",
|
|
178
|
+
value: row.token
|
|
179
|
+
}], { expires_ts: Date.now() });
|
|
180
|
+
this.cache.delete(row.token);
|
|
181
|
+
await this.hs.cluster.publish("auth", {
|
|
182
|
+
from: this.hs.instanceId,
|
|
183
|
+
token: row.token
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return this.issueToken(row.user_id, row.device_id, true, {
|
|
187
|
+
clientId: row.client_id,
|
|
188
|
+
lifetimeMs: opts.lifetimeMs,
|
|
189
|
+
parent: row.token,
|
|
190
|
+
scope: row.scope
|
|
191
|
+
});
|
|
192
|
+
});
|
|
130
193
|
}
|
|
131
194
|
/** Drop cached sessions of a user (after admin/deactivation changes). */
|
|
132
195
|
async invalidateUser(userId) {
|
|
133
|
-
for (const [token,
|
|
196
|
+
for (const [token, e] of this.cache) if (e.session.userId === userId) this.cache.delete(token);
|
|
134
197
|
await this.hs.cluster.publish("auth", {
|
|
135
198
|
from: this.hs.instanceId,
|
|
136
199
|
userId
|
package/dist/core/oauth.js
CHANGED
|
@@ -89,7 +89,8 @@ var OAuthDelegation = class {
|
|
|
89
89
|
deviceId,
|
|
90
90
|
accessToken: token,
|
|
91
91
|
isGuest: false,
|
|
92
|
-
isAdmin: !!user.is_admin || scopes.includes(this.options.adminScope ?? "urn:synapse:admin:*")
|
|
92
|
+
isAdmin: !!user.is_admin || scopes.includes(this.options.adminScope ?? "urn:synapse:admin:*"),
|
|
93
|
+
oauth: true
|
|
93
94
|
};
|
|
94
95
|
const ttl = this.options.cacheTtlMs ?? 6e4;
|
|
95
96
|
const exp = typeof claims.exp === "number" ? claims.exp * 1e3 : Infinity;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Homeserver } from "../homeserver.js";
|
|
2
|
+
//#region src/core/oauthserver.d.ts
|
|
3
|
+
export interface OAuthServerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Keep the legacy login API (`/login`, `/register`) next to the OAuth 2.0 API. Its `m.login.sso`
|
|
6
|
+
* flow then goes through the same web login and is marked `oauth_aware_preferred`. Default: true.
|
|
7
|
+
*/
|
|
8
|
+
legacyApi?: boolean;
|
|
9
|
+
/** Lifetime of access tokens issued through the OAuth 2.0 API, in ms. Default: 5 minutes. */
|
|
10
|
+
accessTokenLifetimeMs?: number;
|
|
11
|
+
/** Shown on the login and registration pages: the server's terms of service and privacy policy. */
|
|
12
|
+
termsUrl?: string;
|
|
13
|
+
policyUrl?: string;
|
|
14
|
+
/** How long a browser stays logged in to the web pages, in ms. Default: 7 days. */
|
|
15
|
+
webSessionLifetimeMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/** An OAuth error response (RFC 6749 section 5.2, RFC 7591 section 3.2.2). */
|
|
18
|
+
export declare class OAuthError extends Error {
|
|
19
|
+
readonly error: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
readonly status: number;
|
|
22
|
+
constructor(error: string, description: string, status?: number);
|
|
23
|
+
}
|
|
24
|
+
export interface ClientMetadata {
|
|
25
|
+
client_id: string;
|
|
26
|
+
client_uri: string;
|
|
27
|
+
client_name?: string;
|
|
28
|
+
logo_uri?: string;
|
|
29
|
+
tos_uri?: string;
|
|
30
|
+
policy_uri?: string;
|
|
31
|
+
redirect_uris: string[];
|
|
32
|
+
response_types: string[];
|
|
33
|
+
grant_types: string[];
|
|
34
|
+
token_endpoint_auth_method: "none";
|
|
35
|
+
application_type: "web" | "native";
|
|
36
|
+
}
|
|
37
|
+
/** A validated authorization request, waiting for the user to log in and consent. */
|
|
38
|
+
export interface PendingGrant {
|
|
39
|
+
clientId: string;
|
|
40
|
+
redirectUri: string;
|
|
41
|
+
responseMode: "query" | "fragment";
|
|
42
|
+
state?: string;
|
|
43
|
+
scope: string;
|
|
44
|
+
deviceId: string;
|
|
45
|
+
codeChallenge: string;
|
|
46
|
+
nonce?: string;
|
|
47
|
+
/** The client asked for the registration page (`prompt=create`). */
|
|
48
|
+
create: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface DeviceGrant {
|
|
51
|
+
clientId: string;
|
|
52
|
+
scope: string;
|
|
53
|
+
deviceId: string;
|
|
54
|
+
userCode: string;
|
|
55
|
+
status: "pending" | "approved" | "denied";
|
|
56
|
+
userId?: string;
|
|
57
|
+
lastPoll: number;
|
|
58
|
+
expires: number;
|
|
59
|
+
}
|
|
60
|
+
export interface TokenResponse {
|
|
61
|
+
access_token: string;
|
|
62
|
+
token_type: "Bearer";
|
|
63
|
+
expires_in: number;
|
|
64
|
+
refresh_token: string;
|
|
65
|
+
scope: string;
|
|
66
|
+
id_token?: string;
|
|
67
|
+
}
|
|
68
|
+
export declare class OAuthServer {
|
|
69
|
+
private hs;
|
|
70
|
+
readonly options: OAuthServerOptions;
|
|
71
|
+
/** The issuer identifier: the public base URL with a trailing slash. */
|
|
72
|
+
readonly issuer: string;
|
|
73
|
+
constructor(hs: Homeserver, options: OAuthServerOptions, publicBaseUrl: string);
|
|
74
|
+
get legacyApi(): boolean;
|
|
75
|
+
get accessTokenLifetimeMs(): number;
|
|
76
|
+
/** Absolute URL of a path under the OAuth prefix. */
|
|
77
|
+
url(path: string): string;
|
|
78
|
+
get accountManagementUri(): string;
|
|
79
|
+
/** Authorization server metadata (RFC 8414), served at `/_matrix/client/v1/auth_metadata`. */
|
|
80
|
+
metadata(): Record<string, unknown>;
|
|
81
|
+
/** The server's signing key as a JWK set (RFC 8037), for ID tokens. */
|
|
82
|
+
jwks(): {
|
|
83
|
+
keys: Record<string, string>[];
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Register a client. Each login registers again (the spec asks clients to), so identical
|
|
87
|
+
* metadata gets the same client ID, derived from it, instead of a new row every time.
|
|
88
|
+
*/
|
|
89
|
+
registerClient(raw: Record<string, unknown>): Promise<ClientMetadata>;
|
|
90
|
+
client(clientId: string | null): Promise<ClientMetadata | null>;
|
|
91
|
+
/** Delete old registrations that no token uses (the spec forbids deleting ones in use). */
|
|
92
|
+
prune(): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Check an authorization request. Problems with the client or the redirect URI are thrown as
|
|
95
|
+
* `OAuthError` with `redirect: false` semantics (shown to the user: RFC 6749 section 4.1.2.1
|
|
96
|
+
* forbids redirecting to an unverified URI); the rest come back as `{ error }` to redirect.
|
|
97
|
+
*/
|
|
98
|
+
checkAuthorizationRequest(params: URLSearchParams): Promise<{
|
|
99
|
+
client: ClientMetadata;
|
|
100
|
+
grant: PendingGrant;
|
|
101
|
+
} | {
|
|
102
|
+
client: ClientMetadata;
|
|
103
|
+
redirect: string;
|
|
104
|
+
error: OAuthError;
|
|
105
|
+
}>;
|
|
106
|
+
/** The client's redirect URI with the response parameters (and RFC 9207 `iss`). */
|
|
107
|
+
callbackUrl(redirectUri: string, mode: "query" | "fragment", params: Record<string, string | undefined>): string;
|
|
108
|
+
saveGrant(grant: PendingGrant): Promise<string>;
|
|
109
|
+
grant(id: string): Promise<PendingGrant | null>;
|
|
110
|
+
/** The user approved: issue a code and return where the browser goes next. */
|
|
111
|
+
approveGrant(id: string, grant: PendingGrant, userId: string, authTime: number): Promise<string>;
|
|
112
|
+
denyGrant(id: string, grant: PendingGrant): Promise<string>;
|
|
113
|
+
token(params: URLSearchParams, clientId: string | null): Promise<TokenResponse>;
|
|
114
|
+
private exchangeCode;
|
|
115
|
+
private issue;
|
|
116
|
+
private tokenResponse;
|
|
117
|
+
/** An OpenID Connect ID token (EdDSA with the server's signing key). */
|
|
118
|
+
private idToken;
|
|
119
|
+
startDeviceGrant(clientId: string | null, scope: string | null): Promise<{
|
|
120
|
+
device_code: string;
|
|
121
|
+
user_code: string;
|
|
122
|
+
verification_uri: string;
|
|
123
|
+
verification_uri_complete: string;
|
|
124
|
+
expires_in: number;
|
|
125
|
+
interval: number;
|
|
126
|
+
}>;
|
|
127
|
+
/** The device grant a user code belongs to (typed in any case, with or without the dash). */
|
|
128
|
+
deviceGrantByUserCode(raw: string): Promise<{
|
|
129
|
+
deviceCode: string;
|
|
130
|
+
grant: DeviceGrant;
|
|
131
|
+
} | null>;
|
|
132
|
+
decideDeviceGrant(deviceCode: string, grant: DeviceGrant, userId: string, approve: boolean): Promise<void>;
|
|
133
|
+
private pollDevice;
|
|
134
|
+
/**
|
|
135
|
+
* Revoke an access or refresh token (RFC 7009). The spec revokes both tokens of the pair; a
|
|
136
|
+
* logout through this endpoint ends the device's session like `/logout` does. Unknown tokens
|
|
137
|
+
* are not an error.
|
|
138
|
+
*/
|
|
139
|
+
revoke(token: string | null): Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|