From vaadin-claude
Guides Claude on securing Vaadin 25 apps with Spring Security: login views, view access annotations, OAuth2/OpenID Connect, logout, and AuthenticationContext.
npx claudepluginhub vaadin/claude-plugin --plugin vaadin-claudeThis skill uses the workspace's default tool permissions.
Use the Vaadin MCP tools (`search_vaadin_docs`, `get_component_java_api`, `get_component_styling`) to look up the latest documentation whenever uncertain about a specific API detail. Always set `vaadin_version` to `"25"` and `ui_language` to `"java"`.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Use the Vaadin MCP tools (search_vaadin_docs, get_component_java_api, get_component_styling) to look up the latest documentation whenever uncertain about a specific API detail. Always set vaadin_version to "25" and ui_language to "java".
This skill covers: Spring Security configuration with VaadinSecurityConfigurer, login views with LoginForm, view access control annotations (@AnonymousAllowed, @PermitAll, @RolesAllowed, @DenyAll), AuthenticationContext, logout handling, and OAuth2/OpenID Connect integration with providers like Google, Keycloak, GitHub, and Okta.
Use views-and-navigation instead when the question is about @Route, @Layout, AppLayout, SideNav, or URL parameters. This skill covers how to secure views, not how to create or navigate between them.
Use client-side-views instead when securing React/Hilla views with ViewConfig.loginRequired and ViewConfig.rolesAllowed. This skill covers Java/Flow view security, though the annotation-based approach also applies to @BrowserCallable endpoints.
Add the Spring Security starter dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Create a security configuration class that uses VaadinSecurityConfigurer:
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
configurer.loginView(LoginView.class);
});
return http.build();
}
@Bean
public UserDetailsManager userDetailsManager() {
// WARNING: In-memory users for development only.
// Use JDBC, LDAP, or OAuth2 in production.
var user = User.withUsername("user")
.password("{noop}user")
.roles("USER")
.build();
var admin = User.withUsername("admin")
.password("{noop}admin")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
VaadinSecurityConfigurer.vaadin() automatically handles:
Use the built-in LoginForm component to create a login page. It provides a form with username and password fields, is compatible with password managers, and handles CSRF tokens automatically.
@Route(value = "login", autoLayout = false)
@PageTitle("Login")
@AnonymousAllowed
public class LoginView extends Main implements BeforeEnterObserver {
private final LoginForm login;
public LoginView() {
login = new LoginForm();
login.setAction("login");
addClassNames(LumoUtility.Display.FLEX,
LumoUtility.JustifyContent.CENTER,
LumoUtility.AlignItems.CENTER);
setSizeFull();
add(login);
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
if (event.getLocation()
.getQueryParameters()
.getParameters()
.containsKey("error")) {
login.setError(true);
}
}
}
Key points:
autoLayout = false — prevents the login view from rendering inside the application's main layout (e.g., AppLayout with navigation menu)@AnonymousAllowed — required so unauthenticated users can access the pagelogin.setAction("login") — makes the form POST to Spring Security's /login endpointBeforeEnterObserver — checks for the ?error query parameter that Spring Security adds after a failed login attemptconfigurer.loginView(LoginView.class) tells VaadinSecurityConfigurer which view is the login pageControl who can access each view using Jakarta and Vaadin security annotations on the view class:
| Annotation | Access Level | Typical Use |
|---|---|---|
@AnonymousAllowed | Anyone (no login required) | Login view, public landing page |
@PermitAll | Any authenticated user | Dashboard, user profile |
@RolesAllowed("ADMIN") | Users with specified role(s) | Admin panel, user management |
@DenyAll | Nobody | Default when no annotation is present |
Note on
@PermitAll: Vaadin's use of@PermitAlldiffers from the Jakarta Security standard. In standard Jakarta Security,@PermitAllmeans "anyone, including unauthenticated users" — similar to Vaadin's@AnonymousAllowed. In Vaadin,@PermitAllmeans "any authenticated user." Developers familiar with standard Jakarta security may be confused when access is denied to unauthenticated users on a view they explicitly "permitted all" — use@AnonymousAllowedfor truly public views.
@Route("public")
@AnonymousAllowed
public class PublicView extends VerticalLayout { }
@Route("dashboard")
@PermitAll
public class DashboardView extends VerticalLayout { }
@Route("admin")
@RolesAllowed("ADMIN")
public class AdminView extends VerticalLayout { }
@DenyAll applies (access denied by default)@PermitAll inside a layout with no annotation (default @DenyAll) is inaccessible@DenyAll > @AnonymousAllowed > @RolesAllowed > @PermitAllDefine role names as constants to avoid typos in @RolesAllowed annotations:
public final class Roles {
public static final String ADMIN = "ADMIN";
public static final String USER = "USER";
private Roles() {
}
}
// Usage:
@RolesAllowed(Roles.ADMIN)
public class AdminView extends VerticalLayout { }
Inject AuthenticationContext to check roles or get user information within a view:
@Route("settings")
@PermitAll
public class SettingsView extends VerticalLayout {
public SettingsView(AuthenticationContext authContext) {
authContext.getAuthenticatedUser(UserDetails.class)
.ifPresent(user -> add(new H2("Welcome " + user.getUsername())));
if (authContext.hasRole(Roles.ADMIN)) {
add(new Button("Admin Settings", event -> {
// show admin-only settings
}));
}
}
}
The simplest and most reliable way to log out. AuthenticationContext handles session invalidation and redirect automatically. Add a logout button to your MainLayout:
public class MainLayout extends AppLayout {
private final transient AuthenticationContext authContext;
public MainLayout(AuthenticationContext authContext) {
this.authContext = authContext;
var title = new H1("My App");
title.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
var logout = new Button("Logout", event -> authContext.logout());
var header = new HorizontalLayout(title, logout);
header.setWidthFull();
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.addClassNames(LumoUtility.Padding.Horizontal.MEDIUM);
addToNavbar(header);
}
}
AuthenticationContext must be declared transient because it is not Serializable.
By default, logout redirects to /. To customize the post-logout redirect, specify a second parameter when registering the login view:
configurer.loginView(LoginView.class, "/goodbye");
When AuthenticationContext is not available, use SecurityContextLogoutHandler directly. The redirect must happen before the handler invalidates the session:
public void logout() {
UI.getCurrent().getPage().setLocation("/");
var logoutHandler = new SecurityContextLogoutHandler();
logoutHandler.logout(
VaadinServletRequest.getCurrent().getHttpServletRequest(),
null, null);
}
To authenticate users via an external identity provider (Google, GitHub, Keycloak, Okta, Azure AD), use Spring Security's OAuth2 client support.
Add the OAuth2 client starter alongside Spring Security:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Google is a Spring Security "common provider", so minimal configuration is needed. Only the client ID and secret are required:
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=openid,profile,email
Providers that are not built into Spring Security require full configuration including the issuer URI:
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.client-id=my-client-id
spring.security.oauth2.client.registration.keycloak.client-secret=${KEYCLOAK_CLIENT_SECRET}
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.scope=openid,profile
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://keycloak.local:8180/realms/my-app
Replace loginView() with oauth2LoginPage(), pointing to the provider's authorization URI:
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
configurer.oauth2LoginPage(
"/oauth2/authorization/google",
"{baseUrl}");
});
return http.build();
}
}
The {baseUrl} template variable resolves to the application's base URL and is used as the post-logout redirect URI. Other supported variables: {baseScheme}, {baseHost}, {basePort}, {basePath}.
When using OAuth2, no LoginView class is needed — the identity provider handles the login UI. Users are redirected to the provider's login page and back to the application after authentication.
For OAuth2 applications, use AuthenticationContext.logout() the same way as with form login. The VaadinSecurityConfigurer handles the OAuth2-specific logout flow.
The same pattern works for GitHub, Okta, and Azure AD. The only differences are:
application.properties — the registration and provider keys change per provideroauth2LoginPage() URL — use /oauth2/authorization/{registrationId} where {registrationId} matches the key in application.propertiesclient-id, client-secret, and scopeissuer-uri and authorization-grant-typeUserDetailsManager with {noop} passwords is for development only.@DenyAll. Layouts are checked independently; both must grant access.AuthenticationContext for logout and user info — it integrates with Spring Security and handles session cleanup correctly. Declare the field transient.autoLayout = false on the login view — the login view should not render inside the application's main layout.VaadinSecurityConfigurer over manual Spring Security config — it handles CSRF, logout, request caching, and exception handling for Vaadin automatically.application.properties with environment variable references (${GOOGLE_CLIENT_SECRET}) or Spring profiles.Roles class with public static final String fields to avoid typos in @RolesAllowed annotations.autoLayout = false on the login view — the login form renders inside the app shell with the navigation menu, which is confusing and may cause access control issues.@PermitAll inside a layout with no annotation (default @DenyAll) is inaccessible. Both must independently grant access.@Secured or @PreAuthorize on views — these Spring Security annotations are not supported on Vaadin views. Use @AnonymousAllowed, @PermitAll, @RolesAllowed, or @DenyAll.SecurityConfig — in-memory {noop} passwords are for prototyping only. Production applications must use JDBC, LDAP, or OAuth2 authentication.SecurityContextLogoutHandler without redirecting first — the logout handler invalidates the session, so UI.getCurrent().getPage().setLocation() must happen before the handler call. Prefer AuthenticationContext.logout() which handles this correctly.HttpSecurity (e.g., .requestMatchers("/admin/**").hasRole("ADMIN")) do not control access to Vaadin views. Use annotation-based access control (@RolesAllowed, @PermitAll, etc.) exclusively for view security. URL-pattern rules are still appropriate for non-Vaadin endpoints such as REST APIs.For a quick-reference cheatsheet of security annotations, OAuth2 provider configurations, login view checklist, and SecurityConfig templates, see references/security-patterns.md.