diff --git a/src/@types/auth.ts b/src/@types/auth.ts new file mode 100644 index 00000000000..592974221ba --- /dev/null +++ b/src/@types/auth.ts @@ -0,0 +1,29 @@ +/* +Copyright 2022 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. +*/ + +// disable lint because these are wire responses +/* eslint-disable camelcase */ + +/** + * Represents a response to the CSAPI `/refresh` endpoint. + */ +export interface IRefreshTokenResponse { + access_token: string; + expires_in_ms: number; + refresh_token: string; +} + +/* eslint-enable camelcase */ diff --git a/src/client.ts b/src/client.ts index 64d669b3dcf..dafa4937f93 100644 --- a/src/client.ts +++ b/src/client.ts @@ -20,7 +20,7 @@ limitations under the License. */ import { EventEmitter } from "events"; -import { EmoteEvent, MessageEvent, NoticeEvent, IPartialEvent } from "matrix-events-sdk"; +import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk"; import { ISyncStateData, SyncApi, SyncState } from "./sync"; import { EventStatus, IContent, IDecryptOptions, IEvent, MatrixEvent } from "./models/event"; @@ -52,6 +52,7 @@ import { PREFIX_MEDIA_R0, PREFIX_R0, PREFIX_UNSTABLE, + PREFIX_V1, retryNetworkOperation, UploadContentResponseType, } from "./http-api"; @@ -87,14 +88,7 @@ import { } from "./crypto/keybackup"; import { IIdentityServerProvider } from "./@types/IIdentityServerProvider"; import { MatrixScheduler } from "./scheduler"; -import { - IAuthData, - ICryptoCallbacks, - IMinimalEvent, - IRoomEvent, - IStateEvent, - NotificationCountType, -} from "./matrix"; +import { IAuthData, ICryptoCallbacks, IMinimalEvent, IRoomEvent, IStateEvent, NotificationCountType } from "./matrix"; import { CrossSigningKey, IAddSecretStorageKeyOpts, @@ -160,6 +154,7 @@ import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, Rule import { IThreepid } from "./@types/threepids"; import { CryptoStore } from "./crypto/store/base"; import { MediaHandler } from "./webrtc/mediaHandler"; +import { IRefreshTokenResponse } from "./@types/auth"; export type Store = IStore; export type SessionStore = WebStorageSessionStore; @@ -6619,6 +6614,14 @@ export class MatrixClient extends EventEmitter { return this.http.opts.accessToken || null; } + /** + * Set the access token associated with this account. + * @param {string} token The new access token. + */ + public setAccessToken(token: string) { + this.http.opts.accessToken = token; + } + /** * @return {boolean} true if there is a valid access_token for this client. */ @@ -6695,6 +6698,7 @@ export class MatrixClient extends EventEmitter { const params: any = { auth: auth, + refresh_token: true, // always ask for a refresh token - does nothing if unsupported }; if (username !== undefined && username !== null) { params.username = username; @@ -6772,6 +6776,31 @@ export class MatrixClient extends EventEmitter { return this.http.request(callback, Method.Post, "/register", params, data); } + /** + * Refreshes an access token using a provided refresh token. The refresh token + * must be valid for the current access token known to the client instance. + * + * Note that this function will not cause a logout if the token is deemed + * unknown by the server - the caller is responsible for managing logout + * actions on error. + * @param {string} refreshToken The refresh token. + * @return {Promise} Resolves to the new token. + * @return {module:http-api.MatrixError} Rejects with an error response. + */ + public refreshToken(refreshToken: string): Promise { + return this.http.authedRequest( + undefined, + Method.Post, + "/refresh", + undefined, + { refresh_token: refreshToken }, + { + prefix: PREFIX_V1, + inhibitLogoutEmit: true, // we don't want to cause logout loops + }, + ); + } + /** * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: TODO diff --git a/src/http-api.ts b/src/http-api.ts index 331fcb24821..fd016c731e8 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -111,6 +111,12 @@ interface IRequestOpts { json?: boolean; // defaults to true qsStringifyOptions?: CoreOptions["qsStringifyOptions"]; bodyParser?(body: string): T; + + // Set to true to prevent the request function from emitting + // a Session.logged_out event. This is intended for use on + // endpoints where M_UNKNOWN_TOKEN is a valid/notable error + // response, such as with token refreshes. + inhibitLogoutEmit?: boolean; } export interface IUpload { @@ -596,7 +602,7 @@ export class MatrixHttpApi { const requestPromise = this.request(callback, method, path, queryParams, data, requestOpts); requestPromise.catch((err: MatrixError) => { - if (err.errcode == 'M_UNKNOWN_TOKEN') { + if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) { this.eventEmitter.emit("Session.logged_out", err); } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') { this.eventEmitter.emit(