Use correct host URL for Admin Console requests (#30535)

Closes #30432

Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Jon Koops 2024-06-19 15:21:53 +02:00 committed by GitHub
parent 5fc12480fd
commit 77fb3c4dd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 105 additions and 132 deletions

View file

@ -445,22 +445,15 @@ If you wish to use Oracle DB, you must manually install a version of the Oracle
= Deprecated theme variables
The following variables were deprecated in the Account theme:
The following variables were deprecated in the Admin theme and will be removed in a future version:
* `authUrl`. Use `authServerUrl` instead.
* `authServerUrl`. Use `serverBaseUrl` instead.
* `authUrl`. Use `adminBaseUrl` instead.
The following variables from the environment script injected into the page of the Account theme are deprecated:
The following variables were deprecated in the Account theme and will be removed in a future version:
* `authUrl`. Use `authServerUrl` instead.
* `features.isInternationalizationEnabled`. Do not use this variable.
The following variables were deprecated in the Admin theme:
* `authUrl`. Do not use this variable.
The following variables from the environment script injected into the page of the Admin theme are deprecated:
* `authUrl`. Do not use this variable.
* `authServerUrl`. Use `serverBaseUrl` instead, note `serverBaseUrl` does not include trailing slash.
* `authUrl`. Use `serverBaseUrl` instead, note `serverBaseUrl` does not include trailing slash.
= Methods to get and set current refresh token in client session are now deprecated

View file

@ -22,7 +22,7 @@ import { KeycloakProvider } from "@keycloak/keycloak-account-ui";
//...
<KeycloakProvider environment={{
authServerUrl: "http://localhost:8080",
serverBaseUrl: "http://localhost:8080",
realm: "master",
clientId: "security-admin-console"
}}>

View file

@ -110,6 +110,7 @@
<noscript>JavaScript is required to use the Account Console.</noscript>
<script id="environment" type="application/json">
{
"serverBaseUrl": "${serverBaseUrl}",
"authUrl": "${authUrl}",
"authServerUrl": "${authServerUrl}",
"realm": "${realm.name}",

View file

@ -81,17 +81,19 @@ function checkResponse<T>(response: T) {
export async function getIssuer(context: KeycloakContext<BaseEnvironment>) {
const response = await request(
"/realms/" +
context.environment.realm +
joinPath(
"/realms/",
context.environment.realm,
"/.well-known/openid-credential-issuer",
),
context,
{},
new URL(
joinPath(
context.environment.authServerUrl +
"/realms/" +
context.environment.realm +
"/.well-known/openid-credential-issuer",
context.environment.serverBaseUrl,
"/realms/",
context.environment.realm,
"/.well-known/openid-credential-issuer",
),
),
);

View file

@ -134,7 +134,7 @@ export async function linkAccount(
) {
const redirectUri = encodeURIComponent(
joinPath(
context.environment.authServerUrl,
context.environment.serverBaseUrl,
"realms",
context.environment.realm,
"account",

View file

@ -54,7 +54,7 @@ export async function request(
export const url = (environment: BaseEnvironment, path: string) =>
new URL(
joinPath(
environment.authServerUrl,
environment.serverBaseUrl,
"realms",
environment.realm,
"account",

View file

@ -28,34 +28,4 @@ export type Feature = {
isOid4VciEnabled: boolean;
};
// During development the realm can be passed as a query parameter when redirecting back from Keycloak.
const realm =
new URLSearchParams(window.location.search).get("realm") ||
location.pathname.match("/realms/(.*?)/account")?.[1] ||
"master";
const defaultEnvironment: Environment = {
// Base environment variables
authServerUrl: "http://localhost:8180",
realm: realm,
clientId: "security-admin-console-v2",
resourceUrl: "http://localhost:8080",
logo: "/logo.svg",
logoUrl: "/",
// Account Console specific environment variables
baseUrl: `http://localhost:8180/realms/${realm}/account/`,
locale: "en",
features: {
isRegistrationEmailAsUsername: false,
isEditUserNameAllowed: true,
isLinkedAccountsEnabled: true,
isMyResourcesEnabled: true,
deleteAccountAllowed: true,
updateEmailFeatureEnabled: true,
updateEmailActionEnabled: true,
isViewGroupsEnabled: true,
isOid4VciEnabled: true,
},
};
export const environment = getInjectedEnvironment(defaultEnvironment);
export const environment = getInjectedEnvironment<Environment>();

View file

@ -29,7 +29,7 @@ export const i18n = createInstance({
},
backend: {
loadPath: joinPath(
environment.authServerUrl,
environment.serverBaseUrl,
`resources/${environment.realm}/account/{{lng}}`,
),
parse: (data: string) => {

View file

@ -110,6 +110,8 @@
<noscript>JavaScript is required to use the Administration Console.</noscript>
<script id="environment" type="application/json">
{
"serverBaseUrl": "${serverBaseUrl}",
"adminBaseUrl": "${adminBaseUrl}",
"authUrl": "${authUrl}",
"authServerUrl": "${authServerUrl}",
"realm": "${loginRealm!"master"}",

View file

@ -25,7 +25,7 @@ export async function initAdminClient(
const adminClient = new KeycloakAdminClient();
adminClient.setConfig({ realmName: environment.realm });
adminClient.baseUrl = environment.authServerUrl;
adminClient.baseUrl = environment.adminBaseUrl;
adminClient.registerTokenProvider({
async getAccessToken() {
try {

View file

@ -4,6 +4,15 @@ import {
} from "@keycloak/keycloak-ui-shared";
export type Environment = BaseEnvironment & {
/**
* The URL to the root of the Administration Console, including the path if present, this takes into account the configured hostname of the Administration Console.
* For example, the Keycloak server could be hosted on `auth.example.com` and Admin Console may be hosted on `admin.example.com/some/path`.
*
* Note that this URL is normalized not to include a trailing slash, so take this into account when constructing URLs.
*
* @see {@link https://www.keycloak.org/server/hostname#_administration_console}
*/
adminBaseUrl: string;
/** The URL to the base of the Admin Console. */
consoleBaseUrl: string;
/** The name of the master realm. */
@ -12,22 +21,4 @@ export type Environment = BaseEnvironment & {
resourceVersion: string;
};
// During development the realm can be passed as a query parameter when redirecting back from Keycloak.
const realm =
new URLSearchParams(window.location.search).get("realm") || "master";
const defaultEnvironment: Environment = {
// Base environment variables
authServerUrl: "http://localhost:8180",
realm: realm,
clientId: "security-admin-console-v2",
resourceUrl: "http://localhost:8080",
logo: "/logo.svg",
logoUrl: "",
// Admin Console specific environment variables
consoleBaseUrl: "/admin/master/console/",
masterRealm: "master",
resourceVersion: "unknown",
};
export const environment = getInjectedEnvironment(defaultEnvironment);
export const environment = getInjectedEnvironment<Environment>();

View file

@ -18,7 +18,7 @@ export const i18n = createInstance({
},
backend: {
loadPath: joinPath(
environment.authServerUrl,
environment.adminBaseUrl,
`resources/${environment.realm}/admin/{{lng}}`,
),
parse: (data: string) => {

View file

@ -91,7 +91,7 @@ export const SamlConnectSettings = () => {
name="config.entityId"
label={t("serviceProviderEntityId")}
labelIcon={t("serviceProviderEntityIdHelp")}
defaultValue={`${environment.authServerUrl}/realms/${realm}`}
defaultValue={`${environment.serverBaseUrl}/realms/${realm}`}
rules={{
required: t("required"),
}}

View file

@ -56,7 +56,7 @@ export const SamlGeneralSettings = ({
>
<FormattedLink
title={t("samlEndpointsLabel")}
href={`${environment.authServerUrl}/realms/${realm}/broker/${alias}/endpoint/descriptor`}
href={`${environment.adminBaseUrl}/realms/${realm}/broker/${alias}/endpoint/descriptor`}
isInline
/>
</FormGroup>

View file

@ -49,7 +49,7 @@ export const KeycloakProvider = <T extends BaseEnvironment>({
const [error, setError] = useState<unknown>();
const keycloak = useMemo(() => {
const keycloak = new Keycloak({
url: environment.authServerUrl,
url: environment.serverBaseUrl,
realm: environment.realm,
clientId: environment.clientId,
});

View file

@ -1,12 +1,14 @@
/** The base environment variables that are shared between the Admin and Account Consoles. */
export type BaseEnvironment = {
/**
* The URL to the root of the Keycloak server, this is **NOT** always equivalent to the URL of the Admin Console.
* For example, the Keycloak server could be hosted on `auth.example.com` and Admin Console may be hosted on `admin.example.com`.
* The URL to the root of the Keycloak server, including the path if present, this is **NOT** always equivalent to the URL of the Admin Console.
* For example, the Keycloak server could be hosted on `auth.example.com` and Admin Console may be hosted on `admin.example.com/some/path`.
*
* Note that this URL is normalized not to include a trailing slash, so take this into account when constructing URLs.
*
* @see {@link https://www.keycloak.org/server/hostname#_administration_console}
*/
authServerUrl: string;
serverBaseUrl: string;
/** The identifier of the realm used to authenticate the user. */
realm: string;
/** The identifier of the client used to authenticate the user. */
@ -20,11 +22,19 @@ export type BaseEnvironment = {
};
/**
* Extracts the environment variables from the document, these variables are injected by Keycloak as a script tag, the contents of which can be parsed as JSON.
* Extracts the environment variables from the document, these variables are injected by Keycloak as a script tag, the contents of which can be parsed as JSON. For example:
*
* @argument defaults - The default values to fall to if a value is not found in the environment.
*```html
* <script id="environment" type="application/json">
* {
* "realm": "master",
* "clientId": "security-admin-console",
* "etc": "..."
* }
* </script>
* ```
*/
export function getInjectedEnvironment<T>(defaults: T): T {
export function getInjectedEnvironment<T>(): T {
const element = document.getElementById("environment");
const contents = element?.textContent;
@ -33,7 +43,7 @@ export function getInjectedEnvironment<T>(defaults: T): T {
}
try {
return { ...defaults, ...JSON.parse(contents) };
return JSON.parse(contents);
} catch (error) {
throw new Error("Unable to parse environment variables as JSON.");
}

View file

@ -96,22 +96,34 @@ public class AccountConsole implements AccountResourceProvider {
@NoCache
@Path("{any:.*}")
public Response getMainPage() throws IOException, FreeMarkerException {
UriInfo uriInfo = session.getContext().getUri(UrlType.FRONTEND);
URI accountBaseUrl = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(realm.getName())
.path(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).path("/").build(realm);
// Get the URI info of the server and admin console.
final var serverUriInfo = session.getContext().getUri(UrlType.FRONTEND);
final var adminUriInfo = session.getContext().getUri(UrlType.ADMIN);
Map<String, Object> map = new HashMap<>();
// Get the base URLs of the server and admin console.
final var serverBaseUri = serverUriInfo.getBaseUri();
final var adminBaseUri = adminUriInfo.getBaseUri();
URI adminBaseUri = session.getContext().getUri(UrlType.ADMIN).getBaseUri();
URI authUrl = uriInfo.getBaseUri();
var authServerUrl = authUrl.getPath().endsWith("/") ? authUrl : authUrl + "/";
// TODO: The 'authUrl' variable is deprecated and only exists to provide backwards compatibility for older themes, it should be removed in a future version.
map.put("authUrl", authServerUrl);
map.put("authServerUrl", authServerUrl);
// Strip any trailing slashes from the URLs.
final var serverBaseUrl = serverBaseUri.toString().replaceFirst("/+$", "");
final var map = new HashMap<String, Object>();
final var accountBaseUrl = serverUriInfo.getBaseUriBuilder()
.path(RealmsResource.class)
.path(realm.getName())
.path(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)
.path("/")
.build(realm);
map.put("serverBaseUrl", serverBaseUrl);
// TODO: Some variables are deprecated and only exist to provide backwards compatibility for older themes, they should be removed in a future version.
// Note that these should be removed from the template of the Account Console as well.
map.put("authUrl", serverBaseUrl + "/"); // Superseded by 'serverBaseUrl', remove in the future.
map.put("authServerUrl", serverBaseUrl + "/"); // Superseded by 'serverBaseUrl', remove in the future.
map.put("baseUrl", accountBaseUrl.getPath().endsWith("/") ? accountBaseUrl : accountBaseUrl + "/");
map.put("realm", realm);
map.put("clientId", Constants.ACCOUNT_CONSOLE_CLIENT_ID);
map.put("resourceUrl", Urls.themeRoot(authUrl).getPath() + "/" + Constants.ACCOUNT_MANAGEMENT_CLIENT_ID + "/" + theme.getName());
map.put("resourceUrl", Urls.themeRoot(serverBaseUri).getPath() + "/" + Constants.ACCOUNT_MANAGEMENT_CLIENT_ID + "/" + theme.getName());
map.put("resourceCommonUrl", Urls.themeRoot(adminBaseUri).getPath() + "/common/keycloak");
map.put("resourceVersion", Version.RESOURCES_VERSION);

View file

@ -313,47 +313,39 @@ public class AdminConsole {
}
/**
* Main page of this realm's admin console
*
* @return
* @throws URISyntaxException
* Main page of this realm's admin console.
*/
@GET
@NoCache
public Response getMainPage() throws IOException, FreeMarkerException {
if (!session.getContext().getUri(UrlType.ADMIN).getRequestUri().getPath().endsWith("/")) {
return Response.status(302).location(session.getContext().getUri(UrlType.ADMIN).getRequestUriBuilder().path("/").build()).build();
final var baseUriInfo = session.getContext().getUri(UrlType.FRONTEND);
final var adminUriInfo = session.getContext().getUri(UrlType.ADMIN);
// Redirect to a URL with a trailing slash if the current URL doesn't have one.
if (!adminUriInfo.getRequestUri().getPath().endsWith("/")) {
return Response.status(302).location(adminUriInfo.getRequestUriBuilder().path("/").build()).build();
} else {
Theme theme = AdminRoot.getTheme(session, realm);
// Get the base URLs of the server and admin console.
final var serverBaseUri = baseUriInfo.getBaseUri();
final var adminBaseUri = adminUriInfo.getBaseUri();
Map<String, Object> map = new HashMap<>();
// Strip any trailing slashes from the URLs.
final var serverBaseUrl = serverBaseUri.toString().replaceFirst("/+$", "");
final var adminBaseUrl = adminBaseUri.toString().replaceFirst("/+$", "");
URI adminBaseUri = session.getContext().getUri(UrlType.ADMIN).getBaseUri();
String adminBaseUrl = adminBaseUri.toString();
if (adminBaseUrl.endsWith("/")) {
adminBaseUrl = adminBaseUrl.substring(0, adminBaseUrl.length() - 1);
}
final var map = new HashMap<String, Object>();
final var theme = AdminRoot.getTheme(session, realm);
String kcJsRelativeBasePath = adminBaseUri.getPath();
if(!kcJsRelativeBasePath.endsWith("/")) {
kcJsRelativeBasePath = kcJsRelativeBasePath + "/";
}
URI authServerBaseUri = session.getContext().getUri(UrlType.FRONTEND).getBaseUri();
String authServerBaseUrl = authServerBaseUri.toString();
if (authServerBaseUrl.endsWith("/")) {
authServerBaseUrl = authServerBaseUrl.substring(0, authServerBaseUrl.length() - 1);
}
map.put("authServerUrl", authServerBaseUrl);
// TODO: The 'authUrl' variable is deprecated and only exists to provide backwards compatibility for older themes, it should be removed in a future version.
map.put("authUrl", adminBaseUrl);
map.put("serverBaseUrl", serverBaseUrl);
map.put("adminBaseUrl", adminBaseUrl);
// TODO: Some variables are deprecated and only exist to provide backwards compatibility for older themes, they should be removed in a future version.
// Note that these should be removed from the template of the Administration Console as well.
map.put("authServerUrl", serverBaseUrl); // Superseded by 'serverBaseUrl', remove in the future.
map.put("authUrl", adminBaseUrl); // Superseded by 'adminBaseUrl', remove in the future.
map.put("consoleBaseUrl", Urls.adminConsoleRoot(adminBaseUri, realm.getName()).getPath());
map.put("resourceUrl", Urls.themeRoot(adminBaseUri).getPath() + "/admin/" + theme.getName());
map.put("resourceCommonUrl", Urls.themeRoot(adminBaseUri).getPath() + "/common/keycloak");
map.put("keycloakJsUrl", kcJsRelativeBasePath + "js/keycloak.js?version=" + Version.RESOURCES_VERSION);
map.put("keycloakJsUrl", adminBaseUrl + "/js/keycloak.js?version=" + Version.RESOURCES_VERSION);
map.put("masterRealm", Config.getAdminRealm());
map.put("resourceVersion", Version.RESOURCES_VERSION);
map.put("loginRealm", realm.getName());
@ -380,13 +372,13 @@ public class AdminConsole {
map.put("entryImports", entryImports);
}
FreeMarkerProvider freeMarkerUtil = session.getProvider(FreeMarkerProvider.class);
String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme);
Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result);
final var freeMarkerUtil = session.getProvider(FreeMarkerProvider.class);
final var result = freeMarkerUtil.processTemplate(map, "index.ftl", theme);
final var builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result);
// Replace CSP if admin is hosted on different URL
if (!adminBaseUri.equals(authServerBaseUri)) {
session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(UriUtils.getOrigin(authServerBaseUri));
// Allow iframes to be embedded from the server if the admin console is running on a different URL.
if (!adminBaseUri.equals(serverBaseUri)) {
session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(UriUtils.getOrigin(serverBaseUri));
}
return builder.build();