Adding Hilla as a frontend to an existing Sping Boot Rest API

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'