2016-08-02 13:04:20 +00:00
/ *
Copyright 2015 , 2016 OpenMarket Ltd
2017-02-15 18:44:15 +00:00
Copyright 2017 Vector Creations Ltd
2018-04-27 10:25:13 +00:00
Copyright 2018 New Vector Ltd
2023-07-11 04:09:18 +00:00
Copyright 2019 , 2020 , 2023 The Matrix . org Foundation C . I . C .
2016-08-02 13:04:20 +00:00
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 .
* /
2023-06-29 22:33:44 +00:00
import { ReactNode } from "react" ;
2023-08-15 15:00:17 +00:00
import { createClient , MatrixClient , SSOAction } from "matrix-js-sdk/src/matrix" ;
2020-10-07 11:14:36 +00:00
import { InvalidStoreError } from "matrix-js-sdk/src/errors" ;
2021-06-24 18:20:02 +00:00
import { decryptAES , encryptAES , IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes" ;
2021-07-16 12:11:43 +00:00
import { QueryDict } from "matrix-js-sdk/src/utils" ;
2021-10-22 22:23:32 +00:00
import { logger } from "matrix-js-sdk/src/logger" ;
2023-08-14 08:25:13 +00:00
import { MINIMUM_MATRIX_VERSION } from "matrix-js-sdk/src/version-support" ;
2016-08-10 10:33:58 +00:00
2021-06-29 12:11:58 +00:00
import { IMatrixClientCreds , MatrixClientPeg } from "./MatrixClientPeg" ;
2020-10-08 15:35:17 +00:00
import SecurityCustomisations from "./customisations/Security" ;
2019-11-19 11:52:12 +00:00
import EventIndexPeg from "./indexing/EventIndexPeg" ;
2017-05-31 16:28:46 +00:00
import createMatrixClient from "./utils/createMatrixClient" ;
2017-01-20 14:22:27 +00:00
import Notifier from "./Notifier" ;
2016-08-02 13:04:20 +00:00
import UserActivity from "./UserActivity" ;
import Presence from "./Presence" ;
2020-05-14 02:41:41 +00:00
import dis from "./dispatcher/dispatcher" ;
2016-09-26 17:02:14 +00:00
import DMRoomMap from "./utils/DMRoomMap" ;
2017-02-15 18:44:15 +00:00
import Modal from "./Modal" ;
2018-07-24 15:21:43 +00:00
import ActiveWidgetStore from "./stores/ActiveWidgetStore" ;
2018-09-26 15:25:21 +00:00
import PlatformPeg from "./PlatformPeg" ;
2019-03-25 17:43:53 +00:00
import { sendLoginRequest } from "./Login" ;
import * as StorageManager from "./utils/StorageManager" ;
2019-02-08 16:44:03 +00:00
import SettingsStore from "./settings/SettingsStore" ;
2020-01-17 11:43:35 +00:00
import ToastStore from "./stores/ToastStore" ;
2021-06-29 12:11:58 +00:00
import { IntegrationManagers } from "./integrations/IntegrationManagers" ;
import { Mjolnir } from "./mjolnir/Mjolnir" ;
2020-01-17 11:43:35 +00:00
import DeviceListener from "./DeviceListener" ;
2021-06-29 12:11:58 +00:00
import { Jitsi } from "./widgets/Jitsi" ;
import { SSO_HOMESERVER_URL_KEY , SSO_ID_SERVER_URL_KEY , SSO_IDP_ID_KEY } from "./BasePlatform" ;
2020-09-12 02:20:33 +00:00
import ThreepidInviteStore from "./stores/ThreepidInviteStore" ;
2021-08-03 10:55:02 +00:00
import { PosthogAnalytics } from "./PosthogAnalytics" ;
2022-08-30 19:13:39 +00:00
import LegacyCallHandler from "./LegacyCallHandler" ;
2020-11-27 11:19:44 +00:00
import LifecycleCustomisations from "./customisations/Lifecycle" ;
2021-02-01 16:25:50 +00:00
import ErrorDialog from "./components/views/dialogs/ErrorDialog" ;
2021-06-29 12:11:58 +00:00
import { _t } from "./languageHandler" ;
2021-07-02 10:12:41 +00:00
import LazyLoadingResyncDialog from "./components/views/dialogs/LazyLoadingResyncDialog" ;
import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDisabledDialog" ;
import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog" ;
import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog" ;
2021-10-29 08:34:25 +00:00
import { setSentryUser } from "./sentry" ;
2022-03-01 18:06:17 +00:00
import SdkConfig from "./SdkConfig" ;
2022-03-24 21:55:57 +00:00
import { DialogOpener } from "./utils/DialogOpener" ;
2022-03-24 22:11:01 +00:00
import { Action } from "./dispatcher/actions" ;
2022-05-02 02:23:43 +00:00
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler" ;
2022-07-05 18:26:44 +00:00
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload" ;
2022-10-19 13:14:14 +00:00
import { SdkContextClass } from "./contexts/SDKContext" ;
2023-04-27 08:05:31 +00:00
import { messageForLoginError } from "./utils/ErrorUtils" ;
2023-07-11 04:09:18 +00:00
import { completeOidcLogin } from "./utils/oidc/authorize" ;
2023-07-20 21:30:19 +00:00
import { persistOidcAuthenticatedSettings } from "./utils/oidc/persistOidcSettings" ;
2023-08-14 08:25:13 +00:00
import GenericToast from "./components/views/toasts/GenericToast" ;
2021-09-21 15:48:09 +00:00
2020-06-25 21:01:41 +00:00
const HOMESERVER_URL_KEY = "mx_hs_url" ;
const ID_SERVER_URL_KEY = "mx_is_url" ;
2016-08-02 13:04:20 +00:00
2022-03-24 22:11:01 +00:00
dis . register ( ( payload ) = > {
if ( payload . action === Action . TriggerLogout ) {
// noinspection JSIgnoredPromiseFromCall - we don't care if it fails
onLoggedOut ( ) ;
2022-07-05 18:26:44 +00:00
} else if ( payload . action === Action . OverwriteLogin ) {
const typed = < OverwriteLoginPayload > payload ;
// noinspection JSIgnoredPromiseFromCall - we don't care if it fails
doSetLoggedIn ( typed . credentials , true ) ;
2022-03-24 22:11:01 +00:00
}
} ) ;
2020-10-07 11:14:36 +00:00
interface ILoadSessionOpts {
enableGuest? : boolean ;
guestHsUrl? : string ;
guestIsUrl? : string ;
ignoreGuest? : boolean ;
defaultDeviceDisplayName? : string ;
2021-07-16 12:11:43 +00:00
fragmentQueryParams? : QueryDict ;
2020-10-07 11:14:36 +00:00
}
2016-08-10 10:33:58 +00:00
/ * *
* Called at startup , to attempt to build a logged - in Matrix session . It tries
* a number of things :
*
2017-06-16 13:33:14 +00:00
* 1 . if we have a guest access token in the fragment query params , it uses
2016-08-11 10:00:15 +00:00
* that .
2017-06-16 13:33:14 +00:00
* 2 . if an access token is stored in local storage ( from a previous session ) ,
2016-08-10 10:33:58 +00:00
* it uses that .
2017-06-16 13:33:14 +00:00
* 3 . it attempts to auto - register as a guest user .
2016-08-10 10:33:58 +00:00
*
2017-05-31 16:28:46 +00:00
* If any of steps 1 - 4 are successful , it will call { _doSetLoggedIn } , which in
2016-08-10 10:33:58 +00:00
* turn will raise on_logged_in and will_start_client events .
*
2020-10-07 11:14:36 +00:00
* @param { object } [ opts ]
* @param { object } [ opts . fragmentQueryParams ] : string - > string map of the
2016-08-11 10:00:15 +00:00
* query - parameters extracted from the # - fragment of the starting URI .
2020-10-07 11:14:36 +00:00
* @param { boolean } [ opts . enableGuest ] : set to true to enable guest access
* tokens and auto - guest registrations .
* @param { string } [ opts . guestHsUrl ] : homeserver URL . Only used if enableGuest
* is true ; defines the HS to register against .
* @param { string } [ opts . guestIsUrl ] : homeserver URL . Only used if enableGuest
* is true ; defines the IS to use .
* @param { bool } [ opts . ignoreGuest ] : If the stored session is a guest account ,
* ignore it and don ' t load it .
* @param { string } [ opts . defaultDeviceDisplayName ] : Default display name to use
* when registering as a guest .
2017-05-04 17:03:35 +00:00
* @returns { Promise } a promise which resolves when the above process completes .
2017-06-16 11:20:52 +00:00
* Resolves to ` true ` if we ended up starting a session , or ` false ` if we
* failed .
2016-08-10 10:33:58 +00:00
* /
2020-10-07 11:14:36 +00:00
export async function loadSession ( opts : ILoadSessionOpts = { } ) : Promise < boolean > {
2018-04-27 16:49:53 +00:00
try {
let enableGuest = opts . enableGuest || false ;
const guestHsUrl = opts . guestHsUrl ;
const guestIsUrl = opts . guestIsUrl ;
const fragmentQueryParams = opts . fragmentQueryParams || { } ;
const defaultDeviceDisplayName = opts . defaultDeviceDisplayName ;
2018-04-27 13:20:09 +00:00
2019-08-02 10:08:37 +00:00
if ( enableGuest && ! guestHsUrl ) {
2021-10-15 14:31:29 +00:00
logger . warn ( "Cannot enable guest access: can't determine HS URL to use" ) ;
2018-04-27 10:25:13 +00:00
enableGuest = false ;
}
2016-08-10 10:33:58 +00:00
2023-04-06 10:10:14 +00:00
if ( enableGuest && guestHsUrl && fragmentQueryParams . guest_user_id && fragmentQueryParams . guest_access_token ) {
2021-09-21 15:48:09 +00:00
logger . log ( "Using guest access credentials" ) ;
2020-10-07 12:32:30 +00:00
return doSetLoggedIn (
{
2021-07-16 12:11:43 +00:00
userId : fragmentQueryParams.guest_user_id as string ,
accessToken : fragmentQueryParams.guest_access_token as string ,
2018-04-27 10:25:13 +00:00
homeserverUrl : guestHsUrl ,
identityServerUrl : guestIsUrl ,
guest : true ,
} ,
true ,
) . then ( ( ) = > true ) ;
}
2020-10-07 12:32:30 +00:00
const success = await restoreFromLocalStorage ( {
2019-08-02 15:44:49 +00:00
ignoreGuest : Boolean ( opts . ignoreGuest ) ,
} ) ;
2017-02-15 18:44:15 +00:00
if ( success ) {
2017-06-16 11:20:52 +00:00
return true ;
2017-02-15 18:44:15 +00:00
}
2016-08-10 10:33:58 +00:00
2023-04-06 10:10:14 +00:00
if ( enableGuest && guestHsUrl ) {
2020-10-07 12:32:30 +00:00
return registerAsGuest ( guestHsUrl , guestIsUrl , defaultDeviceDisplayName ) ;
2017-02-15 18:44:15 +00:00
}
2016-08-10 10:33:58 +00:00
2019-03-28 16:22:17 +00:00
// fall back to welcome screen
2017-06-16 11:20:52 +00:00
return false ;
2018-04-27 16:49:53 +00:00
} catch ( e ) {
2019-03-28 16:22:17 +00:00
if ( e instanceof AbortLoginAndRebuildStorage ) {
// If we're aborting login because of a storage inconsistency, we don't
// need to show the general failure dialog. Instead, just go back to welcome.
return false ;
}
2020-10-07 12:32:30 +00:00
return handleLoadSessionFailure ( e ) ;
2018-04-27 16:49:53 +00:00
}
2016-08-10 10:33:58 +00:00
}
2019-03-08 00:04:50 +00:00
/ * *
* Gets the user ID of the persisted session , if one exists . This does not validate
* that the user ' s credentials still work , just that they exist and that a user ID
* is associated with them . The session is not loaded .
2022-05-26 10:12:49 +00:00
* @returns { [ string , boolean ] } The persisted session ' s owner and whether the stored
2020-12-09 23:40:31 +00:00
* session is for a guest user , if an owner exists . If there is no stored session ,
* return [ null , null ] .
2019-03-08 00:04:50 +00:00
* /
2023-04-06 10:10:14 +00:00
export async function getStoredSessionOwner ( ) : Promise < [ string , boolean ] | [ null , null ] > {
2021-06-29 12:11:58 +00:00
const { hsUrl , userId , hasAccessToken , isGuest } = await getStoredSessionVars ( ) ;
2023-04-25 08:28:48 +00:00
return hsUrl && userId && hasAccessToken ? [ userId , ! ! isGuest ] : [ null , null ] ;
2019-05-14 10:59:38 +00:00
}
2017-06-16 13:33:14 +00:00
/ * *
2023-07-11 04:09:18 +00:00
* If query string includes OIDC authorization code flow parameters attempt to login using oidc flow
* Else , we may be returning from SSO - attempt token login
*
2017-06-16 13:33:14 +00:00
* @param { Object } queryParams string - > string map of the
* query - parameters extracted from the real query - string of the starting
* URI .
*
2021-02-01 16:25:50 +00:00
* @param { string } defaultDeviceDisplayName
* @param { string } fragmentAfterLogin path to go to after a successful login , only used for "Try again"
2017-06-16 13:33:14 +00:00
*
2023-07-11 04:09:18 +00:00
* @returns { Promise } promise which resolves to true if we completed the delegated auth login
* else false
* /
export async function attemptDelegatedAuthLogin (
queryParams : QueryDict ,
defaultDeviceDisplayName? : string ,
fragmentAfterLogin? : string ,
) : Promise < boolean > {
if ( queryParams . code && queryParams . state ) {
2023-08-14 12:52:08 +00:00
console . log ( "We have OIDC params - attempting OIDC login" ) ;
2023-07-11 04:09:18 +00:00
return attemptOidcNativeLogin ( queryParams ) ;
}
return attemptTokenLogin ( queryParams , defaultDeviceDisplayName , fragmentAfterLogin ) ;
}
/ * *
* Attempt to login by completing OIDC authorization code flow
* @param queryParams string - > string map of the query - parameters extracted from the real query - string of the starting URI .
* @returns Promise that resolves to true when login succceeded , else false
* /
async function attemptOidcNativeLogin ( queryParams : QueryDict ) : Promise < boolean > {
try {
2023-07-20 21:30:19 +00:00
const { accessToken , homeserverUrl , identityServerUrl , clientId , issuer } = await completeOidcLogin (
queryParams ,
) ;
2023-07-11 04:09:18 +00:00
const {
user_id : userId ,
device_id : deviceId ,
is_guest : isGuest ,
} = await getUserIdFromAccessToken ( accessToken , homeserverUrl , identityServerUrl ) ;
const credentials = {
accessToken ,
homeserverUrl ,
identityServerUrl ,
deviceId ,
userId ,
isGuest ,
} ;
logger . debug ( "Logged in via OIDC native flow" ) ;
await onSuccessfulDelegatedAuthLogin ( credentials ) ;
2023-07-20 21:30:19 +00:00
// this needs to happen after success handler which clears storages
persistOidcAuthenticatedSettings ( clientId , issuer ) ;
2023-07-11 04:09:18 +00:00
return true ;
} catch ( error ) {
logger . error ( "Failed to login via OIDC" , error ) ;
// TODO(kerrya) nice error messages https://github.com/vector-im/element-web/issues/25665
await onFailedDelegatedAuthLogin ( _t ( "Something went wrong." ) ) ;
return false ;
}
}
/ * *
* Gets information about the owner of a given access token .
* @param accessToken
* @param homeserverUrl
* @param identityServerUrl
* @returns Promise that resolves with whoami response
* @throws when whoami request fails
* /
async function getUserIdFromAccessToken (
accessToken : string ,
homeserverUrl : string ,
identityServerUrl? : string ,
) : Promise < ReturnType < MatrixClient [ " whoami " ] > > {
try {
const client = createClient ( {
baseUrl : homeserverUrl ,
accessToken : accessToken ,
idBaseUrl : identityServerUrl ,
} ) ;
return await client . whoami ( ) ;
} catch ( error ) {
logger . error ( "Failed to retrieve userId using accessToken" , error ) ;
throw new Error ( "Failed to retrieve userId using accessToken" ) ;
}
}
/ * *
* @param { QueryDict } queryParams string - > string map of the
* query - parameters extracted from the real query - string of the starting
* URI .
*
* @param { string } defaultDeviceDisplayName
* @param { string } fragmentAfterLogin path to go to after a successful login , only used for "Try again"
*
2017-06-16 13:33:14 +00:00
* @returns { Promise } promise which resolves to true if we completed the token
* login , else false
* /
2020-10-07 11:14:36 +00:00
export function attemptTokenLogin (
2021-07-16 12:11:43 +00:00
queryParams : QueryDict ,
2020-10-07 11:14:36 +00:00
defaultDeviceDisplayName? : string ,
2021-02-01 16:25:50 +00:00
fragmentAfterLogin? : string ,
2020-10-07 11:14:36 +00:00
) : Promise < boolean > {
2017-06-16 13:33:14 +00:00
if ( ! queryParams . loginToken ) {
2017-07-12 13:02:00 +00:00
return Promise . resolve ( false ) ;
2017-06-16 13:33:14 +00:00
}
2023-08-14 12:52:08 +00:00
console . log ( "We have token login params - attempting token login" ) ;
2020-06-25 20:59:46 +00:00
const homeserver = localStorage . getItem ( SSO_HOMESERVER_URL_KEY ) ;
2023-03-31 08:26:15 +00:00
const identityServer = localStorage . getItem ( SSO_ID_SERVER_URL_KEY ) ? ? undefined ;
2020-06-02 15:26:07 +00:00
if ( ! homeserver ) {
2021-10-15 14:31:29 +00:00
logger . warn ( "Cannot log in with token: can't determine HS URL to use" ) ;
2023-06-29 22:33:44 +00:00
onFailedDelegatedAuthLogin (
_t (
2023-08-22 15:32:05 +00:00
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again." ,
2021-02-02 12:58:31 +00:00
) ,
2023-06-29 22:33:44 +00:00
) ;
2017-07-12 13:02:00 +00:00
return Promise . resolve ( false ) ;
2017-06-16 13:33:14 +00:00
}
2018-12-05 16:39:38 +00:00
return sendLoginRequest ( homeserver , identityServer , "m.login.token" , {
2021-07-16 12:11:43 +00:00
token : queryParams.loginToken as string ,
2016-08-11 15:15:42 +00:00
initial_device_display_name : defaultDeviceDisplayName ,
} )
2023-06-29 22:33:44 +00:00
. then ( async function ( creds ) {
2021-09-21 15:48:09 +00:00
logger . log ( "Logged in with token" ) ;
2023-06-29 22:33:44 +00:00
await onSuccessfulDelegatedAuthLogin ( creds ) ;
return true ;
2017-06-16 13:33:14 +00:00
} )
2023-06-29 22:33:44 +00:00
. catch ( ( error ) = > {
const tryAgainCallback : TryAgainFunction = ( ) = > {
const cli = createClient ( {
baseUrl : homeserver ,
idBaseUrl : identityServer ,
} ) ;
const idpId = localStorage . getItem ( SSO_IDP_ID_KEY ) || undefined ;
PlatformPeg . get ( ) ? . startSingleSignOn ( cli , "sso" , fragmentAfterLogin , idpId , SSOAction . LOGIN ) ;
} ;
onFailedDelegatedAuthLogin (
messageForLoginError ( error , {
2023-04-27 08:05:31 +00:00
hsUrl : homeserver ,
hsName : homeserver ,
} ) ,
2023-06-29 22:33:44 +00:00
tryAgainCallback ,
) ;
logger . error ( "Failed to log in with login token:" , error ) ;
2017-06-16 13:33:14 +00:00
return false ;
2021-02-01 16:25:50 +00:00
} ) ;
2016-08-11 10:00:15 +00:00
}
2023-06-29 22:33:44 +00:00
/ * *
* Called after a successful token login or OIDC authorization .
* Clear storage then save new credentials in storage
* @param credentials as returned from login
* /
async function onSuccessfulDelegatedAuthLogin ( credentials : IMatrixClientCreds ) : Promise < void > {
await clearStorage ( ) ;
await persistCredentials ( credentials ) ;
// remember that we just logged in
sessionStorage . setItem ( "mx_fresh_login" , String ( true ) ) ;
}
type TryAgainFunction = ( ) = > void ;
/ * *
* Display a friendly error to the user when token login or OIDC authorization fails
* @param description error description
* @param tryAgain OPTIONAL function to call on try again button from error dialog
* /
async function onFailedDelegatedAuthLogin ( description : string | ReactNode , tryAgain? : TryAgainFunction ) : Promise < void > {
Modal . createDialog ( ErrorDialog , {
title : _t ( "We couldn't log you in" ) ,
description ,
button : _t ( "Try again" ) ,
// if we have a tryAgain callback, call it the primary 'try again' button was clicked in the dialog
onFinished : tryAgain ? ( shouldTryAgain? : boolean ) = > shouldTryAgain && tryAgain ( ) : undefined ,
} ) ;
}
2023-04-06 10:10:14 +00:00
export function handleInvalidStoreError ( e : InvalidStoreError ) : Promise < void > | void {
2020-10-07 11:14:36 +00:00
if ( e . reason === InvalidStoreError . TOGGLED_LAZY_LOADING ) {
2018-10-10 16:07:17 +00:00
return Promise . resolve ( )
. then ( ( ) = > {
const lazyLoadEnabled = e . value ;
if ( lazyLoadEnabled ) {
2023-02-28 10:31:48 +00:00
return new Promise < void > ( ( resolve ) = > {
2018-10-10 16:07:17 +00:00
Modal . createDialog ( LazyLoadingResyncDialog , {
onFinished : resolve ,
2022-12-12 11:24:14 +00:00
} ) ;
2018-10-10 16:07:17 +00:00
} ) ;
} else {
// show warning about simultaneous use
// between LL/non-LL version on same host.
// as disabling LL when previously enabled
// is a strong indicator of this (/develop & /app)
2023-02-28 10:31:48 +00:00
return new Promise < void > ( ( resolve ) = > {
2018-10-10 16:07:17 +00:00
Modal . createDialog ( LazyLoadingDisabledDialog , {
onFinished : resolve ,
host : window.location.host ,
2022-12-12 11:24:14 +00:00
} ) ;
2018-10-10 16:07:17 +00:00
} ) ;
}
} )
. then ( ( ) = > {
2023-06-21 16:29:44 +00:00
return MatrixClientPeg . safeGet ( ) . store . deleteAllData ( ) ;
2018-10-10 16:07:17 +00:00
} )
. then ( ( ) = > {
2023-02-24 15:28:40 +00:00
PlatformPeg . get ( ) ? . reload ( ) ;
2018-10-10 16:07:17 +00:00
} ) ;
}
}
2023-04-06 10:10:14 +00:00
function registerAsGuest ( hsUrl : string , isUrl? : string , defaultDeviceDisplayName? : string ) : Promise < boolean > {
2021-09-21 15:48:09 +00:00
logger . log ( ` Doing guest login on ${ hsUrl } ` ) ;
2016-08-10 10:33:58 +00:00
2016-08-11 12:50:38 +00:00
// create a temporary MatrixClient to do the login
2021-03-19 02:50:34 +00:00
const client = createClient ( {
2016-08-11 12:50:38 +00:00
baseUrl : hsUrl ,
} ) ;
2016-08-11 15:15:42 +00:00
return client
. registerGuest ( {
body : {
initial_device_display_name : defaultDeviceDisplayName ,
} ,
} )
. then (
( creds ) = > {
2021-09-21 15:48:09 +00:00
logger . log ( ` Registered as guest: ${ creds . user_id } ` ) ;
2020-10-07 12:32:30 +00:00
return doSetLoggedIn (
{
2016-08-10 10:33:58 +00:00
userId : creds.user_id ,
2016-08-11 13:21:52 +00:00
deviceId : creds.device_id ,
2023-07-04 13:49:27 +00:00
accessToken : creds.access_token ! ,
2016-08-10 10:33:58 +00:00
homeserverUrl : hsUrl ,
identityServerUrl : isUrl ,
guest : true ,
2017-06-16 11:20:52 +00:00
} ,
true ,
) . then ( ( ) = > true ) ;
2016-08-10 10:33:58 +00:00
} ,
( err ) = > {
2021-10-15 14:30:53 +00:00
logger . error ( "Failed to register as guest" , err ) ;
2017-06-16 11:20:52 +00:00
return false ;
2016-08-10 10:33:58 +00:00
} ,
) ;
}
2020-12-11 02:52:18 +00:00
export interface IStoredSession {
2020-10-07 11:14:36 +00:00
hsUrl : string ;
isUrl : string ;
2020-12-11 02:52:18 +00:00
hasAccessToken : boolean ;
2021-06-24 18:20:02 +00:00
accessToken : string | IEncryptedPayload ;
2020-10-07 11:14:36 +00:00
userId : string ;
deviceId : string ;
isGuest : boolean ;
}
2019-05-29 18:09:04 +00:00
/ * *
2020-12-11 02:52:18 +00:00
* Retrieves information about the stored session from the browser ' s storage . The session
2019-05-29 18:09:04 +00:00
* may not be valid , as it is not tested for consistency here .
* @returns { Object } Information about the session - see implementation for variables .
* /
2023-04-25 08:28:48 +00:00
export async function getStoredSessionVars ( ) : Promise < Partial < IStoredSession > > {
const hsUrl = localStorage . getItem ( HOMESERVER_URL_KEY ) ? ? undefined ;
const isUrl = localStorage . getItem ( ID_SERVER_URL_KEY ) ? ? undefined ;
2023-04-06 10:10:14 +00:00
let accessToken : string | undefined ;
2020-12-11 02:52:18 +00:00
try {
accessToken = await StorageManager . idbLoad ( "account" , "mx_access_token" ) ;
2021-11-17 22:01:45 +00:00
} catch ( e ) {
logger . error ( "StorageManager.idbLoad failed for account:mx_access_token" , e ) ;
}
2020-12-09 23:40:31 +00:00
if ( ! accessToken ) {
2023-04-06 10:10:14 +00:00
accessToken = localStorage . getItem ( "mx_access_token" ) ? ? undefined ;
2020-12-09 23:40:31 +00:00
if ( accessToken ) {
2020-12-11 02:52:18 +00:00
try {
// try to migrate access token to IndexedDB if we can
await StorageManager . idbSave ( "account" , "mx_access_token" , accessToken ) ;
localStorage . removeItem ( "mx_access_token" ) ;
2021-11-17 22:01:45 +00:00
} catch ( e ) {
logger . error ( "migration of access token to IndexedDB failed" , e ) ;
}
2020-12-09 23:40:31 +00:00
}
}
2020-12-11 02:52:18 +00:00
// if we pre-date storing "mx_has_access_token", but we retrieved an access
// token, then we should say we have an access token
const hasAccessToken = localStorage . getItem ( "mx_has_access_token" ) === "true" || ! ! accessToken ;
2023-04-25 08:28:48 +00:00
const userId = localStorage . getItem ( "mx_user_id" ) ? ? undefined ;
const deviceId = localStorage . getItem ( "mx_device_id" ) ? ? undefined ;
2019-03-08 00:04:50 +00:00
2023-04-06 10:10:14 +00:00
let isGuest : boolean ;
2019-05-14 10:59:38 +00:00
if ( localStorage . getItem ( "mx_is_guest" ) !== null ) {
isGuest = localStorage . getItem ( "mx_is_guest" ) === "true" ;
} else {
// legacy key name
isGuest = localStorage . getItem ( "matrix-is-guest" ) === "true" ;
}
2022-02-16 19:32:38 +00:00
return { hsUrl , isUrl , hasAccessToken , accessToken , userId , deviceId , isGuest } ;
2019-03-08 00:04:50 +00:00
}
2020-12-09 23:40:31 +00:00
// The pickle key is a string of unspecified length and format. For AES, we
2021-06-24 18:20:02 +00:00
// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
2020-12-09 23:40:31 +00:00
// key. The AES key should be zeroed after it is used.
async function pickleKeyToAesKey ( pickleKey : string ) : Promise < Uint8Array > {
const pickleKeyBuffer = new Uint8Array ( pickleKey . length ) ;
for ( let i = 0 ; i < pickleKey . length ; i ++ ) {
pickleKeyBuffer [ i ] = pickleKey . charCodeAt ( i ) ;
}
const hkdfKey = await window . crypto . subtle . importKey ( "raw" , pickleKeyBuffer , "HKDF" , false , [ "deriveBits" ] ) ;
pickleKeyBuffer . fill ( 0 ) ;
return new Uint8Array (
await window . crypto . subtle . deriveBits (
{
name : "HKDF" ,
hash : "SHA-256" ,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
salt : new Uint8Array ( 32 ) ,
info : new Uint8Array ( 0 ) ,
} ,
hkdfKey ,
256 ,
2022-12-12 11:24:14 +00:00
) ,
2020-12-09 23:40:31 +00:00
) ;
}
2023-01-12 13:25:14 +00:00
async function abortLogin ( ) : Promise < void > {
2020-12-11 02:52:18 +00:00
const signOut = await showStorageEvictedDialog ( ) ;
if ( signOut ) {
await clearStorage ( ) ;
// This error feels a bit clunky, but we want to make sure we don't go any
// further and instead head back to sign in.
throw new AbortLoginAndRebuildStorage ( "Aborting login in progress because of storage inconsistency" ) ;
}
}
2017-02-15 18:44:15 +00:00
// returns a promise which resolves to true if a session is found in
// localstorage
2017-05-16 10:58:37 +00:00
//
// N.B. Lifecycle.js should not maintain any further localStorage state, we
// are moving towards using SessionStore to keep track of state related
// to the current session (which is typically backed by localStorage).
//
// The plan is to gradually move the localStorage access done here into
// SessionStore to avoid bugs where the view becomes out-of-sync with
2019-01-25 22:10:54 +00:00
// localStorage (e.g. isGuest etc.)
2021-01-27 12:50:12 +00:00
export async function restoreFromLocalStorage ( opts ? : { ignoreGuest? : boolean } ) : Promise < boolean > {
2020-10-07 11:14:36 +00:00
const ignoreGuest = opts ? . ignoreGuest ;
2019-08-02 15:44:49 +00:00
2018-04-27 16:49:53 +00:00
if ( ! localStorage ) {
return false ;
}
2022-02-16 19:32:38 +00:00
const { hsUrl , isUrl , hasAccessToken , accessToken , userId , deviceId , isGuest } = await getStoredSessionVars ( ) ;
2020-12-11 02:52:18 +00:00
if ( hasAccessToken && ! accessToken ) {
2022-10-18 16:07:23 +00:00
await abortLogin ( ) ;
2020-12-11 02:52:18 +00:00
}
2016-08-10 09:33:27 +00:00
2018-04-27 16:49:53 +00:00
if ( accessToken && userId && hsUrl ) {
2019-08-02 10:22:42 +00:00
if ( ignoreGuest && isGuest ) {
2021-09-21 15:48:09 +00:00
logger . log ( "Ignoring stored guest account: " + userId ) ;
2019-08-02 10:22:42 +00:00
return false ;
}
2022-02-16 19:32:38 +00:00
let decryptedAccessToken = accessToken ;
2023-04-25 08:28:48 +00:00
const pickleKey = await PlatformPeg . get ( ) ? . getPickleKey ( userId , deviceId ? ? "" ) ;
2022-02-16 19:32:38 +00:00
if ( pickleKey ) {
logger . log ( "Got pickle key" ) ;
if ( typeof accessToken !== "string" ) {
const encrKey = await pickleKeyToAesKey ( pickleKey ) ;
decryptedAccessToken = await decryptAES ( accessToken , encrKey , "access_token" ) ;
encrKey . fill ( 0 ) ;
}
} else {
logger . log ( "No pickle key available" ) ;
}
2020-05-28 04:05:45 +00:00
2020-10-07 11:14:36 +00:00
const freshLogin = sessionStorage . getItem ( "mx_fresh_login" ) === "true" ;
2020-09-30 04:52:47 +00:00
sessionStorage . removeItem ( "mx_fresh_login" ) ;
2020-09-03 20:28:42 +00:00
2021-09-21 15:48:09 +00:00
logger . log ( ` Restoring session for ${ userId } ` ) ;
2020-10-07 12:32:30 +00:00
await doSetLoggedIn (
{
2018-04-27 16:49:53 +00:00
userId : userId ,
deviceId : deviceId ,
2020-12-11 02:52:18 +00:00
accessToken : decryptedAccessToken as string ,
2018-04-27 16:49:53 +00:00
homeserverUrl : hsUrl ,
identityServerUrl : isUrl ,
guest : isGuest ,
2023-04-06 10:10:14 +00:00
pickleKey : pickleKey ? ? undefined ,
2020-09-30 04:52:47 +00:00
freshLogin : freshLogin ,
2018-04-27 16:49:53 +00:00
} ,
false ,
) ;
2023-08-14 08:25:13 +00:00
checkServerVersions ( ) ;
2018-04-27 16:49:53 +00:00
return true ;
} else {
2021-09-21 15:48:09 +00:00
logger . log ( "No previous session found." ) ;
2018-04-27 16:49:53 +00:00
return false ;
}
2016-08-10 09:33:27 +00:00
}
2016-08-10 10:33:58 +00:00
2023-08-14 08:25:13 +00:00
async function checkServerVersions ( ) : Promise < void > {
MatrixClientPeg . get ( )
? . getVersions ( )
. then ( ( response ) = > {
if ( ! response . versions . includes ( MINIMUM_MATRIX_VERSION ) ) {
const toastKey = "LEGACY_SERVER" ;
ToastStore . sharedInstance ( ) . addOrReplaceToast ( {
key : toastKey ,
title : _t ( "Your server is unsupported" ) ,
props : {
description : _t (
"This server is using an older version of Matrix. Upgrade to Matrix %(version)s to use %(brand)s without errors." ,
{
version : MINIMUM_MATRIX_VERSION ,
brand : SdkConfig.get ( ) . brand ,
} ,
) ,
acceptLabel : _t ( "OK" ) ,
onAccept : ( ) = > {
ToastStore . sharedInstance ( ) . dismissToast ( toastKey ) ;
} ,
} ,
component : GenericToast ,
priority : 98 ,
} ) ;
}
} ) ;
}
2023-07-07 13:46:12 +00:00
async function handleLoadSessionFailure ( e : unknown ) : Promise < boolean > {
2021-10-15 14:30:53 +00:00
logger . error ( "Unable to load session" , e ) ;
2017-02-15 18:44:15 +00:00
2022-06-14 16:51:51 +00:00
const modal = Modal . createDialog ( SessionRestoreErrorDialog , {
2021-10-29 08:34:25 +00:00
error : e ,
2017-02-15 18:44:15 +00:00
} ) ;
2020-03-22 09:47:00 +00:00
const [ success ] = await modal . finished ;
if ( success ) {
// user clicked continue.
2020-10-07 12:32:30 +00:00
await clearStorage ( ) ;
2020-03-22 09:47:00 +00:00
return false ;
}
2017-02-15 18:44:15 +00:00
2020-03-22 09:47:00 +00:00
// try, try again
return loadSession ( ) ;
2017-02-15 18:44:15 +00:00
}
2016-08-02 17:52:56 +00:00
/ * *
2017-05-31 16:28:46 +00:00
* Transitions to a logged - in state using the given credentials .
*
* Starts the matrix client and all other react - sdk services that
* listen for events while a session is logged in .
*
* Also stops the old MatrixClient and clears old credentials / etc out of
* storage before starting the new client .
*
2022-05-26 10:12:49 +00:00
* @param { IMatrixClientCreds } credentials The credentials to use
2017-06-19 09:22:18 +00:00
*
* @returns { Promise } promise which resolves to the new MatrixClient once it has been started
2016-08-02 17:52:56 +00:00
* /
2020-10-07 11:14:36 +00:00
export async function setLoggedIn ( credentials : IMatrixClientCreds ) : Promise < MatrixClient > {
2020-09-30 04:52:47 +00:00
credentials . freshLogin = true ;
2017-05-31 16:28:46 +00:00
stopMatrixClient ( ) ;
2020-05-28 04:05:45 +00:00
const pickleKey =
credentials . userId && credentials . deviceId
2023-02-24 15:28:40 +00:00
? await PlatformPeg . get ( ) ? . createPickleKey ( credentials . userId , credentials . deviceId )
2020-10-07 11:14:36 +00:00
: null ;
2020-05-28 04:05:45 +00:00
2020-07-28 21:31:27 +00:00
if ( pickleKey ) {
2021-09-21 15:48:09 +00:00
logger . log ( "Created pickle key" ) ;
2020-07-28 21:31:27 +00:00
} else {
2021-09-21 15:48:09 +00:00
logger . log ( "Pickle key not created" ) ;
2020-07-28 21:31:27 +00:00
}
2021-06-29 12:11:58 +00:00
return doSetLoggedIn ( Object . assign ( { } , credentials , { pickleKey } ) , true ) ;
2017-05-31 16:28:46 +00:00
}
2017-05-04 17:04:47 +00:00
2019-07-04 22:45:40 +00:00
/ * *
* Hydrates an existing session by using the credentials provided . This will
* not clear any local storage , unlike setLoggedIn ( ) .
*
* Stops the existing Matrix client ( without clearing its data ) and starts a
* new one in its place . This additionally starts all other react - sdk services
* which use the new Matrix client .
*
2019-07-05 20:45:34 +00:00
* If the credentials belong to a different user from the session already stored ,
* the old session will be cleared automatically .
2022-02-16 19:32:38 +00:00
*
2022-05-26 10:12:49 +00:00
* @param { IMatrixClientCreds } credentials The credentials to use
2022-02-16 19:32:38 +00:00
*
2019-07-04 22:45:40 +00:00
* @returns { Promise } promise which resolves to the new MatrixClient once it has been started
* /
2022-01-20 20:54:25 +00:00
export async function hydrateSession ( credentials : IMatrixClientCreds ) : Promise < MatrixClient > {
2023-06-21 16:29:44 +00:00
const oldUserId = MatrixClientPeg . safeGet ( ) . getUserId ( ) ;
const oldDeviceId = MatrixClientPeg . safeGet ( ) . getDeviceId ( ) ;
2019-07-05 20:45:34 +00:00
stopMatrixClient ( ) ; // unsets MatrixClientPeg.get()
2019-07-04 22:45:40 +00:00
localStorage . removeItem ( "mx_soft_logout" ) ;
_isLoggingOut = false ;
2019-07-05 20:45:34 +00:00
2019-07-08 21:35:34 +00:00
const overwrite = credentials . userId !== oldUserId || credentials . deviceId !== oldDeviceId ;
2019-07-05 20:45:34 +00:00
if ( overwrite ) {
2021-10-15 14:31:29 +00:00
logger . warn ( "Clearing all data: Old session belongs to a different user/session" ) ;
2019-07-05 20:45:34 +00:00
}
2023-06-01 13:43:24 +00:00
if ( ! credentials . pickleKey && credentials . deviceId !== undefined ) {
2022-01-20 20:54:25 +00:00
logger . info ( "Lifecycle#hydrateSession: Pickle key not provided - trying to get one" ) ;
2023-04-06 10:10:14 +00:00
credentials . pickleKey =
( await PlatformPeg . get ( ) ? . getPickleKey ( credentials . userId , credentials . deviceId ) ) ? ? undefined ;
2022-01-20 20:54:25 +00:00
}
2020-10-07 12:32:30 +00:00
return doSetLoggedIn ( credentials , overwrite ) ;
2019-07-04 22:45:40 +00:00
}
2017-05-31 16:28:46 +00:00
/ * *
2023-03-07 15:24:44 +00:00
* optionally clears localstorage , persists new credentials
2017-05-31 16:28:46 +00:00
* to localstorage , starts the new client .
*
2021-12-02 13:46:44 +00:00
* @param { IMatrixClientCreds } credentials
* @param { Boolean } clearStorageEnabled
2017-05-31 16:28:46 +00:00
*
2017-06-19 09:22:18 +00:00
* @returns { Promise } promise which resolves to the new MatrixClient once it has been started
2017-05-31 16:28:46 +00:00
* /
2020-10-07 11:14:36 +00:00
async function doSetLoggedIn ( credentials : IMatrixClientCreds , clearStorageEnabled : boolean ) : Promise < MatrixClient > {
2017-05-31 16:28:46 +00:00
credentials . guest = Boolean ( credentials . guest ) ;
2017-05-29 18:04:37 +00:00
2019-07-03 22:46:37 +00:00
const softLogout = isSoftLogout ( ) ;
2021-09-21 15:48:09 +00:00
logger . log (
2017-06-20 16:38:02 +00:00
"setLoggedIn: mxid: " +
credentials . userId +
" deviceId: " +
credentials . deviceId +
" guest: " +
credentials . guest +
2019-07-03 22:46:37 +00:00
" hs: " +
credentials . homeserverUrl +
2022-02-16 19:32:38 +00:00
" softLogout: " +
softLogout ,
" freshLogin: " + credentials . freshLogin ,
2017-05-04 17:04:47 +00:00
) ;
2017-05-31 16:28:46 +00:00
2020-10-07 14:10:11 +00:00
if ( clearStorageEnabled ) {
2020-10-07 12:32:30 +00:00
await clearStorage ( ) ;
2017-05-31 16:28:46 +00:00
}
2019-03-28 16:22:17 +00:00
const results = await StorageManager . checkConsistency ( ) ;
// If there's an inconsistency between account data in local storage and the
// crypto store, we'll be generally confused when handling encrypted data.
// Show a modal recommending a full reset of storage.
2019-05-15 12:47:48 +00:00
if ( results . dataInLocalStorage && results . cryptoInited && ! results . dataInCryptoStore ) {
2020-12-11 02:52:18 +00:00
await abortLogin ( ) ;
2019-03-28 16:22:17 +00:00
}
2019-03-25 17:43:53 +00:00
2020-09-30 04:52:47 +00:00
MatrixClientPeg . replaceUsingCreds ( credentials ) ;
2023-06-21 16:29:44 +00:00
const client = MatrixClientPeg . safeGet ( ) ;
2021-09-14 13:57:26 +00:00
2021-10-29 08:34:25 +00:00
setSentryUser ( credentials . userId ) ;
2021-09-14 13:57:26 +00:00
2021-12-05 22:39:33 +00:00
if ( PosthogAnalytics . instance . isEnabled ( ) ) {
2023-06-01 13:43:24 +00:00
PosthogAnalytics . instance . startListeningToSettingsChanges ( client ) ;
2021-12-05 22:39:33 +00:00
}
2020-09-30 04:52:47 +00:00
2020-10-02 21:43:49 +00:00
if ( credentials . freshLogin && SettingsStore . getValue ( "feature_dehydration" ) ) {
2020-09-30 04:52:47 +00:00
// If we just logged in, try to rehydrate a device instead of using a
// new device. If it succeeds, we'll get a new device ID, so make sure
// we persist that ID to localStorage
const newDeviceId = await client . rehydrateDevice ( ) ;
if ( newDeviceId ) {
credentials . deviceId = newDeviceId ;
}
delete credentials . freshLogin ;
}
2016-08-10 17:04:22 +00:00
if ( localStorage ) {
try {
2020-12-11 02:52:18 +00:00
await persistCredentials ( credentials ) ;
2022-02-16 19:32:38 +00:00
// make sure we don't think that it's a fresh login any more
2020-09-30 04:52:47 +00:00
sessionStorage . removeItem ( "mx_fresh_login" ) ;
2016-08-10 17:04:22 +00:00
} catch ( e ) {
2021-10-15 14:31:29 +00:00
logger . warn ( "Error using local storage: can't persist session!" , e ) ;
2016-08-10 17:04:22 +00:00
}
} else {
2021-10-15 14:31:29 +00:00
logger . warn ( "No local storage available: can't persist session!" ) ;
2016-08-10 17:04:22 +00:00
}
2022-05-26 08:56:53 +00:00
dis . fire ( Action . OnLoggedIn ) ;
2023-05-23 15:24:12 +00:00
await startMatrixClient ( client , /*startSyncing=*/ ! softLogout ) ;
2022-07-29 14:03:25 +00:00
2020-09-30 04:52:47 +00:00
return client ;
2016-08-02 13:04:20 +00:00
}
2023-04-06 10:10:14 +00:00
async function showStorageEvictedDialog ( ) : Promise < boolean > {
const { finished } = Modal . createDialog ( StorageEvictedDialog ) ;
const [ ok ] = await finished ;
return ! ! ok ;
2019-03-28 16:22:17 +00:00
}
// Note: Babel 6 requires the `transform-builtin-extend` plugin for this to satisfy
// `instanceof`. Babel 7 supports this natively in their class handling.
class AbortLoginAndRebuildStorage extends Error { }
2020-12-11 02:52:18 +00:00
async function persistCredentials ( credentials : IMatrixClientCreds ) : Promise < void > {
2020-06-03 19:23:01 +00:00
localStorage . setItem ( HOMESERVER_URL_KEY , credentials . homeserverUrl ) ;
2019-08-07 10:41:00 +00:00
if ( credentials . identityServerUrl ) {
2020-06-03 19:23:01 +00:00
localStorage . setItem ( ID_SERVER_URL_KEY , credentials . identityServerUrl ) ;
2019-08-07 10:41:00 +00:00
}
2017-06-16 13:33:14 +00:00
localStorage . setItem ( "mx_user_id" , credentials . userId ) ;
localStorage . setItem ( "mx_is_guest" , JSON . stringify ( credentials . guest ) ) ;
2020-12-11 02:52:18 +00:00
// store whether we expect to find an access token, to detect the case
// where IndexedDB is blown away
if ( credentials . accessToken ) {
localStorage . setItem ( "mx_has_access_token" , "true" ) ;
} else {
2023-06-27 23:45:11 +00:00
localStorage . removeItem ( "mx_has_access_token" ) ;
2020-12-11 02:52:18 +00:00
}
2020-07-28 21:31:27 +00:00
if ( credentials . pickleKey ) {
2023-04-06 10:10:14 +00:00
let encryptedAccessToken : IEncryptedPayload | undefined ;
2020-12-11 02:52:18 +00:00
try {
// try to encrypt the access token using the pickle key
const encrKey = await pickleKeyToAesKey ( credentials . pickleKey ) ;
encryptedAccessToken = await encryptAES ( credentials . accessToken , encrKey , "access_token" ) ;
encrKey . fill ( 0 ) ;
} catch ( e ) {
2021-10-15 14:31:29 +00:00
logger . warn ( "Could not encrypt access token" , e ) ;
2020-12-11 02:52:18 +00:00
}
try {
// save either the encrypted access token, or the plain access
// token if we were unable to encrypt (e.g. if the browser doesn't
// have WebCrypto).
await StorageManager . idbSave ( "account" , "mx_access_token" , encryptedAccessToken || credentials . accessToken ) ;
} catch ( e ) {
// if we couldn't save to indexedDB, fall back to localStorage. We
// store the access token unencrypted since localStorage only saves
// strings.
2023-06-28 13:05:36 +00:00
if ( ! ! credentials . accessToken ) {
localStorage . setItem ( "mx_access_token" , credentials . accessToken ) ;
} else {
localStorage . removeItem ( "mx_access_token" ) ;
}
2020-12-11 02:52:18 +00:00
}
2020-10-07 11:14:36 +00:00
localStorage . setItem ( "mx_has_pickle_key" , String ( true ) ) ;
2020-07-28 21:31:27 +00:00
} else {
2020-12-11 02:52:18 +00:00
try {
await StorageManager . idbSave ( "account" , "mx_access_token" , credentials . accessToken ) ;
} catch ( e ) {
2023-06-28 13:05:36 +00:00
if ( ! ! credentials . accessToken ) {
localStorage . setItem ( "mx_access_token" , credentials . accessToken ) ;
} else {
localStorage . removeItem ( "mx_access_token" ) ;
}
2020-12-11 02:52:18 +00:00
}
2022-10-14 06:57:14 +00:00
if ( localStorage . getItem ( "mx_has_pickle_key" ) === "true" ) {
2021-10-15 14:30:53 +00:00
logger . error ( "Expected a pickle key, but none provided. Encryption may not work." ) ;
2020-07-28 21:31:27 +00:00
}
}
2017-06-16 13:33:14 +00:00
// if we didn't get a deviceId from the login, leave mx_device_id unset,
// rather than setting it to "undefined".
//
// (in this case MatrixClient doesn't bother with the crypto stuff
// - that's fine for us).
if ( credentials . deviceId ) {
localStorage . setItem ( "mx_device_id" , credentials . deviceId ) ;
}
2020-10-08 15:35:17 +00:00
SecurityCustomisations . persistCredentials ? . ( credentials ) ;
2021-09-21 15:48:09 +00:00
logger . log ( ` Session persisted for ${ credentials . userId } ` ) ;
2017-06-16 13:33:14 +00:00
}
2018-07-15 21:33:00 +00:00
let _isLoggingOut = false ;
2016-08-02 17:55:13 +00:00
/ * *
* Logs the current session out and transitions to the logged - out state
* /
2020-10-07 11:14:36 +00:00
export function logout ( ) : void {
2023-07-10 15:01:59 +00:00
const client = MatrixClientPeg . get ( ) ;
if ( ! client ) return ;
2017-12-05 11:38:25 +00:00
2021-08-03 10:55:02 +00:00
PosthogAnalytics . instance . logout ( ) ;
2023-07-10 15:01:59 +00:00
if ( client . isGuest ( ) ) {
2016-08-02 13:04:20 +00:00
// logout doesn't work for guest sessions
2020-11-12 12:46:55 +00:00
// Also we sometimes want to re-log in a guest session if we abort the login.
2022-05-26 10:12:49 +00:00
// defer until next tick because it calls a synchronous dispatch, and we are likely here from a dispatch.
2020-11-12 12:46:55 +00:00
setImmediate ( ( ) = > onLoggedOut ( ) ) ;
2016-08-02 13:04:20 +00:00
return ;
}
2018-07-15 21:33:00 +00:00
_isLoggingOut = true ;
2023-04-25 08:28:48 +00:00
PlatformPeg . get ( ) ? . destroyPickleKey ( client . getSafeUserId ( ) , client . getDeviceId ( ) ? ? "" ) ;
2022-10-12 17:59:07 +00:00
client . logout ( true ) . then ( onLoggedOut , ( err ) = > {
2022-05-26 10:12:49 +00:00
// Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and
// you want to log into a different server, so just forget the
// access token. It's annoying that this will leave the access
// token still valid, but we should fix this by having access
// tokens expire (and if you really think you've been compromised,
// change your password).
logger . warn ( "Failed to call logout API: token will not be invalidated" , err ) ;
onLoggedOut ( ) ;
} ) ;
2016-08-02 13:04:20 +00:00
}
2020-10-07 11:14:36 +00:00
export function softLogout ( ) : void {
2019-07-03 22:46:37 +00:00
if ( ! MatrixClientPeg . get ( ) ) return ;
// Track that we've detected and trapped a soft logout. This helps prevent other
// parts of the app from starting if there's no point (ie: don't sync if we've
// been soft logged out, despite having credentials and data for a MatrixClient).
localStorage . setItem ( "mx_soft_logout" , "true" ) ;
2019-10-02 15:45:53 +00:00
// Dev note: please keep this log line around. It can be useful for track down
// random clients stopping in the middle of the logs.
2021-09-21 15:48:09 +00:00
logger . log ( "Soft logout initiated" ) ;
2019-07-03 22:46:37 +00:00
_isLoggingOut = true ; // to avoid repeated flags
2019-10-08 16:06:12 +00:00
// Ensure that we dispatch a view change **before** stopping the client so
// so that React components unmount first. This avoids React soft crashes
// that can occur when components try to use a null client.
2021-06-29 12:11:58 +00:00
dis . dispatch ( { action : "on_client_not_viable" } ) ; // generic version of on_logged_out
2019-10-08 16:06:12 +00:00
stopMatrixClient ( /*unsetClient=*/ false ) ;
2019-07-03 22:46:37 +00:00
// DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
}
2020-10-07 11:14:36 +00:00
export function isSoftLogout ( ) : boolean {
2019-07-03 22:46:37 +00:00
return localStorage . getItem ( "mx_soft_logout" ) === "true" ;
}
2020-10-07 11:14:36 +00:00
export function isLoggingOut ( ) : boolean {
2018-07-15 21:33:00 +00:00
return _isLoggingOut ;
}
2016-08-02 17:56:12 +00:00
/ * *
* Starts the matrix client and all other react - sdk services that
* listen for events while a session is logged in .
2023-05-23 15:24:12 +00:00
* @param client the matrix client to start
2019-07-03 22:46:37 +00:00
* @param { boolean } startSyncing True ( default ) to actually start
* syncing the client .
2016-08-02 17:56:12 +00:00
* /
2023-05-23 15:24:12 +00:00
async function startMatrixClient ( client : MatrixClient , startSyncing = true ) : Promise < void > {
2021-09-21 15:48:09 +00:00
logger . log ( ` Lifecycle: Starting MatrixClient ` ) ;
2017-07-05 15:20:21 +00:00
2016-08-02 13:04:20 +00:00
// dispatch this before starting the matrix client: it's used
// to add listeners for the 'sync' event so otherwise we'd have
// a race condition (and we need to dispatch synchronously for this
// to work).
2021-06-29 12:11:58 +00:00
dis . dispatch ( { action : "will_start_client" } , true ) ;
2016-08-02 13:04:20 +00:00
2020-05-22 20:55:43 +00:00
// reset things first just in case
2022-10-19 13:14:14 +00:00
SdkContextClass . instance . typingStore . reset ( ) ;
2020-05-22 20:55:43 +00:00
ToastStore . sharedInstance ( ) . reset ( ) ;
2023-05-23 15:24:12 +00:00
DialogOpener . instance . prepare ( client ) ;
2016-08-02 13:04:20 +00:00
Notifier . start ( ) ;
2019-03-08 12:46:38 +00:00
UserActivity . sharedInstance ( ) . start ( ) ;
2023-05-23 15:24:12 +00:00
DMRoomMap . makeShared ( client ) . start ( ) ;
2019-08-09 23:35:59 +00:00
IntegrationManagers . sharedInstance ( ) . startWatching ( ) ;
2021-09-26 17:57:02 +00:00
ActiveWidgetStore . instance . start ( ) ;
2022-08-30 19:13:39 +00:00
LegacyCallHandler . instance . start ( ) ;
2016-08-02 17:58:18 +00:00
2019-10-31 19:28:00 +00:00
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
// the thing just wastes CPU cycles, but should result in no actual functionality
// being exposed to the user.
Mjolnir . sharedInstance ( ) . start ( ) ;
2019-07-03 22:46:37 +00:00
if ( startSyncing ) {
2020-01-20 09:06:20 +00:00
// The client might want to populate some views with events from the
// index (e.g. the FilePanel), therefore initialize the event index
// before the client.
2019-11-12 12:29:07 +00:00
await EventIndexPeg . init ( ) ;
2020-01-15 11:06:29 +00:00
await MatrixClientPeg . start ( ) ;
2019-07-03 22:46:37 +00:00
} else {
2021-10-15 14:31:29 +00:00
logger . warn ( "Caller requested only auxiliary services be started" ) ;
2019-07-04 22:45:40 +00:00
await MatrixClientPeg . assign ( ) ;
2019-07-03 22:46:37 +00:00
}
2017-11-05 03:13:23 +00:00
2022-08-08 19:48:28 +00:00
// Run the migrations after the MatrixClientPeg has been assigned
SettingsStore . runMigrations ( ) ;
2020-01-17 11:43:35 +00:00
// This needs to be started after crypto is set up
2023-06-01 13:43:24 +00:00
DeviceListener . sharedInstance ( ) . start ( client ) ;
2020-04-07 11:07:41 +00:00
// Similarly, don't start sending presence updates until we've started
// the client
if ( ! SettingsStore . getValue ( "lowBandwidth" ) ) {
Presence . start ( ) ;
}
2020-01-17 11:43:35 +00:00
2020-04-06 22:04:41 +00:00
// Now that we have a MatrixClientPeg, update the Jitsi info
2022-05-03 21:04:37 +00:00
Jitsi . getInstance ( ) . start ( ) ;
2020-04-06 22:04:41 +00:00
2017-11-05 03:13:23 +00:00
// dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up.
2021-06-29 12:11:58 +00:00
dis . dispatch ( { action : "client_started" } ) ;
2019-07-03 22:46:37 +00:00
if ( isSoftLogout ( ) ) {
softLogout ( ) ;
}
2016-08-02 13:04:20 +00:00
}
2016-08-04 09:49:34 +00:00
/ *
2017-05-31 16:28:46 +00:00
* Stops a running client and all related services , and clears persistent
* storage . Used after a session has been logged out .
2016-08-04 09:49:34 +00:00
* /
2020-10-07 11:14:36 +00:00
export async function onLoggedOut ( ) : Promise < void > {
2022-05-26 08:56:53 +00:00
// Ensure that we dispatch a view change **before** stopping the client,
2022-05-26 10:12:49 +00:00
// that React components unmount first. This avoids React soft crashes
2019-10-08 16:06:12 +00:00
// that can occur when components try to use a null client.
2022-05-26 08:56:53 +00:00
dis . fire ( Action . OnLoggedOut , true ) ;
2016-08-11 12:50:38 +00:00
stopMatrixClient ( ) ;
2021-06-29 12:11:58 +00:00
await clearStorage ( { deleteEverything : true } ) ;
2020-11-27 11:19:44 +00:00
LifecycleCustomisations . onLoggedOutAndStorageCleared ? . ( ) ;
2023-03-15 15:56:29 +00:00
await PlatformPeg . get ( ) ? . clearStorage ( ) ;
2022-03-01 18:06:17 +00:00
2022-05-26 08:56:53 +00:00
// Do this last, so we can make sure all storage has been cleared and all
2022-03-01 18:06:17 +00:00
// customisations got the memo.
if ( SdkConfig . get ( ) . logout_redirect_url ) {
logger . log ( "Redirecting to external provider to finish logout" ) ;
2022-05-26 10:12:49 +00:00
// XXX: Defer this so that it doesn't race with MatrixChat unmounting the world by going to /#/login
2022-11-30 11:32:56 +00:00
window . setTimeout ( ( ) = > {
2023-04-06 10:10:14 +00:00
window . location . href = SdkConfig . get ( ) . logout_redirect_url ! ;
2022-05-26 10:12:49 +00:00
} , 100 ) ;
2022-03-01 18:06:17 +00:00
}
2022-05-26 10:12:49 +00:00
// Do this last to prevent racing `stopMatrixClient` and `on_logged_out` with MatrixChat handling Session.logged_out
_isLoggingOut = false ;
2016-08-02 13:04:20 +00:00
}
2017-05-31 16:28:46 +00:00
/ * *
2020-09-12 03:09:42 +00:00
* @param { object } opts Options for how to clear storage .
2017-05-31 16:28:46 +00:00
* @returns { Promise } promise which resolves once the stores have been cleared
* /
2020-10-07 12:32:30 +00:00
async function clearStorage ( opts ? : { deleteEverything? : boolean } ) : Promise < void > {
2017-05-31 16:28:46 +00:00
if ( window . localStorage ) {
2022-04-13 18:05:08 +00:00
// try to save any 3pid invites from being obliterated and registration time
2020-09-12 02:20:33 +00:00
const pendingInvites = ThreepidInviteStore . instance . getWireInvites ( ) ;
2022-04-13 18:05:08 +00:00
const registrationTime = window . localStorage . getItem ( "mx_registration_time" ) ;
2020-09-12 02:20:33 +00:00
2017-05-31 16:28:46 +00:00
window . localStorage . clear ( ) ;
2022-05-02 02:23:43 +00:00
AbstractLocalStorageSettingsHandler . clear ( ) ;
2020-09-12 02:20:33 +00:00
2020-12-11 02:52:18 +00:00
try {
await StorageManager . idbDelete ( "account" , "mx_access_token" ) ;
2021-11-17 22:01:45 +00:00
} catch ( e ) {
logger . error ( "idbDelete failed for account:mx_access_token" , e ) ;
}
2020-12-09 23:40:31 +00:00
2022-04-13 18:05:08 +00:00
// now restore those invites and registration time
2020-09-12 02:24:51 +00:00
if ( ! opts ? . deleteEverything ) {
2023-07-04 13:49:27 +00:00
pendingInvites . forEach ( ( { roomId , . . . invite } ) = > {
ThreepidInviteStore . instance . storeInvite ( roomId , invite ) ;
2020-09-12 02:24:51 +00:00
} ) ;
2022-04-13 18:05:08 +00:00
if ( registrationTime ) {
window . localStorage . setItem ( "mx_registration_time" , registrationTime ) ;
}
2020-09-12 02:24:51 +00:00
}
2016-09-02 10:25:10 +00:00
}
2017-05-31 16:28:46 +00:00
2022-05-26 10:12:49 +00:00
window . sessionStorage ? . clear ( ) ;
2020-04-06 15:40:53 +00:00
2017-05-31 16:28:46 +00:00
// create a temporary client to clear out the persistent stores.
const cli = createMatrixClient ( {
// we'll never make any requests, so can pass a bogus HS URL
baseUrl : "" ,
} ) ;
2019-11-13 08:52:59 +00:00
2019-11-19 10:05:37 +00:00
await EventIndexPeg . deleteEventIndex ( ) ;
await cli . clearStores ( ) ;
2016-09-02 10:25:10 +00:00
}
2016-08-04 09:49:34 +00:00
/ * *
2017-05-31 16:28:46 +00:00
* Stop all the background processes related to the current client .
2019-07-03 22:46:37 +00:00
* @param { boolean } unsetClient True ( default ) to abandon the client
* on MatrixClientPeg after stopping .
2016-08-04 09:49:34 +00:00
* /
2020-10-07 11:14:36 +00:00
export function stopMatrixClient ( unsetClient = true ) : void {
2016-08-02 13:04:20 +00:00
Notifier . stop ( ) ;
2022-08-30 19:13:39 +00:00
LegacyCallHandler . instance . stop ( ) ;
2019-03-08 15:09:44 +00:00
UserActivity . sharedInstance ( ) . stop ( ) ;
2022-10-19 13:14:14 +00:00
SdkContextClass . instance . typingStore . reset ( ) ;
2016-08-02 13:04:20 +00:00
Presence . stop ( ) ;
2021-09-26 17:57:02 +00:00
ActiveWidgetStore . instance . stop ( ) ;
2019-08-09 23:35:59 +00:00
IntegrationManagers . sharedInstance ( ) . stopWatching ( ) ;
2019-10-31 19:28:00 +00:00
Mjolnir . sharedInstance ( ) . stop ( ) ;
2020-01-17 11:43:35 +00:00
DeviceListener . sharedInstance ( ) . stop ( ) ;
2022-05-26 10:12:49 +00:00
DMRoomMap . shared ( ) ? . stop ( ) ;
2019-11-12 14:58:38 +00:00
EventIndexPeg . stop ( ) ;
2017-05-04 17:03:35 +00:00
const cli = MatrixClientPeg . get ( ) ;
2016-08-11 12:50:38 +00:00
if ( cli ) {
cli . stopClient ( ) ;
cli . removeAllListeners ( ) ;
2019-07-03 22:46:37 +00:00
if ( unsetClient ) {
MatrixClientPeg . unset ( ) ;
2019-11-19 10:05:37 +00:00
EventIndexPeg . unset ( ) ;
2023-05-16 15:08:01 +00:00
cli . store . destroy ( ) ;
2019-07-03 22:46:37 +00:00
}
2016-08-11 12:50:38 +00:00
}
2016-08-02 13:04:20 +00:00
}
2021-12-02 13:46:44 +00:00
// Utility method to perform a login with an existing access_token
window . mxLoginWithAccessToken = async ( hsUrl : string , accessToken : string ) : Promise < void > = > {
const tempClient = createClient ( {
baseUrl : hsUrl ,
accessToken ,
} ) ;
const { user_id : userId } = await tempClient . whoami ( ) ;
await doSetLoggedIn (
{
homeserverUrl : hsUrl ,
accessToken ,
userId ,
} ,
true ,
) ;
} ;