From caf2fbca85a3fdc28645e926d7713c322d7e2556 Mon Sep 17 00:00:00 2001 From: ulisseslima Date: Wed, 4 Dec 2024 19:09:27 -0300 Subject: [PATCH 1/4] bael 8916 - Automated Testing for OpenAPI Endpoints Using CATS --- .../spring-boot-springdoc-2/pom.xml | 7 +- .../com/baeldung/cats/CatsApplication.java | 21 +++++ .../config/DefaultSecurityHeadersFilter.java | 27 +++++++ .../baeldung/cats/config/JacksonConfig.java | 30 ++++++++ .../config/SimpleAuthenticationFilter.java | 55 ++++++++++++++ .../cats/config/ZeroWidthIntDeserializer.java | 16 ++++ .../config/ZeroWidthStringDeserializer.java | 16 ++++ .../cats/controller/ItemController.java | 75 ++++++++++++++++++ .../baeldung/cats/model/BadApiRequest.java | 41 ++++++++++ .../java/com/baeldung/cats/model/Item.java | 76 +++++++++++++++++++ .../baeldung/cats/service/ItemService.java | 55 ++++++++++++++ .../com/baeldung/cats/utils/RegexUtils.java | 17 +++++ .../cats/ItemControllerIntegrationTest.java | 34 +++++++++ .../com/baeldung/cats/CatsApplication.java | 30 ++++++++ .../dto/ItemDto.java | 57 ++++++++++++++ 15 files changed, 555 insertions(+), 2 deletions(-) create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/CatsApplication.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/DefaultSecurityHeadersFilter.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/JacksonConfig.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SimpleAuthenticationFilter.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/ZeroWidthIntDeserializer.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/ZeroWidthStringDeserializer.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/controller/ItemController.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/model/BadApiRequest.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/model/Item.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/service/ItemService.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/utils/RegexUtils.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/test/java/com/baeldung/cats/ItemControllerIntegrationTest.java create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/cats/CatsApplication.java create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ItemDto.java diff --git a/spring-boot-modules/spring-boot-springdoc-2/pom.xml b/spring-boot-modules/spring-boot-springdoc-2/pom.xml index d790c40b4acc..76fad86aed97 100644 --- a/spring-boot-modules/spring-boot-springdoc-2/pom.xml +++ b/spring-boot-modules/spring-boot-springdoc-2/pom.xml @@ -58,6 +58,9 @@ org.springframework.boot spring-boot-maven-plugin + + com.baeldung.springdoc.SpringdocApplication + org.apache.maven.plugins @@ -118,8 +121,8 @@ - 2.2.0 - 2.5.0 + 2.6.0 + 2.6.0 1.4 diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/CatsApplication.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/CatsApplication.java new file mode 100644 index 000000000000..d2a21ad26e27 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/CatsApplication.java @@ -0,0 +1,21 @@ +package com.baeldung.cats; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "Automated Testing for OpenAPI Endpoints Using CATS", version = "1.0.0"), security = { @SecurityRequirement(name = "Authorization") }) +@SecurityScheme(type = SecuritySchemeType.HTTP, name = "Authorization", in = SecuritySchemeIn.HEADER, scheme = "Bearer") +public class CatsApplication { + + public static void main(String[] args) { + SpringApplication.run(CatsApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/DefaultSecurityHeadersFilter.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/DefaultSecurityHeadersFilter.java new file mode 100644 index 000000000000..ff2e062b085b --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/DefaultSecurityHeadersFilter.java @@ -0,0 +1,27 @@ +package com.baeldung.cats.config; + +import java.io.IOException; + +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class DefaultSecurityHeadersFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { + if (res instanceof HttpServletResponse) { + HttpServletResponse response = res; + response.setHeader("Cache-Control", "no-store"); + response.setHeader("X-Content-Type-Options", "nosniff"); + response.setHeader("X-Frame-Options", "DENY"); + response.setHeader("Content-Security-Policy", "frame-ancestors 'none'"); + } + chain.doFilter(req, res); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/JacksonConfig.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/JacksonConfig.java new file mode 100644 index 000000000000..8131dae255df --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/JacksonConfig.java @@ -0,0 +1,30 @@ +package com.baeldung.cats.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonFactoryBuilder; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + JsonFactory factory = new JsonFactoryBuilder().streamReadConstraints(StreamReadConstraints.builder() + .maxStringLength(100) + .build()) + .build(); + + ObjectMapper mapper = new ObjectMapper(factory); + mapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + mapper.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT); + mapper.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES); + return mapper; + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SimpleAuthenticationFilter.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SimpleAuthenticationFilter.java new file mode 100644 index 000000000000..b4a9a04d9ebc --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SimpleAuthenticationFilter.java @@ -0,0 +1,55 @@ +package com.baeldung.cats.config; + +import java.io.IOException; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class SimpleAuthenticationFilter extends OncePerRequestFilter { + + private static final String TEST_TOKEN = "valid-token"; + private static final String BEARER = "Bearer "; + private static final List AUTHORIZE = List.of("swagger", "docs"); + + private final Logger log = LoggerFactory.getLogger(SimpleAuthenticationFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + String uri = request.getRequestURI(); + if (AUTHORIZE.stream() + .noneMatch(uri::contains)) { + String authorizationHeader = request.getHeader("Authorization"); + if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER)) { + accessDenied(response, uri); + return; + } + + String token = authorizationHeader.substring(BEARER.length()); + if (!valid(token)) { + accessDenied(response, uri); + return; + } + } + chain.doFilter(request, response); + } + + private void accessDenied(HttpServletResponse response, String uri) throws IOException { + log.warn("access denied to {}", uri); + response.setStatus(401); + response.getWriter() + .write("access denied"); + } + + private boolean valid(String token) { + return TEST_TOKEN.equals(token); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/ZeroWidthIntDeserializer.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/ZeroWidthIntDeserializer.java new file mode 100644 index 000000000000..b7c55a0cc680 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/ZeroWidthIntDeserializer.java @@ -0,0 +1,16 @@ +package com.baeldung.cats.config; + +import java.io.IOException; + +import com.baeldung.cats.utils.RegexUtils; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +public class ZeroWidthIntDeserializer extends JsonDeserializer { + + @Override + public Integer deserialize(JsonParser parser, DeserializationContext context) throws IOException { + return Integer.valueOf(RegexUtils.removeZeroWidthChars(parser.getText())); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/ZeroWidthStringDeserializer.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/ZeroWidthStringDeserializer.java new file mode 100644 index 000000000000..90d33c189e7c --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/ZeroWidthStringDeserializer.java @@ -0,0 +1,16 @@ +package com.baeldung.cats.config; + +import java.io.IOException; + +import com.baeldung.cats.utils.RegexUtils; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +public class ZeroWidthStringDeserializer extends JsonDeserializer { + + @Override + public String deserialize(JsonParser parser, DeserializationContext context) throws IOException { + return RegexUtils.removeZeroWidthChars(parser.getText()); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/controller/ItemController.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/controller/ItemController.java new file mode 100644 index 000000000000..5370ee29aab7 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/controller/ItemController.java @@ -0,0 +1,75 @@ +package com.baeldung.cats.controller; + +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.cats.model.BadApiRequest; +import com.baeldung.cats.model.Item; +import com.baeldung.cats.service.ItemService; + +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/v2/item") +@ApiResponse(responseCode = "401", description = "Unauthorized", content = { @Content(mediaType = MediaType.TEXT_PLAIN_VALUE, schema = @Schema(implementation = String.class)) }) +@ApiResponse(responseCode = "400", description = "Bad Request", content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = BadApiRequest.class)) }) +public class ItemController { + + private ItemService service; + + public ItemController(ItemService service) { + this.service = service; + } + + @PostMapping + @ApiResponse(responseCode = "200", description = "Success", content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = Item.class)) }) + public ResponseEntity post(@Valid @RequestBody Item item) { + service.insert(item); + return ResponseEntity.ok(item); + } + + @GetMapping + @ApiResponse(responseCode = "200", description = "Success", content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = Item.class))) }) + public ResponseEntity> get() { + return ResponseEntity.ok(service.findAll()); + } + + @GetMapping("/{id}") + @ApiResponse(responseCode = "200", description = "Success", content = { @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = Item.class)) }) + public ResponseEntity get(@PathVariable String id) { + try { + return ResponseEntity.ok(service.findById(id)); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(404) + .build(); + } + } + + @DeleteMapping("/{id}") + @ApiResponse(responseCode = "204", description = "Success") + public ResponseEntity delete(@PathVariable String id) { + try { + service.deleteById(id); + + return ResponseEntity.status(204) + .build(); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(404) + .build(); + } + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/model/BadApiRequest.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/model/BadApiRequest.java new file mode 100644 index 000000000000..148cb2815ea8 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/model/BadApiRequest.java @@ -0,0 +1,41 @@ +package com.baeldung.cats.model; + +public class BadApiRequest { + + private long timestamp; + private int status; + private String error; + private String path; + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/model/Item.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/model/Item.java new file mode 100644 index 000000000000..35bcbcbf701d --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/model/Item.java @@ -0,0 +1,76 @@ +package com.baeldung.cats.model; + +import java.util.Objects; + +import com.baeldung.cats.config.ZeroWidthIntDeserializer; +import com.baeldung.cats.config.ZeroWidthStringDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class Item { + + @Size(min = 37, max = 37) + @JsonDeserialize(using = ZeroWidthStringDeserializer.class) + private String id; + + @NotNull + @Size(min = 1, max = 20) + @JsonDeserialize(using = ZeroWidthStringDeserializer.class) + private String name; + + @Min(1) + @Max(100) + @NotNull + @JsonDeserialize(using = ZeroWidthIntDeserializer.class) + private int value; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + @Override + public String toString() { + return "Item [id=" + id + ", name=" + name + ", value=" + value + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Item other = (Item) obj; + return Objects.equals(id, other.id); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/service/ItemService.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/service/ItemService.java new file mode 100644 index 000000000000..cbcb0670f797 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/service/ItemService.java @@ -0,0 +1,55 @@ +package com.baeldung.cats.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.springframework.stereotype.Service; + +import com.baeldung.cats.model.Item; + +import jakarta.persistence.EntityNotFoundException; + +@Service +public class ItemService { + List db = new ArrayList<>(); + + public Item insert(Item item) { + if (item.getId() == null) { + item.setId(UUID.randomUUID() + .toString()); + } + db.add(item); + return item; + } + + public List findAll() { + return db; + } + + public List findAllByName(String name) { + return db.stream() + .filter(i -> i.getName() + .equals(name)) + .toList(); + } + + public Item findById(String id) { + return db.stream() + .filter(i -> i.getId() + .equals(id)) + .findFirst() + .orElseThrow(EntityNotFoundException::new); + } + + public void deleteById(String id) { + Item item = findById(id); + db.remove(item); + } + + public List deleteAllByName(String name) { + List allByName = findAllByName(name); + db.removeAll(allByName); + return allByName; + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/utils/RegexUtils.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/utils/RegexUtils.java new file mode 100644 index 000000000000..0b81aa470d88 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/utils/RegexUtils.java @@ -0,0 +1,17 @@ +package com.baeldung.cats.utils; + +import java.util.regex.Pattern; + +public class RegexUtils { + + private static final Pattern ZERO_WIDTH_PATTERN = Pattern.compile("[\u200B\u200C\u200D\u200F\u202B\u200E\uFEFF]"); + + private RegexUtils() { + } + + public static String removeZeroWidthChars(String value) { + return value == null ? null + : ZERO_WIDTH_PATTERN.matcher(value) + .replaceAll(""); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/test/java/com/baeldung/cats/ItemControllerIntegrationTest.java b/spring-boot-modules/spring-boot-springdoc-2/src/test/java/com/baeldung/cats/ItemControllerIntegrationTest.java new file mode 100644 index 000000000000..b1abd9bc8599 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/test/java/com/baeldung/cats/ItemControllerIntegrationTest.java @@ -0,0 +1,34 @@ +package com.baeldung.cats; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest +class ItemControllerIntegrationTest { + + static final String ITEM_API = "/api/v2/item"; + + MockMvc mvc; + + public ItemControllerIntegrationTest(WebApplicationContext wac) { + this.mvc = MockMvcBuilders.webAppContextSetup(wac) + .build(); + } + + @Test + void whenPostingItem_thenReturnContainsUuid() throws Exception { + mvc.perform(post(ITEM_API).contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"foo\", \"value\": 1}")) + .andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id") + .isNotEmpty()); + } +} diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/cats/CatsApplication.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/cats/CatsApplication.java new file mode 100644 index 000000000000..931441a252b3 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/cats/CatsApplication.java @@ -0,0 +1,30 @@ +package com.baeldung.cats; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "Automated Testing for OpenAPI Endpoints Using CATS", version = "1.0.0"), security = { @SecurityRequirement(name = "Authorization") }) +@SecurityScheme(type = SecuritySchemeType.HTTP, name = "Authorization", in = SecuritySchemeIn.HEADER, scheme = "Bearer") +public class CatsApplication { + + // @Bean + // public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // return http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.requestMatchers("/swagger-ui-custom.html", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", "/swagger-ui/index.html", "/api-docs/**") + // .permitAll() + // .anyRequest() + // .authenticated()) + // .build(); + // } + + public static void main(String[] args) { + SpringApplication.run(CatsApplication.class, args); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ItemDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ItemDto.java new file mode 100644 index 000000000000..d8f7daa09c5e --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ItemDto.java @@ -0,0 +1,57 @@ +package com.baeldung.defaultglobalsecurityscheme.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * LoginDto + */ + +@JsonTypeName("Token") +public class TokenDto { + + @JsonProperty("raw") + private String raw; + + @Schema(name = "raw", example = "app token") + public String getRaw() { + return raw; + } + + public void setRaw(String raw) { + this.raw = raw; + } + + @Override + public String toString() { + return "TokenDto [raw=" + raw + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((raw == null) ? 0 : raw.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TokenDto other = (TokenDto) obj; + if (raw == null) { + if (other.raw != null) + return false; + } else if (!raw.equals(other.raw)) + return false; + return true; + } + +} From 624bb54dcbd66b2969f3fb08fa1bcf966e35cc28 Mon Sep 17 00:00:00 2001 From: ulisseslima Date: Wed, 4 Dec 2024 19:17:04 -0300 Subject: [PATCH 2/4] removing unnecessary property --- spring-boot-modules/spring-boot-springdoc-2/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-springdoc-2/pom.xml b/spring-boot-modules/spring-boot-springdoc-2/pom.xml index fa8757105b0e..b1c2df103827 100644 --- a/spring-boot-modules/spring-boot-springdoc-2/pom.xml +++ b/spring-boot-modules/spring-boot-springdoc-2/pom.xml @@ -122,7 +122,6 @@ 2.6.0 - 2.6.0 1.4 From 0ced99f48b41c1f2ddeda72481e3025ff60cee8f Mon Sep 17 00:00:00 2001 From: ulisseslima Date: Wed, 4 Dec 2024 19:23:28 -0300 Subject: [PATCH 3/4] removing incorrect commits --- .../com/baeldung/cats/CatsApplication.java | 30 ---------- .../dto/ItemDto.java | 57 ------------------- 2 files changed, 87 deletions(-) delete mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/cats/CatsApplication.java delete mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ItemDto.java diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/cats/CatsApplication.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/cats/CatsApplication.java deleted file mode 100644 index 931441a252b3..000000000000 --- a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/cats/CatsApplication.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.baeldung.cats; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; -import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityScheme; - -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "Automated Testing for OpenAPI Endpoints Using CATS", version = "1.0.0"), security = { @SecurityRequirement(name = "Authorization") }) -@SecurityScheme(type = SecuritySchemeType.HTTP, name = "Authorization", in = SecuritySchemeIn.HEADER, scheme = "Bearer") -public class CatsApplication { - - // @Bean - // public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // return http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.requestMatchers("/swagger-ui-custom.html", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", "/swagger-ui/index.html", "/api-docs/**") - // .permitAll() - // .anyRequest() - // .authenticated()) - // .build(); - // } - - public static void main(String[] args) { - SpringApplication.run(CatsApplication.class, args); - } -} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ItemDto.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ItemDto.java deleted file mode 100644 index d8f7daa09c5e..000000000000 --- a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/defaultglobalsecurityscheme/dto/ItemDto.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.baeldung.defaultglobalsecurityscheme.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; - -import io.swagger.v3.oas.annotations.media.Schema; - -/** - * LoginDto - */ - -@JsonTypeName("Token") -public class TokenDto { - - @JsonProperty("raw") - private String raw; - - @Schema(name = "raw", example = "app token") - public String getRaw() { - return raw; - } - - public void setRaw(String raw) { - this.raw = raw; - } - - @Override - public String toString() { - return "TokenDto [raw=" + raw + "]"; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((raw == null) ? 0 : raw.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - TokenDto other = (TokenDto) obj; - if (raw == null) { - if (other.raw != null) - return false; - } else if (!raw.equals(other.raw)) - return false; - return true; - } - -} From d607c6c94828899bc62b6deae684ddd49838fd75 Mon Sep 17 00:00:00 2001 From: ulisseslima Date: Thu, 19 Dec 2024 15:46:55 -0300 Subject: [PATCH 4/4] review 2 * added spring security to module * added SecurityConfig to SpringDocApplication * added SecurityConfig to CatsApplication --- .../spring-boot-springdoc-2/pom.xml | 13 +++++ .../config/DefaultSecurityHeadersFilter.java | 27 --------- .../baeldung/cats/config/SecurityConfig.java | 47 ++++++++++++++++ .../config/SimpleAuthenticationFilter.java | 55 ------------------- .../springdoc/config/SecurityConfig.java | 21 +++++++ 5 files changed, 81 insertions(+), 82 deletions(-) delete mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/DefaultSecurityHeadersFilter.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SecurityConfig.java delete mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SimpleAuthenticationFilter.java create mode 100644 spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/springdoc/config/SecurityConfig.java diff --git a/spring-boot-modules/spring-boot-springdoc-2/pom.xml b/spring-boot-modules/spring-boot-springdoc-2/pom.xml index b1c2df103827..b8cfe30bf5ad 100644 --- a/spring-boot-modules/spring-boot-springdoc-2/pom.xml +++ b/spring-boot-modules/spring-boot-springdoc-2/pom.xml @@ -20,6 +20,19 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-oauth2-jose + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + org.springframework.boot spring-boot-starter-validation diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/DefaultSecurityHeadersFilter.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/DefaultSecurityHeadersFilter.java deleted file mode 100644 index ff2e062b085b..000000000000 --- a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/DefaultSecurityHeadersFilter.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.baeldung.cats.config; - -import java.io.IOException; - -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class DefaultSecurityHeadersFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { - if (res instanceof HttpServletResponse) { - HttpServletResponse response = res; - response.setHeader("Cache-Control", "no-store"); - response.setHeader("X-Content-Type-Options", "nosniff"); - response.setHeader("X-Frame-Options", "DENY"); - response.setHeader("Content-Security-Policy", "frame-ancestors 'none'"); - } - chain.doFilter(req, res); - } -} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SecurityConfig.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SecurityConfig.java new file mode 100644 index 000000000000..003a76eefe21 --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SecurityConfig.java @@ -0,0 +1,47 @@ +package com.baeldung.cats.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private static final String TEST_TOKEN = "valid-token"; + private static final String ACCESS_DENIED = "access denied"; + + private final Logger log = LoggerFactory.getLogger(SecurityConfig.class); + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(auth -> auth.requestMatchers("/swagger-ui-custom.html", "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", "/swagger-ui/index.html", "/api-docs/**") + .permitAll() + .anyRequest() + .authenticated()) + .oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder()))) + .build(); + } + + @Bean + JwtDecoder jwtDecoder() { + return token -> { + if (!TEST_TOKEN.equals(token)) { + log.warn(ACCESS_DENIED); + throw new JwtException(ACCESS_DENIED); + } + + return Jwt.withTokenValue(token) + .claim("token", TEST_TOKEN) + .header("token", TEST_TOKEN) + .build(); + }; + } +} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SimpleAuthenticationFilter.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SimpleAuthenticationFilter.java deleted file mode 100644 index b4a9a04d9ebc..000000000000 --- a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/cats/config/SimpleAuthenticationFilter.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.baeldung.cats.config; - -import java.io.IOException; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class SimpleAuthenticationFilter extends OncePerRequestFilter { - - private static final String TEST_TOKEN = "valid-token"; - private static final String BEARER = "Bearer "; - private static final List AUTHORIZE = List.of("swagger", "docs"); - - private final Logger log = LoggerFactory.getLogger(SimpleAuthenticationFilter.class); - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - String uri = request.getRequestURI(); - if (AUTHORIZE.stream() - .noneMatch(uri::contains)) { - String authorizationHeader = request.getHeader("Authorization"); - if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER)) { - accessDenied(response, uri); - return; - } - - String token = authorizationHeader.substring(BEARER.length()); - if (!valid(token)) { - accessDenied(response, uri); - return; - } - } - chain.doFilter(request, response); - } - - private void accessDenied(HttpServletResponse response, String uri) throws IOException { - log.warn("access denied to {}", uri); - response.setStatus(401); - response.getWriter() - .write("access denied"); - } - - private boolean valid(String token) { - return TEST_TOKEN.equals(token); - } -} diff --git a/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/springdoc/config/SecurityConfig.java b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/springdoc/config/SecurityConfig.java new file mode 100644 index 000000000000..327785520b7e --- /dev/null +++ b/spring-boot-modules/spring-boot-springdoc-2/src/main/java/com/baeldung/springdoc/config/SecurityConfig.java @@ -0,0 +1,21 @@ +package com.baeldung.springdoc.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth.anyRequest() + .permitAll()) + .build(); + } +}