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

Commit 580bb55

Browse files
authored
OIDC: pass id_token via id_token_hint on Manage Account interaction (#12499)
* Store id_token rather than just id_token_claims Signed-off-by: Michael Telatynski <[email protected]> * Pass id_token via `id_token_hint` on `Manage Account` interaction Signed-off-by: Michael Telatynski <[email protected]> * Fix tests Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent e2310e6 commit 580bb55

File tree

8 files changed

+89
-33
lines changed

8 files changed

+89
-33
lines changed

src/Lifecycle.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ export async function attemptDelegatedAuthLogin(
289289
*/
290290
async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean> {
291291
try {
292-
const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idTokenClaims, clientId, issuer } =
292+
const { accessToken, refreshToken, homeserverUrl, identityServerUrl, idToken, clientId, issuer } =
293293
await completeOidcLogin(queryParams);
294294

295295
const {
@@ -311,7 +311,7 @@ async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean>
311311
logger.debug("Logged in via OIDC native flow");
312312
await onSuccessfulDelegatedAuthLogin(credentials);
313313
// this needs to happen after success handler which clears storages
314-
persistOidcAuthenticatedSettings(clientId, issuer, idTokenClaims);
314+
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
315315
return true;
316316
} catch (error) {
317317
logger.error("Failed to login via OIDC", error);

src/stores/oidc/OidcClientStore.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import { MatrixClient, discoverAndValidateOIDCIssuerWellKnown } from "matrix-js-
1818
import { logger } from "matrix-js-sdk/src/logger";
1919
import { OidcClient } from "oidc-client-ts";
2020

21-
import { getStoredOidcTokenIssuer, getStoredOidcClientId } from "../../utils/oidc/persistOidcSettings";
21+
import {
22+
getStoredOidcTokenIssuer,
23+
getStoredOidcClientId,
24+
getStoredOidcIdToken,
25+
} from "../../utils/oidc/persistOidcSettings";
2226
import PlatformPeg from "../../PlatformPeg";
2327

2428
/**
@@ -58,7 +62,7 @@ export class OidcClientStore {
5862
const { accountManagementEndpoint, metadata } = await discoverAndValidateOIDCIssuerWellKnown(
5963
authIssuer.issuer,
6064
);
61-
this._accountManagementEndpoint = accountManagementEndpoint ?? metadata.issuer;
65+
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
6266
} catch (e) {
6367
console.log("Auth issuer not found", e);
6468
}
@@ -72,6 +76,16 @@ export class OidcClientStore {
7276
return !!this.authenticatedIssuer;
7377
}
7478

79+
private setAccountManagementEndpoint(endpoint: string | undefined, issuer: string): void {
80+
// if no account endpoint is configured default to the issuer
81+
const url = new URL(endpoint ?? issuer);
82+
const idToken = getStoredOidcIdToken();
83+
if (idToken) {
84+
url.searchParams.set("id_token_hint", idToken);
85+
}
86+
this._accountManagementEndpoint = url.toString();
87+
}
88+
7589
public get accountManagementEndpoint(): string | undefined {
7690
return this._accountManagementEndpoint;
7791
}
@@ -150,8 +164,7 @@ export class OidcClientStore {
150164
const { accountManagementEndpoint, metadata, signingKeys } = await discoverAndValidateOIDCIssuerWellKnown(
151165
this.authenticatedIssuer,
152166
);
153-
// if no account endpoint is configured default to the issuer
154-
this._accountManagementEndpoint = accountManagementEndpoint ?? metadata.issuer;
167+
this.setAccountManagementEndpoint(accountManagementEndpoint, metadata.issuer);
155168
this.oidcClient = new OidcClient({
156169
...metadata,
157170
authority: metadata.issuer,

src/utils/oidc/authorize.ts

+3
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ type CompleteOidcLoginResponse = {
8686
accessToken: string;
8787
// refreshToken gained from OIDC token issuer, when falsy token cannot be refreshed
8888
refreshToken?: string;
89+
// idToken gained from OIDC token issuer
90+
idToken: string;
8991
// this client's id as registered with the OIDC issuer
9092
clientId: string;
9193
// issuer used during authentication
@@ -109,6 +111,7 @@ export const completeOidcLogin = async (queryParams: QueryDict): Promise<Complet
109111
identityServerUrl,
110112
accessToken: tokenResponse.access_token,
111113
refreshToken: tokenResponse.refresh_token,
114+
idToken: tokenResponse.id_token,
112115
clientId: oidcClientSettings.clientId,
113116
issuer: oidcClientSettings.issuer,
114117
idTokenClaims,

src/utils/oidc/persistOidcSettings.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,28 @@ limitations under the License.
1515
*/
1616

1717
import { IdTokenClaims } from "oidc-client-ts";
18+
import { decodeIdToken } from "matrix-js-sdk/src/matrix";
1819

1920
const clientIdStorageKey = "mx_oidc_client_id";
2021
const tokenIssuerStorageKey = "mx_oidc_token_issuer";
22+
const idTokenStorageKey = "mx_oidc_id_token";
23+
/**
24+
* @deprecated in favour of using idTokenStorageKey
25+
*/
2126
const idTokenClaimsStorageKey = "mx_oidc_id_token_claims";
2227

2328
/**
2429
* Persists oidc clientId and issuer in local storage
2530
* Only set after successful authentication
2631
* @param clientId
2732
* @param issuer
33+
* @param idToken
34+
* @param idTokenClaims
2835
*/
29-
export const persistOidcAuthenticatedSettings = (
30-
clientId: string,
31-
issuer: string,
32-
idTokenClaims: IdTokenClaims,
33-
): void => {
36+
export const persistOidcAuthenticatedSettings = (clientId: string, issuer: string, idToken: string): void => {
3437
localStorage.setItem(clientIdStorageKey, clientId);
3538
localStorage.setItem(tokenIssuerStorageKey, issuer);
36-
localStorage.setItem(idTokenClaimsStorageKey, JSON.stringify(idTokenClaims));
39+
localStorage.setItem(idTokenStorageKey, idToken);
3740
};
3841

3942
/**
@@ -59,13 +62,26 @@ export const getStoredOidcClientId = (): string => {
5962
};
6063

6164
/**
62-
* Retrieve stored id token claims from local storage
63-
* @returns idtokenclaims or undefined
65+
* Retrieve stored id token claims from stored id token or local storage
66+
* @returns idTokenClaims or undefined
6467
*/
6568
export const getStoredOidcIdTokenClaims = (): IdTokenClaims | undefined => {
69+
const idToken = getStoredOidcIdToken();
70+
if (idToken) {
71+
return decodeIdToken(idToken);
72+
}
73+
6674
const idTokenClaims = localStorage.getItem(idTokenClaimsStorageKey);
6775
if (!idTokenClaims) {
6876
return;
6977
}
7078
return JSON.parse(idTokenClaims) as IdTokenClaims;
7179
};
80+
81+
/**
82+
* Retrieve stored id token from local storage
83+
* @returns idToken or undefined
84+
*/
85+
export const getStoredOidcIdToken = (): string | undefined => {
86+
return localStorage.getItem(idTokenStorageKey) ?? undefined;
87+
};

test/Lifecycle-test.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -657,13 +657,8 @@ describe("Lifecycle", () => {
657657
const issuer = "https://auth.com/";
658658

659659
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
660-
const idTokenClaims = {
661-
aud: "123",
662-
iss: issuer,
663-
sub: "123",
664-
exp: 123,
665-
iat: 456,
666-
};
660+
const idToken =
661+
"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh4ZEhXb0Y5bW4ifQ.eyJzdWIiOiIwMUhQUDJGU0JZREU5UDlFTU04REQ3V1pIUiIsImlzcyI6Imh0dHBzOi8vYXV0aC1vaWRjLmxhYi5lbGVtZW50LmRldi8iLCJpYXQiOjE3MTUwNzE5ODUsImF1dGhfdGltZSI6MTcwNzk5MDMxMiwiY19oYXNoIjoidGt5R1RhUjU5aTk3YXoyTU4yMGdidyIsImV4cCI6MTcxNTA3NTU4NSwibm9uY2UiOiJxaXhwM0hFMmVaIiwiYXVkIjoiMDFIWDk0Mlg3QTg3REgxRUs2UDRaNjI4WEciLCJhdF9oYXNoIjoiNFlFUjdPRlVKTmRTeEVHV2hJUDlnZyJ9.HxODneXvSTfWB5Vc4cf7b8GiN2gdwUuTiyVqZuupWske2HkZiJZUt5Lsxg9BW3gz28POkE0Ln17snlkmy02B_AD3DQxKOOxQCzIIARHdfFvZxgGWsMdFcVQZDW7rtXcqgj-SpVaUQ_8acsgxSrz_DF2o0O4tto0PT6wVUiw8KlBmgWTscWPeAWe-39T-8EiQ8Wi16h6oSPcz2NzOQ7eOM_S9fDkOorgcBkRGLl1nrahrPSdWJSGAeruk5mX4YxN714YThFDyEA2t9YmKpjaiSQ2tT-Xkd7tgsZqeirNs2ni9mIiFX3bRX6t2AhUNzA7MaX9ZyizKGa6go3BESO_oDg";
667662

668663
beforeAll(() => {
669664
fetchMock.get(
@@ -682,7 +677,7 @@ describe("Lifecycle", () => {
682677
beforeEach(() => {
683678
initSessionStorageMock();
684679
// set values in session storage as they would be after a successful oidc authentication
685-
persistOidcAuthenticatedSettings(clientId, issuer, idTokenClaims);
680+
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
686681
});
687682

688683
it("should not try to create a token refresher without a refresh token", async () => {
@@ -712,7 +707,7 @@ describe("Lifecycle", () => {
712707
clientId,
713708
// @ts-ignore set undefined issuer
714709
undefined,
715-
idTokenClaims,
710+
idToken,
716711
);
717712
await setLoggedIn({
718713
...credentials,
@@ -744,7 +739,7 @@ describe("Lifecycle", () => {
744739

745740
it("should create a client when creating token refresher fails", async () => {
746741
// set invalid value in session storage for a malformed oidc authentication
747-
persistOidcAuthenticatedSettings(null as any, issuer, idTokenClaims);
742+
persistOidcAuthenticatedSettings(null as any, issuer, idToken);
748743

749744
// succeeded
750745
expect(

test/components/structures/MatrixChat-test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ describe("<MatrixChat />", () => {
284284
const tokenResponse: BearerTokenResponse = {
285285
access_token: accessToken,
286286
refresh_token: "def456",
287+
id_token: "ghi789",
287288
scope: "test",
288289
token_type: "Bearer",
289290
expires_at: 12345,

test/utils/oidc/authorize-test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ describe("OIDC authorization", () => {
115115
const tokenResponse: BearerTokenResponse = {
116116
access_token: "abc123",
117117
refresh_token: "def456",
118+
id_token: "ghi789",
118119
scope: "test",
119120
token_type: "Bearer",
120121
expires_at: 12345,
@@ -163,6 +164,7 @@ describe("OIDC authorization", () => {
163164
identityServerUrl,
164165
issuer,
165166
clientId,
167+
idToken: "ghi789",
166168
idTokenClaims: result.idTokenClaims,
167169
});
168170
});

test/utils/oidc/persistOidcSettings-test.ts

+35-9
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@ limitations under the License.
1515
*/
1616

1717
import { IdTokenClaims } from "oidc-client-ts";
18+
import { decodeIdToken } from "matrix-js-sdk/src/matrix";
19+
import { mocked } from "jest-mock";
1820

1921
import {
2022
getStoredOidcClientId,
23+
getStoredOidcIdToken,
2124
getStoredOidcIdTokenClaims,
2225
getStoredOidcTokenIssuer,
2326
persistOidcAuthenticatedSettings,
2427
} from "../../../src/utils/oidc/persistOidcSettings";
2528

29+
jest.mock("matrix-js-sdk/src/matrix");
30+
2631
describe("persist OIDC settings", () => {
2732
jest.spyOn(Storage.prototype, "getItem");
2833
jest.spyOn(Storage.prototype, "setItem");
@@ -33,6 +38,7 @@ describe("persist OIDC settings", () => {
3338

3439
const clientId = "test-client-id";
3540
const issuer = "https://auth.org/";
41+
const idToken = "test-id-token";
3642
const idTokenClaims: IdTokenClaims = {
3743
// audience is this client
3844
aud: "123",
@@ -44,45 +50,65 @@ describe("persist OIDC settings", () => {
4450
};
4551

4652
describe("persistOidcAuthenticatedSettings", () => {
47-
it("should set clientId and issuer in session storage", () => {
48-
persistOidcAuthenticatedSettings(clientId, issuer, idTokenClaims);
53+
it("should set clientId and issuer in localStorage", () => {
54+
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
4955
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_client_id", clientId);
5056
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_token_issuer", issuer);
51-
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_id_token_claims", JSON.stringify(idTokenClaims));
57+
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_id_token", idToken);
5258
});
5359
});
5460

5561
describe("getStoredOidcTokenIssuer()", () => {
56-
it("should return issuer from session storage", () => {
62+
it("should return issuer from localStorage", () => {
5763
localStorage.setItem("mx_oidc_token_issuer", issuer);
5864
expect(getStoredOidcTokenIssuer()).toEqual(issuer);
5965
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_token_issuer");
6066
});
6167

62-
it("should return undefined when no issuer in session storage", () => {
68+
it("should return undefined when no issuer in localStorage", () => {
6369
expect(getStoredOidcTokenIssuer()).toBeUndefined();
6470
});
6571
});
6672

6773
describe("getStoredOidcClientId()", () => {
68-
it("should return clientId from session storage", () => {
74+
it("should return clientId from localStorage", () => {
6975
localStorage.setItem("mx_oidc_client_id", clientId);
7076
expect(getStoredOidcClientId()).toEqual(clientId);
7177
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_client_id");
7278
});
73-
it("should throw when no clientId in session storage", () => {
79+
it("should throw when no clientId in localStorage", () => {
7480
expect(() => getStoredOidcClientId()).toThrow("Oidc client id not found in storage");
7581
});
7682
});
7783

84+
describe("getStoredOidcIdToken()", () => {
85+
it("should return token from localStorage", () => {
86+
localStorage.setItem("mx_oidc_id_token", idToken);
87+
expect(getStoredOidcIdToken()).toEqual(idToken);
88+
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token");
89+
});
90+
91+
it("should return undefined when no token in localStorage", () => {
92+
expect(getStoredOidcIdToken()).toBeUndefined();
93+
});
94+
});
95+
7896
describe("getStoredOidcIdTokenClaims()", () => {
79-
it("should return issuer from session storage", () => {
97+
it("should return claims from localStorage", () => {
8098
localStorage.setItem("mx_oidc_id_token_claims", JSON.stringify(idTokenClaims));
8199
expect(getStoredOidcIdTokenClaims()).toEqual(idTokenClaims);
82100
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token_claims");
83101
});
84102

85-
it("should return undefined when no issuer in session storage", () => {
103+
it("should return claims extracted from id_token in localStorage", () => {
104+
localStorage.setItem("mx_oidc_id_token", idToken);
105+
mocked(decodeIdToken).mockReturnValue(idTokenClaims);
106+
expect(getStoredOidcIdTokenClaims()).toEqual(idTokenClaims);
107+
expect(decodeIdToken).toHaveBeenCalledWith(idToken);
108+
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token_claims");
109+
});
110+
111+
it("should return undefined when no claims in localStorage", () => {
86112
expect(getStoredOidcIdTokenClaims()).toBeUndefined();
87113
});
88114
});

0 commit comments

Comments
 (0)