diff --git a/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/build.gradle b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/build.gradle index cf82a0f..0776a33 100644 --- a/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/build.gradle +++ b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/build.gradle @@ -11,6 +11,8 @@ dependencies { transitive = false } + implementation 'io.swagger.core.v3:swagger-models:2.2.22' + implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'jakarta.servlet:jakarta.servlet-api' @@ -21,6 +23,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' + + testImplementation 'org.assertj:assertj-core:3.4.1' } publishing.publications { diff --git a/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/ApiAuthenticationFilter.java b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/ApiAuthenticationFilter.java index c26fbc3..008a85f 100644 --- a/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/ApiAuthenticationFilter.java +++ b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/ApiAuthenticationFilter.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; @@ -11,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.GenericFilterBean; @@ -41,17 +43,18 @@ protected ApiAuthenticationFilter(ApiAuthenticationService authenticationService * @param response the http response object * @param filterChain the current filter chain * @throws IOException - + * @throws ServletException - */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) - throws IOException { + throws IOException, ServletException { try { Authentication authentication = authenticationService.getAuthentication((HttpServletRequest) request); SecurityContextHolder.getContext().setAuthentication(authentication); log.info("Endpoint '{} {}' requested by {}.", ((HttpServletRequest) request).getMethod(), ((HttpServletRequest) request).getRequestURI(), authentication.getPrincipal().toString()); filterChain.doFilter(request, response); - } catch (Exception ex) { + } catch (AuthenticationException ex) { int code = HttpServletResponse.SC_UNAUTHORIZED; HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(code); diff --git a/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/AuthenticationProperties.java b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/AuthenticationProperties.java index 8b7712e..611f483 100644 --- a/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/AuthenticationProperties.java +++ b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/AuthenticationProperties.java @@ -1,6 +1,6 @@ package uk.gov.laa.ccms.springboot.auth; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -16,7 +16,7 @@ public class AuthenticationProperties { /** * The name of the HTTP header used to store the API access token. */ - @NotNull(message = "authenticationHeader is required") + @NotBlank(message = "authenticationHeader is required") private String authenticationHeader; /** @@ -24,7 +24,7 @@ public class AuthenticationProperties { * JSON formatted string, with the top level being a list and each contained item * representing a {@link ClientCredential}. */ - @NotNull(message = "authorizedClients is required") + @NotBlank(message = "authorizedClients is required") private String authorizedClients; /** @@ -32,13 +32,13 @@ public class AuthenticationProperties { * JSON formatted string, with the top level being a list and each contained item representing * an {@link AuthorizedRole}. */ - @NotNull(message = "authorizedRoles is required") + @NotBlank(message = "authorizedRoles is required") private String authorizedRoles; /** * The list of URIs which do not require any authentication. */ - @NotNull(message = "unprotectedURIs is required") + @NotBlank(message = "unprotectedURIs is required") private String[] unprotectedURIs; } diff --git a/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/config/OpenApiConfiguration.java b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/config/OpenApiConfiguration.java new file mode 100644 index 0000000..38c2824 --- /dev/null +++ b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/config/OpenApiConfiguration.java @@ -0,0 +1,39 @@ +package uk.gov.laa.ccms.springboot.auth.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +@ConditionalOnProperty(value = "laa.ccms.springboot.starter.open-api.security-scheme.enabled", matchIfMissing = true) +public class OpenApiConfiguration { + + @Value("${laa.ccms.springboot.starter.auth.authentication-header}") + String authenticationHeader; + + @Bean + public OpenAPI openAPI() { + String securitySchemeName = "ApiKeyAuth"; + OpenAPI openApiSpec = new OpenAPI() + .components( + new Components() + .addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name(authenticationHeader))) + .addSecurityItem( + new SecurityRequirement() + .addList(securitySchemeName)); + log.info("OpenAPI Security Scheme '{}' added for all endpoints.", securitySchemeName); + return openApiSpec; + } + +} diff --git a/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/test/java/uk/gov/laa/ccms/springboot/auth/config/OpenApiConfigurationTest.java b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/test/java/uk/gov/laa/ccms/springboot/auth/config/OpenApiConfigurationTest.java new file mode 100644 index 0000000..f34b8ff --- /dev/null +++ b/laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/src/test/java/uk/gov/laa/ccms/springboot/auth/config/OpenApiConfigurationTest.java @@ -0,0 +1,59 @@ +package uk.gov.laa.ccms.springboot.auth.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OpenApiConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withBean(OpenApiConfiguration.class) + .withPropertyValues("laa.ccms.springboot.starter.auth.authentication-header=Authorization"); + + private final String OPEN_API_CONFIGURATION_BEAN = "openApiConfiguration"; + private final String OPEN_API_BEAN = "openAPI"; + private final String SECURITY_SCHEME_NAME = "ApiKeyAuth"; + + @Test + void testOpenApiBeanIsCreatedWhenApplicationPropertyOmitted() { + contextRunner.run((context) -> { + assertThat(context).hasBean(OPEN_API_CONFIGURATION_BEAN); + assertSecuritySchemeApplied(context); + }); + } + + @Test + void testOpenApiBeanIsCreatedWhenApplicationPropertyEnabled() { + contextRunner.withPropertyValues("laa.ccms.springboot.starter.open-api.security-scheme.enabled=true").run((context) -> { + assertThat(context).hasBean(OPEN_API_CONFIGURATION_BEAN); + assertSecuritySchemeApplied(context); + }); + } + + @Test + void testNoOpenApiBeanIsCreatedWhenApplicationPropertyDisabled() { + contextRunner.withPropertyValues("laa.ccms.springboot.starter.open-api.security-scheme.enabled=false").run((context) -> { + assertThat(context).doesNotHaveBean(OPEN_API_CONFIGURATION_BEAN); + assertThat(context).doesNotHaveBean(OPEN_API_BEAN); + }); + } + + private void assertSecuritySchemeApplied(AssertableApplicationContext context) { + OpenAPI openApiSpec = context.getBean(OPEN_API_BEAN, OpenAPI.class); + assertThat(openApiSpec.getComponents().getSecuritySchemes()).isEqualTo(Map.of(SECURITY_SCHEME_NAME, getSecurityScheme())); + } + + private SecurityScheme getSecurityScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name("Authorization"); + } + +}