Only allow a known refferer URI for the Account Console (#28743) (#31814)

Closes #27628

Signed-off-by: Jon Koops <jonkoops@gmail.com>
(cherry picked from commit 3216e7c781a9bb6399d33255e6b10275b3cc81f9)
This commit is contained in:
Jon Koops 2024-08-01 13:08:52 +02:00 committed by GitHub
parent a1cfc4d816
commit bd38e1d323
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 125 additions and 33 deletions

View File

@ -166,7 +166,9 @@
"updateEmailFeatureEnabled": ${updateEmailFeatureEnabled?c},
"updateEmailActionEnabled": ${updateEmailActionEnabled?c},
"isViewGroupsEnabled": ${isViewGroupsEnabled?c}
}
},
"referrerName": "${referrerName!""}",
"referrerUrl": "${referrer_uri!""}"
}
</script>
</body>

View File

@ -29,6 +29,10 @@ export type Environment = {
locale: string;
/** Feature flags */
features: Feature;
/** Name of the referrer application in the back link */
referrerName?: string;
/** UR to the referrer application in the back link */
referrerUrl?: string;
};
// The default environment, used during development.

View File

@ -1,3 +1,5 @@
import { Button } from "@patternfly/react-core";
import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons";
import {
KeycloakMasthead,
KeycloakProvider,
@ -7,28 +9,30 @@ import {
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useHref } from "react-router-dom";
import { useEnvironment } from "./KeycloakContext";
import { label } from "ui-shared";
import { environment } from "../environment";
import { joinPath } from "../utils/joinPath";
import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons";
import { Button } from "@patternfly/react-core";
import { useEnvironment } from "./KeycloakContext";
import style from "./header.module.css";
const ReferrerLink = () => {
const { t } = useTranslation();
const searchParams = new URLSearchParams(location.search);
return searchParams.has("referrer_uri") ? (
return environment.referrerUrl ? (
<Button
data-testid="referrer-link"
component="a"
href={searchParams.get("referrer_uri")!.replace("_hash_", "#")}
href={environment.referrerUrl.replace("_hash_", "#")}
variant="link"
icon={<ExternalLinkSquareAltIcon />}
iconPosition="right"
isInline
>
{t("backTo", { app: searchParams.get("referrer") })}
{t("backTo", {
app: label(t, environment.referrerName, environment.referrerUrl),
})}
</Button>
) : null;
};

View File

@ -0,0 +1,5 @@
export const SERVER_URL = "http://localhost:8080";
export const ROOT_PATH = "/realms/:realm/account";
export const DEFAULT_REALM = "master";
export const ADMIN_USER = "admin";
export const ADMIN_PASSWORD = "admin";

View File

@ -1,15 +1,26 @@
import { Page } from "@playwright/test";
import { ADMIN_PASSWORD, ADMIN_USER, DEFAULT_REALM } from "./constants";
import { getRootPath } from "./utils";
export const login = async (
page: Page,
username: string,
password: string,
realm?: string,
username = ADMIN_USER,
password = ADMIN_PASSWORD,
realm = DEFAULT_REALM,
queryParams?: Record<string, string>,
) => {
if (realm)
await page.goto(
process.env.CI ? `/realms/${realm}/account` : `/?realm=${realm}`,
);
const params = new URLSearchParams(queryParams);
if (!process.env.CI) {
params.set("realm", realm);
}
const rootPath =
(process.env.CI ? getRootPath(realm) : "/") +
(params.size > 0 ? `?${params.toString()}` : "");
await page.goto(rootPath);
await page.getByLabel("Username").fill(username);
await page.getByLabel("Password", { exact: true }).fill(password);
await page.getByRole("button", { name: "Sign In" }).click();

View File

@ -0,0 +1,47 @@
import { expect, test } from "@playwright/test";
import { ADMIN_PASSWORD, ADMIN_USER, DEFAULT_REALM } from "./constants";
import { login } from "./login";
import { getAdminUrl } from "./utils";
// NOTE: This test suite will only pass when running a production build, as the referrer is extracted on the server side.
// This will change once https://github.com/keycloak/keycloak/pull/27311 has been merged.
test.describe("Signing in with referrer link", () => {
test("shows a referrer link when a matching client exists", async ({
page,
}) => {
const referrer = "security-admin-console";
const referrerUrl = getAdminUrl();
const referrerName = "security admin console";
const queryParams = {
referrer,
referrer_uri: referrerUrl,
};
await login(page, ADMIN_USER, ADMIN_PASSWORD, DEFAULT_REALM, queryParams);
await expect(page.getByTestId("referrer-link")).toContainText(referrerName);
// Navigate around to ensure the referrer is still shown.
await page.getByTestId("accountSecurity").click();
await expect(page.getByTestId("account-security/signing-in")).toBeVisible();
await expect(page.getByTestId("referrer-link")).toContainText(referrerName);
});
test("shows no referrer link when an invalid URL is passed", async ({
page,
}) => {
const referrer = "security-admin-console";
const referrerUrl = "http://i-am-not-an-allowed-url.com";
const queryParams = {
referrer,
referrer_uri: referrerUrl,
};
await login(page, ADMIN_USER, ADMIN_PASSWORD, DEFAULT_REALM, queryParams);
await expect(page.getByText("Manage your basic information")).toBeVisible();
await expect(page.getByTestId("referrer-link")).toBeHidden();
});
});

View File

@ -0,0 +1,14 @@
import { generatePath } from "react-router-dom";
import { DEFAULT_REALM, ROOT_PATH, SERVER_URL } from "./constants";
export function getAccountUrl() {
return SERVER_URL + getRootPath();
}
export function getAdminUrl() {
return SERVER_URL + "/admin/master/console/";
}
export const getRootPath = (realm = DEFAULT_REALM) =>
generatePath(ROOT_PATH, { realm });

View File

@ -204,40 +204,45 @@ public class AccountConsole implements AccountResourceProvider {
return propertyValue;
}
@GET
@Path("index.html")
public Response getIndexHtmlRedirect() {
return Response.status(302).location(session.getContext().getUri().getRequestUriBuilder().path("../").build()).build();
}
private String[] getReferrer() {
String referrer = session.getContext().getUri().getQueryParameters().getFirst("referrer");
if (referrer == null) {
return null;
}
ClientModel referrerClient = realm.getClientByClientId(referrer);
if (referrerClient == null) {
return null;
}
String referrerUri = session.getContext().getUri().getQueryParameters().getFirst("referrer_uri");
ClientModel referrerClient = realm.getClientByClientId(referrer);
if (referrerClient != null) {
if (referrerUri != null) {
referrerUri = RedirectUtils.verifyRedirectUri(session, referrerUri, referrerClient);
} else {
referrerUri = ResolveRelative.resolveRelativeUri(session, referrerClient.getRootUrl(), referrerClient.getBaseUrl());
}
if (referrerUri != null) {
String referrerName = referrerClient.getName();
if (Validation.isBlank(referrerName)) {
referrerName = referrer;
}
return new String[]{referrer, referrerName, referrerUri};
}
if (referrerUri != null) {
referrerUri = RedirectUtils.verifyRedirectUri(session, referrerUri, referrerClient);
} else {
referrerUri = ResolveRelative.resolveRelativeUri(session, referrerClient.getRootUrl(), referrerClient.getBaseUrl());
}
return null;
if (referrerUri == null) {
return null;
}
String referrerName = referrerClient.getName();
if (Validation.isBlank(referrerName)) {
referrerName = referrer;
}
return new String[]{referrer, referrerName, referrerUri};
}
}