8000 Upgrade login UI to PatternFly 6 by edewit · Pull Request #52371 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public enum Feature {

ADMIN_V2("New Admin Console", Type.DEFAULT, 2, Feature.ADMIN_API),

LOGIN_V2("New Login Theme", Type.DEFAULT, 2, FeatureUpdatePolicy.ROLLING_NO_UPGRADE),
LOGIN_V3("Login Theme version 3", Type.DEFAULT, 3, FeatureUpdatePolicy.ROLLING_NO_UPGRADE),

QUICK_THEME("WYSIWYG theme configuration tool", Type.EXPERIMENTAL, 1),

Expand Down Expand Up @@ -358,7 +358,7 @@ public String getLabel() {
private static final Set<String> ESSENTIAL_FEATURES = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(
Feature.HOSTNAME_V2.getUnversionedKey(),
Feature.LOGIN_V2.getUnversionedKey(),
Feature.LOGIN_V3.getUnversionedKey(),
Feature.ROLLING_UPDATES_V2.getUnversionedKey())));

private static final Logger logger = Logger.getLogger(Profile.class);
Expand Down
3 changes: 3 additions & 0 deletions common/src/test/java/org/keycloak/common/ProfileTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ public void testKeys() {
Assertions.assertEquals("account-v3", Profile.Feature.ACCOUNT_V3.getKey());
Assertions.assertEquals("account", Profile.Feature.ACCOUNT_V3.getUnversionedKey());
Assertions.assertEquals("account:v3", Profile.Feature.ACCOUNT_V3.getVersionedKey());
Assertions.assertEquals("login-v3", Profile.Feature.LOGIN_V3.getKey());
Assertions.assertEquals("login", Profile.Feature.LOGIN_V3.getUnversionedKey());
Assertions.assertEquals("login:v3", Profile.Feature.LOGIN_V3.getVersionedKey());
}

@Test
Expand Down
9 changes: 9 additions & 0 deletions docs/documentation/release_notes/topics/27_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ See the upgrading guide for details.

The `UPDATE_CREDENTIAL` events emitted by built-in required actions now carry the `credential_id` detail when a credential was created or updated in the {project_name} credential store, as the `REMOVE_CREDENTIAL` event already did. The detail is omitted when an external user-storage provider handles the update because there is no {project_name} credential ID to report.

= Themes

== Login and welcome UI upgraded to PatternFly 6

The built-in login theme is now `keycloak.v3`, based on PatternFly 6. The previous `keycloak.v2` login theme has been removed.
The built-in welcome theme also uses PatternFly 6.

Realms that explicitly used `keycloak.v2` are migrated to `keycloak.v3` during upgrade. If you maintain a custom login theme based on `keycloak.v2`, or a custom welcome theme based on PatternFly 5, see the link:{upgradingguide_link}[{upgradingguide_name}] for migration steps.

== OID4VP verifier infers the DCQL query from mappers (experimental)

