Skip to content

Commit

Permalink
Merge pull request #443 from pblanchardie/270-add-spring-security-oauth2
Browse files Browse the repository at this point in the history
Spring Boot Security: OAuth 2.0 and OpenID Connect
  • Loading branch information
pascalgrimaud authored Mar 4, 2022
2 parents e6b1c9c + c5b5ad7 commit 8d9eb4d
Show file tree
Hide file tree
Showing 45 changed files with 4,703 additions and 5 deletions.
1 change: 1 addition & 0 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ jobs:
matrix:
app:
- fullapp
- oauth2app
- mysqlapp
- mariadbapp
- flywayapp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ private Constants() {}
public static final String MAIN_JAVA = getPath("src/main/java");
public static final String MAIN_WEBAPP = getPath("src/main/webapp");
public static final String MAIN_RESOURCES = getPath("src/main/resources");
public static final String MAIN_DOCKER = getPath("src/main/docker");

public static final String TEMPLATE_FOLDER = "generator";
public static final String DEPENDENCIES_FOLDER = "dependencies";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ public class DefaultConfig {
public static final String PACKAGE_NAME = "packageName";
public static final String PRETTIER_DEFAULT_INDENT = "prettierDefaultIndent";

public static final String DEFAULT_PACKAGE_NAME = "com.mycompany.myapp";
public static final String PACKAGE_PATH = "com/mycompany/myapp";
public static final String DEFAULT_BASE_NAME = "jhipster";
public static final String DEFAULT_PROJECT_NAME = "JHipster Project";

// prettier-ignore
public static final Map<String, Object> defaultMap = Map.of(
BASE_NAME, "jhipster",
PROJECT_NAME, "JHipster Project",
PACKAGE_NAME, "com.mycompany.myapp",
BASE_NAME, DEFAULT_BASE_NAME,
PROJECT_NAME, DEFAULT_PROJECT_NAME,
PACKAGE_NAME, DEFAULT_PACKAGE_NAME,
PRETTIER_DEFAULT_INDENT, DEFAULT_INDENTATION
);

public static final String PACKAGE_PATH = "com/mycompany/myapp";

private DefaultConfig() {}

public static Optional<Object> get(String key) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.common.domain;

import static tech.jhipster.lite.common.domain.FileUtils.getPath;
import static tech.jhipster.lite.generator.project.domain.Constants.MAIN_JAVA;
import static tech.jhipster.lite.generator.project.domain.Constants.TEST_JAVA;
import static tech.jhipster.lite.generator.project.domain.DefaultConfig.PACKAGE_PATH;

import tech.jhipster.lite.generator.project.domain.Project;
import tech.jhipster.lite.generator.project.domain.ProjectRepository;

public class CommonSecurityDomainService implements CommonSecurityService {

private final ProjectRepository projectRepository;

public CommonSecurityDomainService(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}

@Override
public void updateExceptionTranslator(Project project) {
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.spring.web.advice.ProblemHandling;";
String newImport =
"""
import org.zalando.problem.spring.web.advice.ProblemHandling;
import org.zalando.problem.spring.web.advice.security.SecurityAdviceTrait;""";
projectRepository.replaceText(project, exceptionTranslatorPath, exceptionTranslatorFile, oldImport, newImport);

String oldImplements = "public class ExceptionTranslator implements ProblemHandling \\{";
String newImplements = "public class ExceptionTranslator implements ProblemHandling, SecurityAdviceTrait \\{";
projectRepository.replaceText(project, exceptionTranslatorPath, exceptionTranslatorFile, oldImplements, newImplements);
}

@Override
public void updateIntegrationTestWithMockUser(Project project) {
String packageNamePath = project.getPackageNamePath().orElse(getPath(PACKAGE_PATH));
String integrationTestPath = getPath(TEST_JAVA, packageNamePath);

String oldImport = "import org.springframework.boot.test.context.SpringBootTest;";
String newImport =
"""
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;""";
projectRepository.replaceText(project, integrationTestPath, "IntegrationTest.java", oldImport, newImport);

String oldAnnotation = "public @interface";
String newAnnotation = """
@WithMockUser
public @interface""";
projectRepository.replaceText(project, integrationTestPath, "IntegrationTest.java", oldAnnotation, newAnnotation);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.common.domain;

import tech.jhipster.lite.generator.project.domain.Project;

public interface CommonSecurityService {
void updateExceptionTranslator(Project project);
void updateIntegrationTestWithMockUser(Project project);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.common.infrastructure.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.jhipster.lite.generator.project.domain.ProjectRepository;
import tech.jhipster.lite.generator.server.springboot.mvc.security.common.domain.CommonSecurityDomainService;
import tech.jhipster.lite.generator.server.springboot.mvc.security.common.domain.CommonSecurityService;

@Configuration
public class CommonSecurityBeanConfiguration {

private final ProjectRepository projectRepository;

public CommonSecurityBeanConfiguration(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}

@Bean
public CommonSecurityService commonSecurityService() {
return new CommonSecurityDomainService(projectRepository);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@tech.jhipster.lite.SharedKernel
package tech.jhipster.lite.generator.server.springboot.mvc.security.common;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.application;

import org.springframework.stereotype.Service;
import tech.jhipster.lite.generator.project.domain.Project;
import tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain.OAuth2SecurityService;

@Service
public class OAuth2SecurityApplicationService {

private final OAuth2SecurityService oauth2SecurityService;

public OAuth2SecurityApplicationService(OAuth2SecurityService oauth2SecurityService) {
this.oauth2SecurityService = oauth2SecurityService;
}

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

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";

private static final String DOCKER_KEYCLOAK_IMAGE = "jboss/keycloak";
private static final String DOCKER_KEYCLOAK_VERSION = "16.1.0";

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";

private OAuth2Security() {}

public static String getDockerKeycloakImage() {
return DOCKER_KEYCLOAK_IMAGE + ":" + DOCKER_KEYCLOAK_VERSION;
}

public static String getDockerKeycloakVersion() {
return DOCKER_KEYCLOAK_VERSION;
}

public static Dependency springBootStarterSecurityDependency() {
return Dependency.builder().groupId(SPRINGBOOT_PACKAGE).artifactId(STARTER_SECURITY).build();
}

public static Dependency springBootStarterOAuth2ClientDependency() {
return Dependency.builder().groupId(SPRINGBOOT_PACKAGE).artifactId(STARTER_OAUTH2_CLIENT).build();
}

public static Dependency springBootStarterOAuth2ResourceServerDependency() {
return Dependency.builder().groupId(SPRINGBOOT_PACKAGE).artifactId(STARTER_OAUTH2_RESOURCE_SERVER).build();
}

public static Dependency springSecurityTestDependency() {
return Dependency.builder().groupId("org.springframework.security").artifactId("spring-security-test").scope("test").build();
}

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

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

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

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);

return map;
}

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

map.put("SecurityUtilsTest.java", "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("WithUnauthenticatedMockUser.java", "infrastructure");

return map;
}

public static Map<String, String> properties() {
return Map.of(
"spring.security.oauth2.client.provider.oidc.issuer-uri",
"http://localhost:9080/auth/realms/jhipster",
"spring.security.oauth2.client.registration.oidc.client-id",
"web_app",
"spring.security.oauth2.client.registration.oidc.client-secret",
"web_app",
"spring.security.oauth2.client.registration.oidc.scope",
"openid,profile,email",
"application.security.oauth2.audience",
"account,api://default"
);
}

public static Map<String, String> propertiesForTests() {
return Map.of(
"spring.main.allow-bean-definition-overriding",
"true",
"spring.security.oauth2.client.provider.oidc.issuer-uri",
"http://DO_NOT_CALL:9080/auth/realms/jhipster"
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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 static tech.jhipster.lite.generator.project.domain.DefaultConfig.*;
import static tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain.OAuth2Security.*;

import tech.jhipster.lite.common.domain.WordUtils;
import tech.jhipster.lite.generator.buildtool.generic.domain.BuildToolService;
import tech.jhipster.lite.generator.project.domain.Project;
import tech.jhipster.lite.generator.project.domain.ProjectRepository;
import tech.jhipster.lite.generator.server.springboot.common.domain.SpringBootCommonService;
import tech.jhipster.lite.generator.server.springboot.mvc.security.common.domain.CommonSecurityService;

public class OAuth2SecurityDomainService implements OAuth2SecurityService {

public static final String SOURCE = "server/springboot/mvc/security/oauth2";
public static final String SECURITY_OAUTH2_PATH = "security/oauth2";

private final ProjectRepository projectRepository;
private final BuildToolService buildToolService;
private final SpringBootCommonService springBootCommonService;
private final CommonSecurityService commonSecurityService;

public OAuth2SecurityDomainService(
ProjectRepository projectRepository,
BuildToolService buildToolService,
SpringBootCommonService springBootCommonService,
CommonSecurityService commonSecurityService
) {
this.projectRepository = projectRepository;
this.buildToolService = buildToolService;
this.springBootCommonService = springBootCommonService;
this.commonSecurityService = commonSecurityService;
}

@Override
public void addOAuth2(Project project) {
addDependencies(project);
addKeycloakDocker(project);
addJavaFiles(project);
addSpringBootProperties(project);

updateExceptionTranslator(project);
updateIntegrationTestWithMockUser(project);
updateIntegrationTestWithTestSecurityConfiguration(project);
}

private void addDependencies(Project project) {
buildToolService.addDependency(project, springBootStarterSecurityDependency());
buildToolService.addDependency(project, springBootStarterOAuth2ClientDependency());
buildToolService.addDependency(project, springSecurityTestDependency());
buildToolService.addDependency(project, springBootStarterOAuth2ResourceServerDependency());
}

private void addKeycloakDocker(Project project) {
project.addConfig("dockerKeycloakImage", getDockerKeycloakImage());
project.addConfig("dockerKeycloakVersion", getDockerKeycloakVersion());

String dockerSourcePath = getPath(SOURCE, "docker");
String dockerPathRealm = getPath(MAIN_DOCKER, "keycloak-realm-config");
projectRepository.template(project, dockerSourcePath, "keycloak.yml", MAIN_DOCKER, "keycloak.yml");
projectRepository.template(project, dockerSourcePath, "jhipster-realm.json", dockerPathRealm, "jhipster-realm.json");
projectRepository.template(project, dockerSourcePath, "jhipster-users-0.json", dockerPathRealm, "jhipster-users-0.json");
}

private void addJavaFiles(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, SECURITY_OAUTH2_PATH);
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);
oauth2TestSecurityFiles()
.forEach((javaFile, folder) ->
projectRepository.template(project, getPath(sourceTest, folder), javaFile, getPath(destinationTest, folder))
);
}

private void addSpringBootProperties(Project project) {
springBootCommonService.addPropertiesComment(project, "Spring Security OAuth2");
properties().forEach((k, v) -> springBootCommonService.addProperties(project, k, v));
springBootCommonService.addPropertiesNewLine(project);

springBootCommonService.addPropertiesTestComment(project, "Spring Security OAuth2");
propertiesForTests().forEach((k, v) -> springBootCommonService.addPropertiesTest(project, k, v));
springBootCommonService.addPropertiesTestNewLine(project);
}

private void updateExceptionTranslator(Project project) {
commonSecurityService.updateExceptionTranslator(project);
}

private void updateIntegrationTestWithMockUser(Project project) {
commonSecurityService.updateIntegrationTestWithMockUser(project);
}

private void updateIntegrationTestWithTestSecurityConfiguration(Project project) {
project.addDefaultConfig(PACKAGE_NAME);
String packageName = project.getPackageName().orElse(DEFAULT_PACKAGE_NAME);
String packageNamePath = project.getPackageNamePath().orElse(getPath(PACKAGE_PATH));
String integrationTestPath = getPath(TEST_JAVA, packageNamePath);

project.addDefaultConfig(BASE_NAME);
String baseName = project.getBaseName().orElse(DEFAULT_BASE_NAME);
String className = WordUtils.upperFirst(baseName);

String oldImport = "import org.springframework.boot.test.context.SpringBootTest;";
String newImport = String.format(
"""
import org.springframework.boot.test.context.SpringBootTest;
import %s.security.oauth2.infrastructure.config.TestSecurityConfiguration;""",
packageName
);
projectRepository.replaceText(project, integrationTestPath, "IntegrationTest.java", oldImport, newImport);

String oldClass = className + "App.class";
String newClass = className + "App.class, TestSecurityConfiguration.class";
projectRepository.replaceText(project, integrationTestPath, "IntegrationTest.java", oldClass, newClass);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain;

import tech.jhipster.lite.generator.project.domain.Project;

public interface OAuth2SecurityService {
void addOAuth2(Project project);
}
Loading

0 comments on commit 8d9eb4d

Please sign in to comment.