From 633c92eae865e957121e08de634aeee11a8b3992 Mon Sep 17 00:00:00 2001 From: RaindropsSys Date: Mon, 24 Apr 2023 14:03:36 +0200 Subject: Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated) --- .../matrix-js-sdk/src/interactive-auth.ts | 617 +++++++++++++++++++++ 1 file changed, 617 insertions(+) create mode 100644 includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts') diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts new file mode 100644 index 0000000..7d9c183 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts @@ -0,0 +1,617 @@ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2019 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 { logger } from "./logger"; +import { MatrixClient } from "./client"; +import { defer, IDeferred } from "./utils"; +import { MatrixError } from "./http-api"; + +const EMAIL_STAGE_TYPE = "m.login.email.identity"; +const MSISDN_STAGE_TYPE = "m.login.msisdn"; + +export interface UIAFlow { + stages: AuthType[]; +} + +export interface IInputs { + // An email address. If supplied, a flow using email verification will be chosen. + emailAddress?: string; + // An ISO two letter country code. Gives the country that opts.phoneNumber should be resolved relative to. + phoneCountry?: string; + // A phone number. If supplied, a flow using phone number validation will be chosen. + phoneNumber?: string; + registrationToken?: string; +} + +export interface IStageStatus { + emailSid?: string; + errcode?: string; + error?: string; +} + +export interface IAuthData { + session?: string; + type?: string; + completed?: string[]; + flows?: UIAFlow[]; + available_flows?: UIAFlow[]; + stages?: string[]; + required_stages?: AuthType[]; + params?: Record>; + data?: Record; + errcode?: string; + error?: string; + user_id?: string; + device_id?: string; + access_token?: string; +} + +export enum AuthType { + Password = "m.login.password", + Recaptcha = "m.login.recaptcha", + Terms = "m.login.terms", + Email = "m.login.email.identity", + Msisdn = "m.login.msisdn", + Sso = "m.login.sso", + SsoUnstable = "org.matrix.login.sso", + Dummy = "m.login.dummy", + RegistrationToken = "m.login.registration_token", + // For backwards compatability with servers that have not yet updated to + // use the stable "m.login.registration_token" type. + // The authentication flow is the same in both cases. + UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token", +} + +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; +} + +class NoAuthFlowFoundError extends Error { + public name = "NoAuthFlowFoundError"; + + // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase + public constructor(m: string, public readonly required_stages: string[], public readonly flows: UIAFlow[]) { + super(m); + } +} + +interface IOpts { + /** + * A matrix client to use for the auth process + */ + matrixClient: MatrixClient; + /** + * Error response from the last request. If null, a request will be made with no auth before starting. + */ + authData?: IAuthData; + /** + * Inputs provided by the user and used by different stages of the auto process. + * The inputs provided will affect what flow is chosen. + */ + inputs?: IInputs; + /** + * If resuming an existing interactive auth session, the sessionId of that session. + */ + sessionId?: string; + /** + * If resuming an existing interactive auth session, the client secret for that session + */ + clientSecret?: string; + /** + * If returning from having completed m.login.email.identity auth, the sid for the email verification session. + */ + emailSid?: string; + + /** + * Called with the new auth dict to submit the request. + * Also passes a second deprecated arg which is a flag set to true if this request is a background request. + * 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: IAuthData | 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. + * The arguments are: the login type (eg m.login.password); and an object which is either an error or an + * informational object specific to the login type. + * If the 'errcode' key is defined, the object is an error, and has keys: + * errcode: string, the textual error code, eg. M_UNKNOWN + * error: string, human readable string describing the error + * + * The login type specific objects are as follows: + * m.login.email.identity: + * * emailSid: string, the sid of the active email auth session + */ + stateUpdated(nextStage: AuthType, status: IStageStatus): void; + + /** + * A function that takes the email address (string), clientSecret (string), attempt number (int) and + * sessionId (string) and calls the relevant requestToken function and returns the promise returned by that + * function. + * If the resulting promise rejects, the rejection will propagate through to the attemptAuth promise. + */ + requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>; + /** + * Called whenever the interactive auth logic becomes busy submitting information provided by the user or finishes. + * After this has been called with true the UI should indicate that a request is in progress + * until it is called again with false. + */ + busyChanged?(busy: boolean): void; + startAuthStage?(nextStage: string): Promise; // LEGACY +} + +/** + * Abstracts the logic used to drive the interactive auth process. + * + *

Components implementing an interactive auth flow should instantiate one of + * these, passing in the necessary callbacks to the constructor. They should + * then call attemptAuth, which will return a promise which will resolve or + * reject when the interactive-auth process completes. + * + *

