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 11 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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.OAuth2Provider;
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 addDefault(Project project, OAuth2Provider provider, String issuerUri) {
oauth2SecurityService.addDefault(project, provider, issuerUri);
}

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

/**
* Spring Security public built-in providers are automatically configured.
* - Google
* - Facebook
* - GitHub
* <p>
* Custom providers require an issuer URI but a default is provided.
*/
public enum OAuth2Provider {
GOOGLE(true, false),
FACEBOOK(true, false),
GITHUB(true, false),
OKTA(true, true, "https://your-okta-domain/oauth2/default"),
KEYCLOAK(false, true, "http://localhost:9080/auth/realms/jhipster"),
AUTHO0(false, true, "https://your-auth0-domain/"),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: AUTH0

OTHER(false, true, "https://your-issuer-uri");

private final boolean builtIn;
private final boolean custom;
private final String defaultIssuerUri;

OAuth2Provider(boolean builtIn, boolean custom) {
this(builtIn, custom, null);
}

OAuth2Provider(boolean builtIn, boolean custom, String defaultIssuerUri) {
this.builtIn = builtIn;
this.custom = custom;
this.defaultIssuerUri = defaultIssuerUri;
}

public boolean isBuiltIn() {
return builtIn;
}

public boolean isCustom() {
return custom;
}

public String getDefaultIssuerUri() {
return defaultIssuerUri;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain;

import tech.jhipster.lite.generator.buildtool.generic.domain.Dependency;

public class OAuth2Security {

public static final OAuth2Provider DEFAULT_PROVIDER = OAuth2Provider.KEYCLOAK;

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 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 springSecurityTestDependency() {
return Dependency.builder().groupId("org.springframework.security").artifactId("spring-security-test").scope("test").build();
}

public static Dependency springBootStarterOAuth2ClientDependency() {
return Dependency.builder().groupId(SPRINGBOOT_PACKAGE).artifactId(STARTER_OAUTH2_CLIENT).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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.TEST_JAVA;
import static tech.jhipster.lite.generator.project.domain.DefaultConfig.PACKAGE_PATH;
import static tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain.OAuth2Security.*;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
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.properties.domain.SpringBootPropertiesService;

public class OAuth2SecurityDomainService implements OAuth2SecurityService {

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

private final ProjectRepository projectRepository;
private final BuildToolService buildToolService;
private final SpringBootPropertiesService springBootPropertiesService;

public OAuth2SecurityDomainService(
ProjectRepository projectRepository,
BuildToolService buildToolService,
SpringBootPropertiesService springBootPropertiesService
) {
this.projectRepository = projectRepository;
this.buildToolService = buildToolService;
this.springBootPropertiesService = springBootPropertiesService;
}

@Override
public void addClient(Project project, OAuth2Provider provider, String issuerUri) {
addCommons(project, provider, issuerUri);
}

@Override
public void addDefault(Project project, OAuth2Provider provider, String issuerUri) {
addCommons(project, provider, issuerUri);
// TODO default security configuration
}

@Override
public void addKeycloakDocker(Project project) {
project.addConfig("dockerKeycloakImage", getDockerKeycloakImage());
project.addConfig("dockerKeycloakVersion", getDockerKeycloakVersion());
projectRepository.template(project, getPath(SOURCE, "src"), "keycloak.yml", "src/main/docker", "keycloak.yml");
projectRepository.template(
project,
getPath(SOURCE, "src"),
"jhipster-realm.json.mustache",
"src/main/docker/keycloak-realm-config",
"jhipster-realm.json"
);
projectRepository.template(
project,
getPath(SOURCE, "src"),
"jhipster-users-0.json.mustache",
"src/main/docker/keycloak-realm-config",
"jhipster-users-0.json"
);
}

private void addCommons(Project project, OAuth2Provider provider, String issuerUri) {
addOAuth2ClientDependencies(project);
OAuth2Provider providerFallback = fallbackToDefault(provider);
addOAuth2ClientProperties(project, providerFallback, issuerUri);
updateExceptionTranslator(project);
if (providerFallback == OAuth2Provider.KEYCLOAK) {
addKeycloakDocker(project);
}
}

private OAuth2Provider fallbackToDefault(OAuth2Provider provider) {
return Optional.ofNullable(provider).orElse(DEFAULT_PROVIDER);
}

private void addOAuth2ClientDependencies(Project project) {
buildToolService.addDependency(project, springBootStarterSecurityDependency());
buildToolService.addDependency(project, springBootStarterOAuth2ClientDependency());
// test
buildToolService.addDependency(project, springSecurityTestDependency());
}

private void addOAuth2ClientProperties(Project project, OAuth2Provider provider, String issuerUri) {
oauth2ClientProperties(provider, issuerUri)
.forEach((k, v) -> {
springBootPropertiesService.addProperties(project, k, v);
springBootPropertiesService.addPropertiesTest(project, k, v);
});
}

private Map<String, Object> oauth2ClientProperties(OAuth2Provider provider, String issuerUri) {
Map<String, Object> result = new LinkedHashMap<>();

String providerId = provider.name().toLowerCase();

if (provider.isCustom()) {
String issuerUriFallback = Optional.ofNullable(issuerUri).orElse(provider.getDefaultIssuerUri());
result.put("spring.security.oauth2.client.provider." + providerId + ".issuer-uri", issuerUriFallback);
}
if (!provider.isBuiltIn()) {
result.put("spring.security.oauth2.client.registration." + providerId + ".client-name", providerId);
}
result.put("spring.security.oauth2.client.registration." + providerId + ".client-id", "web_app");
result.put("spring.security.oauth2.client.registration." + providerId + ".client-secret", "web_app");
result.put("spring.security.oauth2.client.registration." + providerId + ".scope", "openid,profile,email");

return result;
}

private void updateExceptionTranslator(Project project) {
String packageNamePath = project.getPackageNamePath().orElse(getPath(PACKAGE_PATH));
String exceptionPath = getPath(TEST_JAVA, packageNamePath, "technical/infrastructure/primary/exception");

// create ExceptionTranslatorTestConfiguration to disable csrf
projectRepository.template(project, getPath(SOURCE, "test"), "ExceptionTranslatorTestConfiguration.java", exceptionPath);

// import @Import
String oldImport1 = "import org.springframework.context.ApplicationContext;";
String newImport1 =
"""

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;""";
projectRepository.replaceText(project, exceptionPath, "ExceptionTranslatorIT.java", oldImport1, newImport1);

// import @WithMockUser
String oldImport2 = "import org.springframework.test.util.ReflectionTestUtils;";
String newImport2 =
"""

import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.util.ReflectionTestUtils;""";
projectRepository.replaceText(project, exceptionPath, "ExceptionTranslatorIT.java", oldImport2, newImport2);

// add annotations
String oldAnnotation = "@AutoConfigureMockMvc";
String newAnnotation = """
@AutoConfigureMockMvc
@Import(ExceptionTranslatorTestConfiguration.class)
@WithMockUser""";
projectRepository.replaceText(project, exceptionPath, "ExceptionTranslatorIT.java", oldAnnotation, newAnnotation);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain;

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

public interface OAuth2SecurityService {
void addClient(Project project, OAuth2Provider provider, String issuerUri);
void addDefault(Project project, OAuth2Provider provider, String issuerUri);
void addKeycloakDocker(Project project);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.infrastructure.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tech.jhipster.lite.generator.buildtool.generic.domain.BuildToolService;
import tech.jhipster.lite.generator.project.domain.ProjectRepository;
import tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain.OAuth2SecurityDomainService;
import tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain.OAuth2SecurityService;
import tech.jhipster.lite.generator.server.springboot.properties.domain.SpringBootPropertiesService;

@Configuration
public class OAuth2SecurityBeanConfiguration {

private final ProjectRepository projectRepository;
private final BuildToolService buildToolService;
private final SpringBootPropertiesService springBootPropertiesService;

public OAuth2SecurityBeanConfiguration(
ProjectRepository projectRepository,
BuildToolService buildToolService,
SpringBootPropertiesService springBootPropertiesService
) {
this.projectRepository = projectRepository;
this.buildToolService = buildToolService;
this.springBootPropertiesService = springBootPropertiesService;
}

@Bean
public OAuth2SecurityService oauth2SecurityService() {
return new OAuth2SecurityDomainService(projectRepository, buildToolService, springBootPropertiesService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.infrastructure.primary.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import tech.jhipster.lite.generator.project.infrastructure.primary.dto.ProjectDTO;
import tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.domain.OAuth2Provider;

@Schema(description = "OAuth2 Client DTO")
public class OAuth2ClientDTO extends ProjectDTO {

@JsonProperty(value = "provider", defaultValue = "KEYCLOAK")
@Schema(description = "OAuth2-OIDC provider", nullable = true, defaultValue = "KEYCLOAK")
private OAuth2Provider provider;

@JsonProperty("issuerUri")
@Schema(description = "your custom issuer URI for KEYCLOAK, OKTA, AUTH0 or OTHER", nullable = true)
private String issuerUri;

public OAuth2Provider getProvider() {
return provider;
}

public OAuth2ClientDTO provider(OAuth2Provider provider) {
this.provider = provider;
return this;
}

public String getIssuerUri() {
return issuerUri;
}

public OAuth2ClientDTO issuerUri(String issuerUri) {
this.issuerUri = issuerUri;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.infrastructure.rest;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.jhipster.lite.generator.project.domain.Project;
import tech.jhipster.lite.generator.project.infrastructure.primary.dto.ProjectDTO;
import tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.application.OAuth2SecurityApplicationService;
import tech.jhipster.lite.generator.server.springboot.mvc.security.oauth2.infrastructure.primary.dto.OAuth2ClientDTO;

@RestController
@RequestMapping("/api/servers/spring-boot/security")
@Tag(name = "Spring Boot - Security")
class OAuth2SecurityResource {

private final OAuth2SecurityApplicationService oauth2SecurityApplicationService;

public OAuth2SecurityResource(OAuth2SecurityApplicationService oauth2SecurityApplicationService) {
this.oauth2SecurityApplicationService = oauth2SecurityApplicationService;
}

@Operation(summary = "Add a Spring Security OAuth2-OIDC Client")
@ApiResponse(responseCode = "500", description = "An error occurred while adding a Spring Security OAuth2-OIDC Client")
@PostMapping("/oauth2/add-client")
public void addClient(@RequestBody OAuth2ClientDTO oAuth2ClientDTO) {
Project project = ProjectDTO.toProject(oAuth2ClientDTO);
oauth2SecurityApplicationService.addClient(project, oAuth2ClientDTO.getProvider(), oAuth2ClientDTO.getIssuerUri());
}

@Operation(summary = "Add Spring Security default login with OAuth2")
@ApiResponse(responseCode = "500", description = "An error occurred while adding Spring Security default login with OAuth2")
@PostMapping("/oauth2/default")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"default" => "full" means Client + Login (for frontend) + Server (for service).

Which options to provide?
eg.

  • full
  • login-only (for a monolith that does not expose its API)
  • service-only (for web services, microservice)

public void addDefault(@RequestBody OAuth2ClientDTO oAuth2ClientDTO) {
Project project = ProjectDTO.toProject(oAuth2ClientDTO);
oauth2SecurityApplicationService.addDefault(project, oAuth2ClientDTO.getProvider(), oAuth2ClientDTO.getIssuerUri());
}
}
Loading