Skip to content

Commit c0e30ce

Browse files
authored
Switch OIDC primarily to new /auth_metadata API (#4626)
1 parent 61375ef commit c0e30ce

16 files changed

+269
-195
lines changed

Diff for: spec/integ/rendezvous/MSC4108SignInWithQR.spec.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
MatrixError,
3737
MatrixHttpApi,
3838
} from "../../../src";
39-
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
39+
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
4040

4141
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
4242
const baseUrl = "https://example.com";
@@ -57,7 +57,7 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
5757
getDomain: () => "example.com",
5858
getDevice: jest.fn(),
5959
getCrypto: jest.fn(() => crypto),
60-
getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }),
60+
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
6161
} as unknown as MatrixClient;
6262
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
6363
baseUrl: client.baseUrl,
@@ -69,10 +69,6 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
6969

7070
describe("MSC4108SignInWithQR", () => {
7171
beforeEach(() => {
72-
fetchMock.get(
73-
"https://issuer/.well-known/openid-configuration",
74-
mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]),
75-
);
7672
fetchMock.get("https://issuer/jwks", {
7773
status: 200,
7874
headers: {

Diff for: spec/test-utils/oidc.ts

+1-39
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,4 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
18-
19-
/**
20-
* Makes a valid OidcClientConfig with minimum valid values
21-
* @param issuer used as the base for all other urls
22-
* @returns OidcClientConfig
23-
*/
24-
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
25-
const metadata = mockOpenIdConfiguration(issuer);
26-
27-
return {
28-
accountManagementEndpoint: issuer + "account",
29-
registrationEndpoint: metadata.registration_endpoint,
30-
authorizationEndpoint: metadata.authorization_endpoint,
31-
tokenEndpoint: metadata.token_endpoint,
32-
metadata,
33-
};
34-
};
35-
36-
/**
37-
* Useful for mocking <issuer>/.well-known/openid-configuration
38-
* @param issuer used as the base for all other urls
39-
* @returns ValidatedIssuerMetadata
40-
*/
41-
export const mockOpenIdConfiguration = (
42-
issuer = "https://auth.org/",
43-
additionalGrantTypes: string[] = [],
44-
): ValidatedIssuerMetadata => ({
45-
issuer,
46-
revocation_endpoint: issuer + "revoke",
47-
token_endpoint: issuer + "token",
48-
authorization_endpoint: issuer + "auth",
49-
registration_endpoint: issuer + "registration",
50-
device_authorization_endpoint: issuer + "device",
51-
jwks_uri: issuer + "jwks",
52-
response_types_supported: ["code"],
53-
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
54-
code_challenge_methods_supported: ["S256"],
55-
});
17+
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../src/testing.ts";

Diff for: spec/unit/matrix-client.spec.ts

+70-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import { Mocked, mocked } from "jest-mock";
18+
import fetchMock from "fetch-mock-jest";
1819

1920
import { logger } from "../../src/logger";
2021
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
@@ -76,6 +77,7 @@ import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from ".
7677
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
7778
import { KnownMembership } from "../../src/@types/membership";
7879
import { RoomMessageEventContent } from "../../src/@types/events";
80+
import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
7981

8082
jest.useFakeTimers();
8183

@@ -265,13 +267,17 @@ describe("MatrixClient", function () {
265267

266268
if (next.error) {
267269
// eslint-disable-next-line
268-
return Promise.reject({
269-
errcode: (<MatrixError>next.error).errcode,
270-
httpStatus: (<MatrixError>next.error).httpStatus,
271-
name: (<MatrixError>next.error).errcode,
272-
message: "Expected testing error",
273-
data: next.error,
274-
});
270+
return Promise.reject(
271+
new MatrixError(
272+
{
273+
errcode: (<MatrixError>next.error).errcode,
274+
name: (<MatrixError>next.error).errcode,
275+
message: "Expected testing error",
276+
data: next.error,
277+
},
278+
(<MatrixError>next.error).httpStatus,
279+
),
280+
);
275281
}
276282
return Promise.resolve(next.data);
277283
}
@@ -3489,6 +3495,63 @@ describe("MatrixClient", function () {
34893495
});
34903496
});
34913497

3498+
describe("getAuthMetadata", () => {
3499+
beforeEach(() => {
3500+
fetchMock.mockReset();
3501+
// This request is made by oidc-client-ts so is not intercepted by httpLookups
3502+
fetchMock.get("https://auth.org/jwks", {
3503+
status: 200,
3504+
headers: {
3505+
"Content-Type": "application/json",
3506+
},
3507+
keys: [],
3508+
});
3509+
});
3510+
3511+
it("should use unstable prefix", async () => {
3512+
const metadata = mockOpenIdConfiguration();
3513+
httpLookups = [
3514+
{
3515+
method: "GET",
3516+
path: `/auth_metadata`,
3517+
data: metadata,
3518+
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
3519+
},
3520+
];
3521+
3522+
await expect(client.getAuthMetadata()).resolves.toEqual({
3523+
...metadata,
3524+
signingKeys: [],
3525+
});
3526+
expect(httpLookups.length).toEqual(0);
3527+
});
3528+
3529+
it("should fall back to auth_issuer + openid-configuration", async () => {
3530+
const metadata = mockOpenIdConfiguration();
3531+
httpLookups = [
3532+
{
3533+
method: "GET",
3534+
path: `/auth_metadata`,
3535+
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
3536+
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
3537+
},
3538+
{
3539+
method: "GET",
3540+
path: `/auth_issuer`,
3541+
data: { issuer: metadata.issuer },
3542+
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
3543+
},
3544+
];
3545+
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);
3546+
3547+
await expect(client.getAuthMetadata()).resolves.toEqual({
3548+
...metadata,
3549+
signingKeys: [],
3550+
});
3551+
expect(httpLookups.length).toEqual(0);
3552+
});
3553+
});
3554+
34923555
describe("identityHashedLookup", () => {
34933556
it("should return hashed lookup results", async () => {
34943557
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";

Diff for: spec/unit/oidc/authorize.spec.ts

+5-12
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ jest.mock("jwt-decode");
4040

4141
describe("oidc authorization", () => {
4242
const delegatedAuthConfig = makeDelegatedAuthConfig();
43-
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
44-
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
43+
const authorizationEndpoint = delegatedAuthConfig.authorization_endpoint;
44+
const tokenEndpoint = delegatedAuthConfig.token_endpoint;
4545
const clientId = "xyz789";
4646
const baseUrl = "https://test.com";
4747

@@ -52,10 +52,7 @@ describe("oidc authorization", () => {
5252
jest.spyOn(logger, "warn");
5353
jest.setSystemTime(now);
5454

55-
fetchMock.get(
56-
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
57-
mockOpenIdConfiguration(),
58-
);
55+
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
5956
globalThis.TextEncoder = TextEncoder;
6057
});
6158

@@ -127,11 +124,9 @@ describe("oidc authorization", () => {
127124
it("should generate url with correct parameters", async () => {
128125
const nonce = "abc123";
129126

130-
const metadata = delegatedAuthConfig.metadata;
131-
132127
const authUrl = new URL(
133128
await generateOidcAuthorizationUrl({
134-
metadata,
129+
metadata: delegatedAuthConfig,
135130
homeserverUrl: baseUrl,
136131
clientId,
137132
redirectUri: baseUrl,
@@ -156,11 +151,9 @@ describe("oidc authorization", () => {
156151
it("should generate url with create prompt", async () => {
157152
const nonce = "abc123";
158153

159-
const metadata = delegatedAuthConfig.metadata;
160-
161154
const authUrl = new URL(
162155
await generateOidcAuthorizationUrl({
163-
metadata,
156+
metadata: delegatedAuthConfig,
164157
homeserverUrl: baseUrl,
165158
clientId,
166159
redirectUri: baseUrl,

Diff for: spec/unit/oidc/register.spec.ts

+6-9
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ describe("registerOidcClient()", () => {
4242
});
4343

4444
it("should make correct request to register client", async () => {
45-
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
45+
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
4646
status: 200,
4747
body: JSON.stringify({ client_id: dynamicClientId }),
4848
});
4949
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
5050
expect(fetchMockJest).toHaveBeenCalledWith(
51-
delegatedAuthConfig.registrationEndpoint!,
51+
delegatedAuthConfig.registration_endpoint,
5252
expect.objectContaining({
5353
headers: {
5454
"Accept": "application/json",
@@ -72,7 +72,7 @@ describe("registerOidcClient()", () => {
7272
});
7373

7474
it("should throw when registration request fails", async () => {
75-
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
75+
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
7676
status: 500,
7777
});
7878
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
@@ -81,7 +81,7 @@ describe("registerOidcClient()", () => {
8181
});
8282

8383
it("should throw when registration response is invalid", async () => {
84-
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
84+
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
8585
status: 200,
8686
// no clientId in response
8787
body: "{}",
@@ -96,7 +96,7 @@ describe("registerOidcClient()", () => {
9696
registerOidcClient(
9797
{
9898
...delegatedAuthConfig,
99-
registrationEndpoint: undefined,
99+
registration_endpoint: undefined,
100100
},
101101
metadata,
102102
),
@@ -108,10 +108,7 @@ describe("registerOidcClient()", () => {
108108
registerOidcClient(
109109
{
110110
...delegatedAuthConfig,
111-
metadata: {
112-
...delegatedAuthConfig.metadata,
113-
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
114-
},
111+
grant_types_supported: [delegatedAuthConfig.grant_types_supported[0]],
115112
},
116113
metadata,
117114
),

Diff for: spec/unit/oidc/tokenRefresher.spec.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@ describe("OidcTokenRefresher", () => {
5555
});
5656

5757
beforeEach(() => {
58-
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
59-
fetchMock.get(`${config.metadata.issuer}jwks`, {
58+
fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config);
59+
fetchMock.get(`${config.issuer}jwks`, {
6060
status: 200,
6161
headers: {
6262
"Content-Type": "application/json",
6363
},
6464
keys: [],
6565
});
6666

67-
fetchMock.post(config.tokenEndpoint, {
67+
fetchMock.post(config.token_endpoint, {
6868
status: 200,
6969
headers: {
7070
"Content-Type": "application/json",
@@ -81,7 +81,7 @@ describe("OidcTokenRefresher", () => {
8181
it("throws when oidc client cannot be initialised", async () => {
8282
jest.spyOn(logger, "error");
8383
fetchMock.get(
84-
`${config.metadata.issuer}.well-known/openid-configuration`,
84+
`${config.issuer}.well-known/openid-configuration`,
8585
{
8686
ok: false,
8787
status: 404,
@@ -126,7 +126,7 @@ describe("OidcTokenRefresher", () => {
126126

127127
const result = await refresher.doRefreshAccessToken("refresh-token");
128128

129-
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
129+
expect(fetchMock).toHaveFetched(config.token_endpoint, {
130130
method: "POST",
131131
});
132132

@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
153153
it("should only have one inflight refresh request at once", async () => {
154154
fetchMock
155155
.postOnce(
156-
config.tokenEndpoint,
156+
config.token_endpoint,
157157
{
158158
status: 200,
159159
headers: {
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
164164
{ overwriteRoutes: true },
165165
)
166166
.postOnce(
167-
config.tokenEndpoint,
167+
config.token_endpoint,
168168
{
169169
status: 200,
170170
headers: {
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
188188
const result2 = await first;
189189

190190
// only one call to token endpoint
191-
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
191+
expect(fetchMock).toHaveFetchedTimes(1, config.token_endpoint);
192192
expect(result1).toEqual({
193193
accessToken: "first-new-access-token",
194194
refreshToken: "first-new-refresh-token",
@@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => {
208208

209209
it("should log and rethrow when token refresh fails", async () => {
210210
fetchMock.post(
211-
config.tokenEndpoint,
211+
config.token_endpoint,
212212
{
213213
status: 503,
214214
headers: {
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
228228
// make sure inflight request is cleared after a failure
229229
fetchMock
230230
.postOnce(
231-
config.tokenEndpoint,
231+
config.token_endpoint,
232232
{
233233
status: 503,
234234
headers: {
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
238238
{ overwriteRoutes: true },
239239
)
240240
.postOnce(
241-
config.tokenEndpoint,
241+
config.token_endpoint,
242242
{
243243
status: 200,
244244
headers: {

0 commit comments

Comments
 (0)