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

Closes #27628

Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Jon Koops 2024-04-16 17:24:22 +02:00 committed by GitHub
parent f764a9cb4a
commit 3216e7c781
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 73 additions and 75 deletions

View file

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

View file

@ -1,2 +1,4 @@
export const DEFAULT_REALM = "master"; export const DEFAULT_REALM = "master";
export const ROOT_PATH = "/realms/:realm/account"; export const ROOT_PATH = "/realms/:realm/account";
export const ADMIN_USER = "admin";
export const ADMIN_PASSWORD = "admin";

View file

@ -34,10 +34,10 @@ export type Environment = {
locale: string; locale: string;
/** Feature flags */ /** Feature flags */
features: Feature; features: Feature;
/** Client id of the application to add back link */ /** Name of the referrer application in the back link */
referrer?: string; referrerName?: string;
/** URI to the referrer application in the back link */ /** UR to the referrer application in the back link */
referrer_uri?: string; referrerUrl?: string;
}; };
// Detect the current realm from the URL. // Detect the current realm from the URL.
@ -94,14 +94,6 @@ function getInjectedEnvironment(): Record<string, string | number | boolean> {
console.error("Unable to parse environment variables."); console.error("Unable to parse environment variables.");
} }
const searchParams = new URLSearchParams(location.search);
if (searchParams.has("referrer_uri")) {
env["referrer_uri"] = searchParams.get("referrer_uri")!;
}
if (searchParams.has("referrer")) {
env["referrer"] = searchParams.get("referrer")!;
}
// Otherwise, return an empty record. // Otherwise, return an empty record.
return env; return env;
} }

View file

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

View file

@ -1,53 +1,46 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { login } from "./login"; import { login } from "./login";
import { getAdminUrl } from "./utils";
import { ADMIN_PASSWORD, ADMIN_USER, DEFAULT_REALM } from "../src/constants";
// 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.describe("Signing in with referrer link", () => {
// Tests for keycloak account console, section Signing in in Account security test("shows a referrer link when a matching client exists", async ({
test("Should see referrer", async ({ page }) => { page,
const queryParams = { }) => {
referrer: "my-app", const referrer = "security-admin-console";
referrer_uri: "http://localhost:3000", const referrerUrl = getAdminUrl();
}; const referrerName = "security admin console";
await login(page, "jdoe", "jdoe", "groups", queryParams);
await expect(page.getByTestId("referrer-link")).toContainText("my-app"); 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 page.getByTestId("accountSecurity").click();
await expect(page.getByTestId("account-security/signing-in")).toBeVisible(); await expect(page.getByTestId("account-security/signing-in")).toBeVisible();
await expect(page.getByTestId("referrer-link")).toContainText("my-app"); await expect(page.getByTestId("referrer-link")).toContainText(referrerName);
}); });
// Tests for keycloak account console, section Signing in in Account security test("shows no referrer link when an invalid URL is passed", async ({
test("Should see no referrer", async ({ page }) => { page,
const queryParams = {}; }) => {
await login(page, "jdoe", "jdoe", "groups", queryParams); const referrer = "security-admin-console";
const referrerUrl = "http://i-am-not-an-allowed-url.com";
await expect(page.getByTestId("referrer-link")).toBeHidden();
await page.getByTestId("accountSecurity").click();
await expect(page.getByTestId("account-security/signing-in")).toBeVisible();
await expect(page.getByTestId("referrer-link")).toBeHidden();
});
test("Should see no referrer after relogin", async ({ page }) => {
const queryParams = { const queryParams = {
referrer: "my-app", referrer,
referrer_uri: "http://localhost:3000", referrer_uri: referrerUrl,
}; };
await login(page, "jdoe", "jdoe", "groups", queryParams);
await expect(page.getByTestId("referrer-link")).toContainText("my-app"); await login(page, ADMIN_USER, ADMIN_PASSWORD, DEFAULT_REALM, queryParams);
await page.getByTestId("accountSecurity").click(); await expect(page.getByText("Manage your basic information")).toBeVisible();
await expect(page.getByTestId("account-security/signing-in")).toBeVisible();
await expect(page.getByTestId("referrer-link")).toContainText("my-app");
await page.getByTestId("options").click();
await page.getByRole("menuitem", { name: "Sign out" }).click();
const queryParamsNoReferrer = {};
await login(page, "jdoe", "jdoe", "groups", queryParamsNoReferrer);
await expect(page.getByTestId("referrer-link")).toBeHidden();
await page.getByTestId("accountSecurity").click();
await expect(page.getByTestId("account-security/signing-in")).toBeVisible();
await expect(page.getByTestId("referrer-link")).toBeHidden(); await expect(page.getByTestId("referrer-link")).toBeHidden();
}); });
}); });

View file

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