Skip to content

Commit

Permalink
add openapi security scheme configuration, cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
farrell-m committed Jun 5, 2024
1 parent a862d2f commit b2f8726
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,29 +16,29 @@ 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;

/**
* The list of clients who are authorized to access the API, and their roles
* 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;

/**
* The list of roles that can be used to access the API, and the URIs they enable access to.
* 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;

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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");
}

}

0 comments on commit b2f8726

Please sign in to comment.