> Markdown version of [Migrating to Spring Security](https://vaadin.com/docs/next/tools/sso/migrating-to-spring-security). Section index: [llms.txt](https://vaadin.com/docs/next/tools/llms.txt)

# Migrating from SSO Kit to Spring Security (since V25.3)

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 `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 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.

## <a id="before-migrating"></a>Before Migrating

The migration is mostly subtraction. Read this section first to see what actually changes and to scope the work.

### <a id="what-changesand-what-doesnt"></a>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.** `SingleSignOnConfiguration` is replaced by a `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.** `AuthenticationContext`, `getAuthenticatedUser()`, and `logout()` are part of Vaadin’s Spring integration, not of SSO Kit. Views that inject `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.

**The starter’s own beans go with it.** `SingleSignOnDefaultBeans` contributes a `SessionRegistry` — and, in the kit versions that have the `keycloak-roles` property, an `OidcUserService` that maps Keycloak roles — unless the application already declares one. An application that injects `SessionRegistry` anywhere therefore fails to start after Step 1. Declare a `SessionRegistryImpl` bean of your own to replace it: `sessionConcurrency()` in [Step 5](#concurrent-sessions) doesn’t cover this, because Spring Security uses a `SessionRegistry` bean when the application has one, but otherwise keeps its own instance inside the filter chain, where nothing can inject it.

### <a id="scope-the-work"></a>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 `SingleSignOnContext`, `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 `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 [What You Have to Build Yourself](#gaps).

## <a id="feature-mapping"></a>Feature Mapping

| SSO Kit                                              | Replacement                                                                                                                                                                                                                                                      |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `com.vaadin:sso-kit-starter`                         | `org.springframework.boot:spring-boot-starter-oauth2-client`                                                                                                                                                                                                     |
| `SingleSignOnConfiguration` auto-configuration       | Your own `SecurityFilterChain` bean                                                                                                                                                                                                                              |
| `vaadin.sso.login-route`                             | First argument of `oauth2LoginPage()`                                                                                                                                                                                                                            |
| `vaadin.sso.logout-redirect-route`                   | Second argument of `oauth2LoginPage()`                                                                                                                                                                                                                           |
| `vaadin.sso.back-channel-logout`                     | `http.oidcLogout()` with `backChannel()`                                                                                                                                                                                                                         |
| `vaadin.sso.back-channel-logout-route`               | Fixed at `/logout/connect/back-channel/{registrationId}`                                                                                                                                                                                                         |
| `vaadin.sso.maximum-concurrent-sessions`             | `sessionManagement()` with `sessionConcurrency()`                                                                                                                                                                                                                |
| `vaadin.sso.keycloak-roles`                          | `keycloakRoleMapping()` on the configurer — see [Step 6](#keycloak-roles)                                                                                                                                                                                        |
| `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 |
| `AuthenticationContext`, `logout()`                  | Unchanged; both are Vaadin Flow API                                                                                                                                                                                                                              |
| `com.vaadin.sso.starter.UidlRedirectStrategy`        | `com.vaadin.flow.spring.security.UidlRedirectStrategy`, applied automatically                                                                                                                                                                                    |
| `UidlExpiredSessionStrategy`                         | `com.vaadin.flow.spring.security.VaadinExpiredSessionStrategy`, applied automatically                                                                                                                                                                            |
| `UserLogoutEvent`                                    | Vaadin’s `SessionDestroyEvent`, which is broader — see [Smaller Differences](#session-destroy-event)                                                                                                                                                             |
| `KeycloakUserMapper`                                 | `com.vaadin.flow.spring.security.KeycloakOidcUserMapper` — see [Keycloak Role Mapping](https://vaadin.com/docs/next/flow/integrations/spring/oauth2.md#keycloak-role-mapping)                                                                                    |
| `sso-kit-keycloak-lumo` theme                        | No replacement — see [Keycloak Login Theme](#gap-keycloak-theme)                                                                                                                                                                                                 |
| `@vaadin/sso-kit-client-react`                       | `@vaadin/hilla-react-auth`                                                                                                                                                                                                                                       |
| `@vaadin/sso-kit-client-lit`                         | No replacement — see [Hilla Lit Client](#gap-lit)                                                                                                                                                                                                                |
| `SingleSignOnEndpoint`, `UserEndpoint`               | A browser-callable service you write                                                                                                                                                                                                                             |
| `BackChannelLogoutEndpoint`, `onBackChannelLogout()` | No replacement — see [Client-Side Logout Notification](#gap-logout-notification)                                                                                                                                                                                 |

## <a id="migrating-a-flow-application"></a>Migrating a Flow Application

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.

### <a id="step-1-replace-the-dependency"></a>Step 1: Replace the Dependency

Remove the SSO Kit starter and add Spring Boot’s OAuth2 client starter:

`pom.xml`

```xml
<!-- Remove:
<dependency>
    <groupId>com.vaadin</groupId>
    <artifactId>sso-kit-starter</artifactId>
</dependency>
-->

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
```

`build.gradle`

```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.

### <a id="step-2-keep-the-provider-configuration"></a>Step 2: Keep the Provider Configuration

Leave every `spring.security.oauth2.client.`** property untouched. Remove only the `vaadin.sso.`** block:

`application.properties`

```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
```

`application.yaml`

```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.

### <a id="step-3-add-a-security-configuration"></a>Step 3: Add a Security Configuration

Replace the auto-configuration with an explicit `SecurityFilterChain`. The two arguments of `oauth2LoginPage()` are the former `login-route` and `logout-redirect-route`:

`SecurityConfig.java`

```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. `AuthenticationContext.logout()` continues to work and still ends the provider session, because the configurer installs an `OidcClientInitiatedLogoutSuccessHandler` when a post-logout redirect URI is given.

- UIDL-aware redirects. Vaadin’s own `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 [OAuth2 Authentication](https://vaadin.com/docs/next/flow/integrations/spring/oauth2.md) and [Vaadin Security Configurer](https://vaadin.com/docs/next/flow/security/vaadin-security-configurer.md).

> **Note: Views Need No Changes**
>
> Injecting `AuthenticationContext` into a view, calling `getAuthenticatedUser(OidcUser.class)`, and annotating views with `@PermitAll` or `@RolesAllowed` all keep working unchanged. Those are Vaadin Flow APIs, not SSO Kit APIs.

### <a id="step-4-enable-back-channel-logout"></a>Step 4: Enable Back-Channel Logout

Skip this step if the provider was never configured to send back-channel logout requests.

Spring Security implements [Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html) natively. Enable it on the same filter chain:

`SecurityConfig.java`

```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 `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 `UserLogoutEvent` from its own filter. Spring Security has no equivalent event, but it invalidates the HTTP session, which makes Vaadin fire a `SessionDestroyEvent`. Listen for that instead:

```java
@Bean
VaadinServiceInitListener logoutListener() {
    return serviceInitEvent -> serviceInitEvent.getSource()
            .addSessionDestroyListener(sessionDestroyEvent -> {
                // Clean up per-session resources here.
            });
}
```

> **Note: Behind a Reverse Proxy**
>
> 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}"))`.

### <a id="concurrent-sessions"></a>Step 5: Restore Concurrent Session Control

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 request in a way the Vaadin client understands, instead of leaving the UI as if it were hanging — is handled by `VaadinSecurityConfigurer` (V25.4 pre-release), which installs `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:

`SecurityConfig.java`

```java
http.sessionManagement(sessionManagement -> sessionManagement
        .sessionConcurrency(concurrency -> concurrency.maximumSessions(1))); // (1)
```

1. The former `vaadin.sso.maximum-concurrent-sessions`. The default, `-1`, means unlimited.

Pass a strategy of your own to `expiredSessionStrategy()` on the configurer, or turn the whole thing off with `enableSessionManagementConfiguration(false)`.

> **Note: On Earlier Versions**
>
> 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 `sessionConcurrency()`:
>
> ```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
>     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);
>         }
>     }
> }
> ```

### <a id="keycloak-roles"></a>Step 6: Restore Keycloak Role Mapping

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.

`VaadinSecurityConfigurer` has an opt-in that replaces the kit’s property (undefined pre-release):

`SecurityConfig.java`

```java
http.with(vaadin(), configurer -> configurer
        .oauth2LoginPage("/oauth2/authorization/keycloak", "{baseUrl}")
        .keycloakRoleMapping());
```

[Keycloak Role Mapping](https://vaadin.com/docs/next/flow/integrations/spring/oauth2.md#keycloak-role-mapping) covers what the switch grants, why it works only together with an OAuth2 login page, and how to install `KeycloakOidcUserMapper` — the replacement for the kit’s `KeycloakUserMapper` — on a user service that the application builds itself.

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.

> **Note: On Earlier Versions**
>
> In earlier versions the mapping has to be built. The straightforward route is a `GrantedAuthoritiesMapper` bean that reads the claim and adds `ROLE_` authorities:
>
> ```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 -> {
>         var mapped = new LinkedHashSet<GrantedAuthority>(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 `OidcUserService` with a converter that decodes the access token with a `JwtDecoder` and reads `realm_access` and `resource_access` from there. That’s what the kit’s `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.

## <a id="migrating-a-hilla-application"></a>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. A project that still carries the pre-24.7 `<parser><packages>` 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 — 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.

### <a id="replace-the-client-dependency"></a>Replace the Client Dependency

```bash
npm uninstall @vaadin/sso-kit-client-react
npm install @vaadin/hilla-react-auth
```

### <a id="expose-the-user"></a>Expose the User

SSO Kit’s `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:

`UserInfoService.java`

```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,
            @NonNull List<@NonNull String> roles) { // (1)
    }

    public Optional<UserInfo> 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()));
    }
}
```

1. Annotate `roles` inside and out. Without it the generated type is `Array<string | undefined> | undefined`, and passing that to `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 `getFullName()` and `getEmail()` can return `null`, and the client is better off seeing that in the type.

### <a id="replace-the-sso-context"></a>Replace the SSO Context

`configureAuth()` replaces `SsoProvider` and `useSsoContext()`:

`frontend/security/auth.ts`

```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 `isUserInRole()`; roles reach `ViewConfig.rolesAllowed` through this function.

Then swap the calls in your components:

- `useSsoContext()` becomes `useAuth()`.

- `authenticated` becomes `state.user !== undefined`.

- `logout()` is provided by `useAuth()` and needs no `logoutUrl`.

- `login()` has no equivalent, because it was only a redirect. Navigate to the provider directly: `window.location.href = '/oauth2/authorization/keycloak'`.

See [Security](https://vaadin.com/docs/next/building-apps/react/security.md) for the full setup, including where to wrap the application in `<AuthProvider>`.

### <a id="replace-route-protection"></a>Replace Route Protection

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:

`frontend/views/profile.tsx`

```tsx
export const config: ViewConfig = {
  loginRequired: true,
  rolesAllowed: ['ADMIN'] // Optional; replaces isUserInRole checks in the route.
};
```

With a hand-written `routes.tsx`, keep the route list and swap the import: `@vaadin/hilla-react-auth` has its own `protectRoutes()`, which takes the same shape of route tree and the same optional redirect path:

`frontend/routes.tsx`

```tsx
import { protectRoutes } from '@vaadin/hilla-react-auth'; // (1)

export const routes = protectRoutes([
  {
    element: <MainLayout />,
    children: [
      { path: '/', element: <PublicView /> },
      { path: '/profile', element: <ProfileView />, handle: { loginRequired: true } }, // (2)
      { path: '/admin', element: <AdminView />, 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 `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 `getRoles()` function configured in the previous section.

## <a id="gaps"></a>What You Have to Build Yourself

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 [Step 5](#concurrent-sessions) and [Step 6](#keycloak-roles).

### <a id="gap-logout-notification"></a>Client-Side Logout Notification

**Affects:** Hilla applications that call `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 `Flux` and a generated `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:

```java
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 `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 `OidcSessionRegistry` isn’t a substitute for it.

### <a id="gap-keycloak-theme"></a>Keycloak Login Theme

**Affects:** applications using the `sso-kit-keycloak-lumo` theme.

The [Lumo theme for the Keycloak login page](https://vaadin.com/docs/next/tools/sso/theming.md) 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 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 [Theming](https://vaadin.com/docs/next/tools/sso/theming.md) 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}`.

### <a id="gap-lit"></a>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 `SingleSignOnContext` singleton, `protectRoutes()`, and `hasAccess()` all have to be replaced with application code calling a browser-callable service like the one in [Expose the User](#expose-the-user).

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 [Vaadin Router Deprecation](https://vaadin.com/docs/next/upgrading.md#vaadin-router-removal) 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.

### <a id="smaller-differences"></a>Smaller Differences

These cost a few lines each rather than a design decision:

- **Server-side logout events**

  `UserLogoutEvent` is published by the kit’s `BackChannelLogoutFilter` and by nothing else, so a listener for it runs only when the provider ends a session elsewhere. Vaadin’s `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 [Client-Side Logout Notification](#gap-logout-notification) if that distinction matters.

- **Listing configured providers**

  `SingleSignOnContext.getRegisteredProviders()` has no replacement. Iterate the repository yourself, which works as long as it’s the default in-memory implementation:

  ```java
  if (clientRegistrationRepository instanceof InMemoryClientRegistrationRepository repository) {
      StreamSupport.stream(repository.spliterator(), false)
              .map(ClientRegistration::getRegistrationId)
              .toList();
  }
  ```

- **The generated logout link**

  `SingleSignOnContext.getLogoutLink()` built an `end_session_endpoint` URL by hand. Don’t rebuild it: `AuthenticationContext.logout()` in Flow and `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 `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.

## <a id="feature-checklist"></a>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                                                                         |
| `AuthenticationContext` and the authenticated `OidcUser`  | Direct — unchanged                                                             |
| Custom user types through `OidcUserService`               | Direct                                                                         |
| RP-Initiated Logout and post-logout redirect              | Direct                                                                         |
| UIDL-aware logout redirect                                | Direct                                                                         |
| Back-Channel Logout                                       | Direct, after adding `oidcLogout()`                                            |
| Reacting to a back-channel logout on the server           | Direct, through `SessionDestroyEvent` — which also fires on logout and timeout |
| Maximum concurrent sessions                               | Direct, after adding `sessionConcurrency()`                                    |
| Vaadin-aware expired-session handling                     | Direct                                                                         |
| Keycloak realm and client role mapping                    | Direct, after adding `keycloakRoleMapping()`                                   |
| Hilla user information and roles on the client            | Build it                                                                       |
| Hilla route protection                                    | Direct, through `ViewConfig` or `protectRoutes()`                              |
| Hilla back-channel logout notification                    | Missing                                                                        |
| Keycloak Lumo login theme                                 | Missing                                                                        |
| Hilla Lit client                                          | Missing                                                                        |

`b7f4c1de-2a19-4c07-9f0b-5b6c8e3f21ad`
