Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC: Validate m.authentication configuration #3419

Merged
merged 15 commits into from
Jun 11, 2023
Merged
67 changes: 64 additions & 3 deletions spec/unit/autodiscovery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ limitations under the License.
import MockHttpBackend from "matrix-mock-request";

import { AutoDiscovery } from "../../src/autodiscovery";
import { OidcDiscoveryError } from "../../src/oidc/validate";

describe("AutoDiscovery", function () {
const getHttpBackend = (): MockHttpBackend => {
Expand Down Expand Up @@ -368,7 +369,7 @@ describe("AutoDiscovery", function () {
},
);

it("should return SUCCESS when .well-known has a verifiably accurate base_url for " + "m.homeserver", function () {
it("should return SUCCESS when .well-known has a verifiably accurate base_url for m.homeserver", function () {
const httpBackend = getHttpBackend();
httpBackend
.when("GET", "/_matrix/client/versions")
Expand Down Expand Up @@ -397,6 +398,10 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcDiscoveryError.NotSupported,
},
};

expect(conf).toEqual(expected);
Expand Down Expand Up @@ -434,6 +439,54 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcDiscoveryError.NotSupported,
},
};

expect(conf).toEqual(expected);
}),
]);
});

it("should return SUCCESS with authentication error when authentication config is invalid", function () {
const httpBackend = getHttpBackend();
httpBackend
.when("GET", "/_matrix/client/versions")
.check((req) => {
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
})
.respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
invalid: true,
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
"m.authentication": {
state: "FAIL_ERROR",
error: OidcDiscoveryError.Misconfigured,
},
};

expect(conf).toEqual(expected);
Expand Down Expand Up @@ -625,7 +678,7 @@ describe("AutoDiscovery", function () {
},
);

it("should return SUCCESS when the identity server configuration is " + "verifiably accurate", function () {
it("should return SUCCESS when the identity server configuration is verifiably accurate", function () {
const httpBackend = getHttpBackend();
httpBackend
.when("GET", "/_matrix/client/versions")
Expand Down Expand Up @@ -664,14 +717,18 @@ describe("AutoDiscovery", function () {
error: null,
base_url: "https://identity.example.org",
},
"m.authentication": {
state: "IGNORE",
error: OidcDiscoveryError.NotSupported,
},
};

expect(conf).toEqual(expected);
}),
]);
});

it("should return SUCCESS and preserve non-standard keys from the " + ".well-known response", function () {
it("should return SUCCESS and preserve non-standard keys from the .well-known response", function () {
const httpBackend = getHttpBackend();
httpBackend
.when("GET", "/_matrix/client/versions")
Expand Down Expand Up @@ -716,6 +773,10 @@ describe("AutoDiscovery", function () {
"org.example.custom.property": {
cupcakes: "yes",
},
"m.authentication": {
state: "IGNORE",
error: OidcDiscoveryError.NotSupported,
},
};

expect(conf).toEqual(expected);
Expand Down
179 changes: 179 additions & 0 deletions spec/unit/oidc/validate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.

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 { M_AUTHENTICATION } from "../../../src";
import { logger } from "../../../src/logger";
import {
OidcDiscoveryError,
validateOIDCIssuerWellKnown,
validateWellKnownAuthentication,
} from "../../../src/oidc/validate";

describe("validateWellKnownAuthentication()", () => {
const baseWk = {
"m.homeserver": {
base_url: "https://hs.org",
},
};
it("should throw not supported error when wellKnown has no m.authentication section", () => {
expect(() => validateWellKnownAuthentication(baseWk)).toThrow(OidcDiscoveryError.NotSupported);
});

it("should throw misconfigured error when authentication issuer is not a string", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: { url: "test.com" },
},
};
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured);
});

it("should throw misconfigured error when authentication account is not a string", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: { url: "test" },
},
};
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured);
});

it("should return valid config when wk uses stable m.authentication", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: "account.com",
},
};
expect(validateWellKnownAuthentication(wk)).toEqual({
issuer: "test.com",
account: "account.com",
});
});

it("should return valid config when m.authentication account is falsy", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
},
};
expect(validateWellKnownAuthentication(wk)).toEqual({
issuer: "test.com",
});
});

it("should remove unexpected properties", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
somethingElse: "test",
},
};
expect(validateWellKnownAuthentication(wk)).toEqual({
issuer: "test.com",
});
});

it("should return valid config when wk uses unstable prefix for m.authentication", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.unstable!]: {
issuer: "test.com",
account: "account.com",
},
};
expect(validateWellKnownAuthentication(wk)).toEqual({
issuer: "test.com",
account: "account.com",
});
});
});

describe("validateOIDCIssuerWellKnown", () => {
const validWk = {
authorization_endpoint: "https://test.org/authorize",
token_endpoint: "https://authorize.org/token",
registration_endpoint: "https://authorize.org/regsiter",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
};
beforeEach(() => {
// stub to avoid console litter
jest.spyOn(logger, "error")
.mockClear()
.mockImplementation(() => {});
});

it("should throw OP support error when wellKnown is not an object", () => {
expect(() => {
validateOIDCIssuerWellKnown([]);
}).toThrow(OidcDiscoveryError.OpSupport);
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
});

it("should log all errors before throwing", () => {
expect(() => {
validateOIDCIssuerWellKnown({
...validWk,
authorization_endpoint: undefined,
response_types_supported: [],
});
}).toThrow(OidcDiscoveryError.OpSupport);
expect(logger.error).toHaveBeenCalledWith("OIDC issuer configuration: authorization_endpoint is invalid");
expect(logger.error).toHaveBeenCalledWith(
"OIDC issuer configuration: response_types_supported is invalid. code is required.",
);
});

it("should return validated issuer config", () => {
expect(validateOIDCIssuerWellKnown(validWk)).toEqual({
authorizationEndpoint: validWk.authorization_endpoint,
tokenEndpoint: validWk.token_endpoint,
registrationEndpoint: validWk.registration_endpoint,
});
});

type TestCase = [string, any];
it.each<TestCase>([
["authorization_endpoint", undefined],
["authorization_endpoint", { not: "a string" }],
["token_endpoint", undefined],
["token_endpoint", { not: "a string" }],
["registration_endpoint", undefined],
["registration_endpoint", { not: "a string" }],
["response_types_supported", undefined],
["response_types_supported", "not an array"],
["response_types_supported", ["doesnt include code"]],
["grant_types_supported", undefined],
["grant_types_supported", "not an array"],
["grant_types_supported", ["doesnt include authorization_code"]],
["code_challenge_methods_supported", undefined],
["code_challenge_methods_supported", "not an array"],
["code_challenge_methods_supported", ["doesnt include S256"]],
])("should throw OP support error when %s is %s", (key, value) => {
const wk = {
...validWk,
[key]: value,
};
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcDiscoveryError.OpSupport);
});
});
Loading