The OID4VP identity provider no longer requires a hand-written DCQL query.
Expand Down
35 changes: 31 additions & 4 deletions docs/documentation/upgrading/topics/changes/changes-27_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,32 @@ General validation also preserves the legacy web origin literals `${authAdminUrl
Trusted Hosts resolves relative logo URLs against the {project_name} frontend URL and checks that host against its allowlist. The client's root URL does not determine where the browser loads a logo.
The pattern executor continues to match literal configured values, including markers and relative URLs, and respects an explicit field selection. Sector identifier URLs in pairwise protocol mappers remain subject to their separate validation.

=== Login theme moved to `keycloak.v3` and PatternFly 6
// https://github.com/keycloak/keycloak/issues/52342

The built-in login experience now has a new versioned theme, `keycloak.v3`, based on PatternFly 6.
The built-in welcome theme also uses PatternFly 6 for its markup and styles.

For new realms, `keycloak.v3` is now the default login theme.
Existing realms that explicitly use `keycloak.v2` are migrated to `keycloak.v3` during upgrade.

The built-in `keycloak.v2` login theme has been removed. If you maintain a custom login theme that extends or copies `keycloak.v2`, migrate it to PatternFly 6 before switching to `keycloak.v3`.
If you maintain a custom welcome theme that copies PatternFly 5 classes from the built-in welcome theme, update those classes to PatternFly 6.

The `login:v2` feature value has also been removed. If you explicitly enable it (for example `--features=login:v2` or `KC_FEATURES=login:v2`), replace it with `login:v3`, or remove the entry because `login:v3` is enabled by default. Servers that still request `login:v2` fail feature validation at startup.

Login pages and macros live in the Base login theme, with CSS classes supplied by theme properties. `keycloak.v3` provides PatternFly 6 styling through `theme.properties` and resources, without duplicating templates. Custom themes can restyle fields and buttons by overriding individual Base macros or a few properties, instead of copying whole pages such as `login.ftl` or `register.ftl`.

To assist with migration, use the new script:

[source,bash]
----
node misc/scripts/migrate-login-theme-pf5-to-pf6.mjs --theme-path /path/to/your/theme
----

Run the script in dry-run mode first, review the generated report, and then rerun with `--apply` when ready.
After migration, validate your customizations manually for light and dark mode, social provider buttons, and custom CSS overrides.

=== OIDC identity providers accept a single issuer
// https://github.com/keycloak/keycloak/issues/54458

Expand Down Expand Up @@ -243,10 +269,11 @@ Providers extending `OAuth2IdentityProviderConfig` can instead override `getClie
The legacy Login v1 theme (`keycloak`) and the `login:v1` feature have been removed.
PatternFly 3 and PatternFly 4 vendor resources that only served that theme are also removed.

Realms and clients that still had the login theme set to `keycloak` are migrated to `keycloak.v2` on upgrade.
Custom themes that extended `keycloak` for the login type must be updated to extend `keycloak.v2` or `base` instead.
Realms and clients that still had the login theme set to `keycloak` are migrated to `keycloak.v3` on upgrade.
Custom themes that extended `keycloak` for the login type must be updated to extend `keycloak.v3` or `base` instead.
The `--features=login:v1` and `--features-disabled=login:v1` options are no longer valid.
The `login` feature itself can no longer be disabled; `{project_name}` always provides the `keycloak.v2` login theme.
The `login` feature itself can no longer be disabled; `{project_name}` always provides the `keycloak.v3` login theme.
See also the earlier note about the login theme move to `keycloak.v3` and PatternFly 6 for the related `keycloak.v2` removal.

=== CIBA requires client authentication (CVE-2026-109082)
// https://github.com/keycloak/keycloak/issues/53937
Expand Down Expand Up @@ -362,7 +389,7 @@ See the link:{securing_apps_token_exchange_link}#_standard-token-exchange-expira

The new Base login theme provides reusable templates and components for current and future login themes, with styling configurable through theme properties.
Custom themes can override individual macros or properties instead of copying complete pages, reducing maintenance and helping them inherit validation, accessibility, and authentication improvements.
The `keycloak.v2` login theme now inherits these shared templates and retains its styling through theme properties.
The `keycloak.v3` login theme inherits these shared templates and supplies PatternFly 6 styling through theme properties.

The previous Base login templates are no longer provided.
Custom login themes with `parent=base` now inherit the new markup.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/ui-customization/themes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ kcLogoIdP-myProvider = fa fa-lock
----

All icons are available on the official website of PatternFly4.
Icons for social providers are already defined in the built-in login theme properties (`themes/keycloak.v2/login/theme.properties`), where you can inspire yourself.
Icons for social providers are already defined in the built-in login theme properties (`themes/keycloak.v3/login/theme.properties`), where you can inspire yourself.

=== Creating a custom HTML template

Expand Down
155 changes: 116 additions & 39 deletions js/apps/admin-ui/src/realm-settings/themes/LoginPreviewWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,161 @@ import { useEnvironment } from "@keycloak/keycloak-ui-shared";
import { Environment } from "../../environment-types";
import { joinPath } from "../../utils/joinPath";
import { usePreviewBackground } from "./BackgroundContext";
import { LoginForm, LoginPage } from "@patternfly/react-core";
import { pf5VarsToPf6Css } from "./pf5ToPf6Tokens";