Meanwhile, calls will be made to the startAuthStage and doRequest + * callbacks, and information gathered from the user can be submitted with + * submitAuthDict. + * + * @param opts - options object + */ +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 data: IAuthData; + private emailSid?: string; + private requestingEmailToken = false; + private attemptAuthDeferred: IDeferred | null = null; + private chosenFlow: UIAFlow | null = null; + private currentStage: string | null = null; + + private emailAttempt = 1; + + // if we are currently trying to submit an auth dict (which includes polling) + // the promise the will resolve/reject when it completes + private submitPromise: Promise | null = null; + + public constructor(opts: IOpts) { + this.matrixClient = opts.matrixClient; + this.data = opts.authData || {}; + this.requestCallback = opts.doRequest; + this.busyChangedCallback = opts.busyChanged; + // startAuthStage included for backwards compat + this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage; + this.requestEmailTokenCallback = opts.requestEmailToken; + this.inputs = opts.inputs || {}; + + if (opts.sessionId) this.data.session = opts.sessionId; + this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret(); + this.emailSid = opts.emailSid; + } + + /** + * begin the authentication process. + * + * @returns which resolves to the response on success, + * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if + * no suitable authentication flow can be found + */ + public attemptAuth(): Promise { + // This promise will be quite long-lived and will resolve when the + // request is authenticated and completes successfully. + this.attemptAuthDeferred = defer(); + // pluck the promise out now, as doRequest may clear before we return + const promise = this.attemptAuthDeferred.promise; + + // if we have no flows, try a request to acquire the flows + if (!this.data?.flows) { + this.busyChangedCallback?.(true); + // use the existing sessionId, if one is present. + const auth = this.data.session ? { session: this.data.session } : null; + this.doRequest(auth).finally(() => { + this.busyChangedCallback?.(false); + }); + } else { + this.startNextAuthStage(); + } + + return promise; + } + + /** + * Poll to check if the auth session or current stage has been + * completed out-of-band. If so, the attemptAuth promise will + * be resolved. + */ + public async poll(): Promise { + if (!this.data.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 + // another just to check what the status is + if (this.submitPromise) return; + + let authDict: IAuthDict = {}; + if (this.currentStage == EMAIL_STAGE_TYPE) { + // The email can be validated out-of-band, but we need to provide the + // creds so the HS can go & check it. + if (this.emailSid) { + const creds: Record = { + sid: this.emailSid, + client_secret: this.clientSecret, + }; + if (await this.matrixClient.doesServerRequireIdServerParam()) { + const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()!); + creds.id_server = idServerParsedUrl.host; + } + authDict = { + type: EMAIL_STAGE_TYPE, + // TODO: Remove `threepid_creds` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 + // See https://github.com/matrix-org/matrix-doc/issues/2220 + threepid_creds: creds, + threepidCreds: creds, + }; + } + } + + this.submitAuthDict(authDict, true); + } + + /** + * get the auth session ID + * + * @returns session id + */ + public getSessionId(): string | undefined { + return this.data?.session; + } + + /** + * get the client secret used for validation sessions + * with the identity server. + * + * @returns client secret + */ + public getClientSecret(): string { + return this.clientSecret; + } + + /** + * get the server params for a given stage + * + * @param loginType - login type for the stage + * @returns any parameters from the server for this stage + */ + public getStageParams(loginType: string): Record | undefined { + return this.data.params?.[loginType]; + } + + public getChosenFlow(): UIAFlow | null { + return this.chosenFlow; + } + + /** + * submit a new auth dict and fire off the request. This will either + * make attemptAuth resolve/reject, or cause the startAuthStage callback + * to be called for a new stage. + * + * @param authData - new auth dict to send to the server. Should + * include a `type` property denoting the login type, as well as any + * other params for that stage. + * @param background - If true, this request failing will not result + * in the attemptAuth promise being rejected. This can be set to true + * for requests that just poll to see if auth has been completed elsewhere. + */ + public async submitAuthDict(authData: IAuthDict, background = false): Promise { + if (!this.attemptAuthDeferred) { + throw new Error("submitAuthDict() called before attemptAuth()"); + } + + if (!background) { + this.busyChangedCallback?.(true); + } + + // if we're currently trying a request, wait for it to finish + // as otherwise we can get multiple 200 responses which can mean + // things like multiple logins for register requests. + // (but discard any exceptions as we only care when its done, + // not whether it worked or not) + while (this.submitPromise) { + try { + await this.submitPromise; + } catch (e) {} + } + + // use the sessionid from the last request, if one is present. + let auth: IAuthDict; + if (this.data.session) { + auth = { + session: this.data.session, + }; + Object.assign(auth, authData); + } else { + auth = authData; + } + + try { + // NB. the 'background' flag is deprecated by the busyChanged + // callback and is here for backwards compat + this.submitPromise = this.doRequest(auth, background); + await this.submitPromise; + } finally { + this.submitPromise = null; + if (!background) { + this.busyChangedCallback?.(false); + } + } + } + + /** + * Gets the sid for the email validation session + * Specific to m.login.email.identity + * + * @returns The sid of the email auth session + */ + public getEmailSid(): string | undefined { + return this.emailSid; + } + + /** + * Sets the sid for the email validation session + * This must be set in order to successfully poll for completion + * of the email validation. + * Specific to m.login.email.identity + * + * @param sid - The sid for the email validation session + */ + public setEmailSid(sid: string): void { + this.emailSid = sid; + } + + /** + * Requests a new email token and sets the email sid for the validation session + */ + public requestEmailToken = async (): Promise => { + if (!this.requestingEmailToken) { + logger.trace("Requesting email token. Attempt: " + this.emailAttempt); + // If we've picked a flow with email auth, we send the email + // now because we want the request to fail as soon as possible + // if the email address is not valid (ie. already taken or not + // registered, depending on what the operation is). + this.requestingEmailToken = true; + try { + const requestTokenResult = await this.requestEmailTokenCallback( + this.inputs.emailAddress!, + this.clientSecret, + this.emailAttempt++, + this.data.session!, + ); + this.emailSid = requestTokenResult.sid; + logger.trace("Email token request succeeded"); + } finally { + this.requestingEmailToken = false; + } + } else { + logger.warn("Could not request email token: Already requesting"); + } + }; + + /** + * Fire off a request, and either resolve the promise, or call + * startAuthStage. + * + * @internal + * @param auth - new auth dict, including session id + * @param background - If true, this request is a background poll, so it + * failing will not result in the attemptAuth promise being rejected. + * This can be set to true for requests that just poll to see if auth has + * been completed elsewhere. + */ + private async doRequest(auth: IAuthData | null, background = false): Promise { + try { + const result = await this.requestCallback(auth, background); + this.attemptAuthDeferred!.resolve(result); + this.attemptAuthDeferred = null; + } catch (error) { + // 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) { + // doesn't look like an interactive-auth failure. + if (!background) { + this.attemptAuthDeferred?.reject(error); + } else { + // We ignore all failures here (even non-UI auth related ones) + // since we don't want to suddenly fail if the internet connection + // had a blip whilst we were polling + logger.log("Background poll request failed doing UI auth: ignoring", error); + } + } + if (!(error).data) { + (error).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; + } + this.data = (error).data; + try { + this.startNextAuthStage(); + } catch (e) { + this.attemptAuthDeferred!.reject(e); + this.attemptAuthDeferred = null; + return; + } + + if (!this.emailSid && this.chosenFlow?.stages.includes(AuthType.Email)) { + try { + await this.requestEmailToken(); + // NB. promise is not resolved here - at some point, doRequest + // will be called again and if the user has jumped through all + // the hoops correctly, auth will be complete and the request + // will succeed. + // Also, we should expose the fact that this request has compledted + // so clients can know that the email has actually been sent. + } catch (e) { + // we failed to request an email token, so fail the request. + // This could be due to the email already beeing registered + // (or not being registered, depending on what we're trying + // to do) or it could be a network failure. Either way, pass + // the failure up as the user can't complete auth if we can't + // send the email, for whatever reason. + this.attemptAuthDeferred!.reject(e); + this.attemptAuthDeferred = null; + } + } + } + } + + /** + * Pick the next stage and call the callback + * + * @internal + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + private startNextAuthStage(): void { + const nextStage = this.chooseStage(); + if (!nextStage) { + throw new Error("No incomplete flows from the server"); + } + this.currentStage = nextStage; + + if (nextStage === AuthType.Dummy) { + this.submitAuthDict({ + type: "m.login.dummy", + }); + 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 } : {}); + } + + /** + * Pick the next auth stage + * + * @internal + * @returns login type + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + private chooseStage(): AuthType | undefined { + if (this.chosenFlow === null) { + this.chosenFlow = this.chooseFlow(); + } + logger.log("Active flow => %s", JSON.stringify(this.chosenFlow)); + const nextStage = this.firstUncompletedStage(this.chosenFlow); + logger.log("Next stage: %s", nextStage); + return nextStage; + } + + /** + * Pick one of the flows from the returned list + * If a flow using all of the inputs is found, it will + * be returned, otherwise, null will be returned. + * + * Only flows using all given inputs are chosen because it + * is likely to be surprising if the user provides a + * credential and it is not used. For example, for registration, + * this could result in the email not being used which would leave + * the account with no means to reset a password. + * + * @internal + * @returns flow + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + private chooseFlow(): UIAFlow { + const flows = this.data.flows || []; + + // we've been given an email or we've already done an email part + const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid); + const haveMsisdn = Boolean(this.inputs.phoneCountry) && Boolean(this.inputs.phoneNumber); + + for (const flow of flows) { + let flowHasEmail = false; + let flowHasMsisdn = false; + for (const stage of flow.stages) { + if (stage === EMAIL_STAGE_TYPE) { + flowHasEmail = true; + } else if (stage == MSISDN_STAGE_TYPE) { + flowHasMsisdn = true; + } + } + + if (flowHasEmail == haveEmail && flowHasMsisdn == haveMsisdn) { + return flow; + } + } + + const requiredStages: string[] = []; + if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE); + if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE); + // Throw an error with a fairly generic description, but with more + // information such that the app can give a better one if so desired. + throw new NoAuthFlowFoundError("No appropriate authentication flow found", requiredStages, flows); + } + + /** + * Get the first uncompleted stage in the given flow + * + * @internal + * @returns login type + */ + private firstUncompletedStage(flow: UIAFlow): AuthType | undefined { + const completed = this.data.completed || []; + return flow.stages.find((stageType) => !completed.includes(stageType)); + } +} -- cgit