Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

OIDC: persist refresh token #11249

Merged
merged 24 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2607997
test persistCredentials without a pickle key
Jul 11, 2023
3506c06
Merge branch 'develop' into kerry/25708/test-persist-credentials
Jul 12, 2023
609f790
test setLoggedIn with pickle key
Jul 12, 2023
f3092c7
lint
Jul 12, 2023
fad7f33
type error
Jul 12, 2023
32d5fb0
extract token persisting code into function, persist refresh token
Jul 12, 2023
e6529f1
store has_refresh_token too
Jul 12, 2023
66d57e5
pass refreshToken from oidcAuthGrant into credentials
Jul 12, 2023
b33e347
rest restore session with pickle key
Jul 13, 2023
823ba2e
Merge branch 'kerry/25708/test-persist-credentials' into kerry/25708/…
Jul 13, 2023
b7e0603
Merge branch 'develop' into kerry/25708/test-persist-credentials
Jul 13, 2023
b8b0c86
Merge branch 'kerry/25708/test-persist-credentials' into kerry/25708/…
Jul 13, 2023
f059642
Merge branch 'develop' into kerry/25708/test-persist-credentials
Jul 16, 2023
9272110
Merge branch 'kerry/25708/test-persist-credentials' into kerry/25708/…
Jul 17, 2023
d24fbd0
Merge branch 'develop' into kerry/25708/save-refresh-token
Jul 19, 2023
880c258
Merge branch 'kerry/25708/save-refresh-token' of https://github.com/m…
Jul 19, 2023
65c0734
Merge branch 'develop' into kerry/25708/save-refresh-token
Jul 19, 2023
70ddb4a
comments
Jul 20, 2023
56441dc
Merge branch 'develop' into kerry/25708/save-refresh-token
Jul 20, 2023
af481b2
prettier
Jul 20, 2023
e2d2d33
Update src/Lifecycle.ts
Sep 17, 2023
678815d
Merge branch 'develop' into kerry/25708/save-refresh-token
Sep 17, 2023
1f903c9
comments
Sep 18, 2023
777dbba
Merge branch 'develop' into kerry/25708/save-refresh-token
Sep 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions src/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,22 @@ import { persistOidcAuthenticatedSettings } from "./utils/oidc/persistOidcSettin
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";

/**
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* Used as storage key
richvdh marked this conversation as resolved.
Show resolved Hide resolved
*/
const ACCESS_TOKEN_STORAGE_KEY = "mx_access_token";
const REFRESH_TOKEN_STORAGE_KEY = "mx_refresh_token";
/**
* Used during encryption/decryption of token
* Used as initialization vector during encryption in persistTokenInStorage
* And decryption in restoreFromLocalStorage
*/
const ACCESS_TOKEN_NAME = "access_token";
const REFRESH_TOKEN_NAME = "refresh_token";
richvdh marked this conversation as resolved.
Show resolved Hide resolved
/**
* Used in localstorage to store whether we expect a token in idb
richvdh marked this conversation as resolved.
Show resolved Hide resolved
*/
const HAS_ACCESS_TOKEN_STORAGE_KEY = "mx_has_access_token";
const HAS_REFRESH_TOKEN_STORAGE_KEY = "mx_has_refresh_token";