type LoginPreviewWindowProps = {
cssVars: Record<string, string>;
};

/**
* Renders a static PatternFly 6 login preview that mirrors the keycloak.v3
* FreeMarker markup. Admin UI itself remains on PatternFly React 5, so this
* loads the theme CSS instead of using @patternfly/react-core LoginPage.
*/
export const LoginPreviewWindow = ({ cssVars }: LoginPreviewWindowProps) => {
const { environment } = useEnvironment<Environment>();
const contextLogo = usePreviewLogo();
const contextBackground = usePreviewBackground();

// Resources
const resourceUrlRoot = joinPath(
environment.adminBaseUrl,
"resources",
environment.resourceVersion,
);
const loginResourceUrl = `${resourceUrlRoot}/login/keycloak.v2`;
const loginResourceUrl = `${resourceUrlRoot}/login/keycloak.v3`;
Comment thread
Copilot marked this conversation as resolved.
Resolved
Comment thread
mabartos marked this conversation as resolved.
Resolved
const commonResourceUrl = `${resourceUrlRoot}/common/keycloak`;

// Default login theme resources from local files
const defaultBgImage = `${loginResourceUrl}/img/keycloak-bg-darken.svg`;
const defaultLogo = `${loginResourceUrl}/img/keycloak-logo-text.svg`;

// Use uploaded images or fall back to local defaults
// Both logo and background come from context for immediate reactivity
const logoUrl = contextLogo?.logo || defaultLogo;
const bgUrl = contextBackground?.background || defaultBgImage;

// Logo dimensions are theme CSS custom properties, not PatternFly tokens.
const logoWidth = cssVars["logoWidth"];
const logoHeight = cssVars["logoHeight"];

const stylesThemeCssUrl = `${loginResourceUrl}/css/styles.css`;
const themeCssVars = Object.fromEntries(
Object.entries(cssVars).filter(
([key]) => key !== "logoWidth" && key !== "logoHeight",
Comment thread
mabartos marked this conversation as resolved.
Resolved
),
);

return (
<>
<link rel="stylesheet" href={stylesThemeCssUrl} />
<link
rel="stylesheet"
href={`${commonResourceUrl}/vendor/patternfly-v6/patternfly.min.css`}
/>
<link
rel="stylesheet"
href={`${commonResourceUrl}/vendor/patternfly-v6/patternfly-addons.css`}
/>
<link rel="stylesheet" href={`${loginResourceUrl}/css/styles.css`} />
<style>{`
.login-preview {
${Object.entries(cssVars)
.map(([key, value]) => `--pf-v5-global--${key}: ${value};`)
.join("\n")}
${pf5VarsToPf6Css(themeCssVars)}
/* Keycloak login theme variables - override with local/uploaded images */
--keycloak-logo-url: url('${logoUrl}');
--keycloak-bg-logo-url: url('${bgUrl}');
${logoHeight ? `--keycloak-logo-height: ${logoHeight};` : ""}
${logoWidth ? `--keycloak-logo-width: ${logoWidth};` : ""}
}
/* Apply background to #keycloak-bg */
.login-preview {
background: var(--keycloak-bg-logo-url);
/* Theme CSS targets "login-pf body"; preview is not a document body. */
background: var(--keycloak-bg-logo-url) no-repeat center center;
background-size: cover;
}
.login-preview .kc-logo-text {
position: absolute;
position-anchor: --logo;
position-area: start center;
margin-bottom: 3rem;
}
.login-preview .pf-v5-c-login__main {
anchor-name: --logo;
}
.login-preview .pf-v5-c-login__footer {
display: none;
min-height: 40rem;
padding: 2rem 1rem;
}
`}</style>
<div className="login-preview login-pf">
<div className="kc-logo-text">
<span>Keycloak</span>
<div className="login-preview login-pf" data-testid="login-page-preview">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be done in a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the toggle is global and because the admin console itself is not yet using pf6 we'll always have this problem that either one doesn't work

<div className="pf-v6-c-login">
<div className="pf-v6-c-login__container">
<header id="kc-header" className="pf-v6-c-login__header">
<div id="kc-header-wrapper" className="pf-v6-c-brand">
<div className="kc-logo-text">
<span>Keycloak</span>
</div>
</div>
</header>
<main className="pf-v6-c-login__main">
<div className="pf-v6-c-login__main-header">
<h1 className="pf-v6-c-title pf-m-3xl" id="kc-page-title">
Sign in to your account
</h1>
</div>
<div className="pf-v6-c-login__main-body">
<div id="kc-form">
<div id="kc-form-wrapper">
<form
id="kc-form-login"
className="pf-v6-c-form pf-v6-u-w-100"
onSubmit={(event) => event.preventDefault()}
>
<div className="pf-v6-c-form__group">
<div className="pf-v6-c-form__group-label pf-v6-u-pb-xs">
<label
htmlFor="username"
className="pf-v6-c-form__label"
>
<span className="pf-v6-c-form__label-text">
Username or email
</span>
</label>
</div>
<span className="pf-v6-c-form-control">
<input
id="username"
name="username"
type="text"
autoComplete="username"
readOnly
/>
</span>
</div>
<div className="pf-v6-c-form__group">
<div className="pf-v6-c-form__group-label pf-v6-u-pb-xs">
<label
htmlFor="password"
className="pf-v6-c-form__label"
>
<span className="pf-v6-c-form__label-text">
Password
</span>
</label>
</div>
<div className="pf-v6-c-input-group">
<div className="pf-v6-c-input-group__item pf-m-fill">
<span className="pf-v6-c-form-control">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
readOnly
/>
</span>
</div>
</div>
</div>
<div className="pf-v6-c-form__group">
<div className="pf-v6-c-form__actions pf-v6-u-pt-xs pf-v6-u-flex-wrap">
<button
className="pf-v6-c-button pf-m-primary pf-m-block"
id="kc-login"
name="login"
type="submit"
>
Sign In
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</main>
</div>
</div>
<LoginPage loginTitle="Sign in to your account">
<LoginForm
usernameLabel="Username or email"
loginButtonLabel="Sign In"
/>
</LoginPage>
</div>
</>
);
E 10BC8 xpand Down
18 changes: 9 additions & 9 deletions js/apps/admin-ui/src/realm-settings/themes/QuickTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LogoContext } from "./LogoContext";
import { ThemeColors } from "./ThemeColors";
import { BackgroundContext } from "./BackgroundContext";
import type { Environment } from "../../environment-types";
import { pf5VarsToPf5Css, pf5VarsToPf6Css } from "./pf5ToPf6Tokens";

