Get the access token directly
parent
6cf7dca4f9
commit
af1ba39f20
|
@ -1,3 +1,23 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd
|
||||||
|
|
||||||
|
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 { idbLoad } from "matrix-react-sdk/src/utils/StorageAccess";
|
||||||
|
import { ACCESS_TOKEN_IV, tryDecryptToken } from "matrix-react-sdk/src/utils/tokens/tokens";
|
||||||
|
import { encodeUnpaddedBase64 } from "matrix-js-sdk/src/base64";
|
||||||
|
|
||||||
const serverSupportMap: {
|
const serverSupportMap: {
|
||||||
[serverUrl: string]: {
|
[serverUrl: string]: {
|
||||||
supportsMSC3916: boolean;
|
supportsMSC3916: boolean;
|
||||||
|
@ -5,20 +25,16 @@ const serverSupportMap: {
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const credentialStore: {
|
self.addEventListener("install", (event) => {
|
||||||
[serverUrl: string]: string;
|
// We skipWaiting() to update the service worker more frequently, particularly in development environments.
|
||||||
} = {};
|
// @ts-expect-error - service worker types are not available. See 'fetch' event handler.
|
||||||
|
event.waitUntil(skipWaiting());
|
||||||
|
});
|
||||||
|
|
||||||
// We skipWaiting() to update the service worker more frequently, particularly in development environments.
|
self.addEventListener("activate", (event) => {
|
||||||
// @ts-expect-error - service worker types are not available. See 'fetch' event handler.
|
// We force all clients to be under our control, immediately. This could be old tabs.
|
||||||
skipWaiting();
|
// @ts-expect-error - service worker types are not available. See 'fetch' event handler.
|
||||||
|
event.waitUntil(clients.claim());
|
||||||
self.addEventListener("message", (event) => {
|
|
||||||
if (event.data?.type !== "credentials") return; // ignore
|
|
||||||
credentialStore[event.data.homeserverUrl] = event.data.accessToken;
|
|
||||||
console.log(
|
|
||||||
`[Service Worker] Updated access token for ${event.data.homeserverUrl} (accessToken? ${Boolean(event.data.accessToken)})`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error - getting types to work for this is difficult, so we anticipate that "addEventListener" doesn't
|
// @ts-expect-error - getting types to work for this is difficult, so we anticipate that "addEventListener" doesn't
|
||||||
|
@ -42,34 +58,40 @@ self.addEventListener("fetch", (event: FetchEvent) => {
|
||||||
// later on we need to proxy the request through if it turns out the server doesn't support authentication.
|
// later on we need to proxy the request through if it turns out the server doesn't support authentication.
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
(async (): Promise<Response> => {
|
(async (): Promise<Response> => {
|
||||||
// Figure out which homeserver we're communicating with
|
|
||||||
const csApi = url.substring(0, url.indexOf("/_matrix/media/v3"));
|
|
||||||
|
|
||||||
// Locate our access token, and populate the fetchConfig with the authentication header.
|
|
||||||
const accessToken = credentialStore[csApi];
|
|
||||||
let fetchConfig: { headers?: { [key: string]: string } } = {};
|
let fetchConfig: { headers?: { [key: string]: string } } = {};
|
||||||
if (accessToken) {
|
try {
|
||||||
fetchConfig = {
|
// Figure out which homeserver we're communicating with
|
||||||
headers: {
|
const csApi = url.substring(0, url.indexOf("/_matrix/media/v3"));
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update or populate the server support map using a (usually) authenticated `/versions` call.
|
// Locate our access token, and populate the fetchConfig with the authentication header.
|
||||||
if (!serverSupportMap[csApi] || serverSupportMap[csApi].cacheExpires <= new Date().getTime()) {
|
// @ts-expect-error - service worker types are not available. See 'fetch' event handler.
|
||||||
const versions = await (await fetch(`${csApi}/_matrix/client/versions`, fetchConfig)).json();
|
const client = await self.clients.get(event.clientId);
|
||||||
serverSupportMap[csApi] = {
|
const accessToken = await getAccessToken(client);
|
||||||
supportsMSC3916: Boolean(versions?.unstable_features?.["org.matrix.msc3916"]),
|
if (accessToken) {
|
||||||
cacheExpires: new Date().getTime() + 2 * 60 * 60 * 1000, // 2 hours from now
|
fetchConfig = {
|
||||||
};
|
headers: {
|
||||||
}
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// If we have server support (and a means of authentication), rewrite the URL to use MSC3916 endpoints.
|
// Update or populate the server support map using a (usually) authenticated `/versions` call.
|
||||||
if (serverSupportMap[csApi].supportsMSC3916 && accessToken) {
|
if (!serverSupportMap[csApi] || serverSupportMap[csApi].cacheExpires <= new Date().getTime()) {
|
||||||
// Currently unstable only.
|
const versions = await (await fetch(`${csApi}/_matrix/client/versions`, fetchConfig)).json();
|
||||||
url = url.replace(/\/media\/v3\/(.*)\//, "/client/unstable/org.matrix.msc3916/media/$1/");
|
serverSupportMap[csApi] = {
|
||||||
} // else by default we make no changes
|
supportsMSC3916: Boolean(versions?.unstable_features?.["org.matrix.msc3916"]),
|
||||||
|
cacheExpires: new Date().getTime() + 2 * 60 * 60 * 1000, // 2 hours from now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have server support (and a means of authentication), rewrite the URL to use MSC3916 endpoints.
|
||||||
|
if (serverSupportMap[csApi].supportsMSC3916 && accessToken) {
|
||||||
|
// Currently unstable only.
|
||||||
|
url = url.replace(/\/media\/v3\/(.*)\//, "/client/unstable/org.matrix.msc3916/media/$1/");
|
||||||
|
} // else by default we make no changes
|
||||||
|
} catch (err) {
|
||||||
|
console.error("SW: Error in request rewrite.", err);
|
||||||
|
}
|
||||||
|
|
||||||
// Add authentication and send the request. We add authentication even if MSC3916 endpoints aren't
|
// Add authentication and send the request. We add authentication even if MSC3916 endpoints aren't
|
||||||
// being used to ensure patches like this work:
|
// being used to ensure patches like this work:
|
||||||
|
@ -79,3 +101,69 @@ self.addEventListener("fetch", (event: FetchEvent) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ideally we'd use the `Client` interface for `client`, but since it's not available (see 'fetch' listener), we use
|
||||||
|
// unknown for now and force-cast it to something close enough later.
|
||||||
|
async function getAccessToken(client: unknown): Promise<string | undefined> {
|
||||||
|
// Access tokens are encrypted at rest, so while we can grab the "access token", we'll need to do work to get the
|
||||||
|
// real thing.
|
||||||
|
const encryptedAccessToken = await idbLoad("account", "mx_access_token");
|
||||||
|
|
||||||
|
// We need to extract a user ID and device ID from localstorage, which means calling WebPlatform for the
|
||||||
|
// read operation. Service workers can't access localstorage.
|
||||||
|
const { userId, deviceId } = await new Promise<{ userId: string; deviceId: string }>((resolve, reject) => {
|
||||||
|
// Avoid stalling the tab in case something goes wrong.
|
||||||
|
const timeoutId = setTimeout(() => reject(new Error("timeout in postMessage")), 1000);
|
||||||
|
|
||||||
|
// We don't need particularly good randomness here - we just use this to generate a request ID, so we know
|
||||||
|
// which postMessage reply is for our active request.
|
||||||
|
const responseKey = Math.random().toString(36);
|
||||||
|
|
||||||
|
// Add the listener first, just in case the tab is *really* fast.
|
||||||
|
const listener = (event: MessageEvent): void => {
|
||||||
|
if (event.data?.responseKey !== responseKey) return; // not for us
|
||||||
|
clearTimeout(timeoutId); // do this as soon as possible, avoiding a race between resolve and reject.
|
||||||
|
resolve(event.data); // "unblock" the remainder of the thread, if that were such a thing in JavaScript.
|
||||||
|
self.removeEventListener("message", listener); // cleanup, since we're not going to do anything else.
|
||||||
|
};
|
||||||
|
self.addEventListener("message", listener);
|
||||||
|
|
||||||
|
// Ask the tab for the information we need. This is handled by WebPlatform.
|
||||||
|
(client as Window).postMessage({ responseKey, type: "userinfo" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ... and this is why we need the user ID and device ID: they're index keys for the pickle key table.
|
||||||
|
const pickleKeyData = await idbLoad("pickleKey", [userId, deviceId]);
|
||||||
|
if (pickleKeyData && (!pickleKeyData.encrypted || !pickleKeyData.iv || !pickleKeyData.cryptoKey)) {
|
||||||
|
console.error("SW: Invalid pickle key loaded - ignoring");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pickleKey: string | undefined;
|
||||||
|
|
||||||
|
// Extract a useful pickle key out of our queries.
|
||||||
|
if (pickleKeyData) {
|
||||||
|
// We also need to generate the additional data needed for the key
|
||||||
|
const additionalData = new Uint8Array(userId.length + deviceId.length + 1);
|
||||||
|
for (let i = 0; i < userId.length; i++) {
|
||||||
|
additionalData[i] = userId.charCodeAt(i);
|
||||||
|
}
|
||||||
|
additionalData[userId.length] = 124; // "|"
|
||||||
|
for (let i = 0; i < deviceId.length; i++) {
|
||||||
|
additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert pickle key to a base64 key we can use
|
||||||
|
const pickleKeyBuf = await crypto.subtle.decrypt(
|
||||||
|
{ name: "AES-GCM", iv: pickleKeyData.iv, additionalData },
|
||||||
|
pickleKeyData.cryptoKey,
|
||||||
|
pickleKeyData.encrypted,
|
||||||
|
);
|
||||||
|
if (pickleKeyBuf) {
|
||||||
|
pickleKey = encodeUnpaddedBase64(pickleKeyBuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, try decrypting the thing and return that. This may fail, but that's okay.
|
||||||
|
return tryDecryptToken(pickleKey, encryptedAccessToken, ACCESS_TOKEN_IV);
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2017-2020 New Vector Ltd
|
Copyright 2017-2020, 2024 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -52,7 +52,27 @@ export default class WebPlatform extends VectorBasePlatform {
|
||||||
// Jest causes `register()` to return undefined, so swallow that case.
|
// Jest causes `register()` to return undefined, so swallow that case.
|
||||||
if (swPromise) {
|
if (swPromise) {
|
||||||
swPromise
|
swPromise
|
||||||
.then((r) => r.update())
|
.then(async (r) => {
|
||||||
|
await r.update();
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
navigator.serviceWorker.addEventListener("message", (e) => {
|
||||||
|
try {
|
||||||
|
if (e.data?.["type"] === "userinfo" && e.data?.["responseKey"]) {
|
||||||
|
const userId = localStorage.getItem("mx_user_id");
|
||||||
|
const deviceId = localStorage.getItem("mx_device_id");
|
||||||
|
r.active!.postMessage({
|
||||||
|
responseKey: e.data["responseKey"],
|
||||||
|
userId,
|
||||||
|
deviceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error responding to service worker: ", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
.catch((e) => console.error("Error registering/updating service worker:", e));
|
.catch((e) => console.error("Error registering/updating service worker:", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue