1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Improve types around login, registration, UIA and identity servers (#3537)

This commit is contained in:
Michael Telatynski
2023-07-04 14:49:24 +01:00
committed by GitHub
parent 89cabc4912
commit 1c1ac137d3
6 changed files with 443 additions and 148 deletions

View File

@ -94,7 +94,6 @@ describe("InteractiveAuth", () => {
authData: {
session: "sessionId",
flows: [{ stages: [AuthType.Password] }],
errcode: "MockError0",
params: {
[AuthType.Password]: { param: "aa" },
},

View File

@ -15,6 +15,7 @@ limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue";
import { IClientWellKnown } from "../client";
// disable lint because these are wire responses
/* eslint-disable camelcase */
@ -79,19 +80,6 @@ export interface IIdentityProvider {
brand?: IdentityProviderBrand | string;
}
/**
* Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
*/
/* eslint-disable camelcase */
export interface ILoginParams {
identifier?: object;
password?: string;
token?: string;
device_id?: string;
initial_device_display_name?: string;
}
/* eslint-enable camelcase */
export enum SSOAction {
/** The user intends to login to an existing account */
LOGIN = "login",
@ -100,6 +88,160 @@ export enum SSOAction {
REGISTER = "register",
}
/**
* A client can identify a user using their Matrix ID.
* This can either be the fully qualified Matrix user ID, or just the localpart of the user ID.
* @see https://spec.matrix.org/v1.7/client-server-api/#matrix-user-id
*/
type UserLoginIdentifier = {
type: "m.id.user";
user: string;
};
/**
* A client can identify a user using a 3PID associated with the users account on the homeserver,
* where the 3PID was previously associated using the /account/3pid API.
* See the 3PID Types Appendix for a list of Third-party ID media.
* @see https://spec.matrix.org/v1.7/client-server-api/#third-party-id
*/
type ThirdPartyLoginIdentifier = {
type: "m.id.thirdparty";
medium: string;
address: string;
};
/**
* A client can identify a user using a phone number associated with the users account,
* where the phone number was previously associated using the /account/3pid API.
* The phone number can be passed in as entered by the user; the homeserver will be responsible for canonicalising it.
* If the client wishes to canonicalise the phone number,
* then it can use the m.id.thirdparty identifier type with a medium of msisdn instead.
*
* The country is the two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone should be parsed as if it were dialled from.
*
* @see https://spec.matrix.org/v1.7/client-server-api/#phone-number
*/
type PhoneLoginIdentifier = {
type: "m.id.phone";
country: string;
phone: string;
};
type SpecUserIdentifier = UserLoginIdentifier | ThirdPartyLoginIdentifier | PhoneLoginIdentifier;
/**
* User Identifiers usable for login & user-interactive authentication.
*
* Extensibly allows more than Matrix specified identifiers.
*/
export type UserIdentifier =
| SpecUserIdentifier
| { type: Exclude<string, SpecUserIdentifier["type"]>; [key: string]: any };
/**
* Request body for POST /login request
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3login
*/
export interface LoginRequest {
/**
* The login type being used.
*/
type: "m.login.password" | "m.login.token" | string;
/**
* Third-party identifier for the user.
* @deprecated in favour of `identifier`.
*/
address?: string;
/**
* ID of the client device.
* If this does not correspond to a known client device, a new device will be created.
* The given device ID must not be the same as a cross-signing key ID.
* The server will auto-generate a device_id if this is not specified.
*/
device_id?: string;
/**
* Identification information for a user
*/
identifier?: UserIdentifier;
/**
* A display name to assign to the newly-created device.
* Ignored if device_id corresponds to a known device.
*/
initial_device_display_name?: string;
/**
* When logging in using a third-party identifier, the medium of the identifier.
* Must be `email`.
* @deprecated in favour of `identifier`.
*/
medium?: "email";
/**
* Required when type is `m.login.password`. The users password.
*/
password?: string;
/**
* If true, the client supports refresh tokens.
*/
refresh_token?: boolean;
/**
* Required when type is `m.login.token`. Part of Token-based login.
*/
token?: string;
/**
* The fully qualified user ID or just local part of the user ID, to log in.
* @deprecated in favour of identifier.
*/
user?: string;
// Extensible
[key: string]: any;
}
// Export for backwards compatibility
export type ILoginParams = LoginRequest;
/**
* Response body for POST /login request
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3login
*/
export interface LoginResponse {
/**
* An access token for the account.
* This access token can then be used to authorize other requests.
*/
access_token: string;
/**
* ID of the logged-in device.
* Will be the same as the corresponding parameter in the request, if one was specified.
*/
device_id: string;
/**
* The fully-qualified Matrix ID for the account.
*/
user_id: string;
/**
* The lifetime of the access token, in milliseconds.
* Once the access token has expired a new access token can be obtained by using the provided refresh token.
* If no refresh token is provided, the client will need to re-log in to obtain a new access token.
* If not given, the client can assume that the access token will not expire.
*/
expires_in_ms?: number;
/**
* A refresh token for the account.
* This token can be used to obtain a new access token when it expires by calling the /refresh endpoint.
*/
refresh_token?: string;
/**
* Optional client configuration provided by the server.
* If present, clients SHOULD use the provided object to reconfigure themselves, optionally validating the URLs within.
* This object takes the same form as the one returned from .well-known autodiscovery.
*/
well_known?: IClientWellKnown;
/**
* The server_name of the homeserver on which the account has been registered.
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
*/
home_server?: string;
}
/**
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
* `m.login.token` issuance request.

116
src/@types/registration.ts Normal file
View File

@ -0,0 +1,116 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { AuthDict } from "../interactive-auth";
/**
* The request body of a call to `POST /_matrix/client/v3/register`.
*
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
*/
export interface RegisterRequest {
/**
* Additional authentication information for the user-interactive authentication API.
* Note that this information is not used to define how the registered user should be authenticated,
* but is instead used to authenticate the register call itself.
*/
auth?: AuthDict;
/**
* The basis for the localpart of the desired Matrix ID.
* If omitted, the homeserver MUST generate a Matrix ID local part.
*/
username?: string;
/**
* The desired password for the account.
*/
password?: string;
/**
* If true, the client supports refresh tokens.
*/
refresh_token?: boolean;
/**
* If true, an access_token and device_id should not be returned from this call, therefore preventing an automatic login.
* Defaults to false.
*/
inhibit_login?: boolean;
/**
* A display name to assign to the newly-created device.
* Ignored if device_id corresponds to a known device.
*/
initial_device_display_name?: string;
/**
* @deprecated missing in the spec
*/
guest_access_token?: string;
/**
* @deprecated missing in the spec
*/
x_show_msisdn?: boolean;
/**
* @deprecated missing in the spec
*/
bind_msisdn?: boolean;
/**
* @deprecated missing in the spec
*/
bind_email?: boolean;
}
/**
* The result of a successful call to `POST /_matrix/client/v3/register`.
*
* @see https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
*/
export interface RegisterResponse {
/**
* The fully-qualified Matrix user ID (MXID) that has been registered.
*/
user_id: string;
/**
* An access token for the account.
* This access token can then be used to authorize other requests.
* Required if the inhibit_login option is false.
*/
access_token?: string;
/**
* ID of the registered device.
* Will be the same as the corresponding parameter in the request, if one was specified.
* Required if the inhibit_login option is false.
*/
device_id?: string;
/**
* The lifetime of the access token, in milliseconds.
* Once the access token has expired a new access token can be obtained by using the provided refresh token.
* If no refresh token is provided, the client will need to re-log in to obtain a new access token.
* If not given, the client can assume that the access token will not expire.
*
* Omitted if the inhibit_login option is true.
*/
expires_in_ms?: number;
/**
* A refresh token for the account.
* This token can be used to obtain a new access token when it expires by calling the /refresh endpoint.
*
* Omitted if the inhibit_login option is true.
*/
refresh_token?: string;
/**
* The server_name of the homeserver on which the account has been registered.
*
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
*/
home_server?: string;
}

View File

@ -101,7 +101,7 @@ import {
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
import { MatrixScheduler } from "./scheduler";
import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon";
import { IAuthData, IAuthDict } from "./interactive-auth";
import { AuthDict } from "./interactive-auth";
import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator";
import { CrossSigningKey, ICreateSecretStorageOpts, IEncryptedEventInfo, IRecoveryKey } from "./crypto/api";
import { EventTimelineSet } from "./models/event-timeline-set";
@ -178,7 +178,14 @@ import { IThreepid } from "./@types/threepids";
import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base";
import { GroupCall, IGroupCallDataChannelOptions, GroupCallIntent, GroupCallType } from "./webrtc/groupCall";
import { MediaHandler } from "./webrtc/mediaHandler";
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
import {
LoginTokenPostResponse,
ILoginFlowsResponse,
IRefreshTokenResponse,
SSOAction,
LoginResponse,
LoginRequest,
} from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
@ -209,6 +216,7 @@ import {
ServerSideSecretStorage,
ServerSideSecretStorageImpl,
} from "./secret-storage";
import { RegisterRequest, RegisterResponse } from "./@types/registration";
export type Store = IStore;
@ -717,18 +725,8 @@ interface IJoinedMembersResponse {
};
}
export interface IRegisterRequestParams {
auth?: IAuthDict;
username?: string;
password?: string;
refresh_token?: boolean;
guest_access_token?: string;
x_show_msisdn?: boolean;
bind_msisdn?: boolean;
bind_email?: boolean;
inhibit_login?: boolean;
initial_device_display_name?: string;
}
// Re-export for backwards compatibility
export type IRegisterRequestParams = RegisterRequest;
export interface IPublicRoomsChunkRoom {
room_id: string;
@ -7653,7 +7651,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param bindThreepids - Set key 'email' to true to bind any email
* threepid uses during registration in the identity server. Set 'msisdn' to
* true to bind msisdn.
* @returns Promise which resolves: TODO
* @returns Promise which resolves to a RegisterResponse object
* @returns Rejects: with an error response.
*/
public register(
@ -7664,7 +7662,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
bindThreepids?: boolean | null | { email?: boolean; msisdn?: boolean },
guestAccessToken?: string,
inhibitLogin?: boolean,
): Promise<IAuthData> {
): Promise<RegisterResponse> {
// backwards compat
if (bindThreepids === true) {
bindThreepids = { email: true };
@ -7675,7 +7673,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
auth.session = sessionId;
}
const params: IRegisterRequestParams = {
const params: RegisterRequest = {
auth: auth,
refresh_token: true, // always ask for a refresh token - does nothing if unsupported
};
@ -7731,8 +7729,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* `{ user_id, device_id, access_token, home_server }`
* @returns Rejects: with an error response.
*/
public registerGuest({ body }: { body?: any } = {}): Promise<any> {
// TODO: Types
public registerGuest({ body }: { body?: RegisterRequest } = {}): Promise<RegisterResponse> {
return this.registerRequest(body || {}, "guest");
}
@ -7742,7 +7739,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: to the /register response
* @returns Rejects: with an error response.
*/
public registerRequest(data: IRegisterRequestParams, kind?: string): Promise<IAuthData> {
public registerRequest(data: RegisterRequest, kind?: string): Promise<RegisterResponse> {
const params: { kind?: string } = {};
if (kind) {
params.kind = kind;
@ -7795,23 +7792,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* @returns Promise which resolves: TODO
* @returns Promise which resolves to a LoginResponse object
* @returns Rejects: with an error response.
*/
public login(loginType: string, data: any): Promise<any> {
// TODO: Types
const loginData = {
type: loginType,
};
// merge data into loginData
Object.assign(loginData, data);
public login(loginType: LoginRequest["type"], data: Omit<LoginRequest, "type">): Promise<LoginResponse> {
return this.http
.authedRequest<{
access_token?: string;
user_id?: string;
}>(Method.Post, "/login", undefined, loginData)
.authedRequest<LoginResponse>(Method.Post, "/login", undefined, {
...data,
type: loginType,
})
.then((response) => {
if (response.access_token && response.user_id) {
this.http.opts.accessToken = response.access_token;
@ -7824,11 +7813,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* @returns Promise which resolves: TODO
* @returns Promise which resolves to a LoginResponse object
* @returns Rejects: with an error response.
*/
public loginWithPassword(user: string, password: string): Promise<any> {
// TODO: Types
public loginWithPassword(user: string, password: string): Promise<LoginResponse> {
return this.login("m.login.password", {
user: user,
password: password,
@ -7837,11 +7825,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* @param relayState - URL Callback after SAML2 Authentication
* @returns Promise which resolves: TODO
* @returns Promise which resolves to a LoginResponse object
* @returns Rejects: with an error response.
* @deprecated this isn't in the Matrix spec anymore
*/
public loginWithSAML2(relayState: string): Promise<any> {
// TODO: Types
public loginWithSAML2(relayState: string): Promise<LoginResponse> {
return this.login("m.login.saml2", {
relay_state: relayState,
});
@ -7881,11 +7869,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* @param token - Login token previously received from homeserver
* @returns Promise which resolves: TODO
* @returns Promise which resolves to a LoginResponse object
* @returns Rejects: with an error response.
*/
public loginWithToken(token: string): Promise<any> {
// TODO: Types
public loginWithToken(token: string): Promise<LoginResponse> {
return this.login("m.login.token", {
token: token,
});
@ -7929,7 +7916,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* to false.
* @returns Promise which resolves: On success, the empty object
*/
public deactivateAccount(auth?: any, erase?: boolean): Promise<{}> {
public deactivateAccount(auth?: any, erase?: boolean): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
const body: any = {};
if (auth) {
body.auth = auth;
@ -7950,7 +7937,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: On success, the token response
* or UIA auth data.
*/
public async requestLoginToken(auth?: IAuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
public async requestLoginToken(auth?: AuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
// use capabilities to determine which revision of the MSC is being used
const capabilities = await this.getCapabilities();
// use r1 endpoint if capability is exposed otherwise use old r0 endpoint
@ -8590,7 +8577,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: to an empty object `{}`
* @returns Rejects: with an error response.
*/
public setPassword(authDict: IAuthDict, newPassword: string, logoutDevices?: boolean): Promise<{}> {
public setPassword(authDict: AuthDict, newPassword: string, logoutDevices?: boolean): Promise<{}> {
const path = "/account/password";
const data = {
auth: authDict,
@ -8648,7 +8635,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: result object
* @returns Rejects: with an error response.
*/
public deleteDevice(deviceId: string, auth?: IAuthDict): Promise<IAuthData | {}> {
public deleteDevice(deviceId: string, auth?: AuthDict): Promise<{}> {
const path = utils.encodeUri("/devices/$device_id", {
$device_id: deviceId,
});
@ -8670,7 +8657,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: result object
* @returns Rejects: with an error response.
*/
public deleteMultipleDevices(devices: string[], auth?: IAuthDict): Promise<IAuthData | {}> {
public deleteMultipleDevices(devices: string[], auth?: AuthDict): Promise<{}> {
const body: any = { devices };
if (auth) {
@ -8955,7 +8942,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(Method.Get, "/keys/changes", qps);
}
public uploadDeviceSigningKeys(auth?: IAuthDict, keys?: CrossSigningKeys): Promise<{}> {
public uploadDeviceSigningKeys(auth?: AuthDict, keys?: CrossSigningKeys): Promise<{}> {
// API returns empty object
const data = Object.assign({}, keys);
if (auth) Object.assign(data, { auth });
@ -9168,8 +9155,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param identityAccessToken - The access token for the identity server.
* @returns The hashing information for the identity server.
*/
public getIdentityHashDetails(identityAccessToken: string): Promise<any> {
// TODO: Types
public getIdentityHashDetails(identityAccessToken: string): Promise<{
/**
* The algorithms the server supports. Must contain at least sha256.
*/
algorithms: string[];
/**
* The pepper the client MUST use in hashing identifiers,
* and MUST supply to the /lookup endpoint when performing lookups.
*/
lookup_pepper: string;
}> {
return this.http.idServerRequest(
Method.Get,
"/hash_details",
@ -9277,8 +9273,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* exists
* @returns Rejects: with an error response.
*/
public async lookupThreePid(medium: string, address: string, identityAccessToken: string): Promise<any> {
// TODO: Types
public async lookupThreePid(
medium: string,
address: string,
identityAccessToken: string,
): Promise<
| {
address: string;
medium: string;
mxid: string;
}
| {}
> {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
@ -9314,8 +9320,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: Lookup results from IS.
* @returns Rejects: with an error response.
*/
public async bulkLookupThreePids(query: [string, string][], identityAccessToken: string): Promise<any> {
// TODO: Types
public async bulkLookupThreePids(
query: [string, string][],
identityAccessToken: string,
): Promise<{
threepids: [medium: string, address: string, mxid: string][];
}> {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
@ -9353,8 +9363,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: an object with account info.
* @returns Rejects: with an error response.
*/
public getIdentityAccount(identityAccessToken: string): Promise<any> {
// TODO: Types
public getIdentityAccount(identityAccessToken: string): Promise<{ user_id: string }> {
return this.http.idServerRequest(Method.Get, "/account", undefined, IdentityPrefix.V2, identityAccessToken);
}
@ -9447,7 +9456,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves to the result object
*/
public getThirdpartyUser(protocol: string, params: any): Promise<IThirdPartyUser[]> {
// TODO: Types
const path = utils.encodeUri("/thirdparty/user/$protocol", {
$protocol: protocol,
});

View File

@ -21,6 +21,7 @@ import { MatrixClient } from "./client";
import { defer, IDeferred } from "./utils";
import { MatrixError } from "./http-api";
import { UIAResponse } from "./@types/uia";
import { UserIdentifier } from "./@types/auth";
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
@ -51,22 +52,25 @@ export interface IStageStatus {
* @see https://spec.matrix.org/v1.6/client-server-api/#user-interactive-api-in-the-rest-api
*/
export interface IAuthData {
// XXX: many of the fields here (`type`, `available_flows`, `required_stages`, etc) look like they
// shouldn't be here. They aren't in the spec and it's unclear what they are supposed to do. Be wary of using them.
/**
* This is a session identifier that the client must pass back to the home server,
* if one is provided, in subsequent attempts to authenticate in the same API call.
*/
session?: string;
type?: string;
/**
* A list of the stages the client has completed successfully
*/
completed?: string[];
/**
* A list of the login flows supported by the server for this API.
*/
flows?: UIAFlow[];
available_flows?: UIAFlow[];
stages?: string[];
required_stages?: AuthType[];
/**
* Contains any information that the client will need to know in order to use a given type of authentication.
* For each login type presented, that type may be present as a key in this dictionary.
* For example, the public part of an OAuth client ID could be given here.
*/
params?: Record<string, Record<string, any>>;
data?: Record<string, string>;
errcode?: string;
error?: string;
user_id?: string;
device_id?: string;
access_token?: string;
}
export enum AuthType {
@ -85,30 +89,62 @@ export enum AuthType {
UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token",
}
/**
* https://spec.matrix.org/v1.7/client-server-api/#password-based
*/
type PasswordDict = {
type: AuthType.Password;
identifier: UserIdentifier;
password: string;
session: string;
};
/**
* https://spec.matrix.org/v1.7/client-server-api/#google-recaptcha
*/
type RecaptchaDict = {
type: AuthType.Recaptcha;
response: string;
session: string;
};
interface ThreepidCreds {
sid: string;
client_secret: string;
id_server: string;
id_access_token: string;
}
/**
* https://spec.matrix.org/v1.7/client-server-api/#email-based-identity--homeserver
*/
type EmailIdentityDict = {
type: AuthType.Email;
threepid_creds: ThreepidCreds;
/**
* @deprecated in favour of `threepid_creds` - kept for backwards compatibility
*/
threepidCreds?: ThreepidCreds;
session: string;
};
/**
* The parameters which are submitted as the `auth` dict in a UIA request
*
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
export interface IAuthDict {
// [key: string]: any;
type?: string;
session?: string;
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
user?: string;
identifier?: any;
password?: string;
response?: string;
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
// eslint-disable-next-line camelcase
threepid_creds?: any;
threepidCreds?: any;
// For m.login.registration_token type
token?: string;
}
export type AuthDict =
| PasswordDict
| RecaptchaDict
| EmailIdentityDict
| { type: Exclude<string, AuthType>; [key: string]: any }
| {};
/**
* Backwards compatible export
* @deprecated in favour of AuthDict
*/
export type IAuthDict = AuthDict;
export class NoAuthFlowFoundError extends Error {
public name = "NoAuthFlowFoundError";
@ -129,7 +165,7 @@ export class NoAuthFlowFoundError extends Error {
*/
export type UIAuthCallback<T> = (makeRequest: (authData: IAuthDict) => Promise<UIAResponse<T>>) => Promise<T>;
interface IOpts {
interface IOpts<T> {
/**
* A matrix client to use for the auth process
*/
@ -170,7 +206,7 @@ interface IOpts {
* The busyChanged callback should be used instead of the background flag.
* Should return a promise which resolves to the successful response or rejects with a MatrixError.
*/
doRequest(auth: IAuthDict | null, background: boolean): Promise<IAuthData>;
doRequest(auth: AuthDict | null, background: boolean): Promise<T>;
/**
* Called when the status of the UI auth changes,
* ie. when the state of an auth stage changes of when the auth flow moves to a new stage.
@ -215,21 +251,23 @@ interface IOpts {
* submitAuthDict.
*
* @param opts - options object
* @typeParam T - the return type of the request when it is successful
*/
export class InteractiveAuth {
export class InteractiveAuth<T> {
private readonly matrixClient: MatrixClient;
private readonly inputs: IInputs;
private readonly clientSecret: string;
private readonly requestCallback: IOpts["doRequest"];
private readonly busyChangedCallback?: IOpts["busyChanged"];
private readonly stateUpdatedCallback: IOpts["stateUpdated"];
private readonly requestEmailTokenCallback: IOpts["requestEmailToken"];
private readonly requestCallback: IOpts<T>["doRequest"];
private readonly busyChangedCallback?: IOpts<T>["busyChanged"];
private readonly stateUpdatedCallback: IOpts<T>["stateUpdated"];
private readonly requestEmailTokenCallback: IOpts<T>["requestEmailToken"];
private readonly supportedStages?: Set<string>;
// The current latest data received from the server during the user interactive auth flow.
private data: IAuthData;
private emailSid?: string;
private requestingEmailToken = false;
private attemptAuthDeferred: IDeferred<IAuthData> | null = null;
private attemptAuthDeferred: IDeferred<T> | null = null;
private chosenFlow: UIAFlow | null = null;
private currentStage: string | null = null;
@ -239,9 +277,9 @@ export class InteractiveAuth {
// the promise the will resolve/reject when it completes
private submitPromise: Promise<void> | null = null;
public constructor(opts: IOpts) {
public constructor(opts: IOpts<T>) {
this.matrixClient = opts.matrixClient;
this.data = opts.authData || {};
this.data = opts.authData || { flows: [] };
this.requestCallback = opts.doRequest;
this.busyChangedCallback = opts.busyChanged;
// startAuthStage included for backwards compat
@ -262,7 +300,7 @@ export class InteractiveAuth {
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
* no suitable authentication flow can be found
*/
public attemptAuth(): Promise<IAuthData> {
public async attemptAuth(): Promise<T> {
// This promise will be quite long-lived and will resolve when the
// request is authenticated and completes successfully.
this.attemptAuthDeferred = defer();
@ -270,10 +308,10 @@ export class InteractiveAuth {
const promise = this.attemptAuthDeferred.promise;
// if we have no flows, try a request to acquire the flows
if (!this.data?.flows) {
if (!(this.data as IAuthData)?.flows?.length) {
this.busyChangedCallback?.(true);
// use the existing sessionId, if one is present.
const auth = this.data.session ? { session: this.data.session } : null;
const auth = (this.data as IAuthData).session ? { session: (this.data as IAuthData).session } : null;
this.doRequest(auth).finally(() => {
this.busyChangedCallback?.(false);
});
@ -290,7 +328,7 @@ export class InteractiveAuth {
* be resolved.
*/
public async poll(): Promise<void> {
if (!this.data.session) return;
if (!(this.data as IAuthData).session) return;
// likewise don't poll if there is no auth session in progress
if (!this.attemptAuthDeferred) return;
// if we currently have a request in flight, there's no point making
@ -330,7 +368,7 @@ export class InteractiveAuth {
* @returns session id
*/
public getSessionId(): string | undefined {
return this.data?.session;
return (this.data as IAuthData)?.session;
}
/**
@ -350,7 +388,7 @@ export class InteractiveAuth {
* @returns any parameters from the server for this stage
*/
public getStageParams(loginType: string): Record<string, any> | undefined {
return this.data.params?.[loginType];
return (this.data as IAuthData)?.params?.[loginType];
}
public getChosenFlow(): UIAFlow | null {
@ -391,9 +429,9 @@ export class InteractiveAuth {
// use the sessionid from the last request, if one is present.
let auth: IAuthDict;
if (this.data.session) {
if ((this.data as IAuthData)?.session) {
auth = {
session: this.data.session,
session: (this.data as IAuthData).session,
};
Object.assign(auth, authData);
} else {
@ -451,7 +489,7 @@ export class InteractiveAuth {
this.inputs.emailAddress!,
this.clientSecret,
this.emailAttempt++,
this.data.session!,
(this.data as IAuthData).session!,
);
this.emailSid = requestTokenResult.sid;
logger.trace("Email token request succeeded");
@ -480,10 +518,12 @@ export class InteractiveAuth {
this.attemptAuthDeferred!.resolve(result);
this.attemptAuthDeferred = null;
} catch (error) {
const matrixError = error instanceof MatrixError ? error : null;
// sometimes UI auth errors don't come with flows
const errorFlows = (<MatrixError>error).data?.flows ?? null;
const haveFlows = this.data.flows || Boolean(errorFlows);
if ((<MatrixError>error).httpStatus !== 401 || !(<MatrixError>error).data || !haveFlows) {
const errorFlows = matrixError?.data?.flows ?? null;
const haveFlows = (this.data as IAuthData)?.flows || Boolean(errorFlows);
if (!matrixError || matrixError.httpStatus !== 401 || !matrixError.data || !haveFlows) {
// doesn't look like an interactive-auth failure.
if (!background) {
this.attemptAuthDeferred?.reject(error);
@ -494,24 +534,22 @@ export class InteractiveAuth {
logger.log("Background poll request failed doing UI auth: ignoring", error);
}
}
if (!(<MatrixError>error).data) {
(<MatrixError>error).data = {};
if (matrixError && !matrixError.data) {
matrixError.data = {};
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (
!(<MatrixError>error).data.flows &&
!(<MatrixError>error).data.completed &&
!(<MatrixError>error).data.session
) {
(<MatrixError>error).data.flows = this.data.flows;
(<MatrixError>error).data.completed = this.data.completed;
(<MatrixError>error).data.session = this.data.session;
if (matrixError && !matrixError.data.flows && !matrixError.data.completed && !matrixError.data.session) {
matrixError.data.flows = (this.data as IAuthData).flows;
matrixError.data.completed = (this.data as IAuthData).completed;
matrixError.data.session = (this.data as IAuthData).session;
}
if (matrixError) {
this.data = matrixError.data as IAuthData;
}
this.data = (<MatrixError>error).data;
try {
this.startNextAuthStage();
} catch (e) {
@ -563,14 +601,6 @@ export class InteractiveAuth {
return;
}
if (this.data?.errcode || this.data?.error) {
this.stateUpdatedCallback(nextStage, {
errcode: this.data?.errcode || "",
error: this.data?.error || "",
});
return;
}
this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE ? { emailSid: this.emailSid } : {});
}
@ -618,7 +648,7 @@ export class InteractiveAuth {
* @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
private chooseFlow(): UIAFlow {
const flows = this.data.flows || [];
const flows = (this.data as IAuthData)?.flows || [];
// we've been given an email or we've already done an email part
const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid);
@ -659,7 +689,7 @@ export class InteractiveAuth {
* @returns login type
*/
private firstUncompletedStage(flow: UIAFlow): AuthType | string | undefined {
const completed = this.data.completed || [];
const completed = (this.data as IAuthData)?.completed || [];
return flow.stages.find((stageType) => !completed.includes(stageType));
}
}

View File

@ -1376,10 +1376,10 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
const relation = this.getWireContent()?.["m.relates_to"];
if (
this.isState() &&
relation?.rel_type &&
!!relation?.rel_type &&
([RelationType.Replace, RelationType.Thread] as string[]).includes(relation.rel_type)
) {
// State events cannot be m.replace relations
// State events cannot be m.replace or m.thread relations
return false;
}
return !!(relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true));