Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ General:

For UI tests:

| Variable | Default Value | Description |
|--------------------------|-----------------------|------------------------------------------|
| TRUSTIFY_UI_URL | http://localhost:3000 | The UI URL |
| AUTH_REQUIRED | true | Whether or not auth is enabled in the UI |
| PLAYWRIGHT_AUTH_USER | admin | User name to be used when authenticating |
| PLAYWRIGHT_AUTH_PASSWORD | admin | Password to be used when authenticating |
| Variable | Default Value | Description |
|--------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------|
| TRUSTIFY_UI_URL | http://localhost:3000 | The UI URL |
| AUTH_REQUIRED | true | Whether or not auth is enabled in the UI |
| PLAYWRIGHT_AUTH_USER | admin | User name to be used when authenticating (form mode) |
| PLAYWRIGHT_AUTH_PASSWORD | admin | Password to be used when authenticating (form mode) |
| PLAYWRIGHT_UI_AUTH_MODE | form | Auth mode: `form` (fill login form) or `token_injection` (client credentials) |
| PLAYWRIGHT_AUTH_SCOPE | | OAuth2 scope for client_credentials token requests (required for token_injection, for EntraID e.g. `api://<app-id>/.default`) |

For API tests:

Expand All @@ -54,6 +56,7 @@ For API tests:
| PLAYWRIGHT_AUTH_URL | | OIDC Base URL, e.g. `http://localhost:9090/realms/trustd`. If not set, we will try to discover it from `index.html` |
| PLAYWRIGHT_AUTH_CLIENT_ID | cli | OIDC Client ID |
| PLAYWRIGHT_AUTH_CLIENT_SECRET | secret | OIDC Client Secret |
| PLAYWRIGHT_AUTH_SCOPE | | OAuth2 scope for client_credentials (required for Entra ID, e.g. `api://<app-id>/.default`) |

## Available Commands

