REST API next to Vaadin

Hi i want to integrade an REST API next to Vaadin Views. Therefor i have following Code. The /api/auth POST request is permitted to all, but in the controller itself, i check the client credentials and allow creating a JWT or decline it.

Does anyone sees an issue having the REST next to Vaadin with this config or some other thoughts, which i do not think off?

Usually i would split this into 2 services, but in this case its not possible (hosting is limited to one cloud service).

In the JWTFilter i am not sure regarding the username, if this is the correct place to check if a user exist and or is active? Feels like makes no sense there becuase the user already has a valid token or?

Using Spring Security 6.1 and Vaadin 24.6.4

SecurityConfig:

  @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                        .requestMatchers(Arrays.stream(PUBLIC_ENDPOINTS)
                                .map(path -> AntPathRequestMatcher.antMatcher(HttpMethod.GET, path))
                                .toArray(AntPathRequestMatcher[]::new)).permitAll()
                        .requestMatchers(AntPathRequestMatcher.antMatcher("/api/**")).authenticated())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf(csrf -> csrf.disable())
                .cors(withDefaults())
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        super.configure(http);
        setLoginView(http, LoginView.class);
        setStatelessAuthentication(http, new SecretKeySpec(Base64.getDecoder().decode(JWT_AUTH_KEY), JwsAlgorithms.HS256), "oop", 86400);
    }

RestController:

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @PostMapping("/token")
    public ResponseEntity<?> generateToken(@RequestBody AuthRequest authRequest) {
        if (isValidClient(authRequest.getClientId(), authRequest.getClientSecret())) {
            String token = Jwts.builder()
                .setSubject(authRequest.getClientId())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 1 Tag gĂĽltig
                .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode("MY_SECRET_KEY")), SignatureAlgorithm.HS256)
                .compact();
            return ResponseEntity.ok(new AuthResponse(token));
        }
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid client credentials");
    }

    private boolean isValidClient(String clientId, String clientSecret) {
        return "meinClientId".equals(clientId) && "meinClientSecret".equals(clientSecret);
    }
}

My JTW Filter doFilter Method:

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, io.jsonwebtoken.io.IOException, java.io.IOException {
        //extract Token from Header
        final String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        // Skip JWT-Validation for /api/auth/token
        String path = request.getRequestURI();
        if (path.equals("/api/auth/token")) {
            chain.doFilter(request, response);
            return;
        }

        //remove Bearer
        final String token = authHeader.substring(7);
        try {
            //Token validation
            String username = Jwts.parser()
                    .verifyWith(publicKey)
                    .build()
                    .parseSignedClaims(token)
                    .getPayload()
                    .getSubject();

            //TODO check with Database if user is active and exists?
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = User.builder()
                        .username(username)
                        .password("")
                        .authorities(Collections.emptyList())
                        .build();

                UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authToken);
            }

        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");

            ApiResponse<Void> errorResponse = new ApiResponse<>(null, "Authentifizierung fehlgeschlagen oder ungĂĽltiger Token");

            ObjectMapper objectMapper = new ObjectMapper();
            String jsonResponse = objectMapper.writeValueAsString(errorResponse);

            response.getWriter().write(jsonResponse);
            response.getWriter().flush();
        }

        chain.doFilter(request, response);
    }
2 Likes

Insert shameless advertising for upvoting: Create a Spring + Vaadin + REST example · Issue #2504 · vaadin/docs · GitHub

About this part…

I’m not sure how strict your security requirements are, but that falls into CWE 208 (short circuit evaluation (&& and equals))

1 Like

Thanks Christian! The securer, the better. Do you have any suggestions? ( I extended my initial post with my Jwt Filter, maybe you havent seen it)

Edit:
Ah the && is the problem never heared of that! Thanks, learned something new today :)

I would have a seprated security filter chain for the rest API with a custom security matcher (http.securityMatcher("/api/**")) : Java Configuration :: Spring Security

I combine Vaadin and APIs very often

1 Like

would you share some thoughts to my dummy code? Implemented it and it works, dont see any issue (except the hint from @knoobie)

@marcoc_753

You mean this way:

For Frontend:

public class SecurityConfigFrontend extends VaadinWebSecurity {
    @Value("${JWT_AUTH_KEY}")
    private String JWT_AUTH_KEY;

    @Autowired
    private UserDetailsService userDetailsService;

    private static final String[] PUBLIC_ENDPOINTS = {
            "/images/*",
            "/application/health/**",
            "/swagger-ui/**",
            "/v3/**",
            "/css/**",
            "/js/**",
            "/font-awesome/**",
            "/img/**",
            "/fonts/**"
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                        .requestMatchers(Arrays.stream(PUBLIC_ENDPOINTS)
                                .map(path -> AntPathRequestMatcher.antMatcher(HttpMethod.GET, path))
                                .toArray(AntPathRequestMatcher[]::new)).permitAll()
                    )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf(csrf -> csrf.disable())
                .cors(withDefaults())
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        super.configure(http);
        setLoginView(http, LoginView.class);
        setStatelessAuthentication(http, new SecretKeySpec(Base64.getDecoder().decode(JWT_AUTH_KEY), JwsAlgorithms.HS256), "aaa.wp", 86400);
    }

    @Bean("authProvider")
    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(encoder());
        return authProvider;
    }

    @Bean(name = "encoder")
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }
}

For API Requests:

@Configuration
public class SecurityConfigAPI {

    @Bean
    @Order(1)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/api/**") // Nur fĂĽr API
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/token").permitAll()
                        .requestMatchers("/api/**").authenticated()
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf(csrf -> csrf.disable())
                .cors(withDefaults())
                .formLogin(AbstractHttpConfigurer::disable) // WICHTIG: Form-Login deaktivieren
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Works well on a first test and makes it more maintainable, thank you

IMO Both approaches are good. Separating the configuration is helpful for sure.

1 Like

Thanks, i will go with the separation. Maybe better if i want to extend it in the future :) Thanks so much

Does anyone added swagger into an vaadin app?

Cant get it done.

localhost:8080/swagger-ui/index.html

Redirects to login, even it i added “/swagger-ui/**” to permitAll in my SecurityConfig

Edit. Got it anyhow:

    <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI defineOpenApi() {
        Server server = new Server();
        server.setUrl("http://localhost:8080");
        server.setDescription("Development");


        Contact myContact = new io.swagger.v3.oas.models.info.Contact();
        myContact.setName("Jane Doe");
        myContact.setEmail("your.email@gmail.com");

        Info information = new Info()
                .title("Employee Management System API")
                .version("1.0")
                .description("This API exposes endpoints to manage employees.")
                .contact(myContact);
        return new OpenAPI().info(information).servers(List.of(server));
    }
}
@Bean  //EXAMPLE!
    public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html")
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().permitAll()
                )
                .csrf(csrf -> csrf.disable()); 
        return http.build();
    }

You are probably missing the vaadin.exclude-urls setting in application.properties

https://vaadin.com/docs/latest/flow/integrations/spring/configuration#prevent-handling-of-specific-urls

I usually put this into my

vaadin.exclude-urls=/api-docs/,/swagger-ui/

springdoc.api-docs.path=/api-docs