Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring Boot Security: OAuth 2.0 and OpenID Connect #443

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
956475e
oauth2 init
pblanchardie Dec 30, 2021
3445418
oauth2 provider choice with defaults
pblanchardie Dec 31, 2021
e58b6a7
renaming
pblanchardie Dec 31, 2021
750d480
KISS
pblanchardie Dec 31, 2021
9774745
remove unused dependency
pblanchardie Dec 31, 2021
e171171
coverage
pblanchardie Dec 31, 2021
c89b698
remove unused assertOAuth2ResourceServerDependencies
pblanchardie Jan 2, 2022
0c3d17f
add docker consul
pblanchardie Jan 2, 2022
a9cfcf7
add to CI
pblanchardie Jan 2, 2022
87e1022
patch ExceptionTranslatorIT and clear TODO
pblanchardie Jan 2, 2022
6e351b0
rebase
pblanchardie Jan 3, 2022
1b0b655
Merge remote-tracking branch 'upstream/main' into 270-add-spring-secu…
pascalgrimaud Jan 14, 2022
d71f031
Fix renamed methods
pascalgrimaud Jan 14, 2022
17acb55
OAuth2: refactoring templates folder
pascalgrimaud Jan 14, 2022
0b85cf6
fix typo
pblanchardie Jan 17, 2022
0ac13d0
move to mvc/
pblanchardie Jan 17, 2022
c81e359
TODO CSRF
pblanchardie Jan 17, 2022
e3f964f
fix test and revert resource server dependency
pblanchardie Jan 17, 2022
f99230d
Merge remote-tracking branch 'upstream/main' into 270-add-spring-secu…
pascalgrimaud Mar 4, 2022
3126e03
OAuth2: add generator step
pascalgrimaud Mar 4, 2022
7db2b86
OAuth2: add docker-compose files with realms and users
pascalgrimaud Mar 4, 2022
8eb4a61
OAuth2: clean other methods
pascalgrimaud Mar 4, 2022
3a925e6
OAuth2: add java files with tests
pascalgrimaud Mar 4, 2022
b138e40
OAuth2: add dependencies
pascalgrimaud Mar 4, 2022
8cdc246
OAuth2: add properties
pascalgrimaud Mar 4, 2022
0c40780
OAuth2: update ExceptionTranslator and IntegrationTest
pascalgrimaud Mar 4, 2022
5ec3434
OAuth2: clean unused code
pascalgrimaud Mar 4, 2022
26e3f60
OAuth2: clean unused import
pascalgrimaud Mar 4, 2022
f8ac2bb
OAuth2: add oauth2app in CI
pascalgrimaud Mar 4, 2022
0556f38
OAuth2: fix properties for test with spring.main.allow-bean-definitio…
pascalgrimaud Mar 4, 2022
3b5b07d
OAuth2: fix generated IntegrationTest
pascalgrimaud Mar 4, 2022
8f49bd5
OAuth2: clean import
pascalgrimaud Mar 4, 2022
7619106
OAuth2: fix some code smells
pascalgrimaud Mar 4, 2022
355ce80
OAuth2: polish and clean import
pascalgrimaud Mar 4, 2022
75d1442
OAuth2: put back missing tests in Security Utils
pascalgrimaud Mar 4, 2022
c5b5ad7
OAuth2: polish and improve code, reviewed by @Bolo89
pascalgrimaud Mar 4, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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