From 1c1ac137d3626b4c79c1b0c420b164451a9f6bcc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 Jul 2023 14:49:24 +0100 Subject: [PATCH] Improve types around login, registration, UIA and identity servers (#3537) --- spec/unit/interactive-auth.spec.ts | 1 - src/@types/auth.ts | 166 +++++++++++++++++++++++++-- src/@types/registration.ts | 116 +++++++++++++++++++ src/client.ts | 124 ++++++++++---------- src/interactive-auth.ts | 178 +++++++++++++++++------------ src/models/event.ts | 4 +- 6 files changed, 442 insertions(+), 147 deletions(-) create mode 100644 src/@types/registration.ts diff --git a/spec/unit/interactive-auth.spec.ts b/spec/unit/interactive-auth.spec.ts index 554d747352d..ea11fc4c900 100644 --- a/spec/unit/interactive-auth.spec.ts +++ b/spec/unit/interactive-auth.spec.ts @@ -94,7 +94,6 @@ describe("InteractiveAuth", () => { authData: { session: "sessionId", flows: [{ stages: [AuthType.Password] }], - errcode: "MockError0", params: { [AuthType.Password]: { param: "aa" }, }, diff --git a/src/@types/auth.ts b/src/@types/auth.ts index f34e45bf130..ac0f6c9d4af 100644 --- a/src/@types/auth.ts +++ b/src/@types/auth.ts @@ -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,25 +80,166 @@ export interface IIdentityProvider { brand?: IdentityProviderBrand | string; } +export enum SSOAction { + /** The user intends to login to an existing account */ + LOGIN = "login", + + /** The user intends to register for a new account */ + REGISTER = "register", +} + /** - * Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login + * 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 */ -/* eslint-disable camelcase */ -export interface ILoginParams { - identifier?: object; - password?: string; - token?: string; +type UserLoginIdentifier = { + type: "m.id.user"; + user: string; +}; + +/** + * A client can identify a user using a 3PID associated with the user’s 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 user’s 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; [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 user’s 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; } -/* eslint-enable camelcase */ -export enum SSOAction { - /** The user intends to login to an existing account */ - LOGIN = "login", +// Export for backwards compatibility +export type ILoginParams = LoginRequest; - /** The user intends to register for a new account */ - REGISTER = "register", +/** + * 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; } /** diff --git a/src/@types/registration.ts b/src/@types/registration.ts new file mode 100644 index 00000000000..98eb1977eb6 --- /dev/null +++ b/src/@types/registration.ts @@ -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; +} diff --git a/src/client.ts b/src/client.ts index 85d35db1002..87464cf640e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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 { + ): Promise { // backwards compat if (bindThreepids === true) { bindThreepids = { email: true }; @@ -7675,7 +7673,7 @@ export class MatrixClient extends TypedEventEmitter { - // TODO: Types + public registerGuest({ body }: { body?: RegisterRequest } = {}): Promise { return this.registerRequest(body || {}, "guest"); } @@ -7742,7 +7739,7 @@ export class MatrixClient extends TypedEventEmitter { + public registerRequest(data: RegisterRequest, kind?: string): Promise { const params: { kind?: string } = {}; if (kind) { params.kind = kind; @@ -7795,23 +7792,15 @@ export class MatrixClient extends TypedEventEmitter { - // TODO: Types - const loginData = { - type: loginType, - }; - - // merge data into loginData - Object.assign(loginData, data); - + public login(loginType: LoginRequest["type"], data: Omit): Promise { return this.http - .authedRequest<{ - access_token?: string; - user_id?: string; - }>(Method.Post, "/login", undefined, loginData) + .authedRequest(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 { - // TODO: Types + public loginWithPassword(user: string, password: string): Promise { return this.login("m.login.password", { user: user, password: password, @@ -7837,11 +7825,11 @@ export class MatrixClient extends TypedEventEmitter { - // TODO: Types + public loginWithSAML2(relayState: string): Promise { return this.login("m.login.saml2", { relay_state: relayState, }); @@ -7881,11 +7869,10 @@ export class MatrixClient extends TypedEventEmitter { - // TODO: Types + public loginWithToken(token: string): Promise { return this.login("m.login.token", { token: token, }); @@ -7929,7 +7916,7 @@ export class MatrixClient extends TypedEventEmitter { + 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> { + public async requestLoginToken(auth?: AuthDict): Promise> { // 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 { + 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 { + 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 { + public deleteMultipleDevices(devices: string[], auth?: AuthDict): Promise<{}> { const body: any = { devices }; if (auth) { @@ -8955,7 +8942,7 @@ export class MatrixClient extends TypedEventEmitter { + 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 { - // 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 { - // 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 { - // 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 { - // 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 { - // TODO: Types const path = utils.encodeUri("/thirdparty/user/$protocol", { $protocol: protocol, }); diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index 4d3f643f0d7..e85ac9c2770 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -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>; - data?: Record; - 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; [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 = (makeRequest: (authData: IAuthDict) => Promise>) => Promise; -interface IOpts { +interface IOpts { /** * 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; + doRequest(auth: AuthDict | null, background: boolean): Promise; /** * 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 { 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["doRequest"]; + private readonly busyChangedCallback?: IOpts["busyChanged"]; + private readonly stateUpdatedCallback: IOpts["stateUpdated"]; + private readonly requestEmailTokenCallback: IOpts["requestEmailToken"]; private readonly supportedStages?: Set; + // 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 | null = null; + private attemptAuthDeferred: IDeferred | 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 | null = null; - public constructor(opts: IOpts) { + public constructor(opts: IOpts) { 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 { + public async attemptAuth(): Promise { // 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 { - 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 | 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 = (error).data?.flows ?? null; - const haveFlows = this.data.flows || Boolean(errorFlows); - if ((error).httpStatus !== 401 || !(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 (!(error).data) { - (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 ( - !(error).data.flows && - !(error).data.completed && - !(error).data.session - ) { - (error).data.flows = this.data.flows; - (error).data.completed = this.data.completed; - (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 = (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)); } } diff --git a/src/models/event.ts b/src/models/event.ts index 378d37b2811..992044f9fba 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -1376,10 +1376,10 @@ export class MatrixEvent extends TypedEventEmitter