mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-06-08 15:21:53 +03:00
* Bump eslint-plugin-matrix-org to enable @typescript-eslint/consistent-type-imports rule * Re-lint after merge
230 lines
9.5 KiB
TypeScript
230 lines
9.5 KiB
TypeScript
/*
|
|
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 { mocked } from "jest-mock";
|
|
import { jwtDecode } from "jwt-decode";
|
|
|
|
import { logger } from "../../../src/logger";
|
|
import { type ValidatedAuthMetadata, validateIdToken, validateAuthMetadata } from "../../../src/oidc/validate";
|
|
import { OidcError } from "../../../src/oidc/error";
|
|
|
|
jest.mock("jwt-decode");
|
|
|
|
describe("validateOIDCIssuerWellKnown", () => {
|
|
const validWk: ValidatedAuthMetadata = {
|
|
issuer: "https://test.org",
|
|
authorization_endpoint: "https://test.org/authorize",
|
|
token_endpoint: "https://authorize.org/token",
|
|
registration_endpoint: "https://authorize.org/register",
|
|
revocation_endpoint: "https://authorize.org/revoke",
|
|
response_types_supported: ["code"],
|
|
grant_types_supported: ["authorization_code"],
|
|
code_challenge_methods_supported: ["S256"],
|
|
account_management_uri: "https://authorize.org/account",
|
|
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
|
|
};
|
|
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(() => {
|
|
validateAuthMetadata([]);
|
|
}).toThrow(OidcError.OpSupport);
|
|
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
|
|
});
|
|
|
|
it("should log all errors before throwing", () => {
|
|
expect(() => {
|
|
validateAuthMetadata({
|
|
...validWk,
|
|
authorization_endpoint: undefined,
|
|
response_types_supported: [],
|
|
});
|
|
}).toThrow(OidcError.OpSupport);
|
|
expect(logger.error).toHaveBeenCalledWith("Missing or invalid property: authorization_endpoint");
|
|
expect(logger.error).toHaveBeenCalledWith("Invalid property: response_types_supported. code is required.");
|
|
});
|
|
|
|
it("should return validated issuer config", () => {
|
|
expect(validateAuthMetadata(validWk)).toEqual(
|
|
expect.objectContaining({
|
|
issuer: validWk.issuer,
|
|
authorization_endpoint: validWk.authorization_endpoint,
|
|
token_endpoint: validWk.token_endpoint,
|
|
registration_endpoint: validWk.registration_endpoint,
|
|
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
|
|
account_management_uri: "https://authorize.org/account",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should return validated issuer config without registration_endpoint", () => {
|
|
const { registration_endpoint: _, ...wk } = validWk;
|
|
expect(validateAuthMetadata(wk)).toEqual({
|
|
issuer: validWk.issuer,
|
|
authorization_endpoint: validWk.authorization_endpoint,
|
|
token_endpoint: validWk.token_endpoint,
|
|
revocation_endpoint: validWk.revocation_endpoint,
|
|
registration_endpoint: undefined,
|
|
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
|
|
account_management_uri: "https://authorize.org/account",
|
|
code_challenge_methods_supported: ["S256"],
|
|
grant_types_supported: ["authorization_code"],
|
|
response_types_supported: ["code"],
|
|
});
|
|
});
|
|
|
|
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", { 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"]],
|
|
["account_management_uri", { not: "a string" }],
|
|
["account_management_actions_supported", { not: "an array" }],
|
|
])("should throw OP support error when %s is %s", (key, value) => {
|
|
const wk = {
|
|
...validWk,
|
|
[key]: value,
|
|
};
|
|
expect(() => validateAuthMetadata(wk)).toThrow(OidcError.OpSupport);
|
|
});
|
|
});
|
|
|
|
describe("validateIdToken()", () => {
|
|
const nonce = "test-nonce";
|
|
const issuer = "https://auth.org/issuer";
|
|
const clientId = "test-client-id";
|
|
const idToken = "test-id-token";
|
|
|
|
const validDecodedIdToken = {
|
|
// nonce matches
|
|
nonce,
|
|
// not expired
|
|
exp: Date.now() / 1000 + 5555,
|
|
// audience is this client
|
|
aud: clientId,
|
|
// issuer matches
|
|
iss: issuer,
|
|
};
|
|
beforeEach(() => {
|
|
mocked(jwtDecode).mockClear().mockReturnValue(validDecodedIdToken);
|
|
|
|
jest.spyOn(logger, "error").mockClear();
|
|
});
|
|
|
|
it("should throw when idToken is falsy", () => {
|
|
expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
});
|
|
|
|
it("should throw when idToken cannot be decoded", () => {
|
|
mocked(jwtDecode).mockImplementation(() => {
|
|
throw new Error("oh no!");
|
|
});
|
|
expect(() => validateIdToken(undefined, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
});
|
|
|
|
it("should throw when issuer does not match", () => {
|
|
mocked(jwtDecode).mockReturnValue({
|
|
...validDecodedIdToken,
|
|
iss: "https://badissuer.com",
|
|
});
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid issuer"));
|
|
});
|
|
|
|
it("should throw when audience does not include clientId", () => {
|
|
mocked(jwtDecode).mockReturnValue({
|
|
...validDecodedIdToken,
|
|
aud: "qwerty,uiop,asdf",
|
|
});
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
|
|
});
|
|
|
|
it("should throw when audience includes clientId and other audiences", () => {
|
|
mocked(jwtDecode).mockReturnValue({
|
|
...validDecodedIdToken,
|
|
aud: `${clientId},uiop,asdf`,
|
|
});
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
|
|
});
|
|
|
|
it("should not throw when audience is an array that includes clientId", () => {
|
|
mocked(jwtDecode).mockReturnValue({
|
|
...validDecodedIdToken,
|
|
aud: [clientId],
|
|
});
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow();
|
|
});
|
|
|
|
it("should throw when audience is an array that does not include clientId", () => {
|
|
mocked(jwtDecode).mockReturnValue({
|
|
...validDecodedIdToken,
|
|
aud: [`${clientId},uiop`, "asdf"],
|
|
});
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
|
|
});
|
|
|
|
it("should throw when nonce does not match", () => {
|
|
mocked(jwtDecode).mockReturnValue({
|
|
...validDecodedIdToken,
|
|
nonce: "something else",
|
|
});
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid nonce"));
|
|
});
|
|
|
|
it("should throw when token does not have an expiry", () => {
|
|
mocked(jwtDecode).mockReturnValue({
|
|
...validDecodedIdToken,
|
|
exp: undefined,
|
|
});
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry"));
|
|
});
|
|
|
|
it("should throw when token is expired", () => {
|
|
mocked(jwtDecode).mockReturnValue({
|
|
...validDecodedIdToken,
|
|
// expired in the past
|
|
exp: Date.now() / 1000 - 777,
|
|
});
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
|
|
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid expiry"));
|
|
});
|
|
|
|
it("should not throw for a valid id token", () => {
|
|
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow();
|
|
});
|
|
});
|