165 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
/*
 | 
						|
Copyright 2024 New Vector Ltd.
 | 
						|
Copyright 2023 The Matrix.org Foundation C.I.C.
 | 
						|
 | 
						|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
 | 
						|
Please see LICENSE files in the repository root for full details.
 | 
						|
*/
 | 
						|
 | 
						|
import fetchMock from "fetch-mock-jest";
 | 
						|
import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize";
 | 
						|
import * as randomStringUtils from "matrix-js-sdk/src/randomstring";
 | 
						|
import { BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate";
 | 
						|
import { mocked } from "jest-mock";
 | 
						|
import { Crypto } from "@peculiar/webcrypto";
 | 
						|
import { getRandomValues } from "node:crypto";
 | 
						|
 | 
						|
import { completeOidcLogin, startOidcLogin } from "../../../../src/utils/oidc/authorize";
 | 
						|
import { makeDelegatedAuthConfig } from "../../../test-utils/oidc";
 | 
						|
import { OidcClientError } from "../../../../src/utils/oidc/error";
 | 
						|
import { mockPlatformPeg } from "../../../test-utils";
 | 
						|
 | 
						|
jest.unmock("matrix-js-sdk/src/randomstring");
 | 
						|
 | 
						|
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
 | 
						|
    ...jest.requireActual("matrix-js-sdk/src/oidc/authorize"),
 | 
						|
    completeAuthorizationCodeGrant: jest.fn(),
 | 
						|
}));
 | 
						|
 | 
						|
const webCrypto = new Crypto();
 | 
						|
 | 
						|
describe("OIDC authorization", () => {
 | 
						|
    const issuer = "https://auth.com/";
 | 
						|
    const homeserverUrl = "https://matrix.org";
 | 
						|
    const identityServerUrl = "https://is.org";
 | 
						|
    const clientId = "xyz789";
 | 
						|
    const baseUrl = "https://test.com";
 | 
						|
 | 
						|
    const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
 | 
						|
 | 
						|
    // to restore later
 | 
						|
    const realWindowLocation = window.location;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
        // @ts-ignore allow delete of non-optional prop
 | 
						|
        delete window.location;
 | 
						|
        // @ts-ignore ugly mocking
 | 
						|
        window.location = {
 | 
						|
            href: baseUrl,
 | 
						|
            origin: baseUrl,
 | 
						|
        };
 | 
						|
 | 
						|
        jest.spyOn(randomStringUtils, "randomString").mockRestore();
 | 
						|
        mockPlatformPeg();
 | 
						|
        Object.defineProperty(window, "crypto", {
 | 
						|
            value: {
 | 
						|
                getRandomValues,
 | 
						|
                randomUUID: jest.fn().mockReturnValue("not-random-uuid"),
 | 
						|
                subtle: webCrypto.subtle,
 | 
						|
            },
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    beforeAll(() => {
 | 
						|
        fetchMock.get(
 | 
						|
            `${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
 | 
						|
            delegatedAuthConfig.metadata,
 | 
						|
        );
 | 
						|
    });
 | 
						|
 | 
						|
    afterAll(() => {
 | 
						|
        window.location = realWindowLocation;
 | 
						|
    });
 | 
						|
 | 
						|
    describe("startOidcLogin()", () => {
 | 
						|
        it("navigates to authorization endpoint with correct parameters", async () => {
 | 
						|
            await startOidcLogin(delegatedAuthConfig, clientId, homeserverUrl);
 | 
						|
 | 
						|
            const expectedScopeWithoutDeviceId = `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:`;
 | 
						|
 | 
						|
            const authUrl = new URL(window.location.href);
 | 
						|
 | 
						|
            expect(authUrl.searchParams.get("response_mode")).toEqual("query");
 | 
						|
            expect(authUrl.searchParams.get("response_type")).toEqual("code");
 | 
						|
            expect(authUrl.searchParams.get("client_id")).toEqual(clientId);
 | 
						|
            expect(authUrl.searchParams.get("code_challenge_method")).toEqual("S256");
 | 
						|
 | 
						|
            // scope ends with a 10char randomstring deviceId
 | 
						|
            const scope = authUrl.searchParams.get("scope")!;
 | 
						|
            expect(scope.substring(0, scope.length - 10)).toEqual(expectedScopeWithoutDeviceId);
 | 
						|
            expect(scope.substring(scope.length - 10)).toBeTruthy();
 | 
						|
 | 
						|
            // random string, just check they are set
 | 
						|
            expect(authUrl.searchParams.has("state")).toBeTruthy();
 | 
						|
            expect(authUrl.searchParams.has("nonce")).toBeTruthy();
 | 
						|
            expect(authUrl.searchParams.has("code_challenge")).toBeTruthy();
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    describe("completeOidcLogin()", () => {
 | 
						|
        const state = "test-state-444";
 | 
						|
        const code = "test-code-777";
 | 
						|
        const queryDict = {
 | 
						|
            code,
 | 
						|
            state: state,
 | 
						|
        };
 | 
						|
 | 
						|
        const tokenResponse: BearerTokenResponse = {
 | 
						|
            access_token: "abc123",
 | 
						|
            refresh_token: "def456",
 | 
						|
            id_token: "ghi789",
 | 
						|
            scope: "test",
 | 
						|
            token_type: "Bearer",
 | 
						|
            expires_at: 12345,
 | 
						|
        };
 | 
						|
 | 
						|
        beforeEach(() => {
 | 
						|
            mocked(completeAuthorizationCodeGrant)
 | 
						|
                .mockClear()
 | 
						|
                .mockResolvedValue({
 | 
						|
                    oidcClientSettings: {
 | 
						|
                        clientId,
 | 
						|
                        issuer,
 | 
						|
                    },
 | 
						|
                    tokenResponse,
 | 
						|
                    homeserverUrl,
 | 
						|
                    identityServerUrl,
 | 
						|
                    idTokenClaims: {
 | 
						|
                        aud: "123",
 | 
						|
                        iss: issuer,
 | 
						|
                        sub: "123",
 | 
						|
                        exp: 123,
 | 
						|
                        iat: 456,
 | 
						|
                    },
 | 
						|
                });
 | 
						|
        });
 | 
						|
 | 
						|
        it("should throw when query params do not include state and code", async () => {
 | 
						|
            await expect(async () => await completeOidcLogin({})).rejects.toThrow(
 | 
						|
                OidcClientError.InvalidQueryParameters,
 | 
						|
            );
 | 
						|
        });
 | 
						|
 | 
						|
        it("should make request complete authorization code grant", async () => {
 | 
						|
            await completeOidcLogin(queryDict);
 | 
						|
 | 
						|
            expect(completeAuthorizationCodeGrant).toHaveBeenCalledWith(code, state);
 | 
						|
        });
 | 
						|
 | 
						|
        it("should return accessToken, configured homeserver and identityServer", async () => {
 | 
						|
            const result = await completeOidcLogin(queryDict);
 | 
						|
 | 
						|
            expect(result).toEqual({
 | 
						|
                accessToken: tokenResponse.access_token,
 | 
						|
                refreshToken: tokenResponse.refresh_token,
 | 
						|
                homeserverUrl,
 | 
						|
                identityServerUrl,
 | 
						|
                issuer,
 | 
						|
                clientId,
 | 
						|
                idToken: "ghi789",
 | 
						|
                idTokenClaims: result.idTokenClaims,
 | 
						|
            });
 | 
						|
        });
 | 
						|
    });
 | 
						|
});
 |