export type ThemeRealmRepresentation = RealmRepresentation & {
themeName?: string;
Expand Down Expand Up @@ -104,7 +105,7 @@ styles=css/theme-styles.css
zip.file(
`theme/${themeNameClean}/login/theme.properties`,
`
parent=keycloak.v2
parent=keycloak.v3
Comment thread
Copilot marked this conversation as resolved.
Resolved
import=common/${themeNameClean}
styles=css/styles.css css/theme-styles.css
Expand Down Expand Up @@ -142,18 +143,13 @@ styles=css/styles.css css/theme-styles.css
}),
);

const toCss = (obj?: object) =>
Object.entries(obj || {})
.map(([key, value]) => `--pf-v5-global--${key}: ${value};`)
.join("\n");

const loginCss = (
await fetch(
joinPath(
environment.adminBaseUrl,
"resources",
environment.resourceVersion,
"/login/keycloak.v2/css/styles.css",
"/login/keycloak.v3/css/styles.css",
),
)
).text();
Expand All @@ -169,10 +165,14 @@ styles=css/styles.css css/theme-styles.css
${logo ? `--keycloak-logo-url: url('../${logoName}');` : ""}
--keycloak-logo-height: ${realm.logoHeight};
--keycloak-logo-width: ${realm.logoWidth};
${toCss(styles.light)}
${pf5VarsToPf5Css(styles.light)}
${pf5VarsToPf6Css(styles.light)}
}
.pf-v5-theme-dark {
${toCss(styles.dark)}
${pf5VarsToPf5Css(styles.dark)}
}
.pf-v6-theme-dark {
${pf5VarsToPf6Css(styles.dark)}
}
`,
);
Expand Down
Loading
Loading
0