From 65d0d3f1719858ef30016883367a74dd311fc6a0 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 07:16:39 +0000 Subject: [PATCH 01/11] docs: deprecate SSO Kit and add a Spring Security migration guide SSO Kit isn't available in Vaadin 26, so every application using it has to move to Spring Security's OpenID Connect support before upgrading. There was no documentation for that move. Marks every SSO Kit page as deprecated using the same banner and admonition pattern as the other deprecated tools, and adds a migration guide that maps each kit feature to its replacement, gives the security configuration that replaces the auto-configuration, and documents the features that have no drop-in replacement. --- articles/tools/sso/getting-started/hilla.adoc | 8 +- articles/tools/sso/getting-started/index.adoc | 8 +- articles/tools/sso/index.adoc | 22 +- articles/tools/sso/integrations/azure-ad.adoc | 8 +- articles/tools/sso/integrations/index.adoc | 8 +- articles/tools/sso/integrations/keycloak.adoc | 8 +- articles/tools/sso/integrations/okta.adoc | 8 +- .../sso/migrating-to-spring-security.adoc | 595 ++++++++++++++++++ articles/tools/sso/theming.adoc | 8 +- 9 files changed, 663 insertions(+), 10 deletions(-) create mode 100644 articles/tools/sso/migrating-to-spring-security.adoc diff --git a/articles/tools/sso/getting-started/hilla.adoc b/articles/tools/sso/getting-started/hilla.adoc index 9af3a9e3b8..fe1896fc73 100644 --- a/articles/tools/sso/getting-started/hilla.adoc +++ b/articles/tools/sso/getting-started/hilla.adoc @@ -4,12 +4,18 @@ page-title: Getting Started with Hilla and Vaadin SSO Kit description: Step-by-step guide on how to use SSO Kit in a Hilla application. meta-description: Set up Single Sign-On (SSO) in your Vaadin Hilla applications with this comprehensive guide. order: 100 +banner: SSO Kit is deprecated. Use link:../../sso/migrating-to-spring-security[Spring Security] instead. +banner-id: sso-kit-deprecated +banner-style: caution --- -= Getting Started with SSO Kit (Hilla) += [deprecated:com.vaadin:vaadin@V25.3]#Getting Started with SSO Kit (Hilla)# :hilla-react: +:admonition: +include::../index.adoc[tag=deprecation-notice] + .Instructions for Hilla [NOTE] This page guides you in getting started with SSO Kit and Hilla. See the guide for <>. diff --git a/articles/tools/sso/getting-started/index.adoc b/articles/tools/sso/getting-started/index.adoc index 7a36bf8a9f..20676735de 100644 --- a/articles/tools/sso/getting-started/index.adoc +++ b/articles/tools/sso/getting-started/index.adoc @@ -6,10 +6,16 @@ tab-title: Flow description: Step-by-step guide on how to use SSO Kit in a Vaadin Flow application. meta-description: Get started with Single Sign-On (SSO) integration in your Vaadin Flow applications. order: 10 +banner: SSO Kit is deprecated. Use link:../sso/migrating-to-spring-security[Spring Security] instead. +banner-id: sso-kit-deprecated +banner-style: caution --- -= Getting Started with SSO Kit (Flow) += [deprecated:com.vaadin:vaadin@V25.3]#Getting Started with SSO Kit (Flow)# + +:admonition: +include::../index.adoc[tag=deprecation-notice] .Instructions for Vaadin Flow [NOTE] diff --git a/articles/tools/sso/index.adoc b/articles/tools/sso/index.adoc index 44f38d32a8..7e8d430806 100644 --- a/articles/tools/sso/index.adoc +++ b/articles/tools/sso/index.adoc @@ -1,19 +1,35 @@ --- -title: SSO Kit +title: SSO Kit (deprecated) page-title: How to configure single sign-on (SSO) in Vaadin description: Integrate Vaadin applications with third-party identity providers, and allow your users to benefit from single sign-on capabilities. -meta-description: Learn to set up single sign-on (SSO) for seamless authentication in Vaadin applications for a better user experience. +meta-description: SSO Kit is deprecated and isn't available in Vaadin 26. Use Spring Security's OpenID Connect support instead. section-nav: commercial order: 90 +banner: SSO Kit is deprecated. Use link:./sso/migrating-to-spring-security[Spring Security] instead. +banner-id: sso-kit-deprecated +banner-style: caution --- -= SSO Kit += [deprecated:com.vaadin:vaadin@V25.3]#SSO Kit# :commercial-feature: SSO Kit :kit-start-trial: true include::{articles}/_commercial-banner.adoc[opts=optional] +// tag::deprecation-notice[] +ifdef::admonition[] +[WARNING] +==== +endif::admonition[] +SSO Kit is deprecated and isn't available in Vaadin 26. All of its major features are now available directly in Spring Security and in Vaadin's Spring Security integration. + +See <<{articles}/tools/sso/migrating-to-spring-security#,Migrating from SSO Kit to Spring Security>> for how to move an existing application, and which features you have to build yourself. +ifdef::admonition[] +==== +endif::admonition[] +// end::deprecation-notice[] + SSO Kit provides all of the configuration needed to add single sign-on capabilities to your Vaadin applications, based on https://openid.net/connect/[OpenID Connect]. However, it depends on Spring Boot and Spring Security. The following identity providers are supported: diff --git a/articles/tools/sso/integrations/azure-ad.adoc b/articles/tools/sso/integrations/azure-ad.adoc index 9fcadab334..e10417f339 100644 --- a/articles/tools/sso/integrations/azure-ad.adoc +++ b/articles/tools/sso/integrations/azure-ad.adoc @@ -5,10 +5,16 @@ description: How to use SSO Kit with Azure Active Directory (Microsoft Entra ID) meta-description: Learn how to configure Azure AD for secure single sign-on (SSO) in your Vaadin applications. page-links: - https://azure.microsoft.com[Azure website] +banner: SSO Kit is deprecated. Use link:../../sso/migrating-to-spring-security[Spring Security] instead. +banner-id: sso-kit-deprecated +banner-style: caution --- -= Azure AD Integration += [deprecated:com.vaadin:vaadin@V25.3]#Azure AD Integration# + +:admonition: +include::../index.adoc[tag=deprecation-notice] Azure Active Directory (now renamed to https://azure.microsoft.com/en-us/updates/azure-ad-is-becoming-microsoft-entra-id/[Microsoft Entra ID]) is a commercial identification and access management solution. This tutorial shows how to restrict access to a Vaadin view by using SSO Kit together with Azure Active Directory. diff --git a/articles/tools/sso/integrations/index.adoc b/articles/tools/sso/integrations/index.adoc index cf7d1ed558..25d4adad6a 100644 --- a/articles/tools/sso/integrations/index.adoc +++ b/articles/tools/sso/integrations/index.adoc @@ -4,8 +4,14 @@ page-title: Integrating Vaadin apps with SSO providers description: How to use SSO Kit with vendor-specific tutorials. meta-description: Learn to set up single sign-on (SSO) for secure and seamless authentication in Vaadin applications. order: 30 +banner: SSO Kit is deprecated. Use link:../sso/migrating-to-spring-security[Spring Security] instead. +banner-id: sso-kit-deprecated +banner-style: caution --- -= SSO Kit Integrations += [deprecated:com.vaadin:vaadin@V25.3]#SSO Kit Integrations# + +:admonition: +include::../index.adoc[tag=deprecation-notice] section_outline::[] diff --git a/articles/tools/sso/integrations/keycloak.adoc b/articles/tools/sso/integrations/keycloak.adoc index 5a0e8f63a2..9b621f568f 100644 --- a/articles/tools/sso/integrations/keycloak.adoc +++ b/articles/tools/sso/integrations/keycloak.adoc @@ -5,10 +5,16 @@ description: Keycloak is an open-source identity and access management solution. meta-description: Learn how to set up and configure Keycloak Single Sign-On (SSO) integration in your Vaadin applications for secure authentication. page-links: - https://www.keycloak.org[Website] +banner: SSO Kit is deprecated. Use link:../../sso/migrating-to-spring-security[Spring Security] instead. +banner-id: sso-kit-deprecated +banner-style: caution --- -= Keycloak Integration += [deprecated:com.vaadin:vaadin@V25.3]#Keycloak Integration# + +:admonition: +include::../index.adoc[tag=deprecation-notice] This demo application showcases a minimal setup of SSO Kit and Keycloak. It consists of two views: a _public view_, which is accessible to every user; and a _private view_, which is protected by the `@PermitAll` security annotation and therefore only accessible by authenticated users. diff --git a/articles/tools/sso/integrations/okta.adoc b/articles/tools/sso/integrations/okta.adoc index 639af5a36d..c6164507ad 100644 --- a/articles/tools/sso/integrations/okta.adoc +++ b/articles/tools/sso/integrations/okta.adoc @@ -5,10 +5,16 @@ description: Okta is a commercial identity and access management solution. This meta-description: Learn how to set up Okta SSO for secure authentication in your Vaadin applications. Streamline user access with this step-by-step guide. page-links: - https://www.okta.com[Okta website] +banner: SSO Kit is deprecated. Use link:../../sso/migrating-to-spring-security[Spring Security] instead. +banner-id: sso-kit-deprecated +banner-style: caution --- -= Okta Integration += [deprecated:com.vaadin:vaadin@V25.3]#Okta Integration# + +:admonition: +include::../index.adoc[tag=deprecation-notice] This tutorial shows how to restrict access to a Vaadin view by using SSO Kit together with Okta. diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc new file mode 100644 index 0000000000..272e3b2eed --- /dev/null +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -0,0 +1,595 @@ +--- +title: Migrating to Spring Security +page-title: Migrating from Vaadin SSO Kit to Spring Security +description: How to replace SSO Kit with Spring Security's built-in OAuth2 and OpenID Connect support. +meta-description: Map every SSO Kit feature to its Spring Security equivalent, and learn which pieces you have to build yourself before upgrading to Vaadin 26. +order: 4 +--- + + += [since:com.vaadin:vaadin@V25.3]#Migrating from SSO Kit to Spring Security# + +SSO Kit never implemented OpenID Connect itself. It's an auto-configuration layer: it reads a handful of `vaadin.sso.*` properties, assembles a Spring Security filter chain from them, and fills the gaps that Spring Security and Vaadin's Spring integration had when the kit was released with V23.2. + +Those gaps have since closed. Spring Security has built-in OpenID Connect Back-Channel Logout, and Vaadin's [classname]`VaadinSecurityConfigurer` configures OAuth2 login, RP-Initiated Logout, and UIDL-aware redirects for Vaadin applications. What's left of SSO Kit is mostly configuration that you can now write yourself in about twenty lines -- plus a few smaller features that have no direct replacement. + +SSO Kit is deprecated and isn't available in Vaadin 26. Every application using it has to migrate before that upgrade. + +This guide maps each SSO Kit feature to its replacement, gives the configuration to replace the auto-configuration, and is explicit about what you have to build yourself. + + +== Before Migrating + +The migration is mostly subtraction. Read this section first to see what actually changes and to scope the work. + + +=== What Changes -- And What Doesn't + +*Provider configuration doesn't change.* Everything under `spring.security.oauth2.client.provider` and `spring.security.oauth2.client.registration` is Spring Security configuration that SSO Kit only consumed. Issuer URI, client ID, client secret, and scopes stay exactly as they are, and so does the client registered at Keycloak, Okta, or Microsoft Entra ID. + +*Auto-configuration becomes an explicit filter chain.* [classname]`SingleSignOnConfiguration` is replaced by a [classname]`SecurityFilterChain` bean in the application. The `vaadin.sso.*` (or `hilla.sso.*`) properties disappear, and their values move into that bean as method arguments. + +*The Flow API you call every day is unaffected.* [classname]`AuthenticationContext`, [methodname]`getAuthenticatedUser()`, and [methodname]`logout()` are part of Vaadin's Spring integration, not of SSO Kit. Views that inject [classname]`AuthenticationContext` need no change at all. The same is true for `@PermitAll`, `@RolesAllowed`, and `@AnonymousAllowed` on views and services. + +*The commercial license requirement goes away.* SSO Kit is a commercial add-on with a runtime license check. Spring Security's OAuth2 client and Vaadin's Spring Security integration are both open source, so the license, the build-time key, and the license check on startup all become unnecessary. + + +=== Scope the Work + +Three searches tell you how much of the kit an application actually uses: + +- `vaadin.sso.` and `hilla.sso.` in configuration files. Each property maps to a line of configuration below. +- `com.vaadin.sso` and `com.vaadin.hilla.sso` in Java imports. Only [classname]`SingleSignOnContext`, [classname]`UserLogoutEvent`, and the two UIDL strategies are commonly imported directly; anything else is an internal detail of the auto-configuration. +- `@vaadin/sso-kit-client-` in TypeScript imports. This is the part of the migration that costs real work, and it applies only to Hilla applications. + +An application that adds `sso-kit-starter`, sets an issuer URI and a login route, and uses [classname]`AuthenticationContext` in its views migrates in a single commit. One that uses back-channel logout notifications in a Hilla frontend has more to do -- see <<#gaps,What You Have to Build Yourself>>. + + +[[feature-mapping]] +== Feature Mapping + +|=== +|SSO Kit |Replacement + +|`com.vaadin:sso-kit-starter` +|`org.springframework.boot:spring-boot-starter-oauth2-client` + +|[classname]`SingleSignOnConfiguration` auto-configuration +|Your own [classname]`SecurityFilterChain` bean + +|`vaadin.sso.login-route` +|First argument of [methodname]`oauth2LoginPage()` + +|`vaadin.sso.logout-redirect-route` +|Second argument of [methodname]`oauth2LoginPage()` + +|`vaadin.sso.back-channel-logout` +|[methodname]`http.oidcLogout()` with [methodname]`backChannel()` + +|`vaadin.sso.back-channel-logout-route` +|Fixed at `/logout/connect/back-channel/{registrationId}` + +|`vaadin.sso.maximum-concurrent-sessions` +|[methodname]`sessionManagement()` with [methodname]`sessionConcurrency()` + +|`vaadin.sso.keycloak-roles` +|A [interfacename]`GrantedAuthoritiesMapper` you write -- see <<#gap-keycloak-roles,Keycloak Role Mapping>> + +|[classname]`AuthenticationContext`, [methodname]`logout()` +|Unchanged; both are Vaadin Flow API + +|[classname]`com.vaadin.sso.starter.UidlRedirectStrategy` +|[classname]`com.vaadin.flow.spring.security.UidlRedirectStrategy`, applied automatically + +|[classname]`UidlExpiredSessionStrategy` +|A strategy you write -- see <<#gap-session-expiration,Vaadin-Aware Session Expiration>> + +|[classname]`UserLogoutEvent` +|Vaadin's [classname]`SessionDestroyEvent` + +|[classname]`KeycloakUserMapper` +|See <<#gap-keycloak-roles,Keycloak Role Mapping>> + +|`sso-kit-keycloak-lumo` theme +|No replacement -- see <<#gap-keycloak-theme,Keycloak Login Theme>> + +|`@vaadin/sso-kit-client-react` +|`@vaadin/hilla-react-auth` + +|`@vaadin/sso-kit-client-lit` +|No replacement -- see <<#gap-lit,Hilla Lit Client>> + +|[classname]`SingleSignOnEndpoint`, [classname]`UserEndpoint` +|A browser-callable service you write + +|[classname]`BackChannelLogoutEndpoint`, [methodname]`onBackChannelLogout()` +|No replacement -- see <<#gap-logout-notification,Client-Side Logout Notification>> +|=== + + +== Migrating a Flow Application + +The five steps below cover a Flow application. Steps 4 to 6 are conditional: skip them if the corresponding `vaadin.sso.*` property was never set. + + +=== Step 1: Replace the Dependency + +Remove the SSO Kit starter and add Spring Boot's OAuth2 client starter: + +[.example] +-- +.[filename]`pom.xml` +[source,xml] +---- + + + + org.springframework.boot + spring-boot-starter-oauth2-client + +---- +.[filename]`build.gradle` +[source,groovy] +---- +implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' +---- +-- + +If the project has a Vaadin subscription key or license file used only for SSO Kit, it's no longer needed for authentication. + + +=== Step 2: Keep the Provider Configuration + +Leave every `spring.security.oauth2.client.*` property untouched. Remove only the `vaadin.sso.*` block: + +[.example] +-- +.[filename]`application.properties` +[source,properties] +---- +# Keep as is: +spring.security.oauth2.client.provider.keycloak.issuer-uri=https://my-keycloak.io/realms/my-realm +spring.security.oauth2.client.registration.keycloak.client-id=my-client +spring.security.oauth2.client.registration.keycloak.client-secret=very-secret-value +spring.security.oauth2.client.registration.keycloak.scope=profile,openid,email,roles + +# Remove: +# vaadin.sso.login-route=/oauth2/authorization/keycloak +# vaadin.sso.logout-redirect-route=/logout-successful +---- +.[filename]`application.yaml` +[source,yaml] +---- +spring: + security: + oauth2: + client: + provider: + keycloak: + issuer-uri: https://my-keycloak.io/realms/my-realm + registration: + keycloak: + client-id: my-client + client-secret: very-secret-value + scope: + - profile + - openid + - email + - roles +---- +-- + +The values of the removed properties are still needed. They become arguments in the next step. + + +=== Step 3: Add a Security Configuration + +Replace the auto-configuration with an explicit [classname]`SecurityFilterChain`. The two arguments of [methodname]`oauth2LoginPage()` are the former `login-route` and `logout-redirect-route`: + +.[filename]`SecurityConfig.java` +[source,java] +---- +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +import static com.vaadin.flow.spring.security.VaadinSecurityConfigurer.vaadin; + +@EnableWebSecurity +@Configuration +class SecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.with(vaadin(), configurer -> configurer.oauth2LoginPage( + "/oauth2/authorization/keycloak", // <1> + "{baseUrl}" // <2> + )); + return http.build(); + } +} +---- +<1> The former `vaadin.sso.login-route`. Pointing it at `/oauth2/authorization/{registrationId}` sends users straight to the provider; pointing it at a route of your own shows a login view first. +<2> The former `vaadin.sso.logout-redirect-route`. It defaults to `{baseUrl}`, the same default the kit had, and supports the `{baseScheme}`, `{baseHost}`, `{basePort}`, `{basePath}`, and `{baseUrl}` template variables. + +This single call covers what took three pieces of configuration in the kit: + +- OAuth2 login against every client registration in the application configuration. +- RP-Initiated Logout. [methodname]`AuthenticationContext.logout()` continues to work and still ends the provider session, because the configurer installs an [classname]`OidcClientInitiatedLogoutSuccessHandler` when a post-logout redirect URI is given. +- UIDL-aware redirects. Vaadin's own [classname]`UidlRedirectStrategy` is attached to that handler, so logging out from inside a view redirects the browser instead of sending a redirect into a UIDL response. + +For the full set of options, see <<{articles}/flow/integrations/spring/oauth2#,OAuth2 Authentication>> and <<{articles}/flow/security/vaadin-security-configurer#,Vaadin Security Configurer>>. + +.Views Need No Changes +[NOTE] +Injecting [classname]`AuthenticationContext` into a view, calling [methodname]`getAuthenticatedUser(OidcUser.class)`, and annotating views with `@PermitAll` or `@RolesAllowed` all keep working unchanged. Those are Vaadin Flow APIs, not SSO Kit APIs. + + +=== Step 4: Enable Back-Channel Logout + +Skip this step if the provider was never configured to send back-channel logout requests. + +Spring Security implements https://openid.net/specs/openid-connect-backchannel-1_0.html[Back-Channel Logout] natively. Enable it on the same filter chain: + +.[filename]`SecurityConfig.java` +[source,java] +---- +import org.springframework.security.config.Customizer; + +@Bean +SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.with(vaadin(), configurer -> configurer.oauth2LoginPage( + "/oauth2/authorization/keycloak", "{baseUrl}")); + http.oidcLogout(oidc -> oidc.backChannel(Customizer.withDefaults())); + return http.build(); +} +---- + +No session registry bean is required: Spring Security registers an in-memory [classname]`OidcSessionRegistry` and the login strategy that populates it. + +*Check the URL registered at the provider.* Spring Security listens on `/logout/connect/back-channel/{registrationId}`. Applications that were already on SSO Kit 3.1 or later, with `vaadin.sso.back-channel-logout` unset, are on this URL already and need no change at the provider. Applications that enabled the kit's own implementation with `vaadin.sso.back-channel-logout=true` used `/logout/back-channel/{registrationId}` instead, and the provider's client configuration has to be updated to the new path. + +*Reacting to a logout.* The kit published a [classname]`UserLogoutEvent` from its own filter. Spring Security has no equivalent event, but it invalidates the HTTP session, which makes Vaadin fire a [classname]`SessionDestroyEvent`. Listen for that instead: + +[source,java] +---- +@Bean +VaadinServiceInitListener logoutListener() { + return serviceInitEvent -> serviceInitEvent.getSource() + .addSessionDestroyListener(sessionDestroyEvent -> { + // Clean up per-session resources here. + }); +} +---- + +.Behind a Reverse Proxy +[NOTE] +Spring Security completes a back-channel logout by calling its own logout endpoint over HTTP, using the URI template `{baseUrl}/logout/connect/back-channel/{registrationId}`. If the application can't resolve its own external base URL -- typically behind a TLS-terminating proxy without forwarded-header handling -- set an explicit internal address with `oidc.backChannel(backChannel -> backChannel.logoutUri("http://localhost:8080/logout/connect/back-channel/{registrationId}"))`. + + +=== Step 5: Restore Concurrent Session Control + +Skip this step if `vaadin.sso.maximum-concurrent-sessions` was never set. + +Session concurrency is standard Spring Security, but the strategy that runs when a session is forced out has to be Vaadin-aware. See <<#gap-session-expiration,Vaadin-Aware Session Expiration>> for the strategy class, then wire it up: + +.[filename]`SecurityConfig.java` +[source,java] +---- +http.sessionManagement(sessionManagement -> sessionManagement + .sessionConcurrency(concurrency -> concurrency + .maximumSessions(1) // <1> + .expiredSessionStrategy(new UidlExpiredSessionStrategy()))); // <2> +---- +<1> The former `vaadin.sso.maximum-concurrent-sessions`. The default, `-1`, means unlimited. +<2> Without this, an expired session breaks the Vaadin client instead of reloading it. + + +=== Step 6: Restore Keycloak Role Mapping + +Skip this step if `vaadin.sso.keycloak-roles` was never set to `true`. Otherwise see <<#gap-keycloak-roles,Keycloak Role Mapping>>, which has to be built. + + +== Migrating a Hilla Application + +A Hilla application migrates its backend exactly as above, using `hilla.sso.*` as the source of the values instead of `vaadin.sso.*`, and removing `com.vaadin.hilla:sso-kit-starter` in Step 1. The `com.vaadin.hilla.sso.starter` package also has to be removed from the `` list of the Hilla Maven plugin, because its endpoints no longer exist. + +The frontend is where the real work is. SSO Kit shipped three generated endpoints and a React context on top of them; the replacement is Hilla's own authentication support plus a service you write. + + +=== Replace the Client Dependency + +[source,bash] +---- +npm uninstall @vaadin/sso-kit-client-react +npm install @vaadin/hilla-react-auth +---- + + +[[expose-the-user]] +=== Expose the User + +SSO Kit's [classname]`UserEndpoint` returned a `User` object with the standard OpenID Connect claims. Replace it with a browser-callable service that returns the claims your views actually use: + +.[filename]`UserInfoService.java` +[source,java] +---- +@AnonymousAllowed +@BrowserCallable +public class UserInfoService { + + public record UserInfo(String name, String email, List roles) { + } + + public Optional getUserInfo() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getPrincipal) + .filter(OidcUser.class::isInstance) + .map(OidcUser.class::cast) + .map(user -> new UserInfo(user.getFullName(), user.getEmail(), + user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .filter(authority -> authority.startsWith("ROLE_")) + .map(authority -> authority.substring(5)) + .toList())); + } +} +---- + + +=== Replace the SSO Context + +`configureAuth()` replaces [classname]`SsoProvider` and [methodname]`useSsoContext()`: + +.[filename]`frontend/security/auth.ts` +[source,typescript] +---- +import { configureAuth } from '@vaadin/hilla-react-auth'; +import { UserInfoService } from 'Frontend/generated/endpoints'; + +const auth = configureAuth(UserInfoService.getUserInfo, { + getRoles: (userInfo) => userInfo.roles // <1> +}); + +export const useAuth = auth.useAuth; +export const AuthProvider = auth.AuthProvider; +---- +<1> Replaces [methodname]`isUserInRole()`; roles reach `ViewConfig.rolesAllowed` through this function. + +Then swap the calls in your components: + +- [methodname]`useSsoContext()` becomes [methodname]`useAuth()`. +- `authenticated` becomes `state.user !== undefined`. +- [methodname]`logout()` is provided by [methodname]`useAuth()` and needs no `logoutUrl`. +- [methodname]`login()` has no equivalent, because it was only a redirect. Navigate to the provider directly: `window.location.href = '/oauth2/authorization/keycloak'`. + +See <<{articles}/building-apps/react/security#,Security>> for the full setup, including where to wrap the application in ``. + + +=== Replace Route Protection + +[methodname]`protectRoutes()` and the `requireAuthentication` route property are replaced by `ViewConfig`, which Hilla's file-based router reads: + +.[filename]`frontend/views/profile.tsx` +[source,tsx] +---- +export const config: ViewConfig = { + loginRequired: true, + rolesAllowed: ['ADMIN'] // Optional; replaces isUserInRole checks in the route. +}; +---- + +Unauthenticated users are redirected to the configured login view rather than to the kit's `/ssologin` route. + + +[[gaps]] +== What You Have to Build Yourself + +These are the features with no drop-in replacement. The first two are small and mechanical. The last three are genuine gaps. + + +[[gap-session-expiration]] +=== Vaadin-Aware Session Expiration + +*Affects:* applications that set `vaadin.sso.maximum-concurrent-sessions` or `hilla.sso.maximum-concurrent-sessions`. + +When session concurrency forces an older session out, Spring Security's default strategy writes a plain-text message to the response. For a UIDL request that response is meaningless to the Vaadin client, which then appears to hang. The kit installed a strategy that answers framework-internal requests with the `Vaadin-Refresh` token instead, so the client reloads. Spring Security can't know about that protocol, and Vaadin's Spring integration doesn't ship an equivalent, so the class has to live in the application: + +.[filename]`UidlExpiredSessionStrategy.java` +[source,java] +---- +public class UidlExpiredSessionStrategy implements SessionInformationExpiredStrategy { + + @Override + public void onExpiredSessionDetected(SessionInformationExpiredEvent event) + throws IOException { + var request = event.getRequest(); + var response = event.getResponse(); + var redirectRoute = request.getContextPath() + "/"; + var servletMapping = request.getHttpServletMapping().getPattern(); + if (HandlerHelper.isFrameworkInternalRequest(servletMapping, request)) { + response.getWriter().write("Vaadin-Refresh: " + redirectRoute); + } else { + response.sendRedirect(redirectRoute); + } + } +} +---- + +Wire it into [methodname]`sessionConcurrency()` as shown in Step 5. + + +[[gap-keycloak-roles]] +=== Keycloak Role Mapping + +*Affects:* applications that set `vaadin.sso.keycloak-roles=true` or `hilla.sso.keycloak-roles=true`. + +Keycloak puts realm roles in a `realm_access` claim and client roles in `resource_access`, neither of which is part of the OpenID Connect specification. Spring Security therefore maps neither, and `@RolesAllowed` sees no roles after the migration unless you add the mapping. + +The straightforward route is a [interfacename]`GrantedAuthoritiesMapper` bean that reads the claim and adds `ROLE_` authorities: + +.[filename]`SecurityConfig.java` +[source,java] +---- +@Bean +GrantedAuthoritiesMapper keycloakAuthoritiesMapper() { + return authorities -> { + var mapped = new LinkedHashSet(authorities); + authorities.stream() + .filter(OidcUserAuthority.class::isInstance) + .map(OidcUserAuthority.class::cast) + .map(authority -> authority.getIdToken().getClaimAsMap("realm_access")) + .filter(Objects::nonNull) + .forEach(realmAccess -> { + var roles = (Collection) realmAccess.get("roles"); + if (roles != null) { + roles.forEach(role -> + mapped.add(new SimpleGrantedAuthority("ROLE_" + role))); + } + }); + return mapped; + }; +} +---- + +This reads the ID token, so the Keycloak client needs its *realm roles* mapper set to add roles to the ID token. By default that mapper only adds them to the access token. If changing the Keycloak client isn't an option, register an [classname]`OidcUserService` with a converter that decodes the access token with a [interfacename]`JwtDecoder` and reads `realm_access` and `resource_access` from there. That's what the kit's [classname]`KeycloakUserMapper` did, and it's why the kit needed the extra `roles` scope. + +Client roles need the same treatment applied to the `resource_access` claim, keyed by client ID. + + +[[gap-logout-notification]] +=== Client-Side Logout Notification + +*Affects:* Hilla applications that call [methodname]`onBackChannelLogout()`. + +SSO Kit pushed a message to the browser when the provider ended a session elsewhere, which let the application show a dialog offering to log in again. It did this with a server-side [classname]`Flux` and a generated [classname]`BackChannelLogoutEndpoint`. + +Spring Security's back-channel logout invalidates the HTTP session and stops there. There's no event to subscribe to and no client-side notification, so the browser finds out only on its next request, when it's redirected to the login page. + +If the dialog matters, both halves have to be rebuilt. On the server, wrap Spring Security's handler and notify before it invalidates anything: + +[source,java] +---- +http.oidcLogout(oidc -> oidc.backChannel(backChannel -> + backChannel.logoutHandler((request, response, authentication) -> { + // Notify subscribers for this principal, then delegate. + new OidcBackChannelLogoutHandler(sessionRegistry) + .logout(request, response, authentication); + }))); +---- + +On the client, subscribe to a browser-callable service returning a [classname]`Flux` and react to it, as the kit's own React example did. Note that resolving which subscriber to notify means matching the `sub` and `sid` claims of the logout token against your own record of live sessions -- the kit maintained that mapping itself, and Spring Security's [classname]`OidcSessionRegistry` isn't a substitute for it. + + +[[gap-keycloak-theme]] +=== Keycloak Login Theme + +*Affects:* applications using the `sso-kit-keycloak-lumo` theme. + +The <> is a Keycloak theme, not Vaadin code, and Spring Security has nothing to do with login page appearance. Nothing about the migration breaks a theme that's already installed in a Keycloak server: it keeps working, because it depends on the Keycloak version rather than on the Vaadin version. + +What ends is maintenance. The theme is published as part of SSO Kit, so no version is released for Vaadin 26 and no compatibility fixes follow for later Keycloak releases. An application that needs a branded login page long term should either fork the theme -- it's a Keycloak theme directory, and <> describes the structure -- or move the branding to a login view in the application, keeping the provider's page out of the flow with a login route that points at `/oauth2/authorization/{registrationId}`. + + +[[gap-lit]] +=== Hilla Lit Client + +*Affects:* applications using `@vaadin/sso-kit-client-lit`. + +There's no Lit equivalent of `@vaadin/hilla-react-auth`; Hilla's authentication helpers are React-only. The [classname]`SingleSignOnContext` singleton, [methodname]`protectRoutes()`, and [methodname]`hasAccess()` all have to be replaced with application code calling a browser-callable service like the one in <<#expose-the-user,Expose the User>>. + +Because Hilla with Lit is itself discontinued, migrating the frontend to React alongside this migration is usually less work than rebuilding the Lit context. + + +[[smaller-differences]] +=== Smaller Differences + +These cost a few lines each rather than a design decision: + +*Listing configured providers*:: [methodname]`SingleSignOnContext.getRegisteredProviders()` has no replacement. Iterate the repository yourself, which works as long as it's the default in-memory implementation: ++ +[source,java] +---- +if (clientRegistrationRepository instanceof InMemoryClientRegistrationRepository repository) { + StreamSupport.stream(repository.spliterator(), false) + .map(ClientRegistration::getRegistrationId) + .toList(); +} +---- + +*The generated logout link*:: [methodname]`SingleSignOnContext.getLogoutLink()` built an `end_session_endpoint` URL by hand. Don't rebuild it: [methodname]`AuthenticationContext.logout()` in Flow and [methodname]`useAuth().logout()` in Hilla both go through Spring Security's logout filter, which constructs the same URL correctly. + +*The authentication entry point*:: The kit installed a [classname]`LoginUrlAuthenticationEntryPoint` for the login route explicitly. Spring Security's OAuth2 login configurer registers one for the configured login page by itself, so nothing has to be carried over -- but it's worth clicking through an unauthenticated deep link once after the migration to confirm the redirect still happens. + + +== Feature Checklist + +Use this to confirm nothing is left behind. *Direct* means it works after Step 3 with no extra code. + +|=== +|Feature |Status + +|OpenID Connect login (Keycloak, Okta, Microsoft Entra ID) +|Direct + +|Provider and client registration properties +|Direct -- unchanged + +|Login route and automatic provider redirect +|Direct + +|Securing views with `@PermitAll` and `@RolesAllowed` +|Direct + +|[classname]`AuthenticationContext` and the authenticated [classname]`OidcUser` +|Direct -- unchanged + +|Custom user types through [classname]`OidcUserService` +|Direct + +|RP-Initiated Logout and post-logout redirect +|Direct + +|UIDL-aware logout redirect +|Direct + +|Back-Channel Logout +|Direct, after adding [methodname]`oidcLogout()` + +|Reacting to a back-channel logout on the server +|Direct, through [classname]`SessionDestroyEvent` + +|Maximum concurrent sessions +|Direct, after adding [methodname]`sessionConcurrency()` + +|Vaadin-aware expired-session handling +|Build it + +|Keycloak realm and client role mapping +|Build it + +|Hilla user information and roles on the client +|Build it + +|Hilla route protection +|Direct, through `ViewConfig` + +|Hilla back-channel logout notification +|Missing + +|Keycloak Lumo login theme +|Missing + +|Hilla Lit client +|Missing +|=== + + +[discussion-id]`b7f4c1de-2a19-4c07-9f0b-5b6c8e3f21ad` diff --git a/articles/tools/sso/theming.adoc b/articles/tools/sso/theming.adoc index ab984f5e0d..c0f61d5cde 100644 --- a/articles/tools/sso/theming.adoc +++ b/articles/tools/sso/theming.adoc @@ -4,10 +4,16 @@ page-title: How to theme SSO in Vaadin description: How to customize the login screen of SSO Kit authentication providers. meta-description: How to customize the look and feel of Single Sign-On (SSO) in your Vaadin applications. order: 20 +banner: SSO Kit is deprecated. Use link:../sso/migrating-to-spring-security[Spring Security] instead. +banner-id: sso-kit-deprecated +banner-style: caution --- -= Theming += [deprecated:com.vaadin:vaadin@V25.3]#Theming# + +:admonition: +include::index.adoc[tag=deprecation-notice] Each SSO provider has its own way of customizing the login page, with different features and limitations. This page explains how to customize the login screen for authentication providers. From c9d4d339ced8b330c55cbb2294b9dd49968d2bd7 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:08:47 +0000 Subject: [PATCH 02/11] docs: align SSO Kit deprecation wording with AppSec Kit Uses the future tense "won't be available in Vaadin 26" throughout, since Vaadin 26 doesn't exist yet, and states the deprecation in the landing page description as well as the meta description so it shows in navigation and in search results. Also labels the SSO Kit card on the tools overview page as deprecated and mentions that its features are now in Spring Security, so the signal is visible before anyone opens the section. --- articles/tools/index.adoc | 4 ++-- articles/tools/sso/getting-started/hilla.adoc | 2 +- articles/tools/sso/getting-started/index.adoc | 2 +- articles/tools/sso/index.adoc | 10 +++++----- articles/tools/sso/integrations/azure-ad.adoc | 2 +- articles/tools/sso/integrations/index.adoc | 2 +- articles/tools/sso/integrations/keycloak.adoc | 2 +- articles/tools/sso/integrations/okta.adoc | 2 +- articles/tools/sso/migrating-to-spring-security.adoc | 6 +++--- articles/tools/sso/theming.adoc | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/articles/tools/index.adoc b/articles/tools/index.adoc index 24a274b2b2..564167f7fb 100644 --- a/articles/tools/index.adoc +++ b/articles/tools/index.adoc @@ -83,11 +83,11 @@ Get insights into Flow applications at runtime and in production by monitoring a [.card.commercial] -=== SSO Kit +=== SSO Kit (Deprecated) image::{tools-icon-path-prefix}_images/lock-solid.svg[opts=inline, role=icon,32,32] -Integrate your Vaadin application with third-party identity providers, and give users single sign-on capabilities. +Integrate your Vaadin application with third-party identity providers, and give users single sign-on capabilities. SSO Kit won't be available in Vaadin 26; its major features are now available directly in Spring Security. [.sr-only] <<{tools-path-prefix}sso#,See SSO Kit>> diff --git a/articles/tools/sso/getting-started/hilla.adoc b/articles/tools/sso/getting-started/hilla.adoc index fe1896fc73..a78e878f48 100644 --- a/articles/tools/sso/getting-started/hilla.adoc +++ b/articles/tools/sso/getting-started/hilla.adoc @@ -4,7 +4,7 @@ page-title: Getting Started with Hilla and Vaadin SSO Kit description: Step-by-step guide on how to use SSO Kit in a Hilla application. meta-description: Set up Single Sign-On (SSO) in your Vaadin Hilla applications with this comprehensive guide. order: 100 -banner: SSO Kit is deprecated. Use link:../../sso/migrating-to-spring-security[Spring Security] instead. +banner: SSO Kit is deprecated and won't be available in Vaadin 26. See link:../../sso/migrating-to-spring-security[Migrating to Spring Security]. banner-id: sso-kit-deprecated banner-style: caution --- diff --git a/articles/tools/sso/getting-started/index.adoc b/articles/tools/sso/getting-started/index.adoc index 20676735de..a9dc11f5c2 100644 --- a/articles/tools/sso/getting-started/index.adoc +++ b/articles/tools/sso/getting-started/index.adoc @@ -6,7 +6,7 @@ tab-title: Flow description: Step-by-step guide on how to use SSO Kit in a Vaadin Flow application. meta-description: Get started with Single Sign-On (SSO) integration in your Vaadin Flow applications. order: 10 -banner: SSO Kit is deprecated. Use link:../sso/migrating-to-spring-security[Spring Security] instead. +banner: SSO Kit is deprecated and won't be available in Vaadin 26. See link:../sso/migrating-to-spring-security[Migrating to Spring Security]. banner-id: sso-kit-deprecated banner-style: caution --- diff --git a/articles/tools/sso/index.adoc b/articles/tools/sso/index.adoc index 7e8d430806..b05152b08a 100644 --- a/articles/tools/sso/index.adoc +++ b/articles/tools/sso/index.adoc @@ -1,11 +1,11 @@ --- title: SSO Kit (deprecated) page-title: How to configure single sign-on (SSO) in Vaadin -description: Integrate Vaadin applications with third-party identity providers, and allow your users to benefit from single sign-on capabilities. -meta-description: SSO Kit is deprecated and isn't available in Vaadin 26. Use Spring Security's OpenID Connect support instead. +description: Integrate Vaadin applications with third-party identity providers, and allow your users to benefit from single sign-on capabilities. Deprecated, and won't be available in Vaadin 26. +meta-description: SSO Kit integrates Vaadin applications with third-party identity providers for single sign-on. It's deprecated and won't be available in Vaadin 26. section-nav: commercial order: 90 -banner: SSO Kit is deprecated. Use link:./sso/migrating-to-spring-security[Spring Security] instead. +banner: SSO Kit is deprecated and won't be available in Vaadin 26. See link:./sso/migrating-to-spring-security[Migrating to Spring Security]. banner-id: sso-kit-deprecated banner-style: caution --- @@ -22,9 +22,9 @@ ifdef::admonition[] [WARNING] ==== endif::admonition[] -SSO Kit is deprecated and isn't available in Vaadin 26. All of its major features are now available directly in Spring Security and in Vaadin's Spring Security integration. +SSO Kit is deprecated and won't be available in Vaadin 26. -See <<{articles}/tools/sso/migrating-to-spring-security#,Migrating from SSO Kit to Spring Security>> for how to move an existing application, and which features you have to build yourself. +A replacement is already in place: all of the kit's major features are now available directly in Spring Security and in Vaadin's Spring Security integration. See <<{articles}/tools/sso/migrating-to-spring-security#,Migrating from SSO Kit to Spring Security>> for how to move an existing application, and which features you have to build yourself. ifdef::admonition[] ==== endif::admonition[] diff --git a/articles/tools/sso/integrations/azure-ad.adoc b/articles/tools/sso/integrations/azure-ad.adoc index e10417f339..296ad90deb 100644 --- a/articles/tools/sso/integrations/azure-ad.adoc +++ b/articles/tools/sso/integrations/azure-ad.adoc @@ -5,7 +5,7 @@ description: How to use SSO Kit with Azure Active Directory (Microsoft Entra ID) meta-description: Learn how to configure Azure AD for secure single sign-on (SSO) in your Vaadin applications. page-links: - https://azure.microsoft.com[Azure website] -banner: SSO Kit is deprecated. Use link:../../sso/migrating-to-spring-security[Spring Security] instead. +banner: SSO Kit is deprecated and won't be available in Vaadin 26. See link:../../sso/migrating-to-spring-security[Migrating to Spring Security]. banner-id: sso-kit-deprecated banner-style: caution --- diff --git a/articles/tools/sso/integrations/index.adoc b/articles/tools/sso/integrations/index.adoc index 25d4adad6a..ff250972ae 100644 --- a/articles/tools/sso/integrations/index.adoc +++ b/articles/tools/sso/integrations/index.adoc @@ -4,7 +4,7 @@ page-title: Integrating Vaadin apps with SSO providers description: How to use SSO Kit with vendor-specific tutorials. meta-description: Learn to set up single sign-on (SSO) for secure and seamless authentication in Vaadin applications. order: 30 -banner: SSO Kit is deprecated. Use link:../sso/migrating-to-spring-security[Spring Security] instead. +banner: SSO Kit is deprecated and won't be available in Vaadin 26. See link:../sso/migrating-to-spring-security[Migrating to Spring Security]. banner-id: sso-kit-deprecated banner-style: caution --- diff --git a/articles/tools/sso/integrations/keycloak.adoc b/articles/tools/sso/integrations/keycloak.adoc index 9b621f568f..9d26a680a7 100644 --- a/articles/tools/sso/integrations/keycloak.adoc +++ b/articles/tools/sso/integrations/keycloak.adoc @@ -5,7 +5,7 @@ description: Keycloak is an open-source identity and access management solution. meta-description: Learn how to set up and configure Keycloak Single Sign-On (SSO) integration in your Vaadin applications for secure authentication. page-links: - https://www.keycloak.org[Website] -banner: SSO Kit is deprecated. Use link:../../sso/migrating-to-spring-security[Spring Security] instead. +banner: SSO Kit is deprecated and won't be available in Vaadin 26. See link:../../sso/migrating-to-spring-security[Migrating to Spring Security]. banner-id: sso-kit-deprecated banner-style: caution --- diff --git a/articles/tools/sso/integrations/okta.adoc b/articles/tools/sso/integrations/okta.adoc index c6164507ad..76815b4fa9 100644 --- a/articles/tools/sso/integrations/okta.adoc +++ b/articles/tools/sso/integrations/okta.adoc @@ -5,7 +5,7 @@ description: Okta is a commercial identity and access management solution. This meta-description: Learn how to set up Okta SSO for secure authentication in your Vaadin applications. Streamline user access with this step-by-step guide. page-links: - https://www.okta.com[Okta website] -banner: SSO Kit is deprecated. Use link:../../sso/migrating-to-spring-security[Spring Security] instead. +banner: SSO Kit is deprecated and won't be available in Vaadin 26. See link:../../sso/migrating-to-spring-security[Migrating to Spring Security]. banner-id: sso-kit-deprecated banner-style: caution --- diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index 272e3b2eed..f00c0d9d48 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -2,7 +2,7 @@ title: Migrating to Spring Security page-title: Migrating from Vaadin SSO Kit to Spring Security description: How to replace SSO Kit with Spring Security's built-in OAuth2 and OpenID Connect support. -meta-description: Map every SSO Kit feature to its Spring Security equivalent, and learn which pieces you have to build yourself before upgrading to Vaadin 26. +meta-description: Map every SSO Kit feature to its Spring Security equivalent, and learn which pieces you have to build yourself before SSO Kit is gone in Vaadin 26. order: 4 --- @@ -13,7 +13,7 @@ SSO Kit never implemented OpenID Connect itself. It's an auto-configuration laye Those gaps have since closed. Spring Security has built-in OpenID Connect Back-Channel Logout, and Vaadin's [classname]`VaadinSecurityConfigurer` configures OAuth2 login, RP-Initiated Logout, and UIDL-aware redirects for Vaadin applications. What's left of SSO Kit is mostly configuration that you can now write yourself in about twenty lines -- plus a few smaller features that have no direct replacement. -SSO Kit is deprecated and isn't available in Vaadin 26. Every application using it has to migrate before that upgrade. +SSO Kit is deprecated and won't be available in Vaadin 26. Every application using it has to migrate before that upgrade. This guide maps each SSO Kit feature to its replacement, gives the configuration to replace the auto-configuration, and is explicit about what you have to build yourself. @@ -495,7 +495,7 @@ On the client, subscribe to a browser-callable service returning a [classname]`F The <> is a Keycloak theme, not Vaadin code, and Spring Security has nothing to do with login page appearance. Nothing about the migration breaks a theme that's already installed in a Keycloak server: it keeps working, because it depends on the Keycloak version rather than on the Vaadin version. -What ends is maintenance. The theme is published as part of SSO Kit, so no version is released for Vaadin 26 and no compatibility fixes follow for later Keycloak releases. An application that needs a branded login page long term should either fork the theme -- it's a Keycloak theme directory, and <> describes the structure -- or move the branding to a login view in the application, keeping the provider's page out of the flow with a login route that points at `/oauth2/authorization/{registrationId}`. +What ends is maintenance. The theme is published as part of SSO Kit, so there's no version for Vaadin 26 and no compatibility fixes follow for later Keycloak releases. An application that needs a branded login page long term should either fork the theme -- it's a Keycloak theme directory, and <> describes the structure -- or move the branding to a login view in the application, keeping the provider's page out of the flow with a login route that points at `/oauth2/authorization/{registrationId}`. [[gap-lit]] diff --git a/articles/tools/sso/theming.adoc b/articles/tools/sso/theming.adoc index c0f61d5cde..d88b6b405b 100644 --- a/articles/tools/sso/theming.adoc +++ b/articles/tools/sso/theming.adoc @@ -4,7 +4,7 @@ page-title: How to theme SSO in Vaadin description: How to customize the login screen of SSO Kit authentication providers. meta-description: How to customize the look and feel of Single Sign-On (SSO) in your Vaadin applications. order: 20 -banner: SSO Kit is deprecated. Use link:../sso/migrating-to-spring-security[Spring Security] instead. +banner: SSO Kit is deprecated and won't be available in Vaadin 26. See link:../sso/migrating-to-spring-security[Migrating to Spring Security]. banner-id: sso-kit-deprecated banner-style: caution --- From df61fe5aeab859f755364f37a45a24ce1db04594 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:13:30 +0000 Subject: [PATCH 03/11] docs: replace the unsourced Hilla Lit discontinuation claim The Hilla Lit client section asserted that Hilla with Lit is discontinued, which isn't documented anywhere. Replaces it with what the Upgrading Guide actually says: the router Hilla Lit views use is deprecated and no longer actively maintained, and moving Lit views to React is the recommendation. --- articles/tools/sso/migrating-to-spring-security.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index f00c0d9d48..ef9fccbd32 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -505,7 +505,7 @@ What ends is maintenance. The theme is published as part of SSO Kit, so there's There's no Lit equivalent of `@vaadin/hilla-react-auth`; Hilla's authentication helpers are React-only. The [classname]`SingleSignOnContext` singleton, [methodname]`protectRoutes()`, and [methodname]`hasAccess()` all have to be replaced with application code calling a browser-callable service like the one in <<#expose-the-user,Expose the User>>. -Because Hilla with Lit is itself discontinued, migrating the frontend to React alongside this migration is usually less work than rebuilding the Lit context. +Vaadin already recommends moving Lit views to React: `@vaadin/router`, the library Hilla Lit views route with, is deprecated and no longer actively maintained, as noted in the <<{articles}/upgrading#,Upgrading Guide>>. If that move is planned anyway, doing it alongside this migration avoids building a Lit authentication context that then has to be replaced again. [[smaller-differences]] From 76514afc64408f7224c889d58d1e5c5b925dfc42 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:24:21 +0000 Subject: [PATCH 04/11] docs: reflect the Flow session-expiry and Keycloak role mapping additions vaadin/flow#25625 adds UidlExpiredSessionStrategy and makes VaadinSecurityConfigurer install it by default, and vaadin/flow#25627 adds KeycloakOidcUserMapper behind a keycloakRoleMapping() opt-in. Both were ported from SSO Kit, so two of the migration gaps close. Moves the two features out of the gaps section and into the migration steps that need them, with a since badge for the version they arrive in and the previous manual approach kept in a note for earlier versions. Updates the feature mapping table and the checklist to match. --- .../sso/migrating-to-spring-security.adoc | 166 +++++++++--------- 1 file changed, 84 insertions(+), 82 deletions(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index ef9fccbd32..ddb8e7ad30 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -73,7 +73,7 @@ An application that adds `sso-kit-starter`, sets an issuer URI and a login route |[methodname]`sessionManagement()` with [methodname]`sessionConcurrency()` |`vaadin.sso.keycloak-roles` -|A [interfacename]`GrantedAuthoritiesMapper` you write -- see <<#gap-keycloak-roles,Keycloak Role Mapping>> +|[methodname]`keycloakRoleMapping()` on the configurer -- see <<#keycloak-roles,Step 6>> |[classname]`AuthenticationContext`, [methodname]`logout()` |Unchanged; both are Vaadin Flow API @@ -82,13 +82,13 @@ An application that adds `sso-kit-starter`, sets an issuer URI and a login route |[classname]`com.vaadin.flow.spring.security.UidlRedirectStrategy`, applied automatically |[classname]`UidlExpiredSessionStrategy` -|A strategy you write -- see <<#gap-session-expiration,Vaadin-Aware Session Expiration>> +|[classname]`com.vaadin.flow.spring.security.UidlExpiredSessionStrategy`, applied automatically |[classname]`UserLogoutEvent` |Vaadin's [classname]`SessionDestroyEvent` |[classname]`KeycloakUserMapper` -|See <<#gap-keycloak-roles,Keycloak Role Mapping>> +|[classname]`com.vaadin.flow.spring.security.KeycloakOidcUserMapper` |`sso-kit-keycloak-lumo` theme |No replacement -- see <<#gap-keycloak-theme,Keycloak Login Theme>> @@ -109,7 +109,7 @@ An application that adds `sso-kit-starter`, sets an issuer URI and a login route == Migrating a Flow Application -The five steps below cover a Flow application. Steps 4 to 6 are conditional: skip them if the corresponding `vaadin.sso.*` property was never set. +The six steps below cover a Flow application. Steps 4 to 6 are conditional: skip them if the corresponding `vaadin.sso.*` property was never set. === Step 1: Replace the Dependency @@ -274,27 +274,98 @@ VaadinServiceInitListener logoutListener() { Spring Security completes a back-channel logout by calling its own logout endpoint over HTTP, using the URI template `{baseUrl}/logout/connect/back-channel/{registrationId}`. If the application can't resolve its own external base URL -- typically behind a TLS-terminating proxy without forwarded-header handling -- set an explicit internal address with `oidc.backChannel(backChannel -> backChannel.logoutUri("http://localhost:8080/logout/connect/back-channel/{registrationId}"))`. +[[concurrent-sessions]] === Step 5: Restore Concurrent Session Control Skip this step if `vaadin.sso.maximum-concurrent-sessions` was never set. -Session concurrency is standard Spring Security, but the strategy that runs when a session is forced out has to be Vaadin-aware. See <<#gap-session-expiration,Vaadin-Aware Session Expiration>> for the strategy class, then wire it up: +Session concurrency is standard Spring Security. The one Vaadin-specific part -- answering an expired UIDL request with the `Vaadin-Refresh` token, so the client reloads instead of appearing to hang -- [since:com.vaadin:vaadin@V25.4]#is handled by [classname]`VaadinSecurityConfigurer`#, which installs [classname]`UidlExpiredSessionStrategy` whenever the application has session management configured. Only the limit itself has to be carried over: .[filename]`SecurityConfig.java` [source,java] ---- http.sessionManagement(sessionManagement -> sessionManagement - .sessionConcurrency(concurrency -> concurrency - .maximumSessions(1) // <1> - .expiredSessionStrategy(new UidlExpiredSessionStrategy()))); // <2> + .sessionConcurrency(concurrency -> concurrency.maximumSessions(1))); // <1> ---- <1> The former `vaadin.sso.maximum-concurrent-sessions`. The default, `-1`, means unlimited. -<2> Without this, an expired session breaks the Vaadin client instead of reloading it. +Pass a strategy of your own to [methodname]`expiredSessionStrategy()` on the configurer, or turn the whole thing off with [methodname]`enableSessionManagementConfiguration(false)`. +.On Earlier Versions +[NOTE] +==== +In earlier versions Vaadin's Spring integration doesn't ship the strategy, and Spring Security's default writes a plain-text message that means nothing to the Vaadin client. Add the class to the application and wire it up in [methodname]`sessionConcurrency()`: + +[source,java] +---- +public class UidlExpiredSessionStrategy implements SessionInformationExpiredStrategy { + + @Override + public void onExpiredSessionDetected(SessionInformationExpiredEvent event) + throws IOException { + var request = event.getRequest(); + var response = event.getResponse(); + var redirectRoute = request.getContextPath() + "/"; + var servletMapping = request.getHttpServletMapping().getPattern(); + if (HandlerHelper.isFrameworkInternalRequest(servletMapping, request)) { + response.getWriter().write("Vaadin-Refresh: " + redirectRoute); + } else { + response.sendRedirect(redirectRoute); + } + } +} +---- +==== + + +[[keycloak-roles]] === Step 6: Restore Keycloak Role Mapping -Skip this step if `vaadin.sso.keycloak-roles` was never set to `true`. Otherwise see <<#gap-keycloak-roles,Keycloak Role Mapping>>, which has to be built. +Skip this step if `vaadin.sso.keycloak-roles` was never set to `true`. + +Keycloak puts realm roles in a `realm_access` claim and client roles in `resource_access`. Neither is part of the OpenID Connect specification, and by default both live in the access token rather than in the ID token, so Spring Security maps neither and `@RolesAllowed` silently matches nothing. + +[classname]`VaadinSecurityConfigurer` [since:com.vaadin:vaadin@V25.4]#has an opt-in that replaces the kit's property#: + +.[filename]`SecurityConfig.java` +[source,java] +---- +http.with(vaadin(), configurer -> configurer + .oauth2LoginPage("/oauth2/authorization/keycloak", "{baseUrl}") + .keycloakRoleMapping()); // <1> +---- +<1> Decodes the access token and maps the realm roles, the roles `resource_access` grants for the current client, and the token scopes as `SCOPE_` authorities. It has no effect without an OAuth2 login page. + +.On Earlier Versions +[NOTE] +==== +In earlier versions the mapping has to be built. The straightforward route is a [interfacename]`GrantedAuthoritiesMapper` bean that reads the claim and adds `ROLE_` authorities: + +[source,java] +---- +@Bean +GrantedAuthoritiesMapper keycloakAuthoritiesMapper() { + return authorities -> { + var mapped = new LinkedHashSet(authorities); + authorities.stream() + .filter(OidcUserAuthority.class::isInstance) + .map(OidcUserAuthority.class::cast) + .map(authority -> authority.getIdToken().getClaimAsMap("realm_access")) + .filter(Objects::nonNull) + .forEach(realmAccess -> { + var roles = (Collection) realmAccess.get("roles"); + if (roles != null) { + roles.forEach(role -> + mapped.add(new SimpleGrantedAuthority("ROLE_" + role))); + } + }); + return mapped; + }; +} +---- + +This reads the ID token, so the Keycloak client needs its *realm roles* mapper set to add roles to the ID token; by default that mapper only adds them to the access token. If changing the Keycloak client isn't an option, register an [classname]`OidcUserService` with a converter that decodes the access token with a [interfacename]`JwtDecoder` and reads `realm_access` and `resource_access` from there. That's what the kit's [classname]`KeycloakUserMapper` did, and it's why the kit needed the extra `roles` scope. Client roles need the same treatment applied to the `resource_access` claim, keyed by client ID. +==== == Migrating a Hilla Application @@ -392,76 +463,7 @@ Unauthenticated users are redirected to the configured login view rather than to [[gaps]] == What You Have to Build Yourself -These are the features with no drop-in replacement. The first two are small and mechanical. The last three are genuine gaps. - - -[[gap-session-expiration]] -=== Vaadin-Aware Session Expiration - -*Affects:* applications that set `vaadin.sso.maximum-concurrent-sessions` or `hilla.sso.maximum-concurrent-sessions`. - -When session concurrency forces an older session out, Spring Security's default strategy writes a plain-text message to the response. For a UIDL request that response is meaningless to the Vaadin client, which then appears to hang. The kit installed a strategy that answers framework-internal requests with the `Vaadin-Refresh` token instead, so the client reloads. Spring Security can't know about that protocol, and Vaadin's Spring integration doesn't ship an equivalent, so the class has to live in the application: - -.[filename]`UidlExpiredSessionStrategy.java` -[source,java] ----- -public class UidlExpiredSessionStrategy implements SessionInformationExpiredStrategy { - - @Override - public void onExpiredSessionDetected(SessionInformationExpiredEvent event) - throws IOException { - var request = event.getRequest(); - var response = event.getResponse(); - var redirectRoute = request.getContextPath() + "/"; - var servletMapping = request.getHttpServletMapping().getPattern(); - if (HandlerHelper.isFrameworkInternalRequest(servletMapping, request)) { - response.getWriter().write("Vaadin-Refresh: " + redirectRoute); - } else { - response.sendRedirect(redirectRoute); - } - } -} ----- - -Wire it into [methodname]`sessionConcurrency()` as shown in Step 5. - - -[[gap-keycloak-roles]] -=== Keycloak Role Mapping - -*Affects:* applications that set `vaadin.sso.keycloak-roles=true` or `hilla.sso.keycloak-roles=true`. - -Keycloak puts realm roles in a `realm_access` claim and client roles in `resource_access`, neither of which is part of the OpenID Connect specification. Spring Security therefore maps neither, and `@RolesAllowed` sees no roles after the migration unless you add the mapping. - -The straightforward route is a [interfacename]`GrantedAuthoritiesMapper` bean that reads the claim and adds `ROLE_` authorities: - -.[filename]`SecurityConfig.java` -[source,java] ----- -@Bean -GrantedAuthoritiesMapper keycloakAuthoritiesMapper() { - return authorities -> { - var mapped = new LinkedHashSet(authorities); - authorities.stream() - .filter(OidcUserAuthority.class::isInstance) - .map(OidcUserAuthority.class::cast) - .map(authority -> authority.getIdToken().getClaimAsMap("realm_access")) - .filter(Objects::nonNull) - .forEach(realmAccess -> { - var roles = (Collection) realmAccess.get("roles"); - if (roles != null) { - roles.forEach(role -> - mapped.add(new SimpleGrantedAuthority("ROLE_" + role))); - } - }); - return mapped; - }; -} ----- - -This reads the ID token, so the Keycloak client needs its *realm roles* mapper set to add roles to the ID token. By default that mapper only adds them to the access token. If changing the Keycloak client isn't an option, register an [classname]`OidcUserService` with a converter that decodes the access token with a [interfacename]`JwtDecoder` and reads `realm_access` and `resource_access` from there. That's what the kit's [classname]`KeycloakUserMapper` did, and it's why the kit needed the extra `roles` scope. - -Client roles need the same treatment applied to the `resource_access` claim, keyed by client ID. +These three features have no replacement at all. Two more that the kit carried -- Vaadin-aware session expiration and Keycloak role mapping -- are now part of Vaadin's Spring Security integration; see <<#concurrent-sessions,Step 5>> and <<#keycloak-roles,Step 6>>. [[gap-logout-notification]] @@ -570,10 +572,10 @@ Use this to confirm nothing is left behind. *Direct* means it works after Step 3 |Direct, after adding [methodname]`sessionConcurrency()` |Vaadin-aware expired-session handling -|Build it +|Direct |Keycloak realm and client role mapping -|Build it +|Direct, after adding [methodname]`keycloakRoleMapping()` |Hilla user information and roles on the client |Build it From 9b9e80c76b1d645473f777d3c0279c8a09724f35 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 07:58:54 +0000 Subject: [PATCH 05/11] docs: rename to VaadinExpiredSessionStrategy and describe the pass-through --- articles/tools/sso/migrating-to-spring-security.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index ddb8e7ad30..ebdb87bb3f 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -82,7 +82,7 @@ An application that adds `sso-kit-starter`, sets an issuer URI and a login route |[classname]`com.vaadin.flow.spring.security.UidlRedirectStrategy`, applied automatically |[classname]`UidlExpiredSessionStrategy` -|[classname]`com.vaadin.flow.spring.security.UidlExpiredSessionStrategy`, applied automatically +|[classname]`com.vaadin.flow.spring.security.VaadinExpiredSessionStrategy`, applied automatically |[classname]`UserLogoutEvent` |Vaadin's [classname]`SessionDestroyEvent` @@ -279,7 +279,7 @@ Spring Security completes a back-channel logout by calling its own logout endpoi Skip this step if `vaadin.sso.maximum-concurrent-sessions` was never set. -Session concurrency is standard Spring Security. The one Vaadin-specific part -- answering an expired UIDL request with the `Vaadin-Refresh` token, so the client reloads instead of appearing to hang -- [since:com.vaadin:vaadin@V25.4]#is handled by [classname]`VaadinSecurityConfigurer`#, which installs [classname]`UidlExpiredSessionStrategy` whenever the application has session management configured. Only the limit itself has to be carried over: +Session concurrency is standard Spring Security. The one Vaadin-specific part -- answering an expired request in a way the Vaadin client understands, instead of leaving the UI as if it were hanging -- [since:com.vaadin:vaadin@V25.4]#is handled by [classname]`VaadinSecurityConfigurer`#, which installs [classname]`VaadinExpiredSessionStrategy` whenever the application has session management configured. That strategy lets the expired request continue to the servlet, so Flow answers a UIDL request with its session-expired message, a heartbeat with `403`, and a request for a view ends in the login view. Only the limit itself has to be carried over: .[filename]`SecurityConfig.java` [source,java] @@ -298,7 +298,7 @@ In earlier versions Vaadin's Spring integration doesn't ship the strategy, and S [source,java] ---- -public class UidlExpiredSessionStrategy implements SessionInformationExpiredStrategy { +public class VaadinExpiredSessionStrategy implements SessionInformationExpiredStrategy { @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) From 4c157e039b0667ea553d187a0be073c231790dff Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:27:41 +0000 Subject: [PATCH 06/11] docs: fill the gaps reviewed in the SSO Kit migration guide - Note that the starter's SessionRegistry bean disappears with it, so an application that injects one fails to start after Step 1. - Add the legacy dev.hilla and @hilla names to the searches, and the deprecated auto-configure property to the mapping table. - Document protectRoutes() from @vaadin/hilla-react-auth for applications with a hand-written route list, so route protection no longer implies a move to file-based routing. - Separate UserLogoutEvent from SessionDestroyEvent, which also fires on logout and timeout. - State that Vaadin Router is removed in Vaadin 26, so a Lit auth context written now lasts one release. - Fix the code: @NonNull on the UserInfo record, an OidcSessionRegistry bean and a single handler instance in the logout example, and imports in the three snippets that lacked them. --- .../sso/migrating-to-spring-security.adoc | 111 +++++++++++++++--- 1 file changed, 97 insertions(+), 14 deletions(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index ebdb87bb3f..e200f5405c 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -3,7 +3,7 @@ title: Migrating to Spring Security page-title: Migrating from Vaadin SSO Kit to Spring Security description: How to replace SSO Kit with Spring Security's built-in OAuth2 and OpenID Connect support. meta-description: Map every SSO Kit feature to its Spring Security equivalent, and learn which pieces you have to build yourself before SSO Kit is gone in Vaadin 26. -order: 4 +order: 40 --- @@ -33,6 +33,8 @@ The migration is mostly subtraction. Read this section first to see what actuall *The commercial license requirement goes away.* SSO Kit is a commercial add-on with a runtime license check. Spring Security's OAuth2 client and Vaadin's Spring Security integration are both open source, so the license, the build-time key, and the license check on startup all become unnecessary. +*Two beans disappear with the starter.* [classname]`SingleSignOnDefaultBeans` contributes a [classname]`SessionRegistry` -- and, when `keycloak-roles` was set, a Keycloak user mapper -- unless the application declares its own. An application that injects [classname]`SessionRegistry` anywhere fails to start after Step 1, so declare a [classname]`SessionRegistryImpl` bean of your own, or configure one through [methodname]`sessionConcurrency()` as in <<#concurrent-sessions,Step 5>>. + === Scope the Work @@ -42,6 +44,8 @@ Three searches tell you how much of the kit an application actually uses: - `com.vaadin.sso` and `com.vaadin.hilla.sso` in Java imports. Only [classname]`SingleSignOnContext`, [classname]`UserLogoutEvent`, and the two UIDL strategies are commonly imported directly; anything else is an internal detail of the auto-configuration. - `@vaadin/sso-kit-client-` in TypeScript imports. This is the part of the migration that costs real work, and it applies only to Hilla applications. +Applications that started on an earlier version carry the pre-Vaadin-24.4 names instead, and they're easy to miss: the Maven coordinates `dev.hilla:sso-kit-starter` with the `dev.hilla.sso` Java package, and `@hilla/sso-kit-client-react` or `@hilla/sso-kit-client-lit` on the client. Everything in this guide applies to them unchanged. + An application that adds `sso-kit-starter`, sets an issuer URI and a login route, and uses [classname]`AuthenticationContext` in its views migrates in a single commit. One that uses back-channel logout notifications in a Hilla frontend has more to do -- see <<#gaps,What You Have to Build Yourself>>. @@ -75,6 +79,9 @@ An application that adds `sso-kit-starter`, sets an issuer URI and a login route |`vaadin.sso.keycloak-roles` |[methodname]`keycloakRoleMapping()` on the configurer -- see <<#keycloak-roles,Step 6>> +|`vaadin.sso.auto-configure` +|Nothing to carry over. The property has been deprecated and without effect since SSO Kit 2.1; auto-configuration is switched off with `spring.autoconfigure.exclude`, and an application that switched it off already has the filter chain that Step 3 describes + |[classname]`AuthenticationContext`, [methodname]`logout()` |Unchanged; both are Vaadin Flow API @@ -85,7 +92,7 @@ An application that adds `sso-kit-starter`, sets an issuer URI and a login route |[classname]`com.vaadin.flow.spring.security.VaadinExpiredSessionStrategy`, applied automatically |[classname]`UserLogoutEvent` -|Vaadin's [classname]`SessionDestroyEvent` +|Vaadin's [classname]`SessionDestroyEvent`, which is broader -- see <<#session-destroy-event,Smaller Differences>> |[classname]`KeycloakUserMapper` |[classname]`com.vaadin.flow.spring.security.KeycloakOidcUserMapper` @@ -298,6 +305,12 @@ In earlier versions Vaadin's Spring integration doesn't ship the strategy, and S [source,java] ---- +import java.io.IOException; + +import com.vaadin.flow.server.HandlerHelper; +import org.springframework.security.web.session.SessionInformationExpiredEvent; +import org.springframework.security.web.session.SessionInformationExpiredStrategy; + public class VaadinExpiredSessionStrategy implements SessionInformationExpiredStrategy { @Override @@ -343,6 +356,16 @@ In earlier versions the mapping has to be built. The straightforward route is a [source,java] ---- +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; + @Bean GrantedAuthoritiesMapper keycloakAuthoritiesMapper() { return authorities -> { @@ -392,11 +415,23 @@ SSO Kit's [classname]`UserEndpoint` returned a `User` object with the standard O .[filename]`UserInfoService.java` [source,java] ---- +import java.util.List; +import java.util.Optional; + +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.hilla.BrowserCallable; +import org.jspecify.annotations.NonNull; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + @AnonymousAllowed @BrowserCallable public class UserInfoService { - public record UserInfo(String name, String email, List roles) { + public record UserInfo(@NonNull String name, @NonNull String email, + @NonNull List<@NonNull String> roles) { // <1> } public Optional getUserInfo() { @@ -413,6 +448,7 @@ public class UserInfoService { } } ---- +<1> The `@NonNull` annotations matter for the client: without them the generated type of `roles` is `Array | undefined`, and passing it to [methodname]`getRoles()` in the next section doesn't typecheck, because that function has to return `readonly string[]`. === Replace the SSO Context @@ -446,7 +482,9 @@ See <<{articles}/building-apps/react/security#,Security>> for the full setup, in === Replace Route Protection -[methodname]`protectRoutes()` and the `requireAuthentication` route property are replaced by `ViewConfig`, which Hilla's file-based router reads: +Which replacement fits depends on how the application routes. + +With file-based routing, the `requireAuthentication` route property becomes `ViewConfig`, which Hilla's file-based router reads: .[filename]`frontend/views/profile.tsx` [source,tsx] @@ -457,7 +495,29 @@ export const config: ViewConfig = { }; ---- -Unauthenticated users are redirected to the configured login view rather than to the kit's `/ssologin` route. +With a hand-written [filename]`routes.tsx`, keep the route list and swap the import: `@vaadin/hilla-react-auth` has its own [methodname]`protectRoutes()`, which takes the same shape of route tree and the same optional redirect path: + +.[filename]`frontend/routes.tsx` +[source,tsx] +---- +import { protectRoutes } from '@vaadin/hilla-react-auth'; // <1> + +export const routes = protectRoutes([ + { + element: , + children: [ + { path: '/', element: }, + { path: '/profile', element: , handle: { loginRequired: true } }, // <2> + { path: '/admin', element: , handle: { rolesAllowed: ['ADMIN'] } } + ] + } +]); // <3> +---- +<1> Formerly `import { protectRoutes } from '@vaadin/sso-kit-client-react'`. +<2> The kit's `requireAuthentication` becomes `loginRequired` in `handle`; `rolesAllowed` works the same way, and both are the `AccessProps` that [methodname]`useAuth().hasAccess()` evaluates. +<3> A second argument sets the redirect path for unauthenticated users. It defaults to `/login` rather than to the kit's `/ssologin`. + +In both cases the roles come from the [methodname]`getRoles()` function configured in the previous section. [[gaps]] @@ -479,13 +539,33 @@ If the dialog matters, both halves have to be rebuilt. On the server, wrap Sprin [source,java] ---- -http.oidcLogout(oidc -> oidc.backChannel(backChannel -> - backChannel.logoutHandler((request, response, authentication) -> { - // Notify subscribers for this principal, then delegate. - new OidcBackChannelLogoutHandler(sessionRegistry) - .logout(request, response, authentication); - }))); +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcBackChannelLogoutHandler; +import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; +import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; +import org.springframework.security.web.SecurityFilterChain; + +@Bean +OidcSessionRegistry oidcSessionRegistry() { // <1> + return new InMemoryOidcSessionRegistry(); +} + +@Bean +SecurityFilterChain securityFilterChain(HttpSecurity http, + OidcSessionRegistry sessionRegistry) throws Exception { + var delegate = new OidcBackChannelLogoutHandler(sessionRegistry); // <2> + http.oidcLogout(oidc -> oidc.backChannel(backChannel -> + backChannel.logoutHandler((request, response, authentication) -> { + // Notify subscribers for this principal, then delegate. + delegate.logout(request, response, authentication); + }))); + // The rest of the chain, as in Step 3. + return http.build(); +} ---- +<1> Without this bean Spring Security creates the registry as a shared object of the filter chain, where the handler can't reach it. Declaring it makes the same instance available to both. +<2> Build the delegate once, rather than on every logout request. On the client, subscribe to a browser-callable service returning a [classname]`Flux` and react to it, as the kit's own React example did. Note that resolving which subscriber to notify means matching the `sub` and `sid` claims of the logout token against your own record of live sessions -- the kit maintained that mapping itself, and Spring Security's [classname]`OidcSessionRegistry` isn't a substitute for it. @@ -507,7 +587,7 @@ What ends is maintenance. The theme is published as part of SSO Kit, so there's There's no Lit equivalent of `@vaadin/hilla-react-auth`; Hilla's authentication helpers are React-only. The [classname]`SingleSignOnContext` singleton, [methodname]`protectRoutes()`, and [methodname]`hasAccess()` all have to be replaced with application code calling a browser-callable service like the one in <<#expose-the-user,Expose the User>>. -Vaadin already recommends moving Lit views to React: `@vaadin/router`, the library Hilla Lit views route with, is deprecated and no longer actively maintained, as noted in the <<{articles}/upgrading#,Upgrading Guide>>. If that move is planned anyway, doing it alongside this migration avoids building a Lit authentication context that then has to be replaced again. +There's no point in building one to last, either. `@vaadin/router`, the library Hilla Lit views route with, is removed in Vaadin 26 -- the same release that drops SSO Kit -- together with the `vaadin.react.enable=false` option that falls back to it; see <<{articles}/upgrading#vaadin-router-removal,Vaadin Router Deprecation>> in the Upgrading Guide. A Lit authentication context written for this migration therefore survives exactly one release, because the views around it have to move to React and React Router before the same upgrade. Doing both moves together is the only order that pays for the frontend work once. [[smaller-differences]] @@ -515,6 +595,9 @@ Vaadin already recommends moving Lit views to React: `@vaadin/router`, the libra These cost a few lines each rather than a design decision: +[[session-destroy-event]] +*Server-side logout events*:: [classname]`UserLogoutEvent` is published by the kit's [classname]`BackChannelLogoutFilter` and by nothing else, so a listener for it runs only when the provider ends a session elsewhere. Vaadin's [classname]`SessionDestroyEvent` is broader: it fires whenever a session is destroyed, including an ordinary logout and a session timeout. Code that only cleans up per-user state can move over as it is. Code that assumed "the provider logged this user out" has to distinguish the cases itself, because the event carries no reason -- wrap the back-channel logout handler as in <<#gap-logout-notification,Client-Side Logout Notification>> if that distinction matters. + *Listing configured providers*:: [methodname]`SingleSignOnContext.getRegisteredProviders()` has no replacement. Iterate the repository yourself, which works as long as it's the default in-memory implementation: + [source,java] @@ -566,7 +649,7 @@ Use this to confirm nothing is left behind. *Direct* means it works after Step 3 |Direct, after adding [methodname]`oidcLogout()` |Reacting to a back-channel logout on the server -|Direct, through [classname]`SessionDestroyEvent` +|Direct, through [classname]`SessionDestroyEvent` -- which also fires on logout and timeout |Maximum concurrent sessions |Direct, after adding [methodname]`sessionConcurrency()` @@ -581,7 +664,7 @@ Use this to confirm nothing is left behind. *Direct* means it works after Step 3 |Build it |Hilla route protection -|Direct, through `ViewConfig` +|Direct, through `ViewConfig` or [methodname]`protectRoutes()` |Hilla back-channel logout notification |Missing From 29339910bfa3d4a7b23138960f699d22845477f0 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:34:40 +0000 Subject: [PATCH 07/11] docs: correct the bean and nullability notes in the migration guide Name the second starter bean for what it is, an OidcUserService, and drop the sessionConcurrency() escape hatch: Spring Security takes a SessionRegistry bean when one exists but otherwise keeps its instance in the filter chain, so it never satisfies an injection point. Keep name and email nullable in the UserInfo record, since both claims are optional in OpenID Connect; only roles needs the annotations. --- articles/tools/sso/migrating-to-spring-security.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index e200f5405c..2a2606aaf8 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -33,7 +33,7 @@ The migration is mostly subtraction. Read this section first to see what actuall *The commercial license requirement goes away.* SSO Kit is a commercial add-on with a runtime license check. Spring Security's OAuth2 client and Vaadin's Spring Security integration are both open source, so the license, the build-time key, and the license check on startup all become unnecessary. -*Two beans disappear with the starter.* [classname]`SingleSignOnDefaultBeans` contributes a [classname]`SessionRegistry` -- and, when `keycloak-roles` was set, a Keycloak user mapper -- unless the application declares its own. An application that injects [classname]`SessionRegistry` anywhere fails to start after Step 1, so declare a [classname]`SessionRegistryImpl` bean of your own, or configure one through [methodname]`sessionConcurrency()` as in <<#concurrent-sessions,Step 5>>. +*The starter's own beans go with it.* [classname]`SingleSignOnDefaultBeans` contributes a [classname]`SessionRegistry` -- and, in the kit versions that have the `keycloak-roles` property, an [classname]`OidcUserService` that maps Keycloak roles -- unless the application already declares one. An application that injects [classname]`SessionRegistry` anywhere therefore fails to start after Step 1. Declare a [classname]`SessionRegistryImpl` bean of your own to replace it: [methodname]`sessionConcurrency()` in <<#concurrent-sessions,Step 5>> doesn't cover this, because Spring Security uses a [classname]`SessionRegistry` bean when the application has one, but otherwise keeps its own instance inside the filter chain, where nothing can inject it. === Scope the Work @@ -430,7 +430,7 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser; @BrowserCallable public class UserInfoService { - public record UserInfo(@NonNull String name, @NonNull String email, + public record UserInfo(String name, String email, @NonNull List<@NonNull String> roles) { // <1> } @@ -448,7 +448,7 @@ public class UserInfoService { } } ---- -<1> The `@NonNull` annotations matter for the client: without them the generated type of `roles` is `Array | undefined`, and passing it to [methodname]`getRoles()` in the next section doesn't typecheck, because that function has to return `readonly string[]`. +<1> Annotate `roles` inside and out. Without it the generated type is `Array | undefined`, and passing that to [methodname]`getRoles()` in the next section doesn't typecheck, because the function has to return `readonly string[]`. Leave `name` and `email` as they are: both claims are optional in OpenID Connect, so [methodname]`getFullName()` and [methodname]`getEmail()` can return `null`, and the client is better off seeing that in the type. === Replace the SSO Context From e04f027065dc7421e4e5d3ffec413e36bde785e4 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:40:43 +0000 Subject: [PATCH 08/11] docs: scope the Hilla parser-packages step to pre-24.7 projects --- articles/tools/sso/migrating-to-spring-security.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index 2a2606aaf8..904bd12897 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -393,9 +393,9 @@ This reads the ID token, so the Keycloak client needs its *realm roles* mapper s == Migrating a Hilla Application -A Hilla application migrates its backend exactly as above, using `hilla.sso.*` as the source of the values instead of `vaadin.sso.*`, and removing `com.vaadin.hilla:sso-kit-starter` in Step 1. The `com.vaadin.hilla.sso.starter` package also has to be removed from the `` list of the Hilla Maven plugin, because its endpoints no longer exist. +A Hilla application migrates its backend exactly as above, using `hilla.sso.*` as the source of the values instead of `vaadin.sso.*`, and removing `com.vaadin.hilla:sso-kit-starter` in Step 1. A project that still carries the pre-24.7 `` configuration of the Hilla Maven plugin drops `com.vaadin.hilla.sso.starter` from that list as well; since Vaadin 24.7 there's no such list, because browser-callable services are found among the Spring beans. -The frontend is where the real work is. SSO Kit shipped three generated endpoints and a React context on top of them; the replacement is Hilla's own authentication support plus a service you write. +The frontend is where the real work is. SSO Kit shipped three generated endpoints -- browser-callable services, in today's terms -- and a React context on top of them; the replacement is Hilla's own authentication support plus a service you write. === Replace the Client Dependency From 5da8cf69c8c40a3387255688b358ec5fe0d80fc2 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:39:03 +0000 Subject: [PATCH 09/11] docs: document using KeycloakOidcUserMapper with an existing OidcUserService --- articles/tools/sso/migrating-to-spring-security.adoc | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index 904bd12897..d9c18ad41a 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -347,7 +347,16 @@ http.with(vaadin(), configurer -> configurer .oauth2LoginPage("/oauth2/authorization/keycloak", "{baseUrl}") .keycloakRoleMapping()); // <1> ---- -<1> Decodes the access token and maps the realm roles, the roles `resource_access` grants for the current client, and the token scopes as `SCOPE_` authorities. It has no effect without an OAuth2 login page. +<1> Decodes the access token and maps the realm roles, the roles `resource_access` grants for the current client, and the token scopes as `SCOPE_` authorities. Roles granted to other clients are ignored, and the role prefix follows the one configured for the application instead of a hardcoded `ROLE_`, which is the one behavior difference from the kit's mapper. The switch has no effect without an OAuth2 login page. + +The switch creates an [classname]`OidcUserService` of its own, so leave it off in an application that already declares one and set the mapper on that service instead: + +[source,java] +---- +var oidcUserService = new OidcUserService(); +oidcUserService.setOidcUserConverter(new KeycloakOidcUserMapper()); // <1> +---- +<1> [classname]`com.vaadin.flow.spring.security.KeycloakOidcUserMapper`, the replacement for the kit's [classname]`KeycloakUserMapper`. A constructor argument overrides the role prefix. .On Earlier Versions [NOTE] From c90fe69369628f656b041d9b55f2232ac18f094a Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:42:50 +0000 Subject: [PATCH 10/11] docs: state the role prefix and decode fallback of the Keycloak mapper --- articles/tools/sso/migrating-to-spring-security.adoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index d9c18ad41a..01800842d6 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -347,16 +347,18 @@ http.with(vaadin(), configurer -> configurer .oauth2LoginPage("/oauth2/authorization/keycloak", "{baseUrl}") .keycloakRoleMapping()); // <1> ---- -<1> Decodes the access token and maps the realm roles, the roles `resource_access` grants for the current client, and the token scopes as `SCOPE_` authorities. Roles granted to other clients are ignored, and the role prefix follows the one configured for the application instead of a hardcoded `ROLE_`, which is the one behavior difference from the kit's mapper. The switch has no effect without an OAuth2 login page. +<1> Decodes the access token and maps the realm roles, the roles `resource_access` grants for the current client, and the token scopes as `SCOPE_` authorities. Roles granted to other clients are ignored. The switch has no effect without an OAuth2 login page. + +Two details differ from the kit's mapper. On this route the prefix of a mapped role follows the role prefix configured for the application, rather than a hardcoded `ROLE_`. And an access token that can't be decoded -- a client registration without a JWK set URI, or a provider that doesn't issue JWT access tokens -- no longer fails the login: the user is mapped without any role authorities, and the reason is logged at debug level. Keep an eye on the second one right after the migration, because a user with no roles looks like a mistake somewhere else entirely. The switch creates an [classname]`OidcUserService` of its own, so leave it off in an application that already declares one and set the mapper on that service instead: [source,java] ---- var oidcUserService = new OidcUserService(); -oidcUserService.setOidcUserConverter(new KeycloakOidcUserMapper()); // <1> +oidcUserService.setOidcUserConverter(new KeycloakOidcUserMapper("ROLE_")); // <1> ---- -<1> [classname]`com.vaadin.flow.spring.security.KeycloakOidcUserMapper`, the replacement for the kit's [classname]`KeycloakUserMapper`. A constructor argument overrides the role prefix. +<1> [classname]`com.vaadin.flow.spring.security.KeycloakOidcUserMapper`, the replacement for the kit's [classname]`KeycloakUserMapper`. Constructed by hand it can't read the role prefix from the filter chain, so pass the prefix the application uses -- `ROLE_` unless a [classname]`GrantedAuthorityDefaults` bean says otherwise. The no-argument constructor also uses `ROLE_`. .On Earlier Versions [NOTE] From 8b940cb55cad891187da625b6e7f1d7cbbc8d011 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:48:13 +0000 Subject: [PATCH 11/11] docs: point Keycloak role mapping at the reference section main now documents the feature in the OAuth2 Authentication page, with an anchor of its own. Step 6 no longer repeats what the switch grants, why it needs an OAuth2 login page, or how to install the mapper on a user service the application builds itself, and links that section instead, so the two can't drift apart. What stays is the part that belongs to a migration: the property this replaces, and the two behaviour differences from the kit's own mapper. Also fixes three style checks in text added earlier on the branch: a spelling error, an undefined abbreviation, and a cliche. --- .../sso/migrating-to-spring-security.adoc | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/articles/tools/sso/migrating-to-spring-security.adoc b/articles/tools/sso/migrating-to-spring-security.adoc index 01800842d6..cc8026924c 100644 --- a/articles/tools/sso/migrating-to-spring-security.adoc +++ b/articles/tools/sso/migrating-to-spring-security.adoc @@ -95,7 +95,7 @@ An application that adds `sso-kit-starter`, sets an issuer URI and a login route |Vaadin's [classname]`SessionDestroyEvent`, which is broader -- see <<#session-destroy-event,Smaller Differences>> |[classname]`KeycloakUserMapper` -|[classname]`com.vaadin.flow.spring.security.KeycloakOidcUserMapper` +|[classname]`com.vaadin.flow.spring.security.KeycloakOidcUserMapper` -- see <<{articles}/flow/integrations/spring/oauth2#keycloak-role-mapping,Keycloak Role Mapping>> |`sso-kit-keycloak-lumo` theme |No replacement -- see <<#gap-keycloak-theme,Keycloak Login Theme>> @@ -345,20 +345,12 @@ Keycloak puts realm roles in a `realm_access` claim and client roles in `resourc ---- http.with(vaadin(), configurer -> configurer .oauth2LoginPage("/oauth2/authorization/keycloak", "{baseUrl}") - .keycloakRoleMapping()); // <1> + .keycloakRoleMapping()); ---- -<1> Decodes the access token and maps the realm roles, the roles `resource_access` grants for the current client, and the token scopes as `SCOPE_` authorities. Roles granted to other clients are ignored. The switch has no effect without an OAuth2 login page. -Two details differ from the kit's mapper. On this route the prefix of a mapped role follows the role prefix configured for the application, rather than a hardcoded `ROLE_`. And an access token that can't be decoded -- a client registration without a JWK set URI, or a provider that doesn't issue JWT access tokens -- no longer fails the login: the user is mapped without any role authorities, and the reason is logged at debug level. Keep an eye on the second one right after the migration, because a user with no roles looks like a mistake somewhere else entirely. +<<{articles}/flow/integrations/spring/oauth2#keycloak-role-mapping,Keycloak Role Mapping>> covers what the switch grants, why it works only together with an OAuth2 login page, and how to install [classname]`KeycloakOidcUserMapper` -- the replacement for the kit's [classname]`KeycloakUserMapper` -- on a user service that the application builds itself. -The switch creates an [classname]`OidcUserService` of its own, so leave it off in an application that already declares one and set the mapper on that service instead: - -[source,java] ----- -var oidcUserService = new OidcUserService(); -oidcUserService.setOidcUserConverter(new KeycloakOidcUserMapper("ROLE_")); // <1> ----- -<1> [classname]`com.vaadin.flow.spring.security.KeycloakOidcUserMapper`, the replacement for the kit's [classname]`KeycloakUserMapper`. Constructed by hand it can't read the role prefix from the filter chain, so pass the prefix the application uses -- `ROLE_` unless a [classname]`GrantedAuthorityDefaults` bean says otherwise. The no-argument constructor also uses `ROLE_`. +Two details differ from the kit's mapper. The prefix of a mapped role follows the role prefix configured for the application, rather than a hardcoded `ROLE_`. And an access token that can't be decoded -- a client registration without a JSON Web Key Set (JWKS) URI, or a provider that doesn't issue JWT access tokens -- no longer fails the login: the user is mapped without any role authorities, and the reason is logged at debug level. Watch for that one right after the migration, because a user with no roles looks like a mistake somewhere else entirely. .On Earlier Versions [NOTE] @@ -459,7 +451,7 @@ public class UserInfoService { } } ---- -<1> Annotate `roles` inside and out. Without it the generated type is `Array | undefined`, and passing that to [methodname]`getRoles()` in the next section doesn't typecheck, because the function has to return `readonly string[]`. Leave `name` and `email` as they are: both claims are optional in OpenID Connect, so [methodname]`getFullName()` and [methodname]`getEmail()` can return `null`, and the client is better off seeing that in the type. +<1> Annotate `roles` inside and out. Without it the generated type is `Array | undefined`, and passing that to [methodname]`getRoles()` in the next section doesn't type-check, because the function has to return `readonly string[]`. Leave `name` and `email` as they are: both claims are optional in OpenID Connect, so [methodname]`getFullName()` and [methodname]`getEmail()` can return `null`, and the client is better off seeing that in the type. === Replace the SSO Context