Use correct host URL for Admin Console requests (#30535)
Closes #30432 Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
5fc12480fd
commit
77fb3c4dd4
18 changed files with 105 additions and 132 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}}>
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -81,16 +81,18 @@ 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 +
|
||||
context.environment.serverBaseUrl,
|
||||
"/realms/",
|
||||
context.environment.realm,
|
||||
"/.well-known/openid-credential-issuer",
|
||||
),
|
||||
),
|
||||
|
|
|
@ -134,7 +134,7 @@ export async function linkAccount(
|
|||
) {
|
||||
const redirectUri = encodeURIComponent(
|
||||
joinPath(
|
||||
context.environment.authServerUrl,
|
||||
context.environment.serverBaseUrl,
|
||||
"realms",
|
||||
context.environment.realm,
|
||||
"account",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -29,7 +29,7 @@ export const i18n = createInstance({
|
|||
},
|
||||
backend: {
|
||||
loadPath: joinPath(
|
||||
environment.authServerUrl,
|
||||
environment.serverBaseUrl,
|
||||
`resources/${environment.realm}/account/{{lng}}`,
|
||||
),
|
||||
parse: (data: string) => {
|
||||
|
|
|
@ -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"}",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -18,7 +18,7 @@ export const i18n = createInstance({
|
|||
},
|
||||
backend: {
|
||||
loadPath: joinPath(
|
||||
environment.authServerUrl,
|
||||
environment.adminBaseUrl,
|
||||
`resources/${environment.realm}/admin/{{lng}}`,
|
||||
),
|
||||
parse: (data: string) => {
|
||||
|
|
|
@ -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"),
|
||||
}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue