Align environment variables between consoles (#30125)

* change to make authServerUrl the same as authUrl

fixes: #29641
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* Remove `authUrl` entirely

Signed-off-by: Jon Koops <jonkoops@gmail.com>

* Remove file that is unrelated

Signed-off-by: Jon Koops <jonkoops@gmail.com>

* Split out and align environment variables between consoles

Signed-off-by: Jon Koops <jonkoops@gmail.com>

* Restore removed variables to preserve backwards compatibility

Signed-off-by: Jon Koops <jonkoops@gmail.com>

* Also deprecate the `authUrl` for the Admin Console

Signed-off-by: Jon Koops <jonkoops@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Jon Koops <jonkoops@gmail.com>
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-06-06 08:36:46 +02:00 committed by GitHub
parent 2576429def
commit 5897334ddb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 242 additions and 176 deletions

View file

@ -438,3 +438,22 @@ An external {jdgserver_name} deployment is supported for multi-site setups as ou
The Oracle Database JDBC driver is no longer part of the Keycloak distribution. The Oracle Database JDBC driver is no longer part of the Keycloak distribution.
If you wish to use Oracle DB, you must manually install a version of the Oracle Driver that is compatible with your specific environment. Instructions for this process can be found in the https://www.keycloak.org/server/db[Configuring the database] {section}. If you wish to use Oracle DB, you must manually install a version of the Oracle Driver that is compatible with your specific environment. Instructions for this process can be found in the https://www.keycloak.org/server/db[Configuring the database] {section}.
= Deprecated theme variables
The following variables were deprecated in the Account theme:
* `authUrl`. Use `authServerUrl` instead.
The following variables from the environment script injected into the page of the Account theme are deprecated:
* `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.

View file

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

View file

@ -149,27 +149,28 @@
<script id="environment" type="application/json"> <script id="environment" type="application/json">
{ {
"authUrl": "${authUrl}", "authUrl": "${authUrl}",
"baseUrl": "${baseUrl}", "authServerUrl": "${authServerUrl}",
"realm": "${realm.name}", "realm": "${realm.name}",
"clientId": "${clientId}", "clientId": "${clientId}",
"resourceUrl": "${resourceUrl}", "resourceUrl": "${resourceUrl}",
"logo": "${properties.logo!""}", "logo": "${properties.logo!""}",
"logoUrl": "${properties.logoUrl!""}", "logoUrl": "${properties.logoUrl!""}",
"baseUrl": "${baseUrl}",
"locale": "${locale}", "locale": "${locale}",
"referrerName": "${referrerName!""}",
"referrerUrl": "${referrer_uri!""}",
"features": { "features": {
"isRegistrationEmailAsUsername": ${realm.registrationEmailAsUsername?c}, "isRegistrationEmailAsUsername": ${realm.registrationEmailAsUsername?c},
"isEditUserNameAllowed": ${realm.editUsernameAllowed?c}, "isEditUserNameAllowed": ${realm.editUsernameAllowed?c},
"isInternationalizationEnabled": ${realm.isInternationalizationEnabled()?c}, "isInternationalizationEnabled": ${realm.isInternationalizationEnabled()?c},
"isLinkedAccountsEnabled": ${realm.identityFederationEnabled?c}, "isLinkedAccountsEnabled": ${realm.identityFederationEnabled?c},
"isMyResourcesEnabled": ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c}, "isMyResourcesEnabled": ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
"deleteAccountAllowed": ${deleteAccountAllowed?c}, "deleteAccountAllowed": ${deleteAccountAllowed?c},
"updateEmailFeatureEnabled": ${updateEmailFeatureEnabled?c}, "updateEmailFeatureEnabled": ${updateEmailFeatureEnabled?c},
"updateEmailActionEnabled": ${updateEmailActionEnabled?c}, "updateEmailActionEnabled": ${updateEmailActionEnabled?c},
"isViewGroupsEnabled": ${isViewGroupsEnabled?c}, "isViewGroupsEnabled": ${isViewGroupsEnabled?c},
"isOid4VciEnabled": ${isOid4VciEnabled?c} "isOid4VciEnabled": ${isOid4VciEnabled?c}
}, }
"referrerName": "${referrerName!""}",
"referrerUrl": "${referrer_uri!""}"
} }
</script> </script>
</body> </body>

View file

@ -1,13 +1,16 @@
import { KeycloakContext } from "@keycloak/keycloak-ui-shared"; import {
import { BaseEnvironment } from "@keycloak/keycloak-ui-shared/dist/context/environment"; KeycloakContext,
type BaseEnvironment,
} from "@keycloak/keycloak-ui-shared";
import { CallOptions } from "./api/methods"; import { CallOptions } from "./api/methods";
import { Links, parseLinks } from "./api/parse-links"; import { Links, parseLinks } from "./api/parse-links";
import { parseResponse } from "./api/parse-response"; import { parseResponse } from "./api/parse-response";
import { import {
CredentialsIssuer,
Permission, Permission,
Resource, Resource,
Scope, Scope,
CredentialsIssuer,
SupportedCredentialConfiguration, SupportedCredentialConfiguration,
} from "./api/representations"; } from "./api/representations";
import { request } from "./api/request"; import { request } from "./api/request";
@ -85,7 +88,7 @@ export async function getIssuer(context: KeycloakContext<BaseEnvironment>) {
{}, {},
new URL( new URL(
joinPath( joinPath(
context.environment.authUrl + context.environment.authServerUrl +
"/realms/" + "/realms/" +
context.environment.realm + context.environment.realm +
"/.well-known/openid-credential-issuer", "/.well-known/openid-credential-issuer",

View file

@ -1,8 +1,8 @@
import { import {
AccountEnvironment, BaseEnvironment,
KeycloakContext, type KeycloakContext,
} from "@keycloak/keycloak-ui-shared"; } from "@keycloak/keycloak-ui-shared";
import { BaseEnvironment } from "@keycloak/keycloak-ui-shared/dist/context/environment";
import { joinPath } from "../utils/joinPath"; import { joinPath } from "../utils/joinPath";
import { parseResponse } from "./parse-response"; import { parseResponse } from "./parse-response";
import { import {
@ -45,7 +45,7 @@ export async function getSupportedLocales({
} }
export async function savePersonalInfo( export async function savePersonalInfo(
context: KeycloakContext<AccountEnvironment>, context: KeycloakContext<BaseEnvironment>,
info: UserRepresentation, info: UserRepresentation,
): Promise<void> { ): Promise<void> {
const response = await request("/", context, { body: info, method: "POST" }); const response = await request("/", context, { body: info, method: "POST" });
@ -134,7 +134,7 @@ export async function linkAccount(
) { ) {
const redirectUri = encodeURIComponent( const redirectUri = encodeURIComponent(
joinPath( joinPath(
context.environment.authUrl, context.environment.authServerUrl,
"realms", "realms",
context.environment.realm, context.environment.realm,
"account", "account",

View file

@ -1,6 +1,9 @@
import { KeycloakContext } from "@keycloak/keycloak-ui-shared"; import {
import { BaseEnvironment } from "@keycloak/keycloak-ui-shared/dist/context/environment"; KeycloakContext,
type BaseEnvironment,
} from "@keycloak/keycloak-ui-shared";
import Keycloak from "keycloak-js"; import Keycloak from "keycloak-js";
import { joinPath } from "../utils/joinPath"; import { joinPath } from "../utils/joinPath";
import { CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from "./constants"; import { CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON } from "./constants";
@ -50,7 +53,13 @@ export async function request(
export const url = (environment: BaseEnvironment, path: string) => export const url = (environment: BaseEnvironment, path: string) =>
new URL( new URL(
joinPath(environment.authUrl, "realms", environment.realm, "account", path), joinPath(
environment.authServerUrl,
"realms",
environment.realm,
"account",
path,
),
); );
export const token = (keycloak: Keycloak) => export const token = (keycloak: Keycloak) =>

View file

@ -0,0 +1,61 @@
import {
getInjectedEnvironment,
type BaseEnvironment,
} from "@keycloak/keycloak-ui-shared";
export type Environment = BaseEnvironment & {
/** The URL to the root of the account console. */
baseUrl: string;
/** The locale of the user */
locale: string;
/** Name of the referrer application in the back link */
referrerName?: string;
/** UR to the referrer application in the back link */
referrerUrl?: string;
/** Feature flags */
features: Feature;
};
export type Feature = {
isRegistrationEmailAsUsername: boolean;
isEditUserNameAllowed: boolean;
isLinkedAccountsEnabled: boolean;
isMyResourcesEnabled: boolean;
deleteAccountAllowed: boolean;
updateEmailFeatureEnabled: boolean;
updateEmailActionEnabled: boolean;
isViewGroupsEnabled: boolean;
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);

View file

@ -1,7 +1,8 @@
import { LanguageDetectorModule, createInstance } from "i18next"; import { LanguageDetectorModule, createInstance } from "i18next";
import HttpBackend from "i18next-http-backend"; import HttpBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import { environment } from "@keycloak/keycloak-ui-shared";
import { environment } from "./environment";
import { joinPath } from "./utils/joinPath"; import { joinPath } from "./utils/joinPath";
const DEFAULT_LOCALE = "en"; const DEFAULT_LOCALE = "en";
@ -28,7 +29,7 @@ export const i18n = createInstance({
}, },
backend: { backend: {
loadPath: joinPath( loadPath: joinPath(
environment.authUrl, environment.authServerUrl,
`resources/${environment.realm}/account/{{lng}}`, `resources/${environment.realm}/account/{{lng}}`,
), ),
parse: (data: string) => { parse: (data: string) => {

View file

@ -1,5 +1,4 @@
import { import {
AccountEnvironment,
UserProfileFields, UserProfileFields,
beerify, beerify,
debeerify, debeerify,
@ -20,6 +19,7 @@ import { TFunction } from "i18next";
import { useState } from "react"; import { useState } from "react";
import { ErrorOption, useForm } from "react-hook-form"; import { ErrorOption, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
getPersonalInfo, getPersonalInfo,
getSupportedLocales, getSupportedLocales,
@ -30,12 +30,13 @@ import {
UserRepresentation, UserRepresentation,
} from "../api/representations"; } from "../api/representations";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";
import type { Environment } from "../environment";
import { TFuncKey, i18n } from "../i18n"; import { TFuncKey, i18n } from "../i18n";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
export const PersonalInfo = () => { export const PersonalInfo = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useEnvironment<AccountEnvironment>(); const context = useEnvironment<Environment>();
const [userProfileMetadata, setUserProfileMetadata] = const [userProfileMetadata, setUserProfileMetadata] =
useState<UserProfileMetadata>(); useState<UserProfileMetadata>();
const [supportedLocales, setSupportedLocales] = useState<string[]>([]); const [supportedLocales, setSupportedLocales] = useState<string[]>([]);

View file

@ -1,13 +1,14 @@
import {
KeycloakMasthead,
label,
useEnvironment,
} from "@keycloak/keycloak-ui-shared";
import { Button } from "@patternfly/react-core"; import { Button } from "@patternfly/react-core";
import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons"; import { ExternalLinkSquareAltIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHref } from "react-router-dom"; import { useHref } from "react-router-dom";
import {
KeycloakMasthead, import { environment } from "../environment";
environment,
label,
useEnvironment,
} from "@keycloak/keycloak-ui-shared";
import { joinPath } from "../utils/joinPath"; import { joinPath } from "../utils/joinPath";
import style from "./header.module.css"; import style from "./header.module.css";

View file

@ -1,3 +1,4 @@
import { useEnvironment } from "@keycloak/keycloak-ui-shared";
import { import {
Nav, Nav,
NavExpandable, NavExpandable,
@ -22,13 +23,9 @@ import {
useLinkClickHandler, useLinkClickHandler,
useLocation, useLocation,
} from "react-router-dom"; } from "react-router-dom";
import {
AccountEnvironment,
environment,
useEnvironment,
type Feature,
} from "@keycloak/keycloak-ui-shared";
import fetchContentJson from "../content/fetchContent"; import fetchContentJson from "../content/fetchContent";
import { environment, type Environment, type Feature } from "../environment";
import { TFuncKey } from "../i18n"; import { TFuncKey } from "../i18n";
import { usePromise } from "../utils/usePromise"; import { usePromise } from "../utils/usePromise";
@ -49,7 +46,7 @@ export type MenuItem = RootMenuItem | MenuItemWithChildren;
export const PageNav = () => { export const PageNav = () => {
const [menuItems, setMenuItems] = useState<MenuItem[]>(); const [menuItems, setMenuItems] = useState<MenuItem[]>();
const context = useEnvironment<AccountEnvironment>(); const context = useEnvironment<Environment>();
usePromise((signal) => fetchContentJson({ signal, context }), setMenuItems); usePromise((signal) => fetchContentJson({ signal, context }), setMenuItems);
return ( return (
@ -86,7 +83,7 @@ function NavMenuItem({ menuItem }: NavMenuItemProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
environment: { features }, environment: { features },
} = useEnvironment<AccountEnvironment>(); } = useEnvironment<Environment>();
const { pathname } = useLocation(); const { pathname } = useLocation();
const isActive = useMemo( const isActive = useMemo(
() => matchMenuItem(pathname, menuItem), () => matchMenuItem(pathname, menuItem),

View file

@ -1,7 +1,9 @@
import { KeycloakProvider, environment } from "@keycloak/keycloak-ui-shared"; import { KeycloakProvider } from "@keycloak/keycloak-ui-shared";
import { Page, Spinner } from "@patternfly/react-core"; import { Page, Spinner } from "@patternfly/react-core";
import { Suspense } from "react"; import { Suspense } from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import { environment } from "../environment";
import { Header } from "./Header"; import { Header } from "./Header";
import { PageNav } from "./PageNav"; import { PageNav } from "./PageNav";

View file

@ -1,6 +1,7 @@
import { lazy } from "react"; import { lazy } from "react";
import type { IndexRouteObject, RouteObject } from "react-router-dom"; import type { IndexRouteObject, RouteObject } from "react-router-dom";
import { environment } from "@keycloak/keycloak-ui-shared";
import { environment } from "./environment";
import { ErrorPage } from "./root/ErrorPage"; import { ErrorPage } from "./root/ErrorPage";
import { Root } from "./root/Root"; import { Root } from "./root/Root";

View file

@ -124,16 +124,16 @@
<![CDATA[ <![CDATA[
<script id="environment" type="application/json"> <script id="environment" type="application/json">
{ {
"authUrl": "${authUrl}",
"authServerUrl": "${authServerUrl}",
"realm": "${loginRealm!"master"}", "realm": "${loginRealm!"master"}",
"clientId": "${clientId}", "clientId": "${clientId}",
"authServerUrl": "${authServerUrl}",
"authUrl": "${authUrl}",
"consoleBaseUrl": "${consoleBaseUrl}",
"resourceUrl": "${resourceUrl}", "resourceUrl": "${resourceUrl}",
"masterRealm": "${masterRealm}",
"resourceVersion": "${resourceVersion}",
"logo": "${properties.logo!""}", "logo": "${properties.logo!""}",
"logoUrl": "${properties.logoUrl!""}" "logoUrl": "${properties.logoUrl!""}",
"consoleBaseUrl": "${consoleBaseUrl}",
"masterRealm": "${masterRealm}",
"resourceVersion": "${resourceVersion}"
} }
</script> </script>
</body> </body>

View file

@ -1,11 +1,12 @@
import KeycloakAdminClient from "@keycloak/keycloak-admin-client"; import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import { Page } from "@patternfly/react-core";
import { PropsWithChildren, Suspense, useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { import {
mainPageContentId, mainPageContentId,
useEnvironment, useEnvironment,
} from "@keycloak/keycloak-ui-shared"; } from "@keycloak/keycloak-ui-shared";
import { Page } from "@patternfly/react-core";
import { PropsWithChildren, Suspense, useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import { Header } from "./PageHeader"; import { Header } from "./PageHeader";
import { PageNav } from "./PageNav"; import { PageNav } from "./PageNav";
import { AdminClientContext, initAdminClient } from "./admin-client"; import { AdminClientContext, initAdminClient } from "./admin-client";
@ -23,6 +24,7 @@ import { AccessContextProvider } from "./context/access/Access";
import { RealmContextProvider } from "./context/realm-context/RealmContext"; import { RealmContextProvider } from "./context/realm-context/RealmContext";
import { ServerInfoProvider } from "./context/server-info/ServerInfoProvider"; import { ServerInfoProvider } from "./context/server-info/ServerInfoProvider";
import { WhoAmIContextProvider } from "./context/whoami/WhoAmI"; import { WhoAmIContextProvider } from "./context/whoami/WhoAmI";
import type { Environment } from "./environment";
import { SubGroups } from "./groups/SubGroupsContext"; import { SubGroups } from "./groups/SubGroupsContext";
import { AuthWall } from "./root/AuthWall"; import { AuthWall } from "./root/AuthWall";
@ -47,7 +49,7 @@ const AppContexts = ({ children }: PropsWithChildren) => (
); );
export const App = () => { export const App = () => {
const { keycloak, environment } = useEnvironment(); const { keycloak, environment } = useEnvironment<Environment>();
const [adminClient, setAdminClient] = useState<KeycloakAdminClient>(); const [adminClient, setAdminClient] = useState<KeycloakAdminClient>();
useEffect(() => { useEffect(() => {

View file

@ -1,5 +1,7 @@
import { KeycloakProvider, environment } from "@keycloak/keycloak-ui-shared"; import { KeycloakProvider } from "@keycloak/keycloak-ui-shared";
import { App } from "./App"; import { App } from "./App";
import { environment } from "./environment";
export const Root = () => ( export const Root = () => (
<KeycloakProvider environment={environment}> <KeycloakProvider environment={environment}>

View file

@ -3,9 +3,10 @@ import {
createNamedContext, createNamedContext,
useRequiredContext, useRequiredContext,
} from "@keycloak/keycloak-ui-shared"; } from "@keycloak/keycloak-ui-shared";
import { BaseEnvironment } from "@keycloak/keycloak-ui-shared/dist/context/environment";
import type Keycloak from "keycloak-js"; import type Keycloak from "keycloak-js";
import type { Environment } from "./environment";
export type AdminClientProps = { export type AdminClientProps = {
keycloak: Keycloak; keycloak: Keycloak;
adminClient: KeycloakAdminClient; adminClient: KeycloakAdminClient;
@ -19,12 +20,12 @@ export const useAdminClient = () => useRequiredContext(AdminClientContext);
export async function initAdminClient( export async function initAdminClient(
keycloak: Keycloak, keycloak: Keycloak,
environment: BaseEnvironment, environment: Environment,
) { ) {
const adminClient = new KeycloakAdminClient(); const adminClient = new KeycloakAdminClient();
adminClient.setConfig({ realmName: environment.realm }); adminClient.setConfig({ realmName: environment.realm });
adminClient.baseUrl = environment.authUrl; adminClient.baseUrl = environment.authServerUrl;
adminClient.registerTokenProvider({ adminClient.registerTokenProvider({
async getAccessToken() { async getAccessToken() {
try { try {

View file

@ -0,0 +1,33 @@
import {
getInjectedEnvironment,
type BaseEnvironment,
} from "@keycloak/keycloak-ui-shared";
export type Environment = BaseEnvironment & {
/** The URL to the base of the Admin Console. */
consoleBaseUrl: string;
/** The name of the master realm. */
masterRealm: string;
/** The version hash of the auth server. */
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);

View file

@ -1,7 +1,8 @@
import { createInstance } from "i18next"; import { createInstance } from "i18next";
import HttpBackend from "i18next-http-backend"; import HttpBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import { environment } from "@keycloak/keycloak-ui-shared";
import { environment } from "../environment";
import { joinPath } from "../utils/joinPath"; import { joinPath } from "../utils/joinPath";
type KeyValue = { key: string; value: string }; type KeyValue = { key: string; value: string };

View file

@ -1,18 +1,19 @@
import { fetchWithError } from "@keycloak/keycloak-admin-client"; import { fetchWithError } from "@keycloak/keycloak-admin-client";
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import { FormGroup, Title } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { import {
AdminEnvironment,
FormErrorText, FormErrorText,
HelpItem, HelpItem,
TextControl, TextControl,
useEnvironment, useEnvironment,
} from "@keycloak/keycloak-ui-shared"; } from "@keycloak/keycloak-ui-shared";
import { FormGroup, Title } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../../admin-client"; import { useAdminClient } from "../../admin-client";
import { FileUploadForm } from "../../components/json-file-upload/FileUploadForm"; import { FileUploadForm } from "../../components/json-file-upload/FileUploadForm";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import type { Environment } from "../../environment";
import { addTrailingSlash } from "../../util"; import { addTrailingSlash } from "../../util";
import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders"; import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders";
import { DiscoveryEndpointField } from "../component/DiscoveryEndpointField"; import { DiscoveryEndpointField } from "../component/DiscoveryEndpointField";
@ -24,7 +25,7 @@ type FormFields = IdentityProviderRepresentation & {
export const SamlConnectSettings = () => { export const SamlConnectSettings = () => {
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
const { environment } = useEnvironment<AdminEnvironment>(); const { environment } = useEnvironment<Environment>();
const { t } = useTranslation(); const { t } = useTranslation();
const id = "saml"; const id = "saml";

View file

@ -1,13 +1,15 @@
import { FormGroup } from "@patternfly/react-core";
import { useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { import {
HelpItem, HelpItem,
TextControl, TextControl,
useEnvironment, useEnvironment,
} from "@keycloak/keycloak-ui-shared"; } from "@keycloak/keycloak-ui-shared";
import { FormGroup } from "@patternfly/react-core";
import { useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { FormattedLink } from "../../components/external-link/FormattedLink"; import { FormattedLink } from "../../components/external-link/FormattedLink";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import type { Environment } from "../../environment";
import { DisplayOrder } from "../component/DisplayOrder"; import { DisplayOrder } from "../component/DisplayOrder";
import { RedirectUrl } from "../component/RedirectUrl"; import { RedirectUrl } from "../component/RedirectUrl";
@ -22,7 +24,7 @@ export const SamlGeneralSettings = ({
}: SamlGeneralSettingsProps) => { }: SamlGeneralSettingsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { realm } = useRealm(); const { realm } = useRealm();
const { environment } = useEnvironment(); const { environment } = useEnvironment<Environment>();
const { control } = useFormContext(); const { control } = useFormContext();
const alias = useWatch({ control, name: "alias" }); const alias = useWatch({ control, name: "alias" });
@ -54,7 +56,7 @@ export const SamlGeneralSettings = ({
> >
<FormattedLink <FormattedLink
title={t("samlEndpointsLabel")} title={t("samlEndpointsLabel")}
href={`${environment.authUrl}/realms/${realm}/broker/${alias}/endpoint/descriptor`} href={`${environment.authServerUrl}/realms/${realm}/broker/${alias}/endpoint/descriptor`}
isInline isInline
/> />
</FormGroup> </FormGroup>

View file

@ -1,7 +1,7 @@
import { fetchWithError } from "@keycloak/keycloak-admin-client"; import { fetchWithError } from "@keycloak/keycloak-admin-client";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; import { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import { AdminEnvironment, useEnvironment } from "@keycloak/keycloak-ui-shared"; import { useEnvironment } from "@keycloak/keycloak-ui-shared";
import { import {
AlertVariant, AlertVariant,
ButtonVariant, ButtonVariant,
@ -16,6 +16,7 @@ import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAdminClient } from "../admin-client"; import { useAdminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
@ -29,6 +30,7 @@ import { useRealms } from "../context/RealmsContext";
import { useAccess } from "../context/access/Access"; import { useAccess } from "../context/access/Access";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { toDashboard } from "../dashboard/routes/Dashboard"; import { toDashboard } from "../dashboard/routes/Dashboard";
import type { Environment } from "../environment";
import helpUrls from "../help-urls"; import helpUrls from "../help-urls";
import { convertFormValuesToObject, convertToFormValues } from "../util"; import { convertFormValuesToObject, convertToFormValues } from "../util";
import { getAuthorizationHeaders } from "../utils/getAuthorizationHeaders"; import { getAuthorizationHeaders } from "../utils/getAuthorizationHeaders";
@ -74,7 +76,7 @@ const RealmSettingsHeader = ({
refresh, refresh,
}: RealmSettingsHeaderProps) => { }: RealmSettingsHeaderProps) => {
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
const { environment } = useEnvironment<AdminEnvironment>(); const { environment } = useEnvironment<Environment>();
const { t } = useTranslation(); const { t } = useTranslation();
const { refresh: refreshRealms } = useRealms(); const { refresh: refreshRealms } = useRealms();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();

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.authUrl, url: environment.authServerUrl,
realm: environment.realm, realm: environment.realm,
clientId: environment.clientId, clientId: environment.clientId,
}); });

View file

@ -1,109 +1,33 @@
export const DEFAULT_REALM = "master"; /** The base environment variables that are shared between the Admin and Account Consoles. */
export type Feature = {
isRegistrationEmailAsUsername: boolean;
isEditUserNameAllowed: boolean;
isInternationalizationEnabled: boolean;
isLinkedAccountsEnabled: boolean;
isEventsEnabled: boolean;
isMyResourcesEnabled: boolean;
isTotpConfigured: boolean;
deleteAccountAllowed: boolean;
updateEmailFeatureEnabled: boolean;
updateEmailActionEnabled: boolean;
isViewGroupsEnabled: boolean;
isOid4VciEnabled: boolean;
};
export type BaseEnvironment = { export type BaseEnvironment = {
/** The URL to the root of the auth server. */ /**
authUrl: string; * 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 account console. */ * For example, the Keycloak server could be hosted on `auth.example.com` and Admin Console may be hosted on `admin.example.com`.
baseUrl: string; *
/** The realm used to authenticate the user to the Account Console. */ * @see {@link https://www.keycloak.org/server/hostname#_administration_console}
*/
authServerUrl: string;
/** The identifier of the realm used to authenticate the user. */
realm: string; realm: string;
/** The identifier of the client used to authenticate the user to the Account Console. */ /** The identifier of the client used to authenticate the user. */
clientId: string; clientId: string;
/** The URL to resources such as the files in the `public` directory. */ /** The base URL of the resources. */
resourceUrl: string; resourceUrl: string;
/** Indicates the src for the Brand image */ /** The source URL for the the logo image. */
logo: string; logo: string;
/** Indicates the url to be followed when Brand image is clicked */ /** The URL to be followed when the logo is clicked. */
logoUrl: string; logoUrl: string;
}; };
export type AdminEnvironment = BaseEnvironment & {
/** The URL to the root of the auth server. */
authServerUrl: string;
/** The name of the master realm. */
masterRealm: string;
/** The URL to the base of the Admin UI. */
consoleBaseUrl: string;
/** The version hash of the auth server. */
resourceVersion: string;
};
export type AccountEnvironment = BaseEnvironment & {
/** The locale of the user */
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;
};
// 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];
const defaultEnvironment: AdminEnvironment & AccountEnvironment = {
authUrl: "http://localhost:8180",
authServerUrl: "http://localhost:8180",
baseUrl: `http://localhost:8180/realms/${realm ?? DEFAULT_REALM}/account/`,
realm: realm ?? DEFAULT_REALM,
clientId: "security-admin-console-v2",
resourceUrl: "http://localhost:8080",
logo: "/logo.svg",
logoUrl: "/",
locale: "en",
consoleBaseUrl: "/admin/master/console/",
masterRealm: "master",
resourceVersion: "unknown",
features: {
isRegistrationEmailAsUsername: false,
isEditUserNameAllowed: true,
isInternationalizationEnabled: true,
isLinkedAccountsEnabled: true,
isEventsEnabled: true,
isMyResourcesEnabled: true,
isTotpConfigured: true,
deleteAccountAllowed: true,
updateEmailFeatureEnabled: true,
updateEmailActionEnabled: true,
isViewGroupsEnabled: true,
isOid4VciEnabled: false,
},
};
// Merge the default and injected environment variables together.
const environment = {
...defaultEnvironment,
...getInjectedEnvironment(),
};
export { environment };
/** /**
* Extracts the environment variables that are passed if the application is running as a Keycloak theme. * Extracts the environment variables that are passed if the application is running as a Keycloak theme and combines them with the provided defaults.
* These variables are injected by Keycloak into the `index.ftl` as a script tag, the contents of which can be parsed as JSON. * These variables are injected by Keycloak into the `index.ftl` as a script tag, the contents of which can be parsed as JSON.
*
* @argument defaults - The default values to fall to if a value is not present in the environment.
*/ */
function getInjectedEnvironment(): Record<string, string | number | boolean> { export function getInjectedEnvironment<T>(defaults: T): T {
const element = document.getElementById("environment"); const element = document.getElementById("environment");
let env = {} as T;
let env = {} as Record<string, string | number | boolean>;
// Attempt to parse the contents as JSON and return its value. // Attempt to parse the contents as JSON and return its value.
try { try {
@ -115,6 +39,6 @@ function getInjectedEnvironment(): Record<string, string | number | boolean> {
console.error("Unable to parse environment variables."); console.error("Unable to parse environment variables.");
} }
// Otherwise, return an empty record. // Return the merged environment variables with the defaults.
return env; return { ...defaults, ...env };
} }

View file

@ -7,10 +7,8 @@ export {
type KeycloakContext, type KeycloakContext,
} from "./context/KeycloakContext"; } from "./context/KeycloakContext";
export { export {
environment, getInjectedEnvironment,
type AccountEnvironment, type BaseEnvironment,
type AdminEnvironment,
type Feature,
} from "./context/environment"; } from "./context/environment";
export { ContinueCancelModal } from "./continue-cancel/ContinueCancelModal"; export { ContinueCancelModal } from "./continue-cancel/ContinueCancelModal";
export { export {

View file

@ -105,7 +105,10 @@ public class AccountConsole implements AccountResourceProvider {
URI adminBaseUri = session.getContext().getUri(UrlType.ADMIN).getBaseUri(); URI adminBaseUri = session.getContext().getUri(UrlType.ADMIN).getBaseUri();
URI authUrl = uriInfo.getBaseUri(); URI authUrl = uriInfo.getBaseUri();
map.put("authUrl", authUrl.getPath().endsWith("/") ? authUrl : authUrl + "/"); 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);
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);

View file

@ -346,6 +346,7 @@ public class AdminConsole {
} }
map.put("authServerUrl", authServerBaseUrl); 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("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());