Skip to content

Commit

Permalink
Merge pull request #1005 from pascalgrimaud/oauth2-account-context
Browse files Browse the repository at this point in the history
OAuth2: add account context
  • Loading branch information
pascalgrimaud authored Mar 17, 2022
2 parents b44a51f + 290c462 commit 2d05692
Show file tree
Hide file tree
Showing 45 changed files with 1,090 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ private Constants() {}
public static final String DOCKERFILE_IMAGE_FROM_PREFIX = "FROM";

// Hexagonal Architecture
public static final String APPLICATION = "application";
public static final String DOMAIN = "domain";
public static final String INFRASTRUCTURE = "infrastructure";
public static final String PRIMARY = getPath(INFRASTRUCTURE, "primary");
public static final String SECONDARY = getPath(INFRASTRUCTURE, "secondary");
public static final String CONFIG = getPath(INFRASTRUCTURE, CONFIG_FOLDER);

public static final String TECHNICAL = "technical";
public static final String INFRASTRUCTURE = getPath(TECHNICAL, "infrastructure");
public static final String INFRA_PRIMARY = getPath(INFRASTRUCTURE, "primary");
public static final String INFRA_SECONDARY = getPath(INFRASTRUCTURE, "secondary");
public static final String TECHNICAL_INFRASTRUCTURE = getPath(TECHNICAL, INFRASTRUCTURE);
public static final String TECHNICAL_PRIMARY = getPath(TECHNICAL, PRIMARY);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ public OAuth2SecurityApplicationService(OAuth2SecurityService oauth2SecurityServ
public void addOAuth2(Project project) {
oauth2SecurityService.addOAuth2(project);
}

public void addAccountContext(Project project) {
oauth2SecurityService.addAccountContext(project);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain;

import static tech.jhipster.lite.common.domain.FileUtils.getPath;
import static tech.jhipster.lite.generator.project.domain.Constants.*;

import java.util.HashMap;
import java.util.Map;
import tech.jhipster.lite.generator.buildtool.generic.domain.Dependency;

public class OAuth2Security {

private static final String INFRASTRUCTURE_CONFIG = "infrastructure/config";
public static final String SECURITY_OAUTH2 = "security/oauth2";
public static final String SECURITY_OAUTH2_APPLICATION = getPath(SECURITY_OAUTH2, APPLICATION);
public static final String SECURITY_OAUTH2_DOMAIN = getPath(SECURITY_OAUTH2, DOMAIN);
public static final String SECURITY_OAUTH2_INFRASTRUCTURE = getPath(SECURITY_OAUTH2, INFRASTRUCTURE);
public static final String SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG = getPath(SECURITY_OAUTH2, CONFIG);
public static final String TECHNICAL_INFRASTRUCTURE_PRIMARY_EXCEPTION = getPath(TECHNICAL_PRIMARY, "exception");

public static final String ERROR_DOMAIN = "error/domain";

public static final String ACCOUNT_CONTEXT = "account";
public static final String ACCOUNT_DOMAIN = getPath(ACCOUNT_CONTEXT, DOMAIN);
public static final String ACCOUNT_INFRASTRUCTURE_PRIMARY = getPath(ACCOUNT_CONTEXT, PRIMARY, "rest");

private static final String DOCKER_KEYCLOAK_IMAGE_NAME = "jboss/keycloak";

private static final String SPRINGBOOT_PACKAGE = "org.springframework.boot";

private static final String STARTER_SECURITY = "spring-boot-starter-security";
private static final String STARTER_OAUTH2_CLIENT = "spring-boot-starter-oauth2-client";
private static final String STARTER_OAUTH2_RESOURCE_SERVER = "spring-boot-starter-oauth2-resource-server";
Expand Down Expand Up @@ -41,36 +54,39 @@ public static Dependency springSecurityTestDependency() {
public static Map<String, String> oauth2SecurityFiles() {
Map<String, String> map = new HashMap<>();

map.put("SecurityUtils.java", "application");
map.put("SecurityUtils.java", SECURITY_OAUTH2_APPLICATION);

map.put("AuthoritiesConstants.java", SECURITY_OAUTH2_DOMAIN);
map.put("ApplicationSecurityDefaults.java", SECURITY_OAUTH2_DOMAIN);

map.put("AuthoritiesConstants.java", "domain");
map.put("ApplicationSecurityDefaults.java", "domain");
map.put("ApplicationSecurityProperties.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("AudienceValidator.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("CustomClaimConverter.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("JwtGrantedAuthorityConverter.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("OAuth2Configuration.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("SecurityConfiguration.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);

map.put("ApplicationSecurityProperties.java", INFRASTRUCTURE_CONFIG);
map.put("AudienceValidator.java", INFRASTRUCTURE_CONFIG);
map.put("CustomClaimConverter.java", INFRASTRUCTURE_CONFIG);
map.put("JwtGrantedAuthorityConverter.java", INFRASTRUCTURE_CONFIG);
map.put("OAuth2Configuration.java", INFRASTRUCTURE_CONFIG);
map.put("SecurityConfiguration.java", INFRASTRUCTURE_CONFIG);
map.put("AccountException.java", ERROR_DOMAIN);

return map;
}

public static Map<String, String> oauth2TestSecurityFiles() {
Map<String, String> map = new HashMap<>();

map.put("SecurityUtilsTest.java", "application");
map.put("SecurityUtilsTest.java", SECURITY_OAUTH2_APPLICATION);

map.put("ApplicationSecurityPropertiesTest.java", INFRASTRUCTURE_CONFIG);
map.put("AudienceValidatorTest.java", INFRASTRUCTURE_CONFIG);
map.put("CustomClaimConverterIT.java", INFRASTRUCTURE_CONFIG);
map.put("FakeRequestAttributes.java", INFRASTRUCTURE_CONFIG);
map.put("JwtGrantedAuthorityConverterTest.java", INFRASTRUCTURE_CONFIG);
map.put("SecurityConfigurationIT.java", INFRASTRUCTURE_CONFIG);
map.put("SecurityConfigurationTest.java", INFRASTRUCTURE_CONFIG);
map.put("TestSecurityConfiguration.java", INFRASTRUCTURE_CONFIG);
map.put("ApplicationSecurityPropertiesTest.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("AudienceValidatorTest.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("CustomClaimConverterIT.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("FakeRequestAttributes.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("JwtGrantedAuthorityConverterTest.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("SecurityConfigurationIT.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("SecurityConfigurationTest.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);
map.put("TestSecurityConfiguration.java", SECURITY_OAUTH2_INFRASTRUCTURE_CONFIG);

map.put("WithUnauthenticatedMockUser.java", "infrastructure");
map.put("WithUnauthenticatedMockUser.java", SECURITY_OAUTH2_INFRASTRUCTURE);
map.put("AccountExceptionTest.java", ERROR_DOMAIN);

return map;
}
Expand Down Expand Up @@ -98,4 +114,25 @@ public static Map<String, String> propertiesForTests() {
"http://DO_NOT_CALL:9080/auth/realms/jhipster"
);
}

public static Map<String, String> oauth2AccountContextFiles() {
Map<String, String> map = new HashMap<>();

map.put("AccountConstants.java", ACCOUNT_DOMAIN);
map.put("AccountResource.java", ACCOUNT_INFRASTRUCTURE_PRIMARY);
map.put("UserDTO.java", ACCOUNT_INFRASTRUCTURE_PRIMARY);

return map;
}

public static Map<String, String> oauth2AccountContextTestFiles() {
Map<String, String> map = new HashMap<>();

map.put("AccountResourceIT.java", ACCOUNT_INFRASTRUCTURE_PRIMARY);
map.put("AccountResourceTest.java", ACCOUNT_INFRASTRUCTURE_PRIMARY);
map.put("OAuth2TestUtil.java", ACCOUNT_INFRASTRUCTURE_PRIMARY);
map.put("UserDTOTest.java", ACCOUNT_INFRASTRUCTURE_PRIMARY);

return map;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public void addOAuth2(Project project) {
addSpringBootProperties(project);

updateExceptionTranslator(project);

updateExceptionTranslatorWithAccountExceptionHandler(project);
updateExceptionTranslatorTestController(project);
updateExceptionTranslatorIT(project);

updateIntegrationTestWithMockUser(project);
updateIntegrationTestWithTestSecurityConfiguration(project);
}
Expand Down Expand Up @@ -81,20 +86,36 @@ private void addJavaFiles(Project project) {
String packageNamePath = project.getPackageNamePath().orElse(getPath(PACKAGE_PATH));

String sourceSrc = getPath(SOURCE, "src");
String destinationSrc = getPath(MAIN_JAVA, packageNamePath, SECURITY_OAUTH2_PATH);
String destinationSrc = getPath(MAIN_JAVA, packageNamePath);
oauth2SecurityFiles()
.forEach((javaFile, folder) ->
projectRepository.template(project, getPath(sourceSrc, folder), javaFile, getPath(destinationSrc, folder))
);

String sourceTest = getPath(SOURCE, "test");
String destinationTest = getPath(TEST_JAVA, packageNamePath, SECURITY_OAUTH2_PATH);
String destinationTest = getPath(TEST_JAVA, packageNamePath);
oauth2TestSecurityFiles()
.forEach((javaFile, folder) ->
projectRepository.template(project, getPath(sourceTest, folder), javaFile, getPath(destinationTest, folder))
);
}

@Override
public void addAccountContext(Project project) {
project.addDefaultConfig(PACKAGE_NAME);
String packageNamePath = project.getPackageNamePath().orElse(getPath(PACKAGE_PATH));

String sourceSrc = getPath(SOURCE, "src");
String destinationSrc = getPath(MAIN_JAVA, packageNamePath);
oauth2AccountContextFiles()
.forEach((file, folder) -> projectRepository.template(project, getPath(sourceSrc, folder), file, getPath(destinationSrc, folder)));

String sourceTest = getPath(SOURCE, "test");
String destinationTest = getPath(TEST_JAVA, packageNamePath);
oauth2AccountContextTestFiles()
.forEach((file, folder) -> projectRepository.template(project, getPath(sourceTest, folder), file, getPath(destinationTest, folder)));
}

private void addSpringBootProperties(Project project) {
springBootCommonService.addPropertiesComment(project, "Spring Security OAuth2");
properties().forEach((k, v) -> springBootCommonService.addProperties(project, k, v));
Expand All @@ -113,6 +134,83 @@ private void updateIntegrationTestWithMockUser(Project project) {
commonSecurityService.updateIntegrationTestWithMockUser(project);
}

private void updateExceptionTranslatorWithAccountExceptionHandler(Project project) {
String packageName = project.getPackageName().orElse(DEFAULT_PACKAGE_NAME);
String packageNamePath = project.getPackageNamePath().orElse(getPath(PACKAGE_PATH));
String exceptionTranslatorPath = getPath(MAIN_JAVA, packageNamePath, TECHNICAL_INFRASTRUCTURE_PRIMARY_EXCEPTION);
String exceptionTranslatorFile = "ExceptionTranslator.java";

String oldImport = "import org.zalando.problem.violations.ConstraintViolationProblem;";
String newImport = String.format(
"""
import org.zalando.problem.violations.ConstraintViolationProblem;
import %s.error.domain.AccountException;""",
packageName
);
projectRepository.replaceText(project, exceptionTranslatorPath, exceptionTranslatorFile, oldImport, newImport);

String oldNeedle = "// jhipster-needle-exception-translator";
String newNeedle =
"""
@ExceptionHandler
public ResponseEntity<Problem> handleAccountException(AccountException ex, NativeWebRequest request) {
Problem problem = Problem.builder().withStatus(Status.UNAUTHORIZED).withTitle(ex.getMessage()).build();
return create(ex, problem, request);
}
// jhipster-needle-exception-translator""";
projectRepository.replaceText(project, exceptionTranslatorPath, exceptionTranslatorFile, oldNeedle, newNeedle);
}

private void updateExceptionTranslatorTestController(Project project) {
String packageName = project.getPackageName().orElse(DEFAULT_PACKAGE_NAME);
String packageNamePath = project.getPackageNamePath().orElse(getPath(PACKAGE_PATH));
String exceptionTranslatorPath = getPath(TEST_JAVA, packageNamePath, TECHNICAL_INFRASTRUCTURE_PRIMARY_EXCEPTION);
String fileToReplace = "ExceptionTranslatorTestController.java";

String oldImport = "import org.springframework.http.converter.HttpMessageConversionException;";
String newImport = String.format(
"""
import org.springframework.http.converter.HttpMessageConversionException;
import %s.error.domain.AccountException;""",
packageName
);
projectRepository.replaceText(project, exceptionTranslatorPath, fileToReplace, oldImport, newImport);

String oldNeedle = "// jhipster-needle-exception-translator-test-controller";
String newNeedle =
"""
@GetMapping("/account-exception")
public void accountException() {
throw new AccountException("beer");
}
// jhipster-needle-exception-translator-test-controller""";
projectRepository.replaceText(project, exceptionTranslatorPath, fileToReplace, oldNeedle, newNeedle);
}

private void updateExceptionTranslatorIT(Project project) {
String packageNamePath = project.getPackageNamePath().orElse(getPath(PACKAGE_PATH));
String exceptionTranslatorPath = getPath(TEST_JAVA, packageNamePath, TECHNICAL_INFRASTRUCTURE_PRIMARY_EXCEPTION);
String fileToReplace = "ExceptionTranslatorIT.java";

String oldNeedle = "// jhipster-needle-exception-translator-it";
String newNeedle =
"""
@Test
void shouldHandleAccountException() throws Exception {
mockMvc
.perform(get("/api/exception-translator-test/account-exception"))
.andExpect(status().isUnauthorized())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("\\$.message").value("error.http.401"))
.andExpect(jsonPath("\\$.title").value("beer"));
}
// jhipster-needle-exception-translator-it""";
projectRepository.replaceText(project, exceptionTranslatorPath, fileToReplace, oldNeedle, newNeedle);
}

private void updateIntegrationTestWithTestSecurityConfiguration(Project project) {
project.addDefaultConfig(PACKAGE_NAME);
String packageName = project.getPackageName().orElse(DEFAULT_PACKAGE_NAME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

public interface OAuth2SecurityService {
void addOAuth2(Project project);
void addAccountContext(Project project);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,13 @@ public void addOAuth2(@RequestBody ProjectDTO projectDTO) {
Project project = ProjectDTO.toProject(projectDTO);
oauth2SecurityApplicationService.addOAuth2(project);
}

@Operation(summary = "Add a account context for OAuth 2.0 / OIDC Authentication")
@ApiResponse(responseCode = "500", description = "An error occurred while adding account context for OAuth2 / OIDC Authentication")
@PostMapping("/oauth2/account")
@GeneratorStep(id = "springboot-oauth2-account")
public void addAccountContext(@RequestBody ProjectDTO projectDTO) {
Project project = ProjectDTO.toProject(projectDTO);
oauth2SecurityApplicationService.addAccountContext(project);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package tech.jhipster.lite.generator.server.springboot.mvc.web.domain;

import static tech.jhipster.lite.common.domain.FileUtils.getPath;
import static tech.jhipster.lite.generator.project.domain.Constants.INFRA_PRIMARY;
import static tech.jhipster.lite.generator.project.domain.Constants.MAIN_JAVA;
import static tech.jhipster.lite.generator.project.domain.Constants.TECHNICAL_PRIMARY;
import static tech.jhipster.lite.generator.project.domain.Constants.TEST_JAVA;
import static tech.jhipster.lite.generator.project.domain.DefaultConfig.PACKAGE_NAME;
import static tech.jhipster.lite.generator.project.domain.DefaultConfig.PACKAGE_PATH;
Expand All @@ -19,8 +19,8 @@
public class SpringBootMvcDomainService implements SpringBootMvcService {

public static final String SOURCE = "server/springboot/mvc/web";
public static final String INFRA_PRIMARY_CORS = getPath(INFRA_PRIMARY, "cors");
public static final String INFRA_PRIMARY_EXCEPTION = getPath(INFRA_PRIMARY, "exception");
public static final String INFRA_PRIMARY_CORS = getPath(TECHNICAL_PRIMARY, "cors");
public static final String INFRA_PRIMARY_EXCEPTION = getPath(TECHNICAL_PRIMARY, "exception");

public final ProjectRepository projectRepository;
public final BuildToolService buildToolService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ public ResponseEntity<Problem> handleGenerationException(GeneratorException ex,
return create(ex, problem, request);
}

// jhipster-needle-exception-translator

@Override
public ProblemBuilder prepare(final Throwable throwable, final StatusType status, final URI type) {
if (!exceptionWithDetails) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package {{packageName}}.account.domain;

public class AccountConstants {
public static final String DEFAULT_LANGUAGE = "en";
public static final String SUB = "sub";
public static final String UID = "uid";
public static final String PREFERRED_USERNAME = "preferred_username";
public static final String FAMILY_NAME = "family_name";
public static final String EMAIL_VERIFIED = "email_verified";
public static final String EMAIL = "email";
public static final String LANG_KEY = "langKey";
public static final String LOCALE = "locale";
public static final String PICTURE = "picture";
public static final String GIVEN_NAME = "given_name";
public static final String NAME = "name";
private AccountConstants() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package {{packageName}}.account.infrastructure.primary.rest;

import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import {{packageName}}.error.domain.AccountException;

@RestController
@RequestMapping("/api")
class AccountResource {
private final Logger log = LoggerFactory.getLogger(AccountResource.class);
/**
* {@code GET /account} : get the current user.
*
* @param principal the current user; resolves to {@code null} if not authenticated.
* @return the current user.
* @throws AccountException {@code 500 (Internal Server Error)} if the user couldn't be returned.
*/
@GetMapping("/account")
@SuppressWarnings("unchecked")
public UserDTO getAccount(Principal principal) {
if (principal instanceof AbstractAuthenticationToken authenticationToken) {
return UserDTO.getUserDTOFromToken(authenticationToken);
}
throw new AccountException("User could not be found");
}

/**
* {@code GET /authenticate} : check if the user is authenticated, and return its login.
*
* @param request the HTTP request.
* @return the login if the user is authenticated.
*/
@GetMapping("/authenticate")
public String isAuthenticated(HttpServletRequest request) {
log.debug("REST request to check if the current user is authenticated");
return request.getRemoteUser();
}
}
Loading

0 comments on commit 2d05692

Please sign in to comment.