dis.register((payload) => {
if (payload.action === Action.TriggerLogout) {
Expand Down Expand Up @@ -224,7 +233,8 @@ export async function attemptDelegatedAuthLogin(
*/
async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean> {
try {
const { accessToken, refreshToken, homeserverUrl, identityServerUrl } = await completeOidcLogin(queryParams);
const { accessToken, refreshToken, homeserverUrl, identityServerUrl, clientId, issuer } =
await completeOidcLogin(queryParams);

const {
user_id: userId,
Expand Down Expand Up @@ -482,7 +492,7 @@ export async function getStoredSessionVars(): Promise<Partial<IStoredSession>> {
}
// 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_NAME}`) === "true" || !!accessToken;
const hasAccessToken = localStorage.getItem(HAS_ACCESS_TOKEN_STORAGE_KEY) === "true" || !!accessToken;
const userId = localStorage.getItem("mx_user_id") ?? undefined;
const deviceId = localStorage.getItem("mx_device_id") ?? undefined;

Expand All @@ -499,7 +509,7 @@ export async function getStoredSessionVars(): Promise<Partial<IStoredSession>> {

// The pickle key is a string of unspecified length and format. For AES, we
// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
// key. The AES key should be zeroed after it is used.
// key. The AES key should be zeroed after it is used
richvdh marked this conversation as resolved.
Show resolved Hide resolved
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
for (let i = 0; i < pickleKey.length; i++) {
Expand Down Expand Up @@ -780,16 +790,18 @@ class AbortLoginAndRebuildStorage extends Error {}
*
* @param storageKey key used to store the token
* @param name eg "access_token" used as initialization vector during encryption
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* @param token
* only used when pickleKey is present to encrypt with
* @param token the token to store, when undefined any existing token at the storageKey is removed from storage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param token the token to store, when undefined any existing token at the storageKey is removed from storage
* @param token The token to store. When undefined, any existing token at the `storageKey` is removed from storage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could still use clarification

* @param pickleKey optional pickle key used to encrypt token
* @param hasTokenStorageKey used to store in localstorage whether we expect to have a token in idb, eg "mx_has_access_token"
richvdh marked this conversation as resolved.
Show resolved Hide resolved
*/
async function persistTokenInStorage(
storageKey: string,
name: string,
richvdh marked this conversation as resolved.
Show resolved Hide resolved
token: string | undefined,
pickleKey: IMatrixClientCreds["pickleKey"],
kerryarchibald marked this conversation as resolved.
Show resolved Hide resolved
hasTokenStorageKey: string,
): Promise<void> {
const hasTokenStorageKey = `mx_has_${name}`;
// store whether we expect to find a token, to detect the case
// where IndexedDB is blown away
if (token) {
Expand Down Expand Up @@ -852,12 +864,14 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise<void
ACCESS_TOKEN_NAME,
credentials.accessToken,
credentials.pickleKey,
HAS_ACCESS_TOKEN_STORAGE_KEY,
);
await persistTokenInStorage(
REFRESH_TOKEN_STORAGE_KEY,
REFRESH_TOKEN_NAME,
credentials.refreshToken,
credentials.pickleKey,
HAS_REFRESH_TOKEN_STORAGE_KEY,
);

if (credentials.pickleKey) {
Expand Down
33 changes: 20 additions & 13 deletions src/utils/oidc/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,30 +69,37 @@ const getCodeAndStateFromQueryParams = (queryParams: QueryDict): { code: string;
return { code, state };
};

type CompleteOidcLoginResponse = {
// url of the homeserver selected during login
homeserverUrl: string;
// identity server url as discovered during login
identityServerUrl?: string;
// accessToken gained from OIDC token issuer
accessToken: string;
// refreshToken gained from OIDC token issuer, when falsy token cannot be refreshed
refreshToken?: string;
// this client's id as registered with the OIDC issuer
clientId: string;
// issuer used during authentication
issuer: string;
};
/**
* Attempt to complete authorization code flow to get an access token
* @param queryParams the query-parameters extracted from the real query-string of the starting URI.
* @returns Promise that resolves with accessToken, identityServerUrl, and homeserverUrl when login was successful
* @returns Promise that resolves with a CompleteOidcLoginResponse when login was successful
* @throws When we failed to get a valid access token
*/
export const completeOidcLogin = async (
queryParams: QueryDict,
): Promise<{
homeserverUrl: string;
identityServerUrl?: string;
accessToken: string;
refreshToken?: string;
}> => {
export const completeOidcLogin = async (queryParams: QueryDict): Promise<CompleteOidcLoginResponse> => {
const { code, state } = getCodeAndStateFromQueryParams(queryParams);
const { homeserverUrl, tokenResponse, identityServerUrl, oidcClientSettings } =
await completeAuthorizationCodeGrant(code, state);

// @TODO(kerrya) do something with the refresh token https://github.com/vector-im/element-web/issues/25444

return {
homeserverUrl: homeserverUrl,
identityServerUrl: identityServerUrl,
homeserverUrl,
identityServerUrl,
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,
richvdh marked this conversation as resolved.
Show resolved Hide resolved
clientId: oidcClientSettings.clientId,
issuer: oidcClientSettings.issuer,
};
};