diff --git a/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityApplicationService.java b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityApplicationService.java new file mode 100644 index 00000000000..747ebdc25c8 --- /dev/null +++ b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityApplicationService.java @@ -0,0 +1,19 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.application; + +import org.springframework.stereotype.Service; +import tech.jhipster.light.generator.project.domain.Project; +import tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain.JwtSecurityService; + +@Service +public class JwtSecurityApplicationService { + + private final JwtSecurityService jwtSecurityService; + + public JwtSecurityApplicationService(JwtSecurityService jwtSecurityService) { + this.jwtSecurityService = jwtSecurityService; + } + + public void initBasicAuth(Project project) { + jwtSecurityService.initBasicAuth(project); + } +} diff --git a/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurity.java b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurity.java new file mode 100644 index 00000000000..1b954fc65b2 --- /dev/null +++ b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurity.java @@ -0,0 +1,85 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain; + +import java.util.HashMap; +import java.util.Map; +import tech.jhipster.light.generator.buildtool.generic.domain.Dependency; + +public class JwtSecurity { + + private static final String JJWT_VERSION = "0.11.2"; + private static final String infrastructureConfig = "infrastructure/config"; + private static final String infrastructureRest = "infrastructure/primary/rest"; + + public static final String annotationProcessorPatch = "jwt-security-annotation-processor.patch"; + public static final String exceptionTranslatorPatch = "jwt-security-exception-translator.patch"; + + private JwtSecurity() {} + + public static String jjwtVersion() { + return JJWT_VERSION; + } + + public static Dependency springBootStarterSecurityDependency() { + return Dependency.builder().groupId("org.springframework.boot").artifactId("spring-boot-starter-security").build(); + } + + public static Dependency springBootConfigurationProcessor() { + return Dependency + .builder() + .groupId("org.springframework.boot") + .artifactId("spring-boot-configuration-processor") + .scope("provided") + .build(); + } + + public static Dependency jjwtApiDependency() { + return Dependency.builder().groupId("io.jsonwebtoken").artifactId("jjwt-api").version("\\${jjwt.version}").build(); + } + + public static Dependency jjwtImplDependency() { + return Dependency.builder().groupId("io.jsonwebtoken").artifactId("jjwt-impl").version("\\${jjwt.version}").scope("runtime").build(); + } + + public static Dependency jjwtJacksonDependency() { + return Dependency.builder().groupId("io.jsonwebtoken").artifactId("jjwt-jackson").version("\\${jjwt.version}").scope("runtime").build(); + } + + public static Dependency springSecurityTestDependency() { + return Dependency.builder().groupId("org.springframework.security").artifactId("spring-security-test").scope("test").build(); + } + + public static Map jwtSecurityFiles() { + Map map = new HashMap<>(); + + map.put("AuthoritiesConstants.java", "domain"); + + map.put("ApplicationSecurityDefaults.java", infrastructureConfig); + map.put("ApplicationSecurityProperties.java", infrastructureConfig); + map.put("CorsFilterConfiguration.java", infrastructureConfig); + map.put("CorsProperties.java", infrastructureConfig); + map.put("JWTConfigurer.java", infrastructureConfig); + map.put("JWTFilter.java", infrastructureConfig); + map.put("SecurityConfiguration.java", infrastructureConfig); + map.put("SecurityExceptionTranslator.java", infrastructureConfig); + map.put("TokenProvider.java", infrastructureConfig); + + map.put("AuthenticationResource.java", infrastructureRest); + map.put("LoginDTO.java", infrastructureRest); + + return map; + } + + public static Map jwtTestSecurityFiles() { + Map map = new HashMap<>(); + + map.put("ApplicationSecurityPropertiesTest.java", infrastructureConfig); + map.put("CorsFilterConfigurationIT.java", infrastructureConfig); + map.put("JWTFilterTest.java", infrastructureConfig); + map.put("TokenProviderTest.java", infrastructureConfig); + + map.put("AuthenticationResourceIT.java", infrastructureRest); + map.put("LoginDTOTest.java", infrastructureRest); + + return map; + } +} diff --git a/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityDomainService.java b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityDomainService.java new file mode 100644 index 00000000000..bd2eb3d97db --- /dev/null +++ b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityDomainService.java @@ -0,0 +1,127 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain; + +import static tech.jhipster.light.common.domain.FileUtils.getPath; +import static tech.jhipster.light.generator.project.domain.Constants.MAIN_JAVA; +import static tech.jhipster.light.generator.project.domain.Constants.TEST_JAVA; +import static tech.jhipster.light.generator.project.domain.DefaultConfig.PACKAGE_NAME; +import static tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain.JwtSecurity.*; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import org.eclipse.jgit.api.errors.GitAPIException; +import tech.jhipster.light.error.domain.GeneratorException; +import tech.jhipster.light.generator.buildtool.generic.domain.BuildToolService; +import tech.jhipster.light.generator.project.domain.Project; +import tech.jhipster.light.generator.project.domain.ProjectRepository; +import tech.jhipster.light.generator.project.infrastructure.secondary.GitUtils; +import tech.jhipster.light.generator.server.springboot.properties.domain.SpringBootPropertiesService; + +public class JwtSecurityDomainService implements JwtSecurityService { + + public static final String SOURCE = "server/springboot/mvc/security/jwt"; + public static final String SECURITY_JWT_PATH = "security/jwt"; + + private final ProjectRepository projectRepository; + private final BuildToolService buildToolService; + private final SpringBootPropertiesService springBootPropertiesService; + + public JwtSecurityDomainService( + ProjectRepository projectRepository, + BuildToolService buildToolService, + SpringBootPropertiesService springBootPropertiesService + ) { + this.projectRepository = projectRepository; + this.buildToolService = buildToolService; + this.springBootPropertiesService = springBootPropertiesService; + } + + @Override + public void initBasicAuth(Project project) { + projectRepository.gitInit(project); + + addPropertyAndDependency(project); + addJavaFiles(project); + addProperties(project); + addGitPatch(project); + applyGitPatchAnnotationProcessor(project); + applyGitPatchExceptionTranslator(project); + } + + private void applyGitPatchAnnotationProcessor(Project project) { + projectRepository.gitApplyPatch(project, getPath(project.getFolder(), ".jhipster", annotationProcessorPatch)); + } + + private void applyGitPatchExceptionTranslator(Project project) { + projectRepository.gitApplyPatch(project, getPath(project.getFolder(), ".jhipster", exceptionTranslatorPatch)); + } + + private void addGitPatch(Project project) { + project.addDefaultConfig(PACKAGE_NAME); + String packageNamePath = project.getPackageNamePath().orElse(getPath("com/mycompany/myapp")); + project.addConfig("packageNamePath", packageNamePath); + + projectRepository.add(project, SOURCE, annotationProcessorPatch, ".jhipster"); + projectRepository.template(project, SOURCE, exceptionTranslatorPatch, ".jhipster"); + } + + private void addPropertyAndDependency(Project project) { + buildToolService.addProperty(project, "jjwt", jjwtVersion()); + + buildToolService.addDependency(project, springBootStarterSecurityDependency()); + buildToolService.addDependency(project, springBootConfigurationProcessor()); + buildToolService.addDependency(project, jjwtApiDependency()); + buildToolService.addDependency(project, jjwtImplDependency()); + buildToolService.addDependency(project, jjwtJacksonDependency()); + + buildToolService.addDependency(project, springSecurityTestDependency()); + } + + private void addJavaFiles(Project project) { + project.addDefaultConfig(PACKAGE_NAME); + + String sourceSrc = getPath(SOURCE, "src"); + String sourceTest = getPath(SOURCE, "test"); + String packageNamePath = project.getPackageNamePath().orElse(getPath("com/mycompany/myapp")); + String destinationSrc = getPath(MAIN_JAVA, packageNamePath, SECURITY_JWT_PATH); + String destinationTest = getPath(TEST_JAVA, packageNamePath, SECURITY_JWT_PATH); + + jwtSecurityFiles() + .forEach((javaFile, destination) -> projectRepository.template(project, sourceSrc, javaFile, getPath(destinationSrc, destination))); + + jwtTestSecurityFiles() + .forEach((javaFile, destination) -> projectRepository.template(project, sourceTest, javaFile, getPath(destinationTest, destination))); + } + + private void addProperties(Project project) { + String baseName = project.getBaseName().orElse("jhipster"); + + jwtProperties(baseName) + .forEach((k, v) -> { + springBootPropertiesService.addProperties(project, k, v); + springBootPropertiesService.addPropertiesTest(project, k, v); + }); + } + + private Map jwtProperties(String baseName) { + Map result = new LinkedHashMap<>(); + result.put("spring.security.user.name", "admin"); + result.put("spring.security.user.password", "\\$2a\\$12\\$cRKS9ZURbdJIaRsKDTDUmOrH4.B.2rokv8rrkrQXr2IR2Hkna484O"); + result.put( + "application.security.authentication.jwt.base64-secret", + "bXktc2VjcmV0LWtleS13aGljaC1zaG91bGQtYmUtY2hhbmdlZC1pbi1wcm9kdWN0aW9uLWFuZC1iZS1iYXNlNjQtZW5jb2RlZAo=" + ); + result.put("application.security.authentication.jwt.token-validity-in-seconds", "86400"); + result.put("application.security.authentication.jwt.token-validity-in-seconds-for-remember-me", "2592000"); + result.put("application.cors.allowed-origins", "http://localhost:8100,http://localhost:9000"); + result.put("application.cors.allowed-methods", "*"); + result.put("application.cors.allowed-headers", "*"); + result.put( + "application.cors.exposed-headers", + "Authorization,Link,X-Total-Count,X-" + baseName + "-alert,X-" + baseName + "-error,X-" + baseName + "-params" + ); + result.put("application.cors.allow-credentials", "true"); + result.put("application.cors.max-age", "1800"); + return result; + } +} diff --git a/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityService.java b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityService.java new file mode 100644 index 00000000000..ef665a889c7 --- /dev/null +++ b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityService.java @@ -0,0 +1,7 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain; + +import tech.jhipster.light.generator.project.domain.Project; + +public interface JwtSecurityService { + void initBasicAuth(Project project); +} diff --git a/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/config/JwtSecurityBeanConfiguration.java b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/config/JwtSecurityBeanConfiguration.java new file mode 100644 index 00000000000..8af3829c306 --- /dev/null +++ b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/config/JwtSecurityBeanConfiguration.java @@ -0,0 +1,32 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.infrastructure.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.jhipster.light.generator.buildtool.generic.domain.BuildToolService; +import tech.jhipster.light.generator.project.domain.ProjectRepository; +import tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain.JwtSecurityDomainService; +import tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain.JwtSecurityService; +import tech.jhipster.light.generator.server.springboot.properties.domain.SpringBootPropertiesService; + +@Configuration +public class JwtSecurityBeanConfiguration { + + private final ProjectRepository projectRepository; + private final BuildToolService buildToolService; + private final SpringBootPropertiesService springBootPropertiesService; + + public JwtSecurityBeanConfiguration( + ProjectRepository projectRepository, + BuildToolService buildToolService, + SpringBootPropertiesService springBootPropertiesService + ) { + this.projectRepository = projectRepository; + this.buildToolService = buildToolService; + this.springBootPropertiesService = springBootPropertiesService; + } + + @Bean + public JwtSecurityService jwtSecurityService() { + return new JwtSecurityDomainService(projectRepository, buildToolService, springBootPropertiesService); + } +} diff --git a/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/rest/JwtSecurityResource.java b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/rest/JwtSecurityResource.java new file mode 100644 index 00000000000..83f99d760ca --- /dev/null +++ b/src/main/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/rest/JwtSecurityResource.java @@ -0,0 +1,30 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.infrastructure.rest; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; +import tech.jhipster.light.generator.project.domain.Project; +import tech.jhipster.light.generator.project.infrastructure.primary.dto.ProjectDTO; +import tech.jhipster.light.generator.server.springboot.mvc.security.jwt.application.JwtSecurityApplicationService; + +@RestController +@RequestMapping("/api/servers/spring-boot/mvc/security") +@Tag(name = "Spring Boot - MVC") +class JwtSecurityResource { + + private final JwtSecurityApplicationService jwtSecurityApplicationService; + + public JwtSecurityResource(JwtSecurityApplicationService jwtSecurityApplicationService) { + this.jwtSecurityApplicationService = jwtSecurityApplicationService; + } + + @Operation(summary = "Add Spring Security JWT with Basic Auth") + @ApiResponses({ @ApiResponse(responseCode = "500", description = "An error occurred while adding Spring Security JWT with Basic Auth") }) + @PostMapping("/jwt") + public void initBasicAuth(@RequestBody ProjectDTO projectDTO) { + Project project = ProjectDTO.toProject(projectDTO); + jwtSecurityApplicationService.initBasicAuth(project); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/jwt-security-annotation-processor.patch b/src/main/resources/template/server/springboot/mvc/security/jwt/jwt-security-annotation-processor.patch new file mode 100644 index 00000000000..728f4b230d6 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/jwt-security-annotation-processor.patch @@ -0,0 +1,18 @@ +diff --git a/pom.xml b/pom.xml +index c9a8342..387cd3c 100644 +--- a/pom.xml ++++ b/pom.xml +@@ -146,6 +146,13 @@ + + ${java.version} + ${java.version} ++ ++ ++ org.springframework.boot ++ spring-boot-configuration-processor ++ ${spring-boot.version} ++ ++ + + + diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/jwt-security-exception-translator.patch.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/jwt-security-exception-translator.patch.mustache new file mode 100644 index 00000000000..3e055ee330f --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/jwt-security-exception-translator.patch.mustache @@ -0,0 +1,20 @@ +diff --git a/src/test/java/{{packageNamePath}}/technical/infrastructure/primary/exception/ExceptionTranslatorIT.java b/src/test/java/{{packageNamePath}}/technical/infrastructure/primary/exception/ExceptionTranslatorIT.java +index 42f0c99..426c3e1 100644 +--- a/src/test/java/{{packageNamePath}}/technical/infrastructure/primary/exception/ExceptionTranslatorIT.java ++++ b/src/test/java/{{packageNamePath}}/technical/infrastructure/primary/exception/ExceptionTranslatorIT.java +@@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; + import org.springframework.http.MediaType; ++import org.springframework.security.test.context.support.WithMockUser; + import org.springframework.test.util.ReflectionTestUtils; + import org.springframework.test.web.servlet.MockMvc; + import {{packageName}}.IntegrationTest; +@@ -18,6 +19,7 @@ import {{packageName}}.IntegrationTest; + */ + @IntegrationTest + @AutoConfigureMockMvc ++@WithMockUser + class ExceptionTranslatorIT { + + @Autowired diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/ApplicationSecurityDefaults.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/ApplicationSecurityDefaults.java.mustache new file mode 100644 index 00000000000..507bff06ce0 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/ApplicationSecurityDefaults.java.mustache @@ -0,0 +1,18 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +@SuppressWarnings("java:S2386") +public interface ApplicationSecurityDefaults { + interface Security { + String contentSecurityPolicy = + "default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:"; + + interface Authentication { + interface Jwt { + String secret = null; + String base64Secret = null; + long tokenValidityInSeconds = 1800; // 30 minutes + long tokenValidityInSecondsForRememberMe = 2592000; // 30 days + } + } + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/ApplicationSecurityProperties.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/ApplicationSecurityProperties.java.mustache new file mode 100644 index 00000000000..e68a24aa3d8 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/ApplicationSecurityProperties.java.mustache @@ -0,0 +1,78 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "application.security", ignoreUnknownFields = false) +public class ApplicationSecurityProperties { + + private String contentSecurityPolicy = ApplicationSecurityDefaults.Security.contentSecurityPolicy; + + private final Authentication authentication = new Authentication(); + + public Authentication getAuthentication() { + return authentication; + } + + public String getContentSecurityPolicy() { + return contentSecurityPolicy; + } + + public void setContentSecurityPolicy(String contentSecurityPolicy) { + this.contentSecurityPolicy = contentSecurityPolicy; + } + + public static class Authentication { + + private final Jwt jwt = new Jwt(); + + public Jwt getJwt() { + return jwt; + } + + public static class Jwt { + + private String secret = ApplicationSecurityDefaults.Security.Authentication.Jwt.secret; + + private String base64Secret = ApplicationSecurityDefaults.Security.Authentication.Jwt.base64Secret; + + private long tokenValidityInSeconds = ApplicationSecurityDefaults.Security.Authentication.Jwt.tokenValidityInSeconds; + + private long tokenValidityInSecondsForRememberMe = + ApplicationSecurityDefaults.Security.Authentication.Jwt.tokenValidityInSecondsForRememberMe; + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public String getBase64Secret() { + return base64Secret; + } + + public void setBase64Secret(String base64Secret) { + this.base64Secret = base64Secret; + } + + public long getTokenValidityInSeconds() { + return tokenValidityInSeconds; + } + + public void setTokenValidityInSeconds(long tokenValidityInSeconds) { + this.tokenValidityInSeconds = tokenValidityInSeconds; + } + + public long getTokenValidityInSecondsForRememberMe() { + return tokenValidityInSecondsForRememberMe; + } + + public void setTokenValidityInSecondsForRememberMe(long tokenValidityInSecondsForRememberMe) { + this.tokenValidityInSecondsForRememberMe = tokenValidityInSecondsForRememberMe; + } + } + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/AuthenticationResource.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/AuthenticationResource.java.mustache new file mode 100644 index 00000000000..7273928a165 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/AuthenticationResource.java.mustache @@ -0,0 +1,75 @@ +package {{packageName}}.security.jwt.infrastructure.primary.rest; + +import com.fasterxml.jackson.annotation.JsonProperty; +import {{packageName}}.security.jwt.infrastructure.config.JWTFilter; +import {{packageName}}.security.jwt.infrastructure.config.TokenProvider; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/authenticate") +class AuthenticationResource { + + private final Logger log = LoggerFactory.getLogger(AuthenticationResource.class); + + private final TokenProvider tokenProvider; + + private final AuthenticationManagerBuilder authenticationManagerBuilder; + + public AuthenticationResource(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) { + this.tokenProvider = tokenProvider; + this.authenticationManagerBuilder = authenticationManagerBuilder; + } + + @GetMapping + public String isAuthenticated(HttpServletRequest request) { + log.debug("REST request to check if the current user is authenticated"); + return request.getRemoteUser(); + } + + @PostMapping + public ResponseEntity authorize(@Valid @RequestBody LoginDTO loginDTO) { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginDTO.getUsername(), + loginDTO.getPassword() + ); + + Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = tokenProvider.createToken(authentication, loginDTO.isRememberMe()); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(JWTFilter.AUTHORIZATION_HEADER, "Bearer " + jwt); + return new ResponseEntity<>(new JWTToken(jwt), httpHeaders, HttpStatus.OK); + } + + /** + * Object to return as body in JWT Authentication. + */ + static class JWTToken { + + private String idToken; + + JWTToken(String idToken) { + this.idToken = idToken; + } + + @JsonProperty("id_token") + String getIdToken() { + return idToken; + } + + void setIdToken(String idToken) { + this.idToken = idToken; + } + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/AuthoritiesConstants.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/AuthoritiesConstants.java.mustache new file mode 100644 index 00000000000..5f54bbd96e5 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/AuthoritiesConstants.java.mustache @@ -0,0 +1,15 @@ +package {{packageName}}.security.jwt.domain; + +/** + * Constants for Spring Security authorities. + */ +public final class AuthoritiesConstants { + + public static final String ADMIN = "ROLE_ADMIN"; + + public static final String USER = "ROLE_USER"; + + public static final String ANONYMOUS = "ROLE_ANONYMOUS"; + + private AuthoritiesConstants() {} +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/CorsFilterConfiguration.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/CorsFilterConfiguration.java.mustache new file mode 100644 index 00000000000..93d077370c1 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/CorsFilterConfiguration.java.mustache @@ -0,0 +1,40 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.CollectionUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsFilterConfiguration { + + private final Logger log = LoggerFactory.getLogger(CorsFilterConfiguration.class); + + private final CorsConfiguration corsConfiguration; + + public CorsFilterConfiguration(CorsConfiguration corsConfiguration) { + this.corsConfiguration = corsConfiguration; + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + if ( + !CollectionUtils.isEmpty(corsConfiguration.getAllowedOrigins()) || + !CollectionUtils.isEmpty(corsConfiguration.getAllowedOriginPatterns()) + ) { + log.debug("Registering CORS filter"); + source.registerCorsConfiguration("/api/**", corsConfiguration); + source.registerCorsConfiguration("/management/**", corsConfiguration); + source.registerCorsConfiguration("/v2/api-docs", corsConfiguration); + source.registerCorsConfiguration("/v3/api-docs", corsConfiguration); + source.registerCorsConfiguration("/swagger-resources", corsConfiguration); + source.registerCorsConfiguration("/swagger-ui/**", corsConfiguration); + } + return new CorsFilter(source); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/CorsProperties.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/CorsProperties.java.mustache new file mode 100644 index 00000000000..f480cc1a645 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/CorsProperties.java.mustache @@ -0,0 +1,16 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; + +@Configuration +public class CorsProperties { + + @Bean + @ConfigurationProperties(prefix = "application.cors", ignoreUnknownFields = false) + public CorsConfiguration corsConfiguration() { + return new CorsConfiguration(); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/JWTConfigurer.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/JWTConfigurer.java.mustache new file mode 100644 index 00000000000..38cf72843e9 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/JWTConfigurer.java.mustache @@ -0,0 +1,21 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class JWTConfigurer extends SecurityConfigurerAdapter { + + private final TokenProvider tokenProvider; + + public JWTConfigurer(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void configure(HttpSecurity http) { + JWTFilter customFilter = new JWTFilter(tokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/JWTFilter.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/JWTFilter.java.mustache new file mode 100644 index 00000000000..1849b29b7f7 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/JWTFilter.java.mustache @@ -0,0 +1,47 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +/** + * Filters incoming requests and installs a Spring Security principal if a header corresponding to a valid user is + * found. + */ +public class JWTFilter extends GenericFilterBean { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + private final TokenProvider tokenProvider; + + public JWTFilter(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + String jwt = resolveToken(httpServletRequest); + if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) { + Authentication authentication = this.tokenProvider.getAuthentication(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(servletRequest, servletResponse); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/LoginDTO.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/LoginDTO.java.mustache new file mode 100644 index 00000000000..5093c120a66 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/LoginDTO.java.mustache @@ -0,0 +1,50 @@ +package {{packageName}}.security.jwt.infrastructure.primary.rest; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +public class LoginDTO { + + @NotNull + @Size(min = 1, max = 50) + private String username; + + @NotNull + @Size(min = 4, max = 100) + private String password; + + private boolean rememberMe; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isRememberMe() { + return rememberMe; + } + + public void setRememberMe(boolean rememberMe) { + this.rememberMe = rememberMe; + } + + // prettier-ignore + @Override + public String toString() { + return "LoginDTO{" + + "username='" + username + '\'' + + ", rememberMe=" + rememberMe + + '}'; + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/SecurityConfiguration.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/SecurityConfiguration.java.mustache new file mode 100644 index 00000000000..7e07cb463b4 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/SecurityConfiguration.java.mustache @@ -0,0 +1,107 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import {{packageName}}.security.jwt.domain.AuthoritiesConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; +import org.springframework.web.filter.CorsFilter; +import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport; + +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +@Import(SecurityProblemSupport.class) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + private final ApplicationSecurityProperties applicationSecurityProperties; + private final TokenProvider tokenProvider; + private final CorsFilter corsFilter; + private final SecurityProblemSupport problemSupport; + + public SecurityConfiguration( + TokenProvider tokenProvider, + CorsFilter corsFilter, + ApplicationSecurityProperties applicationSecurityProperties, + SecurityProblemSupport problemSupport + ) { + this.tokenProvider = tokenProvider; + this.corsFilter = corsFilter; + this.problemSupport = problemSupport; + this.applicationSecurityProperties = applicationSecurityProperties; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + public void configure(WebSecurity web) { + web + .ignoring() + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/app/**/*.{js,html}") + .antMatchers("/i18n/**") + .antMatchers("/content/**") + .antMatchers("/h2-console/**") + .antMatchers("/swagger-ui/**") + .antMatchers("/test/**"); + } + + @Override + public void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf() + .disable() + .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling() + .authenticationEntryPoint(problemSupport) + .accessDeniedHandler(problemSupport) + .and() + .headers() + .contentSecurityPolicy(applicationSecurityProperties.getContentSecurityPolicy()) + .and() + .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN) + .and() + .featurePolicy("geolocation 'none'; midi 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; fullscreen 'self'; payment 'none'") + .and() + .frameOptions() + .deny() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/api/authenticate").permitAll() + .antMatchers("/api/register").permitAll() + .antMatchers("/api/activate").permitAll() + .antMatchers("/api/account/reset-password/init").permitAll() + .antMatchers("/api/account/reset-password/finish").permitAll() + .antMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN) + .antMatchers("/api/**").authenticated() + .antMatchers("/management/health").permitAll() + .antMatchers("/management/health/**").permitAll() + .antMatchers("/management/info").permitAll() + .antMatchers("/management/prometheus").permitAll() + .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) + .and() + .httpBasic() + .and() + .apply(securityConfigurerAdapter()); + // @formatter:on + } + + private JWTConfigurer securityConfigurerAdapter() { + return new JWTConfigurer(tokenProvider); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/SecurityExceptionTranslator.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/SecurityExceptionTranslator.java.mustache new file mode 100644 index 00000000000..c3b76e442e6 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/SecurityExceptionTranslator.java.mustache @@ -0,0 +1,8 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import {{packageName}}.technical.infrastructure.primary.exception.ExceptionTranslator; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.zalando.problem.spring.web.advice.security.SecurityAdviceTrait; + +@ControllerAdvice +public class SecurityExceptionTranslator extends ExceptionTranslator implements SecurityAdviceTrait {} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/src/TokenProvider.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/src/TokenProvider.java.mustache new file mode 100644 index 00000000000..98ece65a1da --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/src/TokenProvider.java.mustache @@ -0,0 +1,102 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +@Component +public class TokenProvider { + + private final Logger log = LoggerFactory.getLogger(TokenProvider.class); + + private static final String AUTHORITIES_KEY = "auth"; + + private final Key key; + + private final JwtParser jwtParser; + + private final long tokenValidityInMilliseconds; + + private final long tokenValidityInMillisecondsForRememberMe; + + public TokenProvider(ApplicationSecurityProperties applicationSecurityProperties) { + byte[] keyBytes; + String secret = applicationSecurityProperties.getAuthentication().getJwt().getBase64Secret(); + if (!ObjectUtils.isEmpty(secret)) { + log.debug("Using a Base64-encoded JWT secret key"); + keyBytes = Decoders.BASE64.decode(secret); + } else { + log.warn( + "Warning: the JWT key used is not Base64-encoded. " + + "We recommend using the `jhipster.security.authentication.jwt.base64-secret` key for optimum security." + ); + secret = applicationSecurityProperties.getAuthentication().getJwt().getSecret(); + keyBytes = secret.getBytes(StandardCharsets.UTF_8); + } + key = Keys.hmacShaKeyFor(keyBytes); + jwtParser = Jwts.parserBuilder().setSigningKey(key).build(); + this.tokenValidityInMilliseconds = 1000 * applicationSecurityProperties.getAuthentication().getJwt().getTokenValidityInSeconds(); + this.tokenValidityInMillisecondsForRememberMe = + 1000 * applicationSecurityProperties.getAuthentication().getJwt().getTokenValidityInSecondsForRememberMe(); + } + + public String createToken(Authentication authentication, boolean rememberMe) { + String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + Date validity; + if (rememberMe) { + validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe); + } else { + validity = new Date(now + this.tokenValidityInMilliseconds); + } + + return Jwts + .builder() + .setSubject(authentication.getName()) + .claim(AUTHORITIES_KEY, authorities) + .signWith(key, SignatureAlgorithm.HS512) + .setExpiration(validity) + .compact(); + } + + public Authentication getAuthentication(String token) { + Claims claims = jwtParser.parseClaimsJws(token).getBody(); + + Collection authorities = Arrays + .stream(claims.get(AUTHORITIES_KEY).toString().split(",")) + .filter(auth -> !auth.trim().isEmpty()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + User principal = new User(claims.getSubject(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, token, authorities); + } + + public boolean validateToken(String authToken) { + try { + jwtParser.parseClaimsJws(authToken); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.info("Invalid JWT token."); + log.trace("Invalid JWT token trace.", e); + } + return false; + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/test/ApplicationSecurityPropertiesTest.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/test/ApplicationSecurityPropertiesTest.java.mustache new file mode 100644 index 00000000000..0ee9cee9569 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/test/ApplicationSecurityPropertiesTest.java.mustache @@ -0,0 +1,68 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import {{packageName}}.UnitTest; + +@UnitTest +class ApplicationSecurityPropertiesTest { + + private ApplicationSecurityProperties properties; + + @BeforeEach + void setup() { + properties = new ApplicationSecurityProperties(); + } + + @Test + void testSecurityAuthenticationJwtSecret() { + ApplicationSecurityProperties.Authentication.Jwt obj = properties.getAuthentication().getJwt(); + String val = ApplicationSecurityDefaults.Security.Authentication.Jwt.secret; + assertThat(obj.getSecret()).isEqualTo(val); + val = "1" + val; + obj.setSecret(val); + assertThat(obj.getSecret()).isEqualTo(val); + } + + @Test + void testSecurityAuthenticationJwtBase64Secret() { + ApplicationSecurityProperties.Authentication.Jwt obj = properties.getAuthentication().getJwt(); + String val = ApplicationSecurityDefaults.Security.Authentication.Jwt.base64Secret; + assertThat(obj.getSecret()).isEqualTo(val); + val = "1" + val; + obj.setBase64Secret(val); + assertThat(obj.getBase64Secret()).isEqualTo(val); + } + + @Test + void testSecurityAuthenticationJwtTokenValidityInSeconds() { + ApplicationSecurityProperties.Authentication.Jwt obj = properties.getAuthentication().getJwt(); + long val = ApplicationSecurityDefaults.Security.Authentication.Jwt.tokenValidityInSeconds; + assertThat(obj.getTokenValidityInSeconds()).isEqualTo(val); + val++; + obj.setTokenValidityInSeconds(val); + assertThat(obj.getTokenValidityInSeconds()).isEqualTo(val); + } + + @Test + void testSecurityAuthenticationJwtTokenValidityInSecondsForRememberMe() { + ApplicationSecurityProperties.Authentication.Jwt obj = properties.getAuthentication().getJwt(); + long val = ApplicationSecurityDefaults.Security.Authentication.Jwt.tokenValidityInSecondsForRememberMe; + assertThat(obj.getTokenValidityInSecondsForRememberMe()).isEqualTo(val); + val++; + obj.setTokenValidityInSecondsForRememberMe(val); + assertThat(obj.getTokenValidityInSecondsForRememberMe()).isEqualTo(val); + } + + @Test + void testSecurityContentSecurityPolicy() { + ApplicationSecurityProperties obj = properties; + String val = ApplicationSecurityDefaults.Security.contentSecurityPolicy; + assertThat(obj.getContentSecurityPolicy()).isEqualTo(val); + obj.setContentSecurityPolicy("foobar"); + assertThat(obj.getContentSecurityPolicy()).isEqualTo("foobar"); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/test/AuthenticationResourceIT.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/test/AuthenticationResourceIT.java.mustache new file mode 100644 index 00000000000..e2ab6bd9e76 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/test/AuthenticationResourceIT.java.mustache @@ -0,0 +1,83 @@ +package {{packageName}}.security.jwt.infrastructure.primary.rest; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import {{packageName}}.IntegrationTest; +import {{packageName}}.TestUtil; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@IntegrationTest +@AutoConfigureMockMvc +class AuthenticationResourceIT { + + @Autowired + PasswordEncoder passwordEncoder; + + @Autowired + MockMvc mockMvc; + + @Test + void shouldAuthorize() throws Exception { + LoginDTO login = new LoginDTO(); + login.setUsername("admin"); + login.setPassword("admin"); + mockMvc + .perform(post("/api/authenticate").contentType(MediaType.APPLICATION_JSON).content(TestUtil.convertObjectToJsonBytes(login))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id_token").isString()) + .andExpect(jsonPath("$.id_token").isNotEmpty()) + .andExpect(header().string("Authorization", not(nullValue()))) + .andExpect(header().string("Authorization", not(is(emptyString())))); + } + + @Test + void shouldAuthorizeWithRememberMe() throws Exception { + LoginDTO login = new LoginDTO(); + login.setUsername("admin"); + login.setPassword("admin"); + login.setRememberMe(true); + mockMvc + .perform(post("/api/authenticate").contentType(MediaType.APPLICATION_JSON).content(TestUtil.convertObjectToJsonBytes(login))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id_token").isString()) + .andExpect(jsonPath("$.id_token").isNotEmpty()) + .andExpect(header().string("Authorization", not(nullValue()))) + .andExpect(header().string("Authorization", not(is(emptyString())))); + } + + @Test + void shouldNotAuthorize() throws Exception { + LoginDTO login = new LoginDTO(); + login.setUsername("wrong-user"); + login.setPassword("wrong password"); + mockMvc + .perform(post("/api/authenticate").contentType(MediaType.APPLICATION_JSON).content(TestUtil.convertObjectToJsonBytes(login))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.id_token").doesNotExist()) + .andExpect(header().doesNotExist("Authorization")); + } + + @Test + @WithMockUser + void shouldBeAuthenticated() throws Exception { + mockMvc.perform(get("/api/authenticate")).andExpect(status().isOk()).andExpect(content().string(containsString("user"))); + } + + @Test + void shouldBuildJWTToken() { + AuthenticationResource.JWTToken token = new AuthenticationResource.JWTToken("chips"); + token.setIdToken("beer"); + + assertThat(token.getIdToken()).isEqualTo("beer"); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/test/CorsFilterConfigurationIT.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/test/CorsFilterConfigurationIT.java.mustache new file mode 100644 index 00000000000..53f04d8eb80 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/test/CorsFilterConfigurationIT.java.mustache @@ -0,0 +1,62 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.web.filter.CorsFilter; +import {{packageName}}.IntegrationTest; + +@IntegrationTest +class CorsFilterConfigurationIT { + + @Nested + @SpringBootTest( + properties = { + "application.cors.allowed-origins=http://localhost:8100,http://localhost:9000", "application.cors.allowed-origin-patterns=", + } + ) + class CorsFilterDefault { + + @Autowired + CorsFilter corsFilter; + + @Test + void shouldCorsFilter() { + assertThat(corsFilter).isNotNull(); + } + } + + @Nested + @SpringBootTest(properties = { "application.cors.allowed-origins=", "application.cors.allowed-origin-patterns=" }) + class CorsFilterEmpty { + + @Autowired + CorsFilter corsFilter; + + @Test + void shouldCorsFilter() { + assertThat(corsFilter).isNotNull(); + } + } + + @Nested + @SpringBootTest( + properties = { + "application.cors.allowed-origins=", "application.cors.allowed-origin-patterns=http://localhost:8100,http://localhost:9000", + } + ) + class CorsFilterWithAllowedOriginPatterns { + + @Autowired + CorsFilter corsFilter; + + @Test + void shouldCorsFilter() { + assertThat(corsFilter).isNotNull(); + } + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/test/JWTFilterTest.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/test/JWTFilterTest.java.mustache new file mode 100644 index 00000000000..3b17e70fd44 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/test/JWTFilterTest.java.mustache @@ -0,0 +1,114 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +import {{packageName}}.UnitTest; +import {{packageName}}.security.jwt.domain.AuthoritiesConstants; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.util.ReflectionTestUtils; + +@UnitTest +class JWTFilterTest { + + private TokenProvider tokenProvider; + + private JWTFilter jwtFilter; + + @BeforeEach + public void setup() { + ApplicationSecurityProperties properties = new ApplicationSecurityProperties(); + String base64Secret = "fd54a45s65fds737b9aafcb3412e07ed99b267f33413274720ddbb7f6c5e64e9f14075f2d7ed041592f0b7657baf8"; + properties.getAuthentication().getJwt().setBase64Secret(base64Secret); + tokenProvider = new TokenProvider(properties); + ReflectionTestUtils.setField(tokenProvider, "key", Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret))); + + ReflectionTestUtils.setField(tokenProvider, "tokenValidityInMilliseconds", 60000); + jwtFilter = new JWTFilter(tokenProvider); + SecurityContextHolder.getContext().setAuthentication(null); + } + + @Test + void testJWTFilter() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + "test-user", + "test-password", + Collections.singletonList(new SimpleGrantedAuthority(AuthoritiesConstants.USER)) + ); + String jwt = tokenProvider.createToken(authentication, false); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(JWTFilter.AUTHORIZATION_HEADER, "Bearer " + jwt); + request.setRequestURI("/api/test"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + jwtFilter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("test-user"); + assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).hasToString(jwt); + } + + @Test + void testJWTFilterInvalidToken() throws Exception { + String jwt = "wrong_jwt"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(JWTFilter.AUTHORIZATION_HEADER, "Bearer " + jwt); + request.setRequestURI("/api/test"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + jwtFilter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } + + @Test + void testJWTFilterMissingAuthorization() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/api/test"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + jwtFilter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } + + @Test + void testJWTFilterMissingToken() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(JWTFilter.AUTHORIZATION_HEADER, "Bearer "); + request.setRequestURI("/api/test"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + jwtFilter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } + + @Test + void testJWTFilterWrongScheme() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + "test-user", + "test-password", + Collections.singletonList(new SimpleGrantedAuthority(AuthoritiesConstants.USER)) + ); + String jwt = tokenProvider.createToken(authentication, false); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(JWTFilter.AUTHORIZATION_HEADER, "Basic " + jwt); + request.setRequestURI("/api/test"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + jwtFilter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/test/LoginDTOTest.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/test/LoginDTOTest.java.mustache new file mode 100644 index 00000000000..22c2359e787 --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/test/LoginDTOTest.java.mustache @@ -0,0 +1,26 @@ +package {{packageName}}.security.jwt.infrastructure.primary.rest; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +import {{packageName}}.UnitTest; +import org.junit.jupiter.api.Test; + +@UnitTest +class LoginDTOTest { + + @Test + void shouldBuild() { + LoginDTO loginDTO = new LoginDTO(); + loginDTO.setUsername("admin"); + loginDTO.setPassword("password"); + loginDTO.setRememberMe(true); + + assertThat(loginDTO.getUsername()).isEqualTo("admin"); + assertThat(loginDTO.getPassword()).isEqualTo("password"); + assertThat(loginDTO.isRememberMe()).isTrue(); + + assertThat(loginDTO.toString()).contains("admin"); + assertThat(loginDTO.toString()).doesNotContain("password"); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/test/TestUtil.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/test/TestUtil.java.mustache new file mode 100644 index 00000000000..8bf13138feb --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/test/TestUtil.java.mustache @@ -0,0 +1,31 @@ +package {{packageName}}; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; + +public final class TestUtil { + + private static final ObjectMapper mapper = createObjectMapper(); + + private static ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); + mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + mapper.registerModule(new JavaTimeModule()); + return mapper; + } + + /** + * Convert an object to JSON byte array. + * + * @param object the object to convert. + * @return the JSON byte array. + * @throws IOException + */ + public static byte[] convertObjectToJsonBytes(Object object) throws IOException { + return mapper.writeValueAsBytes(object); + } +} diff --git a/src/main/resources/template/server/springboot/mvc/security/jwt/test/TokenProviderTest.java.mustache b/src/main/resources/template/server/springboot/mvc/security/jwt/test/TokenProviderTest.java.mustache new file mode 100644 index 00000000000..a0089051a7e --- /dev/null +++ b/src/main/resources/template/server/springboot/mvc/security/jwt/test/TokenProviderTest.java.mustache @@ -0,0 +1,135 @@ +package {{packageName}}.security.jwt.infrastructure.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import {{packageName}}.UnitTest; +import {{packageName}}.security.jwt.domain.AuthoritiesConstants; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.util.ReflectionTestUtils; + +@UnitTest +class TokenProviderTest { + + private static final long ONE_MINUTE = 60000; + + private Key key; + private TokenProvider tokenProvider; + + @BeforeEach + public void setup() { + ApplicationSecurityProperties properties = new ApplicationSecurityProperties(); + String base64Secret = "fd54a45s65fds737b9aafcb3412e07ed99b267f33413274720ddbb7f6c5e64e9f14075f2d7ed041592f0b7657baf8"; + properties.getAuthentication().getJwt().setBase64Secret(base64Secret); + tokenProvider = new TokenProvider(properties); + key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret)); + + ReflectionTestUtils.setField(tokenProvider, "key", key); + ReflectionTestUtils.setField(tokenProvider, "tokenValidityInMilliseconds", ONE_MINUTE); + } + + @Test + void testReturnFalseWhenJWThasInvalidSignature() { + boolean isTokenValid = tokenProvider.validateToken(createTokenWithDifferentSignature()); + + assertThat(isTokenValid).isFalse(); + } + + @Test + void testReturnFalseWhenJWTisMalformed() { + Authentication authentication = createAuthentication(); + String token = tokenProvider.createToken(authentication, false); + String invalidToken = token.substring(1); + boolean isTokenValid = tokenProvider.validateToken(invalidToken); + + assertThat(isTokenValid).isFalse(); + } + + @Test + void testReturnFalseWhenJWTisExpired() { + ReflectionTestUtils.setField(tokenProvider, "tokenValidityInMilliseconds", -ONE_MINUTE); + + Authentication authentication = createAuthentication(); + String token = tokenProvider.createToken(authentication, false); + + boolean isTokenValid = tokenProvider.validateToken(token); + + assertThat(isTokenValid).isFalse(); + } + + @Test + void testReturnFalseWhenJWTisUnsupported() { + String unsupportedToken = createUnsupportedToken(); + + boolean isTokenValid = tokenProvider.validateToken(unsupportedToken); + + assertThat(isTokenValid).isFalse(); + } + + @Test + void testReturnFalseWhenJWTisInvalid() { + boolean isTokenValid = tokenProvider.validateToken(""); + + assertThat(isTokenValid).isFalse(); + } + + @Test + void testKeyIsSetFromSecretWhenSecretIsNotEmpty() { + final String secret = "NwskoUmKHZtzGRKJKVjsJF7BtQMMxNWi"; + ApplicationSecurityProperties properties = new ApplicationSecurityProperties(); + properties.getAuthentication().getJwt().setSecret(secret); + + TokenProvider tokenProvider = new TokenProvider(properties); + + Key key = (Key) ReflectionTestUtils.getField(tokenProvider, "key"); + assertThat(key).isNotNull().isEqualTo(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void testKeyIsSetFromBase64SecretWhenSecretIsEmpty() { + final String base64Secret = "fd54a45s65fds737b9aafcb3412e07ed99b267f33413274720ddbb7f6c5e64e9f14075f2d7ed041592f0b7657baf8"; + ApplicationSecurityProperties properties = new ApplicationSecurityProperties(); + properties.getAuthentication().getJwt().setBase64Secret(base64Secret); + + TokenProvider tokenProvider = new TokenProvider(properties); + + Key key = (Key) ReflectionTestUtils.getField(tokenProvider, "key"); + assertThat(key).isNotNull().isEqualTo(Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret))); + } + + private Authentication createAuthentication() { + Collection authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS)); + return new UsernamePasswordAuthenticationToken("anonymous", "anonymous", authorities); + } + + private String createUnsupportedToken() { + return Jwts.builder().setPayload("payload").signWith(key, SignatureAlgorithm.HS512).compact(); + } + + private String createTokenWithDifferentSignature() { + Key otherKey = Keys.hmacShaKeyFor( + Decoders.BASE64.decode("Xfd54a45s65fds737b9aafcb3412e07ed99b267f33413274720ddbb7f6c5e64e9f14075f2d7ed041592f0b7657baf8") + ); + + return Jwts + .builder() + .setSubject("anonymous") + .signWith(otherKey, SignatureAlgorithm.HS512) + .setExpiration(new Date(new Date().getTime() + ONE_MINUTE)) + .compact(); + } +} diff --git a/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityApplicationServiceIT.java b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityApplicationServiceIT.java new file mode 100644 index 00000000000..062d6b33a6c --- /dev/null +++ b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityApplicationServiceIT.java @@ -0,0 +1,51 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.application; + +import static tech.jhipster.light.TestUtils.*; +import static tech.jhipster.light.generator.buildtool.maven.domain.MavenDomainService.POM_XML; +import static tech.jhipster.light.generator.server.springboot.mvc.security.jwt.application.JwtSecurityAssertFiles.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tech.jhipster.light.IntegrationTest; +import tech.jhipster.light.generator.buildtool.maven.domain.MavenService; +import tech.jhipster.light.generator.project.domain.Project; +import tech.jhipster.light.generator.project.infrastructure.secondary.GitUtils; +import tech.jhipster.light.generator.server.javatool.base.application.JavaBaseApplicationService; +import tech.jhipster.light.generator.server.springboot.core.domain.SpringBootService; +import tech.jhipster.light.generator.server.springboot.mvc.web.domain.SpringBootMvcService; + +@IntegrationTest +class JwtSecurityApplicationServiceIT { + + @Autowired + MavenService mavenService; + + @Autowired + JavaBaseApplicationService javaBaseApplicationService; + + @Autowired + SpringBootService springBootService; + + @Autowired + SpringBootMvcService springBootMvcService; + + @Autowired + JwtSecurityApplicationService jwtSecurityApplicationService; + + @Test + void shouldInitBasicAuth() throws Exception { + Project project = tmpProject(); + GitUtils.init(project.getFolder()); + mavenService.addPomXml(project); + javaBaseApplicationService.init(project); + springBootService.init(project); + springBootMvcService.init(project); + + jwtSecurityApplicationService.initBasicAuth(project); + + assertPomXmlProperties(project); + assertJwtSecurityFilesExists(project); + assertJwtSecurityProperties(project); + assertGitPatch(project); + } +} diff --git a/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityAssertFiles.java b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityAssertFiles.java new file mode 100644 index 00000000000..e755f358774 --- /dev/null +++ b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/application/JwtSecurityAssertFiles.java @@ -0,0 +1,130 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.application; + +import static tech.jhipster.light.TestUtils.assertFileContent; +import static tech.jhipster.light.TestUtils.assertFileExist; +import static tech.jhipster.light.common.domain.FileUtils.getPath; +import static tech.jhipster.light.generator.buildtool.maven.domain.MavenDomainService.POM_XML; +import static tech.jhipster.light.generator.project.domain.Constants.*; +import static tech.jhipster.light.generator.project.domain.Constants.TEST_RESOURCES; +import static tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain.JwtSecurity.exceptionTranslatorPatch; +import static tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain.JwtSecurityDomainService.SECURITY_JWT_PATH; + +import java.util.List; +import tech.jhipster.light.generator.project.domain.Project; + +public class JwtSecurityAssertFiles { + + private JwtSecurityAssertFiles() {} + + public static void assertPomXmlProperties(Project project) { + assertFileContent(project, POM_XML, ""); + assertFileContent(project, POM_XML, jwtSecurityDependencies()); + assertFileContent(project, POM_XML, jwtSecurityTestDependency()); + } + + public static void assertGitPatch(Project project) { + assertFileExist(project, ".jhipster", exceptionTranslatorPatch); + assertFileExist(project, ".jhipster", exceptionTranslatorPatch); + } + + public static void assertJwtSecurityFilesExists(Project project) { + List infrastructureConfigJavaFiles = List.of( + "ApplicationSecurityDefaults.java", + "ApplicationSecurityProperties.java", + "CorsFilterConfiguration.java", + "CorsProperties.java", + "JWTConfigurer.java", + "JWTFilter.java", + "SecurityConfiguration.java", + "SecurityExceptionTranslator.java", + "TokenProvider.java" + ); + + String jwtPath = getPath(project.getPackageNamePath().orElse("com/mycompany/myapp"), SECURITY_JWT_PATH); + String domainConfigPath = getPath(jwtPath, "domain"); + String infrastructureConfigPath = getPath(jwtPath, "infrastructure/config"); + String infrastructureRestPath = getPath(jwtPath, "/infrastructure/primary/rest"); + + // main java files + assertFileExist(project, getPath(MAIN_JAVA, domainConfigPath, "AuthoritiesConstants.java")); + infrastructureConfigJavaFiles.forEach(javaFile -> assertFileExist(project, getPath(MAIN_JAVA, infrastructureConfigPath, javaFile))); + assertFileExist(project, getPath(MAIN_JAVA, infrastructureRestPath, "AuthenticationResource.java")); + assertFileExist(project, getPath(MAIN_JAVA, infrastructureRestPath, "LoginDTO.java")); + + // test java files + assertFileExist(project, getPath(TEST_JAVA, infrastructureConfigPath, "ApplicationSecurityPropertiesTest.java")); + assertFileExist(project, getPath(TEST_JAVA, infrastructureConfigPath, "CorsFilterConfigurationIT.java")); + assertFileExist(project, getPath(TEST_JAVA, infrastructureConfigPath, "JWTFilterTest.java")); + assertFileExist(project, getPath(TEST_JAVA, infrastructureConfigPath, "TokenProviderTest.java")); + + assertFileExist(project, getPath(TEST_JAVA, infrastructureRestPath, "AuthenticationResourceIT.java")); + assertFileExist(project, getPath(TEST_JAVA, infrastructureRestPath, "LoginDTOTest.java")); + } + + public static void assertJwtSecurityProperties(Project project) { + String baseName = project.getBaseName().orElse("jhipster"); + List properties = List.of( + "spring.security.user.name=admin", + "spring.security.user.password=$2a$12$cRKS9ZURbdJIaRsKDTDUmOrH4.B.2rokv8rrkrQXr2IR2Hkna484O", + "application.security.authentication.jwt.base64-secret=bXktc2VjcmV0LWtleS13aGljaC1zaG91bGQtYmUtY2hhbmdlZC1pbi1wcm9kdWN0aW9uLWFuZC1iZS1iYXNlNjQtZW5jb2RlZAo=", + "application.security.authentication.jwt.token-validity-in-seconds=86400", + "application.security.authentication.jwt.token-validity-in-seconds-for-remember-me=2592000", + "application.cors.allowed-origins=http://localhost:8100,http://localhost:9000", + "application.cors.allowed-methods=*", + "application.cors.allowed-headers=*", + "application.cors.exposed-headers=Authorization,Link,X-Total-Count,X-" + + baseName + + "-alert,X-" + + baseName + + "-error,X-" + + baseName + + "-params", + "application.cors.allow-credentials=true", + "application.cors.max-age=1800" + ); + + assertFileContent(project, getPath(MAIN_RESOURCES, "config/application.properties"), properties); + assertFileContent(project, getPath(TEST_RESOURCES, "config/application.properties"), properties); + } + + public static List jwtSecurityDependencies() { + return List.of( + "", + "org.springframework.boot", + "spring-boot-starter-security", + "", + "", + "org.springframework.boot", + "spring-boot-configuration-processor", + "provided", + "", + "", + "io.jsonwebtoken", + "jjwt-api", + "${jjwt.version}", + "", + "", + "io.jsonwebtoken", + "jjwt-impl", + "${jjwt.version}", + "runtime", + "", + "", + "io.jsonwebtoken", + "jjwt-jackson", + "${jjwt.version}", + "runtime", + "" + ); + } + + public static List jwtSecurityTestDependency() { + return List.of( + "", + "org.springframework.security", + "spring-security-test", + "test", + "" + ); + } +} diff --git a/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityDomainServiceTest.java b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityDomainServiceTest.java new file mode 100644 index 00000000000..c3e17ec3090 --- /dev/null +++ b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityDomainServiceTest.java @@ -0,0 +1,61 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static tech.jhipster.light.TestUtils.tmpProjectWithPomXml; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import tech.jhipster.light.UnitTest; +import tech.jhipster.light.generator.buildtool.generic.domain.BuildToolService; +import tech.jhipster.light.generator.buildtool.generic.domain.Dependency; +import tech.jhipster.light.generator.project.domain.Project; +import tech.jhipster.light.generator.project.domain.ProjectRepository; +import tech.jhipster.light.generator.project.infrastructure.secondary.GitUtils; +import tech.jhipster.light.generator.server.springboot.properties.domain.SpringBootPropertiesService; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class JwtSecurityDomainServiceTest { + + @Mock + SpringBootPropertiesService springBootPropertiesService; + + @Mock + ProjectRepository projectRepository; + + @Mock + BuildToolService buildToolService; + + @InjectMocks + JwtSecurityDomainService jwtSecurityDomainService; + + @Test + void shouldInitBasicAuth() throws GitAPIException { + Project project = tmpProjectWithPomXml(); + GitUtils.init(project.getFolder()); + + jwtSecurityDomainService.initBasicAuth(project); + + verify(buildToolService).addProperty(any(Project.class), anyString(), anyString()); + verify(buildToolService, times(6)).addDependency(any(Project.class), any(Dependency.class)); + + // 12 classes + 6 tests + 1 patch + verify(projectRepository, times(19)).template(any(Project.class), anyString(), anyString(), anyString()); + + // 1 patch + verify(projectRepository, times(1)).add(any(Project.class), anyString(), anyString(), anyString()); + + verify(springBootPropertiesService, times(11)).addProperties(any(Project.class), anyString(), any()); + verify(springBootPropertiesService, times(11)).addPropertiesTest(any(Project.class), anyString(), any()); + + // 2 git patchs + verify(projectRepository, times(2)).gitApplyPatch(any(Project.class), anyString()); + } +} diff --git a/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityTest.java b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityTest.java new file mode 100644 index 00000000000..bd475840bc5 --- /dev/null +++ b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/domain/JwtSecurityTest.java @@ -0,0 +1,91 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.Test; +import tech.jhipster.light.UnitTest; +import tech.jhipster.light.generator.buildtool.generic.domain.Dependency; + +@UnitTest +class JwtSecurityTest { + + @Test + void shouldJjwtVersion() { + assertThat(JwtSecurity.jjwtVersion()).isEqualTo("0.11.2"); + } + + @Test + void shouldSpringBootStarterSecurity() { + Dependency dependency = JwtSecurity.springBootStarterSecurityDependency(); + assertThat(dependency.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(dependency.getArtifactId()).isEqualTo("spring-boot-starter-security"); + assertThat(dependency.getVersion()).isEmpty(); + assertThat(dependency.getScope()).isEmpty(); + } + + @Test + void shouldSpringBootConfigurationProcessor() { + Dependency dependency = JwtSecurity.springBootConfigurationProcessor(); + assertThat(dependency.getGroupId()).isEqualTo("org.springframework.boot"); + assertThat(dependency.getArtifactId()).isEqualTo("spring-boot-configuration-processor"); + assertThat(dependency.getVersion()).isEmpty(); + assertThat(dependency.getScope()).contains("provided"); + } + + @Test + void shouldJjwtApiDependency() { + Dependency dependency = JwtSecurity.jjwtApiDependency(); + assertThat(dependency.getGroupId()).isEqualTo("io.jsonwebtoken"); + assertThat(dependency.getArtifactId()).isEqualTo("jjwt-api"); + assertThat(dependency.getVersion()).contains("\\${jjwt.version}"); + assertThat(dependency.getScope()).isEmpty(); + } + + @Test + void shouldJjwtImplDependency() { + Dependency dependency = JwtSecurity.jjwtImplDependency(); + assertThat(dependency.getGroupId()).isEqualTo("io.jsonwebtoken"); + assertThat(dependency.getArtifactId()).isEqualTo("jjwt-impl"); + assertThat(dependency.getVersion()).contains("\\${jjwt.version}"); + assertThat(dependency.getScope()).contains("runtime"); + } + + @Test + void shouldJjwtJacksonDependency() { + Dependency dependency = JwtSecurity.jjwtJacksonDependency(); + assertThat(dependency.getGroupId()).isEqualTo("io.jsonwebtoken"); + assertThat(dependency.getArtifactId()).isEqualTo("jjwt-jackson"); + assertThat(dependency.getVersion()).contains("\\${jjwt.version}"); + assertThat(dependency.getScope()).contains("runtime"); + } + + @Test + void shouldSpringSecurityTestDependency() { + Dependency dependency = JwtSecurity.springSecurityTestDependency(); + assertThat(dependency.getGroupId()).isEqualTo("org.springframework.security"); + assertThat(dependency.getArtifactId()).isEqualTo("spring-security-test"); + assertThat(dependency.getVersion()).isEmpty(); + assertThat(dependency.getScope()).contains("test"); + } + + @Test + void shouldJwtSecurityFiles() { + List javaFiles = List.of( + "AuthoritiesConstants.java", + "ApplicationSecurityDefaults.java", + "ApplicationSecurityProperties.java", + "CorsFilterConfiguration.java", + "CorsProperties.java", + "JWTConfigurer.java", + "JWTFilter.java", + "SecurityConfiguration.java", + "SecurityExceptionTranslator.java", + "TokenProvider.java", + "AuthenticationResource.java", + "LoginDTO.java" + ); + + assertThat(JwtSecurity.jwtSecurityFiles().keySet()).containsExactlyInAnyOrderElementsOf(javaFiles); + } +} diff --git a/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/config/JwtSecurityBeanConfigurationIT.java b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/config/JwtSecurityBeanConfigurationIT.java new file mode 100644 index 00000000000..00377c4a00a --- /dev/null +++ b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/config/JwtSecurityBeanConfigurationIT.java @@ -0,0 +1,21 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.infrastructure.config; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import tech.jhipster.light.IntegrationTest; +import tech.jhipster.light.generator.server.springboot.mvc.security.jwt.domain.JwtSecurityDomainService; + +@IntegrationTest +class JwtSecurityBeanConfigurationIT { + + @Autowired + ApplicationContext applicationContext; + + @Test + void shouldGetBean() { + assertThat(applicationContext.getBean("jwtSecurityService")).isNotNull().isInstanceOf(JwtSecurityDomainService.class); + } +} diff --git a/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/rest/JwtSecurityResourceIT.java b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/rest/JwtSecurityResourceIT.java new file mode 100644 index 00000000000..f836c8165a2 --- /dev/null +++ b/src/test/java/tech/jhipster/light/generator/server/springboot/mvc/security/jwt/infrastructure/rest/JwtSecurityResourceIT.java @@ -0,0 +1,70 @@ +package tech.jhipster.light.generator.server.springboot.mvc.security.jwt.infrastructure.rest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static tech.jhipster.light.generator.server.springboot.mvc.security.jwt.application.JwtSecurityAssertFiles.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import tech.jhipster.light.IntegrationTest; +import tech.jhipster.light.TestUtils; +import tech.jhipster.light.common.domain.FileUtils; +import tech.jhipster.light.generator.buildtool.maven.application.MavenApplicationService; +import tech.jhipster.light.generator.init.application.InitApplicationService; +import tech.jhipster.light.generator.project.domain.Project; +import tech.jhipster.light.generator.project.infrastructure.primary.dto.ProjectDTO; +import tech.jhipster.light.generator.project.infrastructure.secondary.GitUtils; +import tech.jhipster.light.generator.server.javatool.base.application.JavaBaseApplicationService; +import tech.jhipster.light.generator.server.springboot.core.application.SpringBootApplicationService; +import tech.jhipster.light.generator.server.springboot.mvc.web.application.SpringBootMvcApplicationService; + +@IntegrationTest +@AutoConfigureMockMvc +class JwtSecurityResourceIT { + + @Autowired + InitApplicationService initApplicationService; + + @Autowired + MavenApplicationService mavenApplicationService; + + @Autowired + JavaBaseApplicationService javaBaseApplicationService; + + @Autowired + SpringBootApplicationService springBootApplicationService; + + @Autowired + SpringBootMvcApplicationService springBootMvcApplicationService; + + @Autowired + MockMvc mockMvc; + + @Test + void shouldInitBasicAuth() throws Exception { + ProjectDTO projectDTO = TestUtils.readFileToObject("json/chips.json", ProjectDTO.class); + projectDTO.folder(FileUtils.tmpDirForTest()); + Project project = ProjectDTO.toProject(projectDTO); + GitUtils.init(project.getFolder()); + initApplicationService.init(project); + mavenApplicationService.init(project); + javaBaseApplicationService.init(project); + springBootApplicationService.init(project); + springBootMvcApplicationService.init(project); + + mockMvc + .perform( + post("/api/servers/spring-boot/mvc/security/jwt") + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtils.convertObjectToJsonBytes(projectDTO)) + ) + .andExpect(status().isOk()); + + assertPomXmlProperties(project); + assertJwtSecurityFilesExists(project); + assertJwtSecurityProperties(project); + } +} diff --git a/tests/generate.sh b/tests/generate.sh index d249a29c528..6bfa35b90ab 100755 --- a/tests/generate.sh +++ b/tests/generate.sh @@ -15,6 +15,7 @@ callApi beer.json "/api/servers/java/base" callApi beer.json "/api/servers/spring-boot" callApi beer.json "/api/servers/spring-boot/banner/jhipster-v7" callApi beer.json "/api/servers/spring-boot/mvc/web/tomcat" +callApi beer.json "/api/servers/spring-boot/mvc/security/jwt" callApi beer.json "/api/servers/spring-boot/databases/postgresql" callApi beer.json "/api/servers/spring-boot/databases/migration/liquibase"