Skip to content

Commit

Permalink
Switch OIDC primarily to new /auth_metadata API (#4626)
Browse files Browse the repository at this point in the history
  • Loading branch information
t3chguy authored Jan 22, 2025
1 parent 61375ef commit c0e30ce
Show file tree
Hide file tree
Showing 16 changed files with 269 additions and 195 deletions.
8 changes: 2 additions & 6 deletions spec/integ/rendezvous/MSC4108SignInWithQR.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
MatrixError,
MatrixHttpApi,
} from "../../../src";
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";

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

describe("MSC4108SignInWithQR", () => {
beforeEach(() => {
fetchMock.get(
"https://issuer/.well-known/openid-configuration",
mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]),
);
fetchMock.get("https://issuer/jwks", {
status: 200,
headers: {
Expand Down
40 changes: 1 addition & 39 deletions spec/test-utils/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";

/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);

return {
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};

/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (
issuer = "https://auth.org/",
additionalGrantTypes: string[] = [],
): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
code_challenge_methods_supported: ["S256"],
});
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../src/testing.ts";
77 changes: 70 additions & 7 deletions spec/unit/matrix-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/

import { Mocked, mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";

import { logger } from "../../src/logger";
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
Expand Down Expand Up @@ -76,6 +77,7 @@ import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from ".
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { KnownMembership } from "../../src/@types/membership";
import { RoomMessageEventContent } from "../../src/@types/events";
import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";

jest.useFakeTimers();

Expand Down Expand Up @@ -265,13 +267,17 @@ describe("MatrixClient", function () {

if (next.error) {
// eslint-disable-next-line
return Promise.reject({
errcode: (<MatrixError>next.error).errcode,
httpStatus: (<MatrixError>next.error).httpStatus,
name: (<MatrixError>next.error).errcode,
message: "Expected testing error",
data: next.error,
});
return Promise.reject(
new MatrixError(
{
errcode: (<MatrixError>next.error).errcode,
name: (<MatrixError>next.error).errcode,
message: "Expected testing error",
data: next.error,
},
(<MatrixError>next.error).httpStatus,
),
);
}
return Promise.resolve(next.data);
}
Expand Down Expand Up @@ -3489,6 +3495,63 @@ describe("MatrixClient", function () {
});
});

describe("getAuthMetadata", () => {
beforeEach(() => {
fetchMock.mockReset();
// This request is made by oidc-client-ts so is not intercepted by httpLookups
fetchMock.get("https://auth.org/jwks", {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
});

it("should use unstable prefix", async () => {
const metadata = mockOpenIdConfiguration();
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
data: metadata,
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];

await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: [],
});
expect(httpLookups.length).toEqual(0);
});

it("should fall back to auth_issuer + openid-configuration", async () => {
const metadata = mockOpenIdConfiguration();
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
{
method: "GET",
path: `/auth_issuer`,
data: { issuer: metadata.issuer },
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);

await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: [],
});
expect(httpLookups.length).toEqual(0);
});
});

describe("identityHashedLookup", () => {
it("should return hashed lookup results", async () => {
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
Expand Down
17 changes: 5 additions & 12 deletions spec/unit/oidc/authorize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ jest.mock("jwt-decode");

describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
const authorizationEndpoint = delegatedAuthConfig.authorization_endpoint;
const tokenEndpoint = delegatedAuthConfig.token_endpoint;
const clientId = "xyz789";
const baseUrl = "https://test.com";

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

fetchMock.get(
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
globalThis.TextEncoder = TextEncoder;
});

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

const metadata = delegatedAuthConfig.metadata;

const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata,
metadata: delegatedAuthConfig,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
Expand All @@ -156,11 +151,9 @@ describe("oidc authorization", () => {
it("should generate url with create prompt", async () => {
const nonce = "abc123";

const metadata = delegatedAuthConfig.metadata;

const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata,
metadata: delegatedAuthConfig,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
Expand Down
15 changes: 6 additions & 9 deletions spec/unit/oidc/register.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ describe("registerOidcClient()", () => {
});

it("should make correct request to register client", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(
delegatedAuthConfig.registrationEndpoint!,
delegatedAuthConfig.registration_endpoint,
expect.objectContaining({
headers: {
"Accept": "application/json",
Expand All @@ -72,7 +72,7 @@ describe("registerOidcClient()", () => {
});

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

it("should throw when registration response is invalid", async () => {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
status: 200,
// no clientId in response
body: "{}",
Expand All @@ -96,7 +96,7 @@ describe("registerOidcClient()", () => {
registerOidcClient(
{
...delegatedAuthConfig,
registrationEndpoint: undefined,
registration_endpoint: undefined,
},
metadata,
),
Expand All @@ -108,10 +108,7 @@ describe("registerOidcClient()", () => {
registerOidcClient(
{
...delegatedAuthConfig,
metadata: {
...delegatedAuthConfig.metadata,
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
},
grant_types_supported: [delegatedAuthConfig.grant_types_supported[0]],
},
metadata,
),
Expand Down
22 changes: 11 additions & 11 deletions spec/unit/oidc/tokenRefresher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ describe("OidcTokenRefresher", () => {
});

beforeEach(() => {
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config);
fetchMock.get(`${config.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});

fetchMock.post(config.tokenEndpoint, {
fetchMock.post(config.token_endpoint, {
status: 200,
headers: {
"Content-Type": "application/json",
Expand All @@ -81,7 +81,7 @@ describe("OidcTokenRefresher", () => {
it("throws when oidc client cannot be initialised", async () => {
jest.spyOn(logger, "error");
fetchMock.get(
`${config.metadata.issuer}.well-known/openid-configuration`,
`${config.issuer}.well-known/openid-configuration`,
{
ok: false,
status: 404,
Expand Down Expand Up @@ -126,7 +126,7 @@ describe("OidcTokenRefresher", () => {

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

expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
expect(fetchMock).toHaveFetched(config.token_endpoint, {
method: "POST",
});

Expand All @@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
it("should only have one inflight refresh request at once", async () => {
fetchMock
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
Expand All @@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
Expand All @@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
const result2 = await first;

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

it("should log and rethrow when token refresh fails", async () => {
fetchMock.post(
config.tokenEndpoint,
config.token_endpoint,
{
status: 503,
headers: {
Expand All @@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
// make sure inflight request is cleared after a failure
fetchMock
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 503,
headers: {
Expand All @@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.tokenEndpoint,
config.token_endpoint,
{
status: 200,
headers: {
Expand Down
Loading

0 comments on commit c0e30ce

Please sign in to comment.