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 = 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. * `authServerUrl`. Use `serverBaseUrl` instead, note `serverBaseUrl` does not include trailing slash.
* `features.isInternationalizationEnabled`. Do not use this variable. * `authUrl`. Use `serverBaseUrl` instead, note `serverBaseUrl` does not include trailing slash.
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.
= Methods to get and set current refresh token in client session are now deprecated = 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={{ <KeycloakProvider environment={{
authServerUrl: "http://localhost:8080", serverBaseUrl: "http://localhost:8080",
realm: "master", realm: "master",
clientId: "security-admin-console" clientId: "security-admin-console"
}}> }}>

View file

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

View file

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

View file

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

View file

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

View file

@ -28,34 +28,4 @@ export type Feature = {
isOid4VciEnabled: boolean; isOid4VciEnabled: boolean;
}; };
// During development the realm can be passed as a query parameter when redirecting back from Keycloak. export const environment = getInjectedEnvironment<Environment>();
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);

View file

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

View file

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

View file

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

View file

@ -4,6 +4,15 @@ import {
} from "@keycloak/keycloak-ui-shared"; } from "@keycloak/keycloak-ui-shared";
export type Environment = BaseEnvironment & { 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. */ /** The URL to the base of the Admin Console. */
consoleBaseUrl: string; consoleBaseUrl: string;
/** The name of the master realm. */ /** The name of the master realm. */
@ -12,22 +21,4 @@ export type Environment = BaseEnvironment & {
resourceVersion: string; resourceVersion: string;
}; };
// During development the realm can be passed as a query parameter when redirecting back from Keycloak. export const environment = getInjectedEnvironment<Environment>();
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);

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,14 @@
/** The base environment variables that are shared between the Admin and Account Consoles. */ /** The base environment variables that are shared between the Admin and Account Consoles. */
export type BaseEnvironment = { export type BaseEnvironment = {
/** /**
* The URL to the root of the Keycloak server, this is **NOT** always equivalent to the URL of the Admin Console. * 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`. * 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} * @see {@link https://www.keycloak.org/server/hostname#_administration_console}
*/ */
authServerUrl: string; serverBaseUrl: string;
/** The identifier of the realm used to authenticate the user. */ /** The identifier of the realm used to authenticate the user. */
realm: string; realm: string;
/** The identifier of the client used to authenticate the user. */ /** 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 element = document.getElementById("environment");
const contents = element?.textContent; const contents = element?.textContent;
@ -33,7 +43,7 @@ export function getInjectedEnvironment<T>(defaults: T): T {
} }
try { try {
return { ...defaults, ...JSON.parse(contents) }; return JSON.parse(contents);
} catch (error) { } catch (error) {
throw new Error("Unable to parse environment variables as JSON."); throw new Error("Unable to parse environment variables as JSON.");
} }

View file

@ -96,22 +96,34 @@ public class AccountConsole implements AccountResourceProvider {
@NoCache @NoCache
@Path("{any:.*}") @Path("{any:.*}")
public Response getMainPage() throws IOException, FreeMarkerException { public Response getMainPage() throws IOException, FreeMarkerException {
UriInfo uriInfo = session.getContext().getUri(UrlType.FRONTEND); // Get the URI info of the server and admin console.
URI accountBaseUrl = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(realm.getName()) final var serverUriInfo = session.getContext().getUri(UrlType.FRONTEND);
.path(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).path("/").build(realm); 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(); // Strip any trailing slashes from the URLs.
URI authUrl = uriInfo.getBaseUri(); final var serverBaseUrl = serverBaseUri.toString().replaceFirst("/+$", "");
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. final var map = new HashMap<String, Object>();
map.put("authUrl", authServerUrl); final var accountBaseUrl = serverUriInfo.getBaseUriBuilder()
map.put("authServerUrl", authServerUrl); .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("baseUrl", accountBaseUrl.getPath().endsWith("/") ? accountBaseUrl : accountBaseUrl + "/");
map.put("realm", realm); map.put("realm", realm);
map.put("clientId", Constants.ACCOUNT_CONSOLE_CLIENT_ID); 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("resourceCommonUrl", Urls.themeRoot(adminBaseUri).getPath() + "/common/keycloak");
map.put("resourceVersion", Version.RESOURCES_VERSION); map.put("resourceVersion", Version.RESOURCES_VERSION);

View file

@ -313,47 +313,39 @@ public class AdminConsole {
} }
/** /**
* Main page of this realm's admin console * Main page of this realm's admin console.
*
* @return
* @throws URISyntaxException
*/ */
@GET @GET
@NoCache @NoCache
public Response getMainPage() throws IOException, FreeMarkerException { public Response getMainPage() throws IOException, FreeMarkerException {
if (!session.getContext().getUri(UrlType.ADMIN).getRequestUri().getPath().endsWith("/")) { final var baseUriInfo = session.getContext().getUri(UrlType.FRONTEND);
return Response.status(302).location(session.getContext().getUri(UrlType.ADMIN).getRequestUriBuilder().path("/").build()).build(); 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 { } 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(); final var map = new HashMap<String, Object>();
String adminBaseUrl = adminBaseUri.toString(); final var theme = AdminRoot.getTheme(session, realm);
if (adminBaseUrl.endsWith("/")) {
adminBaseUrl = adminBaseUrl.substring(0, adminBaseUrl.length() - 1);
}
String kcJsRelativeBasePath = adminBaseUri.getPath(); map.put("serverBaseUrl", serverBaseUrl);
map.put("adminBaseUrl", adminBaseUrl);
if(!kcJsRelativeBasePath.endsWith("/")) { // TODO: Some variables are deprecated and only exist to provide backwards compatibility for older themes, they should be removed in a future version.
kcJsRelativeBasePath = kcJsRelativeBasePath + "/"; // 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.
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("consoleBaseUrl", Urls.adminConsoleRoot(adminBaseUri, realm.getName()).getPath()); map.put("consoleBaseUrl", Urls.adminConsoleRoot(adminBaseUri, realm.getName()).getPath());
map.put("resourceUrl", Urls.themeRoot(adminBaseUri).getPath() + "/admin/" + theme.getName()); map.put("resourceUrl", Urls.themeRoot(adminBaseUri).getPath() + "/admin/" + theme.getName());
map.put("resourceCommonUrl", Urls.themeRoot(adminBaseUri).getPath() + "/common/keycloak"); 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("masterRealm", Config.getAdminRealm());
map.put("resourceVersion", Version.RESOURCES_VERSION); map.put("resourceVersion", Version.RESOURCES_VERSION);
map.put("loginRealm", realm.getName()); map.put("loginRealm", realm.getName());
@ -380,13 +372,13 @@ public class AdminConsole {
map.put("entryImports", entryImports); map.put("entryImports", entryImports);
} }
FreeMarkerProvider freeMarkerUtil = session.getProvider(FreeMarkerProvider.class); final var freeMarkerUtil = session.getProvider(FreeMarkerProvider.class);
String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme); final var 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 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 // Allow iframes to be embedded from the server if the admin console is running on a different URL.
if (!adminBaseUri.equals(authServerBaseUri)) { if (!adminBaseUri.equals(serverBaseUri)) {
session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(UriUtils.getOrigin(authServerBaseUri)); session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(UriUtils.getOrigin(serverBaseUri));
} }
return builder.build(); return builder.build();