Search code examples
javaspringdocspringdoc-openapi-uiswagger-3.0spring-boot-security

A very specific spring boot actuator / swagger integration question


I'm launching my springboot webapp and navigating to localhost/api/myservice/swagger-ui/index.html, but getting a 401 Unauthorized. I've done all the usual things from now countless answers on SO, but none seem to apply specifically to this pairing of spring and spring doc. One limitation I have is Java 1.8, else I would use newer versions of software. Handcuffs.

Short version: spring-boot: 2.7.18 spring-doc: 1.8.0

pom.xml snippets:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>

        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

When I checked, the oldest version of springboot for springdocs was past Java 1.8.

application.yml snippets:

springdoc:
  api-docs:
    path: /api-docs/**
  swagger-ui:
    enabled = true
    path = /swagger-ui.html
    tryItOutEnabled = false
    filter = false
    syntaxHighlight.activated = true
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
server:
  servlet:
    context-path: /api/myservice

SwaggerConfig.java snippets:

@Configuration
public class SwaggerConfig {
    @Bean
    public GroupedOpenApi api() {
        return GroupedOpenApi.builder()
                .group("my/package/**")
                .pathsToMatch("/**")
                .packagesToExclude("/error.**")
                .build();
    }

    @Bean
    public OpenAPI apiInfo() {
        final String securitySchemeName = "bearerAuth";
        return new OpenAPI()
                .addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
                .components(new Components().addSecuritySchemes(
                        securitySchemeName,
                        new SecurityScheme()
                                .name(securitySchemeName)
                                .type(SecurityScheme.Type.HTTP)
                                .in(SecurityScheme.In.HEADER)
                                .scheme("bearer")
                                .bearerFormat("JWT")
                ))
                .info(new Info()
                        .title(title)
                        .version(version)
                        .description("")
                )
                .servers(Collections.singletonList(
                        new Server()
                                .url(contextPath)
                                .description("Default Server URL")
                ));
    }
}

SpringConfiguration.java snippets:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringConfiguration {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// I have tried very many things here, but I don't see how it could get much more permissive than this ;)
        http
                .csrf().disable()
                .authorizeRequests().anyRequest().permitAll();
        return http
                .build();
    }

    @Bean
    public RequestMatcher requestMatcher() {
        this.log.debug("Creating request matcher");
        List<RequestMatcher> requestMatchers = new ArrayList<>();
        requestMatchers.add(new AntPathRequestMatcher("/**"));
        return new OrRequestMatcher(requestMatchers);
    }

    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter(
            RequestMatcher requestMatcher,
            AuthenticationConfiguration authenticationConfiguration)
            throws Exception {
        CustomAuthenticationFilter result = new CustomAuthenticationFilter(requestMatcher);
        result.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
        return result;
    }

}

When I uncomment the CustomAuthenticationFilter stuff, I get the 401 Unauthorized hitting localhost/api/myservice/swagger-ui/index.html, because that class is being applied, but when I comment it out, I hit the swagger page just fine.

I'm new to this version of SpringBoot and new to Swagger 3 and new to springdocs. I have this working in other microservices that use a FilterRegistrationBean instead of something like CustomAuthenticationFilter, and I'm not sure why the same configurations don't work in this repository. Bonus points if this can be done without disabling csrf.

When I navigate to localhost/api/myservice/swagger-ui/index.html I get a 401 Unauthorized. I expect the Swagger page to load without needing authorization.

When I comment out the CustomAuthenticationFilter bean, I get the expected result. I'm not sure why beyond being certain that the CustomAuthenticationFilter is being used to secure Swagger pages when I don't want it to.

I have also tried

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(@Value("${server.servlet.context-path}") String contextPath) {
        return web -> web
                .ignoring()
                .antMatchers("/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", contextPath + "/swagger-ui/**");
    }

which issues printed warnings on the console about using authorizeHttpRequests instead. And I've tried that as well.

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                //.authorizeHttpRequests()
                .antMatchers(
                        "/v2/api-docs",
                        "/v3/api-docs/**",
                        "/swagger-resources/**",
                        "/swagger-ui/**",
                        "/swagger-ui.html",
                        "/swagger-ui/index.html",
                        contextPath + "/swagger-ui/index.html",
                        "/configuration/ui",
                        "/configuration/security",
                        "/webjars/**",
                        "/api/**",
                        contextPath + "/swagger-ui/**"
                ).permitAll()
                .and().authorizeRequests().antMatchers("/api/**").authenticated();
        return http.build();
    }

Solution

  • first thing you have to do is to exclude swagger endpoint from your security context (SecurityFilterChain), you already did the configuration in WebSecurityCustomizer , no need to do it again .

    what you need is :

      @Bean
      public WebSecurityCustomizer webSecurityCustomizer() {
      return (web) ->
        web.ignoring()
            .requestMatchers(
                new AntPathRequestMatcher("/user/**"),
                    new AntPathRequestMatcher("/v3/api-docs/**"),
                    new AntPathRequestMatcher("/swagger-ui/**"),
                    new AntPathRequestMatcher("/swagger-ui.html")
            )
      ; 
      }
    

    application.properties (you can change it to yml) :

    springdoc.api-docs.enabled=true
    springdoc.swagger-ui.path=/swagger-ui.html
    springdoc.swagger-ui.enabled=true
    springdoc.swagger-ui.operationsSorter=method
    springdoc.swagger-ui.tryItOutEnabled=true
    springdoc.swagger-ui.filter=true
    springdoc.swagger-ui.tagsSorter=alpha
    springdoc.api-docs.path=/v3/api-docs
    

    finaly you can keep your OpenAPI() as it is or :

        @OpenAPIDefinition(
            info = @Info(
                    contact = @Contact(
                            name = ""
                    ),
                    description = "",
                    title = "",
                    version = "",
                    license = @License(
                            name = "",
                            url = ""
                    ),
                    termsOfService = "Terms of service"
            ),
            servers = {
                    @Server(
                            description = "Local ENV",
                            url = ""
                    ),
                    @Server(
                            description = "PROD ENV",
                            url = ""
                    ),
                    @Server(
                            description = "TEST ENV",
                            url = ""
                    )
            },
            security = {
                    @SecurityRequirement(
                            name = "bearerAuth"
                    )
            }
    )
    @SecurityScheme(
            name = "bearerAuth",
            description = "JWT auth description",
            scheme = "bearer",
            type = SecuritySchemeType.HTTP,
            bearerFormat = "JWT",
            in = SecuritySchemeIn.HEADER
    )
    
    @Configuration
    public class SwaggerConfig {
    
     @Bean
    public GroupedOpenApi api() {
        return GroupedOpenApi.builder()
                .group("us/cargosphere/**")
                .pathsToMatch("/**")
                .packagesToExclude("/error.**")
                .build();
    }
    

    }