Expand Down
4 changes: 4 additions & 0 deletions e2e/tests/api/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
AUTH_CLIENT_ID,
AUTH_CLIENT_SECRET,
AUTH_REQUIRED,
AUTH_SCOPE,
AUTH_URL,
logger,
TRUSTIFY_API_URL,
Expand Down Expand Up @@ -53,6 +54,9 @@ const getToken = async (baseURL?: string) => {
data.append("grant_type", "client_credentials");
data.append("client_id", AUTH_CLIENT_ID);
data.append("client_secret", AUTH_CLIENT_SECRET);
if (AUTH_SCOPE) {
data.append("scope", AUTH_SCOPE);
}

return await axios.post<TokenResponse>(tokenServiceURL, data, {
headers: {
Expand Down
6 changes: 5 additions & 1 deletion e2e/tests/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ export const TRUSTIFY_API_URL =
"http://localhost:8080/";

/**
* API only environment variables
* Shared auth environment variables (used by both API and UI tests)
*/
export const AUTH_URL = process.env.PLAYWRIGHT_AUTH_URL;
export const AUTH_CLIENT_ID = process.env.PLAYWRIGHT_AUTH_CLIENT_ID ?? "cli";
export const AUTH_CLIENT_SECRET =
process.env.PLAYWRIGHT_AUTH_CLIENT_SECRET ?? "secret";
export const AUTH_SCOPE = process.env.PLAYWRIGHT_AUTH_SCOPE;

/**
* UI only environment variables
*/
export const AUTH_USER = process.env.PLAYWRIGHT_AUTH_USER ?? "admin";
export const AUTH_PASSWORD = process.env.PLAYWRIGHT_AUTH_PASSWORD ?? "admin";
export const UI_AUTH_MODE = process.env.PLAYWRIGHT_UI_AUTH_MODE ?? "form";
export const TRUSTIFY_UI_URL =
process.env.TRUSTIFY_UI_URL ?? "http://localhost:3000/";

/**
* Log definition
Expand Down
141 changes: 130 additions & 11 deletions e2e/tests/ui/helpers/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,142 @@
import { expect, type Page } from "@playwright/test";
import axios from "axios";
import https from "node:https";

import {
AUTH_CLIENT_ID,
AUTH_CLIENT_SECRET,
AUTH_PASSWORD,
AUTH_REQUIRED,
AUTH_SCOPE,
AUTH_URL,
AUTH_USER,
TRUSTIFY_UI_URL,
UI_AUTH_MODE,
logger,
} from "../../common/constants";

export const login = async (page: Page) => {
if (AUTH_REQUIRED === "true") {
const userName = AUTH_USER;
const userPassword = AUTH_PASSWORD;
const httpsAgent = new https.Agent({ rejectUnauthorized: false });

const discoverFrontendOidcConfig = async (baseURL: string) => {
const response = await axios.get<string>(baseURL, {
maxRedirects: 0,
httpsAgent,
timeout: 5000,
timeoutErrorMessage: `Could not fetch index.html from ${baseURL}`,
});

const matcher = response.data.match(/window._env\s*=\s*"([^"]+)"/);
const serverConfig = matcher?.[1];
if (!serverConfig) {
throw new Error(
"Could not discover OIDC config from frontend index.html. " +
"Set PLAYWRIGHT_AUTH_URL explicitly.",
);
}

const envInfo: Record<string, string> = JSON.parse(atob(serverConfig));
return {
oidcServerUrl: envInfo.OIDC_SERVER_URL,
oidcClientId: envInfo.OIDC_CLIENT_ID || "frontend",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): Add an e2e scenario that explicitly exercises token_injection auth mode

Currently loginWithTokenInjection is only reachable via UI_AUTH_MODE, but there’s no e2e coverage to verify it actually works or fixes the SSO/MFA scenario described in the PR. Please add a smoke test (or a small Playwright project) that runs with PLAYWRIGHT_UI_AUTH_MODE=token_injection and AUTH_REQUIRED=true, uses a known-good Entra ID client, and asserts the UI is authenticated without using the login form. This will both prevent regressions and prove the injected token is sufficient for UI access.

};

const getClientCredentialsToken = async (authUrl: string) => {
const oidcConfigResponse = await axios.get(
`${authUrl}/.well-known/openid-configuration`,
{ httpsAgent },
);
const tokenEndpoint = oidcConfigResponse.data.token_endpoint;
expect(
tokenEndpoint,
"Could not discover token_endpoint from OIDC configuration",
).toBeTruthy();

await page.goto("/importers");
const data = new URLSearchParams();
data.append("grant_type", "client_credentials");
data.append("client_id", AUTH_CLIENT_ID);
data.append("client_secret", AUTH_CLIENT_SECRET);
if (AUTH_SCOPE) {
data.append("scope", AUTH_SCOPE);
}

const response = await axios.post(tokenEndpoint, data, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
httpsAgent,
});

return response.data as {
access_token: string;
expires_in: number;
token_type: string;
};
};

const loginWithTokenInjection = async (page: Page) => {
const { oidcServerUrl, oidcClientId } =
await discoverFrontendOidcConfig(TRUSTIFY_UI_URL);
logger.info(
`Discovered frontend OIDC config: server=${oidcServerUrl}, clientId=${oidcClientId}`,
);

const authUrl = AUTH_URL || oidcServerUrl;
if (!authUrl) {
throw new Error(
"Auth URL not available. Set PLAYWRIGHT_AUTH_URL or ensure the frontend exposes OIDC_SERVER_URL.",
);
}

await page.fill('input[name="username"]:visible', userName);
await page.fill('input[name="password"]:visible', userPassword);
await page.keyboard.press("Enter");
const tokenResponse = await getClientCredentialsToken(authUrl);
logger.info("Obtained access token via client_credentials");

const expiresAt =
Math.floor(Date.now() / 1000) + (tokenResponse.expires_in || 3600);

const storageKey = `oidc.user:${oidcServerUrl}:${oidcClientId}`;
const userObject = JSON.stringify({
access_token: tokenResponse.access_token,
token_type: "Bearer",
scope: AUTH_SCOPE || "openid",
expires_at: expiresAt,
profile: {
sub: AUTH_CLIENT_ID,
},
});

logger.debug(`Injecting token into sessionStorage key: ${storageKey}`);

await page.addInitScript(
({ key, value }: { key: string; value: string }) => {
sessionStorage.setItem(key, value);
},
{ key: storageKey, value: userObject },
);

await page.goto("/importers");
await expect(page.getByRole("heading", { name: "Importers" })).toHaveCount(1);
};

const loginWithForm = async (page: Page) => {
const userName = AUTH_USER;
const userPassword = AUTH_PASSWORD;

await page.goto("/importers");

await page.fill('input[name="username"]:visible', userName);
await page.fill('input[name="password"]:visible', userPassword);
await page.keyboard.press("Enter");

await expect(page.getByRole("heading", { name: "Importers" })).toHaveCount(1);
};

export const login = async (page: Page) => {
if (AUTH_REQUIRED !== "true") {
return;
}

await expect(page.getByRole("heading", { name: "Importers" })).toHaveCount(
1,
); // Ensure login was successful
if (UI_AUTH_MODE === "token_injection") {
await loginWithTokenInjection(page);
} else {
await loginWithForm(page);
}
};
Loading