OIDC: Log in (#11199)

* add delegatedauthentication to validated server config

* dynamic client registration functions

* test OP registration functions

* add stubbed nativeOidc flow setup in Login

* cover more error cases in Login

* tidy

* test dynamic client registration in Login

* comment oidc_static_clients

* register oidc inside Login.getFlows

* strict fixes

* remove unused code

* and imports

* comments

* comments 2

* util functions to get static client id

* check static client ids in login flow

* remove dead code

* OidcRegistrationClientMetadata type

* navigate to oidc authorize url

* exchange code for token

* navigate to oidc authorize url

* navigate to oidc authorize url

* test

* adjust for js-sdk code

* login with oidc native flow: messy version

* tidy

* update test for response_mode query

* tidy up some TODOs

* use new types

* add identityServerUrl to stored params

* unit test completeOidcLogin

* test tokenlogin

* strict

* whitespace

* tidy

* unit test oidc login flow in MatrixChat

* strict

* tidy

* extract success/failure handlers from token login function

* typo

* use for no homeserver error dialog too

* reuse post-token login functions, test

* shuffle testing utils around

* shuffle testing utils around

* i18n

* tidy

* Update src/Lifecycle.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* tidy

* comment

* update tests for id token validation

* move try again responsibility

* prettier

* use more future proof config for static clients

* test util for oidcclientconfigs

* rename type and lint

* correct oidc test util

* store issuer and clientId pre auth navigation

* adjust for js-sdk changes

* update for js-sdk userstate, tidy

* update MatrixChat tests

* update tests

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
pull/28788/head^2
Kerry 2023-07-11 16:09:18 +12:00 committed by GitHub
parent 186497a67d
commit 7b3d0ad209
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 490 additions and 67 deletions

View File

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020, 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.
@ -65,6 +65,7 @@ import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLoc
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
import { SdkContextClass } from "./contexts/SDKContext";
import { messageForLoginError } from "./utils/ErrorUtils";
import { completeOidcLogin } from "./utils/oidc/authorize";
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";
@ -182,6 +183,9 @@ export async function getStoredSessionOwner(): Promise<[string, boolean] | [null
}
/**
* If query string includes OIDC authorization code flow parameters attempt to login using oidc flow
* Else, we may be returning from SSO - attempt token login
*
* @param {Object} queryParams string->string map of the
* query-parameters extracted from the real query-string of the starting
* URI.
@ -189,6 +193,92 @@ export async function getStoredSessionOwner(): Promise<[string, boolean] | [null
* @param {string} defaultDeviceDisplayName
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
*
* @returns {Promise} promise which resolves to true if we completed the delegated auth login
* else false
*/
export async function attemptDelegatedAuthLogin(
queryParams: QueryDict,
defaultDeviceDisplayName?: string,
fragmentAfterLogin?: string,
): Promise<boolean> {
if (queryParams.code && queryParams.state) {
return attemptOidcNativeLogin(queryParams);
}
return attemptTokenLogin(queryParams, defaultDeviceDisplayName, fragmentAfterLogin);
}
/**
* Attempt to login by completing OIDC authorization code flow
* @param queryParams string->string map of the query-parameters extracted from the real query-string of the starting URI.
* @returns Promise that resolves to true when login succceeded, else false
*/
async function attemptOidcNativeLogin(queryParams: QueryDict): Promise<boolean> {
try {
const { accessToken, homeserverUrl, identityServerUrl } = await completeOidcLogin(queryParams);
const {
user_id: userId,
device_id: deviceId,
is_guest: isGuest,
} = await getUserIdFromAccessToken(accessToken, homeserverUrl, identityServerUrl);
const credentials = {
accessToken,
homeserverUrl,
identityServerUrl,
deviceId,
userId,
isGuest,
};
logger.debug("Logged in via OIDC native flow");
await onSuccessfulDelegatedAuthLogin(credentials);
return true;
} catch (error) {
logger.error("Failed to login via OIDC", error);
// TODO(kerrya) nice error messages https://github.com/vector-im/element-web/issues/25665
await onFailedDelegatedAuthLogin(_t("Something went wrong."));
return false;
}
}
/**
* Gets information about the owner of a given access token.
* @param accessToken
* @param homeserverUrl
* @param identityServerUrl
* @returns Promise that resolves with whoami response
* @throws when whoami request fails
*/
async function getUserIdFromAccessToken(
accessToken: string,
homeserverUrl: string,
identityServerUrl?: string,
): Promise<ReturnType<MatrixClient["whoami"]>> {
try {
const client = createClient({
baseUrl: homeserverUrl,
accessToken: accessToken,
idBaseUrl: identityServerUrl,
});
return await client.whoami();
} catch (error) {
logger.error("Failed to retrieve userId using accessToken", error);
throw new Error("Failed to retrieve userId using accessToken");
}
}
/**
* @param {QueryDict} queryParams string->string map of the
* query-parameters extracted from the real query-string of the starting
* URI.
*
* @param {string} defaultDeviceDisplayName
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
*
* @returns {Promise} promise which resolves to true if we completed the token
* login, else false
*/

View File

@ -316,13 +316,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// the first thing to do is to try the token params in the query-string
// if the session isn't soft logged out (ie: is a clean session being logged in)
if (!Lifecycle.isSoftLogout()) {
Lifecycle.attemptTokenLogin(
Lifecycle.attemptDelegatedAuthLogin(
this.props.realQueryParams,
this.props.defaultDeviceDisplayName,
this.getFragmentAfterLogin(),
).then(async (loggedIn): Promise<boolean | void> => {
if (this.props.realQueryParams?.loginToken) {
// remove the loginToken from the URL regardless
if (
this.props.realQueryParams?.loginToken ||
this.props.realQueryParams?.code ||
this.props.realQueryParams?.state
) {
// remove the loginToken or auth code from the URL regardless
this.props.onTokenLoginCompleted();
}
@ -341,7 +345,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// if the user has followed a login or register link, don't reanimate
// the old creds, but rather go straight to the relevant page
const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null;
const restoreSuccess = await this.loadSession();
if (restoreSuccess) {
return true;

View File

@ -477,6 +477,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
this.props.serverConfig.delegatedAuthentication!,
flow.clientId,
this.props.serverConfig.hsUrl,
this.props.serverConfig.isUrl,
);
}}
>

View File

@ -101,6 +101,7 @@
"Failed to transfer call": "Failed to transfer call",
"Permission Required": "Permission Required",
"You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room",
"Something went wrong.": "Something went wrong.",
"We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.",
"We couldn't log you in": "We couldn't log you in",
"Try again": "Try again",

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize";
import { QueryDict } from "matrix-js-sdk/src/utils";
import { OidcClientConfig } from "matrix-js-sdk/src/autodiscovery";
import { generateOidcAuthorizationUrl } from "matrix-js-sdk/src/oidc/authorize";
import { randomString } from "matrix-js-sdk/src/randomstring";
@ -49,3 +51,45 @@ export const startOidcLogin = async (
window.location.href = authorizationUrl;
};
/**
* Gets `code` and `state` query params
*
* @param queryParams
* @returns code and state
* @throws when code and state are not valid strings
*/
const getCodeAndStateFromQueryParams = (queryParams: QueryDict): { code: string; state: string } => {
const code = queryParams["code"];
const state = queryParams["state"];
if (!code || typeof code !== "string" || !state || typeof state !== "string") {
throw new Error("Invalid query parameters for OIDC native login. `code` and `state` are required.");
}
return { code, state };
};
/**
* Attempt to complete authorization code flow to get an access token
* @param queryParams the query-parameters extracted from the real query-string of the starting URI.
* @returns Promise that resolves with accessToken, identityServerUrl, and homeserverUrl when login was successful
* @throws When we failed to get a valid access token
*/
export const completeOidcLogin = async (
queryParams: QueryDict,
): Promise<{
homeserverUrl: string;
identityServerUrl?: string;
accessToken: string;
}> => {
const { code, state } = getCodeAndStateFromQueryParams(queryParams);
const { homeserverUrl, tokenResponse, identityServerUrl } = await completeAuthorizationCodeGrant(code, state);
// @TODO(kerrya) do something with the refresh token https://github.com/vector-im/element-web/issues/25444
return {
homeserverUrl: homeserverUrl,
identityServerUrl: identityServerUrl,
accessToken: tokenResponse.access_token,
};
};

View File

@ -16,12 +16,17 @@ limitations under the License.
import React, { ComponentProps } from "react";
import { fireEvent, render, RenderResult, screen, within } from "@testing-library/react";
import fetchMockJest from "fetch-mock-jest";
import fetchMock from "fetch-mock-jest";
import { mocked } from "jest-mock";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { SyncState } from "matrix-js-sdk/src/sync";
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
import * as MatrixJs from "matrix-js-sdk/src/matrix";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize";
import { logger } from "matrix-js-sdk/src/logger";
import { OidcError } from "matrix-js-sdk/src/oidc/error";
import { BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate";
import MatrixChat from "../../../src/components/structures/MatrixChat";
import * as StorageManager from "../../../src/utils/StorageManager";
@ -37,6 +42,10 @@ import {
} from "../../test-utils";
import * as leaveRoomUtils from "../../../src/utils/leave-behaviour";
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
completeAuthorizationCodeGrant: jest.fn(),
}));
describe("<MatrixChat />", () => {
const userId = "@alice:server.org";
const deviceId = "qwertyui";
@ -64,6 +73,7 @@ describe("<MatrixChat />", () => {
setAccountData: jest.fn(),
store: {
destroy: jest.fn(),
startup: jest.fn(),
},
login: jest.fn(),
loginFlows: jest.fn(),
@ -85,6 +95,7 @@ describe("<MatrixChat />", () => {
isStored: jest.fn().mockReturnValue(null),
},
getDehydratedDevice: jest.fn(),
whoami: jest.fn(),
isRoomEncrypted: jest.fn(),
});
let mockClient = getMockClientWithEventEmitter(getMockClientMethods());
@ -124,16 +135,47 @@ describe("<MatrixChat />", () => {
// make test results readable
filterConsole("Failed to parse localStorage object");
/**
* Wait for a bunch of stuff to happen
* between deciding we are logged in and removing the spinner
* including waiting for initial sync
*/
const waitForSyncAndLoad = async (client: MatrixClient, withoutSecuritySetup?: boolean): Promise<void> => {
// need to wait for different elements depending on which flow
// without security setup we go to a loading page
if (withoutSecuritySetup) {
// we think we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Logout");
// initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
// wait for logged in view to load
await screen.findByLabelText("User menu");
// otherwise we stay on login and load from there for longer
} else {
// we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Syncing…");
// initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
}
// let things settle
await flushPromises();
// and some more for good measure
// this proved to be a little flaky
await flushPromises();
};
beforeEach(async () => {
mockClient = getMockClientWithEventEmitter(getMockClientMethods());
fetchMockJest.get("https://test.com/_matrix/client/versions", {
fetchMock.get("https://test.com/_matrix/client/versions", {
unstable_features: {},
versions: [],
});
localStorageGetSpy.mockReset();
localStorageSetSpy.mockReset();
sessionStorageSetSpy.mockReset();
jest.spyOn(StorageManager, "idbLoad").mockRestore();
jest.spyOn(StorageManager, "idbLoad").mockReset();
jest.spyOn(StorageManager, "idbSave").mockResolvedValue(undefined);
jest.spyOn(defaultDispatcher, "dispatch").mockClear();
@ -375,32 +417,6 @@ describe("<MatrixChat />", () => {
return renderResult;
};
const waitForSyncAndLoad = async (client: MatrixClient, withoutSecuritySetup?: boolean): Promise<void> => {
// need to wait for different elements depending on which flow
// without security setup we go to a loading page
if (withoutSecuritySetup) {
// we think we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Logout");
// initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
// wait for logged in view to load
await screen.findByLabelText("User menu");
// otherwise we stay on login and load from there for longer
} else {
// we are logged in, but are still waiting for the /sync to complete
await screen.findByText("Syncing…");
// initial sync
client.emit(ClientEvent.Sync, SyncState.Prepared, null);
}
// let things settle
await flushPromises();
// and some more for good measure
// this proved to be a little flaky
await flushPromises();
};
const getComponentAndLogin = async (withoutSecuritySetup?: boolean): Promise<void> => {
await getComponentAndWaitForReady();
@ -416,7 +432,7 @@ describe("<MatrixChat />", () => {
beforeEach(() => {
loginClient = getMockClientWithEventEmitter(getMockClientMethods());
// this is used to create a temporary client during login
jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient);
jest.spyOn(MatrixJs, "createClient").mockClear().mockReturnValue(loginClient);
loginClient.login.mockClear().mockResolvedValue({
access_token: "TOKEN",
@ -710,4 +726,217 @@ describe("<MatrixChat />", () => {
});
});
});
describe("when query params have a OIDC params", () => {
const issuer = "https://auth.com/";
const homeserverUrl = "https://matrix.org";
const identityServerUrl = "https://is.org";
const clientId = "xyz789";
const code = "test-oidc-auth-code";
const state = "test-oidc-state";
const realQueryParams = {
code,
state: state,
};
const userId = "@alice:server.org";
const deviceId = "test-device-id";
const accessToken = "test-access-token-from-oidc";
const mockLocalStorage: Record<string, string> = {
// these are only going to be set during login
mx_hs_url: homeserverUrl,
mx_is_url: identityServerUrl,
mx_user_id: userId,
mx_device_id: deviceId,
};
const tokenResponse: BearerTokenResponse = {
access_token: accessToken,
refresh_token: "def456",
scope: "test",
token_type: "Bearer",
expires_at: 12345,
};
let loginClient!: ReturnType<typeof getMockClientWithEventEmitter>;
// for now when OIDC fails for any reason we just bump back to welcome
// error handling screens in https://github.com/vector-im/element-web/issues/25665
const expectOIDCError = async (): Promise<void> => {
await flushPromises();
// just check we're back on welcome page
expect(document.querySelector(".mx_Welcome")!).toBeInTheDocument();
};
beforeEach(() => {
mocked(completeAuthorizationCodeGrant).mockClear().mockResolvedValue({
oidcClientSettings: {
clientId,
issuer,
},
tokenResponse,
homeserverUrl,
identityServerUrl,
});
jest.spyOn(logger, "error").mockClear();
});
beforeEach(() => {
loginClient = getMockClientWithEventEmitter(getMockClientMethods());
// this is used to create a temporary client during login
jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient);
jest.spyOn(logger, "error").mockClear();
jest.spyOn(logger, "log").mockClear();
localStorageGetSpy.mockImplementation((key: unknown) => mockLocalStorage[key as string] || "");
loginClient.whoami.mockResolvedValue({
user_id: userId,
device_id: deviceId,
is_guest: false,
});
});
it("should fail when query params do not include valid code and state", async () => {
const queryParams = {
code: 123,
state: "abc",
};
getComponent({ realQueryParams: queryParams });
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(
"Failed to login via OIDC",
new Error("Invalid query parameters for OIDC native login. `code` and `state` are required."),
);
await expectOIDCError();
});
it("should make correct request to complete authorization", async () => {
getComponent({ realQueryParams });
await flushPromises();
expect(completeAuthorizationCodeGrant).toHaveBeenCalledWith(code, state);
});
it("should look up userId using access token", async () => {
getComponent({ realQueryParams });
await flushPromises();
// check we used a client with the correct accesstoken
expect(MatrixJs.createClient).toHaveBeenCalledWith({
baseUrl: homeserverUrl,
accessToken,
idBaseUrl: identityServerUrl,
});
expect(loginClient.whoami).toHaveBeenCalled();
});
it("should log error and return to welcome page when userId lookup fails", async () => {
loginClient.whoami.mockRejectedValue(new Error("oups"));
getComponent({ realQueryParams });
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(
"Failed to login via OIDC",
new Error("Failed to retrieve userId using accessToken"),
);
await expectOIDCError();
});
it("should call onTokenLoginCompleted", async () => {
const onTokenLoginCompleted = jest.fn();
getComponent({ realQueryParams, onTokenLoginCompleted });
await flushPromises();
expect(onTokenLoginCompleted).toHaveBeenCalled();
});
describe("when login fails", () => {
beforeEach(() => {
mocked(completeAuthorizationCodeGrant).mockRejectedValue(new Error(OidcError.CodeExchangeFailed));
});
it("should log and return to welcome page", async () => {
getComponent({ realQueryParams });
await flushPromises();
expect(logger.error).toHaveBeenCalledWith(
"Failed to login via OIDC",
new Error(OidcError.CodeExchangeFailed),
);
// warning dialog
await expectOIDCError();
});
it("should not clear storage", async () => {
getComponent({ realQueryParams });
await flushPromises();
expect(loginClient.clearStores).not.toHaveBeenCalled();
});
});
describe("when login succeeds", () => {
beforeEach(() => {
localStorageGetSpy.mockImplementation((key: unknown) => mockLocalStorage[key as string] || "");
jest.spyOn(StorageManager, "idbLoad").mockImplementation(
async (_table: string, key: string | string[]) => (key === "mx_access_token" ? accessToken : null),
);
loginClient.getProfileInfo.mockResolvedValue({
displayname: "Ernie",
});
});
it("should persist login credentials", async () => {
getComponent({ realQueryParams });
await flushPromises();
expect(localStorageSetSpy).toHaveBeenCalledWith("mx_hs_url", homeserverUrl);
expect(localStorageSetSpy).toHaveBeenCalledWith("mx_user_id", userId);
expect(localStorageSetSpy).toHaveBeenCalledWith("mx_has_access_token", "true");
expect(localStorageSetSpy).toHaveBeenCalledWith("mx_device_id", deviceId);
});
it("should set logged in and start MatrixClient", async () => {
getComponent({ realQueryParams });
await flushPromises();
await flushPromises();
expect(logger.log).toHaveBeenCalledWith(
"setLoggedIn: mxid: " +
userId +
" deviceId: " +
deviceId +
" guest: " +
false +
" hs: " +
homeserverUrl +
" softLogout: " +
false,
" freshLogin: " + false,
);
// client successfully started
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "client_started" });
// check we get to logged in view
await waitForSyncAndLoad(loginClient, true);
});
});
});
});

View File

@ -14,31 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMockJest from "fetch-mock-jest";
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 { startOidcLogin } from "../../../src/utils/oidc/authorize";
import { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../test-utils/oidc";
import { completeOidcLogin, startOidcLogin } from "../../../src/utils/oidc/authorize";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
describe("startOidcLogin()", () => {
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
...jest.requireActual("matrix-js-sdk/src/oidc/authorize"),
completeAuthorizationCodeGrant: jest.fn(),
}));
describe("OIDC authorization", () => {
const issuer = "https://auth.com/";
const homeserver = "https://matrix.org";
const homeserverUrl = "https://matrix.org";
const identityServerUrl = "https://is.org";
const clientId = "xyz789";
const baseUrl = "https://test.com";
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
const sessionStorageGetSpy = jest.spyOn(sessionStorage.__proto__, "setItem").mockReturnValue(undefined);
// to restore later
const realWindowLocation = window.location;
beforeEach(() => {
fetchMockJest.mockClear();
fetchMockJest.resetBehavior();
sessionStorageGetSpy.mockClear();
// @ts-ignore allow delete of non-optional prop
delete window.location;
// @ts-ignore ugly mocking
@ -47,19 +49,20 @@ describe("startOidcLogin()", () => {
origin: baseUrl,
};
fetchMockJest.get(
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
jest.spyOn(randomStringUtils, "randomString").mockRestore();
});
beforeAll(() => {
fetchMock.get(`${delegatedAuthConfig.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, homeserver);
await startOidcLogin(delegatedAuthConfig, clientId, homeserverUrl);
const expectedScopeWithoutDeviceId = `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:`;
@ -80,4 +83,56 @@ describe("startOidcLogin()", () => {
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",
scope: "test",
token_type: "Bearer",
expires_at: 12345,
};
beforeEach(() => {
mocked(completeAuthorizationCodeGrant).mockClear().mockResolvedValue({
oidcClientSettings: {
clientId,
issuer,
},
tokenResponse,
homeserverUrl,
identityServerUrl,
});
});
it("should throw when query params do not include state and code", async () => {
expect(async () => await completeOidcLogin({})).rejects.toThrow(
"Invalid query parameters for OIDC native login. `code` and `state` are required.",
);
});
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,
homeserverUrl,
identityServerUrl,
});
});
});
});