I found the same issue, and have tested a bit.
vaadin:
url-mapping: /ui/*
endpoint:
prefix: /hilla/connect
With a custom client (frontend/connect-client.ts) with hilla/connect
as the prefix. And middleware function which removes /ui
from the requests.
import { ConnectClient as ConnectClient_1 } from "@vaadin/hilla-frontend";
import {MaybePromise, MiddlewareContext, MiddlewareNext} from "@vaadin/hilla-frontend/Connect.js";
const mwareFunction = (context: MiddlewareContext, next: MiddlewareNext): MaybePromise<Response> => {
if (["GET", "POST"].includes(context.request.method) && context.request.url.includes("/ui/hilla/connect/")) {
const currentUrl = new URL(context.request.url);
context.request = new Request(
currentUrl.toString().replace("/ui", ""),
{
method: context.request.method,
headers: context.request.headers,
body: context.request.body,
credentials: context.request.credentials,
cache: context.request.cache,
redirect: context.request.redirect,
integrity: context.request.integrity,
keepalive: context.request.keepalive,
signal: context.request.signal
}
);
}
return next(context);
}
const client_1 = new ConnectClient_1({ prefix: "hilla/connect", middlewares: [mwareFunction]});
export default client_1;
with security config
import com.vaadin.flow.spring.security.VaadinWebSecurity
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.HttpStatus
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.config.http.SessionCreationPolicy
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
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import java.time.Duration
import java.util.stream.Collectors
import java.util.stream.Stream
@Configuration
@EnableMethodSecurity
@Order(1)
class ApiSecurityConfig {
@Bean
fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOriginPatterns = listOf("https://*.nb.no*")
configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE")
configuration.allowedHeaders = listOf("Content-Type")
configuration.allowCredentials = true
configuration.setMaxAge(Duration.ofDays(1))
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
@Bean
fun webSecurityCustomizer(): WebSecurityCustomizer {
return WebSecurityCustomizer { web ->
web.debug(true)
}
}
@Throws(Exception::class)
@Bean
fun standardFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.securityMatcher(
"/api/**",
"/actuator/**",
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs",
"/v3/api-docs/**"
)
.authorizeHttpRequests { // Authorize special paths
it.requestMatchers(
"/api/deposit/*/form/marc",
"/api/external/**",
"/actuator/health",
"/actuator/health/*",
"/actuator/info",
"/actuator/metrics",
"/actuator/prometheus",
"/swagger-ui/",
"/swagger-ui/*",
"/swagger-ui.html",
"/v3/api-docs",
"/v3/api-docs/**",
).permitAll()
}
.authorizeHttpRequests { it.anyRequest().authenticated() }
.cors { it.configurationSource(corsConfigurationSource()) }
.csrf { it.disable() }
.oauth2ResourceServer { oauth2 ->
oauth2.jwt { jwt ->
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
}
}
.sessionManagement { sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
return http.build()
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
grantedAuthoritiesConverter.setAuthoritiesClaimName("groups")
grantedAuthoritiesConverter.setAuthorityPrefix("")
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
return jwtAuthenticationConverter
}
}
@Order(2)
@EnableWebSecurity
@Configuration
class VaadinSecurityConfig: VaadinWebSecurity() {
override fun configure(http: HttpSecurity) {
super.configure(http)
setOAuth2LoginPage(http, "/oauth2/authorization/keycloak")
http.oauth2Login { oauth2 ->
oauth2.userInfoEndpoint { userInfoEndpoint -> userInfoEndpoint.userAuthoritiesMapper(this.userAuthoritiesMapper()) }
}
http.logout {
it.logoutSuccessHandler(HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
}
}
@Bean(name = ["VaadinSecurityFilterChainBean"])
override fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.securityMatcher(
"/",
"/login/**",
"/oauth2/**",
"/ui/**",
"/connect/**",
"/hilla/**",
"/VAADIN/**"
)
return super.filterChain(http)
}
@Bean
fun userAuthoritiesMapper(): GrantedAuthoritiesMapper {
return GrantedAuthoritiesMapper { authorities: Collection<GrantedAuthority?> ->
authorities.stream()
.filter { authority: GrantedAuthority? -> OidcUserAuthority::class.java.isInstance(authority) }
.map { authority: GrantedAuthority? ->
val oidcUserAuthority = authority as OidcUserAuthority?
val userInfo = oidcUserAuthority!!.userInfo
val roles = userInfo.getClaim<List<String>>("groups")
roles.stream().map { r: String? -> SimpleGrantedAuthority("ROLE_$r") }
}
.reduce(Stream.empty()) { joinedAuthorities: Stream<SimpleGrantedAuthority>, roleAuthorities: Stream<SimpleGrantedAuthority> ->
Stream.concat(
joinedAuthorities,
roleAuthorities
)
}
.collect(Collectors.toList())
}
}
}
This works, but i still need to include root /
in the VaadinSecurityFilterChainBean
, or else i get redirect issues with login.
Is there a way to not have vaadin go to root?
edited to add error info for login issues:
2025-06-04T14:02:04.480+02:00 INFO 881859 --- [io-8082-exec-10] Spring Security Debugger :
************************************************************
Request received for POST '/ui/?v-r=uidl&v-uiId=1':
org.apache.catalina.connector.RequestFacade@747ff346
servletPath:/ui
pathInfo:/
headers:
host: localhost:8082
user-agent: Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0
accept: */*
accept-language: en-US,en;q=0.5
accept-encoding: gzip, deflate, br, zstd
content-type: application/json; charset=UTF-8
content-length: 326
origin: http://localhost:8082
dnt: 1
sec-gpc: 1
connection: keep-alive
referer: http://localhost:8082/backend/ui/login
cookie: JSESSIONID=913653EF9272ECD793D3DE77FFE56881; csrfToken=3dca0fdf-96c5-427e-9386-69852d20c566
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-origin
Security filter chain: no match
************************************************************
2025-06-04T14:02:04.486+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.NavigationAccessControl : Checking access for view com.vaadin.flow.component.UI$ClientViewPlaceholder
2025-06-04T14:02:04.486+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.AnnotatedViewAccessChecker : Access to view 'com.vaadin.flow.component.UI$ClientViewPlaceholder' with path 'login' is allowed
2025-06-04T14:02:04.486+02:00 DEBUG 881859 --- [io-8082-exec-10] f.s.a.DefaultAccessCheckDecisionResolver : Access to view 'com.vaadin.flow.component.UI$ClientViewPlaceholder' with path 'login' allowed by 1 out of 1 navigation checkers (0 neutral).
2025-06-04T14:02:04.486+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.NavigationAccessControl : Decision against 1 checker results: Access decision: ALLOW
2025-06-04T14:02:04.495+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.NavigationAccessControl : Checking access for view com.vaadin.flow.router.RouteNotFoundError
2025-06-04T14:02:04.495+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.AnnotatedViewAccessChecker : Access to view 'com.vaadin.flow.router.RouteNotFoundError' with path 'login' is allowed
2025-06-04T14:02:04.495+02:00 DEBUG 881859 --- [io-8082-exec-10] f.s.a.DefaultAccessCheckDecisionResolver : Access to view 'com.vaadin.flow.router.RouteNotFoundError' with path 'login' allowed by 1 out of 1 navigation checkers (0 neutral).
2025-06-04T14:02:04.495+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.NavigationAccessControl : Decision against 1 checker results: Access decision: ALLOW
2025-06-04T14:02:04.500+02:00 DEBUG 881859 --- [io-8082-exec-10] c.vaadin.flow.router.RouteNotFoundError : Route is not found
com.vaadin.flow.router.NotFoundException: Couldn't find route for 'login'
at com.vaadin.flow.component.UI.handleErrorNavigation(UI.java:2117) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.component.UI.renderViewForRoute(UI.java:2025) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.component.UI.browserNavigate(UI.java:1880) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:239) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:488) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:298) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.internal.nodefeature.ElementListenerMap.lambda$fireEvent$2(ElementListenerMap.java:475) ~[flow-server-24.7.6.jar:24.7.6]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ~[na:na]
at com.vaadin.flow.internal.nodefeature.ElementListenerMap.fireEvent(ElementListenerMap.java:475) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.communication.rpc.EventRpcHandler.handleNode(EventRpcHandler.java:62) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.communication.rpc.AbstractRpcInvocationHandler.handle(AbstractRpcInvocationHandler.java:79) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocationData(ServerRpcHandler.java:568) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.communication.ServerRpcHandler.lambda$handleInvocations$6(ServerRpcHandler.java:549) ~[flow-server-24.7.6.jar:24.7.6]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ~[na:na]
at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocations(ServerRpcHandler.java:549) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.communication.ServerRpcHandler.handleRpc(ServerRpcHandler.java:376) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.communication.UidlRequestHandler.synchronizedHandleRequest(UidlRequestHandler.java:138) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.SynchronizedRequestHandler.handleRequest(SynchronizedRequestHandler.java:63) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.VaadinService.handleRequest(VaadinService.java:1664) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.server.VaadinServlet.service(VaadinServlet.java:398) ~[flow-server-24.7.6.jar:24.7.6]
at com.vaadin.flow.spring.SpringServlet.service(SpringServlet.java:106) ~[vaadin-spring-24.7.6.jar:na]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.30.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.security.web.ObservationFilterChainDecorator$FilterObservation$SimpleFilterObservation.lambda$wrap$1(ObservationFilterChainDecorator.java:479) ~[spring-security-web-6.3.3.jar:6.3.3]
at org.springframework.security.web.ObservationFilterChainDecorator.lambda$wrapUnsecured$1(ObservationFilterChainDecorator.java:90) ~[spring-security-web-6.3.3.jar:6.3.3]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:219) ~[spring-security-web-6.3.3.jar:6.3.3]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) ~[spring-security-web-6.3.3.jar:6.3.3]
at org.springframework.security.web.debug.DebugFilter.invokeWithWrappedRequest(DebugFilter.java:90) ~[spring-security-web-6.3.3.jar:6.3.3]
at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:78) ~[spring-security-web-6.3.3.jar:6.3.3]
at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:67) ~[spring-security-web-6.3.3.jar:6.3.3]
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195) ~[spring-webmvc-6.1.13.jar:6.1.13]
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:230) ~[spring-security-config-6.3.3.jar:6.3.3]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268) ~[spring-web-6.1.13.jar:6.1.13]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:113) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.13.jar:6.1.13]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
And in frontend Could not navigate to 'login'