diff options
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts | 617 |
1 files changed, 0 insertions, 617 deletions
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 deleted file mode 100644 index 7d9c183..0000000 --- a/includes/external/matrix/node_modules/matrix-js-sdk/src/interactive-auth.ts +++ /dev/null @@ -1,617 +0,0 @@ -/* -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<string, Record<string, any>>; - data?: Record<string, string>; - 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<IAuthData>; - /** - * 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<void>; // LEGACY -} - -/** - * Abstracts the logic used to drive the interactive auth process. - * - * <p>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. - * - * <p>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<IAuthData> | 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<void> | 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<IAuthData> { - // 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<void> { - 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<string, string> = { - 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<string, any> | 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<void> { - 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<void> => { - 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<void> { - 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 = (<MatrixError>error).data?.flows ?? null; - const haveFlows = this.data.flows || Boolean(errorFlows); - if ((<MatrixError>error).httpStatus !== 401 || !(<MatrixError>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 (!(<MatrixError>error).data) { - (<MatrixError>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 ( - !(<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; - } - this.data = (<MatrixError>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)); - } -} |