Added authentication and authorization via Keycloak
This commit is contained in:
parent
a42e1d0f3e
commit
7139d8c331
|
@ -13,6 +13,7 @@ mapstruct = "1.5.3.Final"
|
|||
junit = "5.9.2"
|
||||
assertj = "3.24.2"
|
||||
mockito = "5.3.0"
|
||||
keycloak = "22.0.0"
|
||||
plugin-nebula-release = "17.1.0"
|
||||
plugin-lombok = "8.0.1"
|
||||
plugin-gradle-versions = "0.46.0"
|
||||
|
@ -28,6 +29,7 @@ spring-boot-mongo = { module = "org.springframework.boot:spring-boot-starter-dat
|
|||
spring-boot-validation = { module = "org.springframework.boot:spring-boot-starter-validation" }
|
||||
spring-boot-test = { module = "org.springframework.boot:spring-boot-starter-test" }
|
||||
spring-boot-security = { module = "org.springframework.boot:spring-boot-starter-security" }
|
||||
spring-boot-oauth2-resource-server = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server" }
|
||||
spring-boot-config = { module = "org.springframework.boot:spring-boot-configuration-processor", version.ref = "spring-boot" }
|
||||
spring-cloud = { module = "org.springframework.cloud:spring-cloud-dependencies", version.ref = "spring-cloud" }
|
||||
spring-cloud-starter = { module = "org.springframework.cloud:spring-cloud-starter" }
|
||||
|
@ -36,6 +38,8 @@ spring-grpc = { module = "net.devh:grpc-spring-boot-starter", version.ref = "spr
|
|||
spring-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "spring-doc" }
|
||||
spring-sec = { module = "org.springdoc:springdoc-openapi-starter-common", version.ref = "spring-doc" }
|
||||
spring-openapi = { module = "org.springdoc:springdoc-openapi-starter-webmvc-api", version.ref = "spring-doc" }
|
||||
keycloak-core = { module = "org.keycloak:keycloak-core", version.ref = "keycloak" }
|
||||
keycloak-policy-enforcer = { module = "org.keycloak:keycloak-policy-enforcer", version.ref = "keycloak" }
|
||||
grpc-api = { module = "io.grpc:grpc-api", version.ref = "grpc" }
|
||||
grpc-context = { module = "io.grpc:grpc-context", version.ref = "grpc" }
|
||||
grpc-core = { module = "io.grpc:grpc-core", version.ref = "grpc" }
|
||||
|
@ -104,6 +108,14 @@ spring-boot-server = [
|
|||
"spring-sec",
|
||||
"spring-ui",
|
||||
]
|
||||
|
||||
spring-boot-security = [
|
||||
"spring-boot-security",
|
||||
"spring-boot-oauth2-resource-server",
|
||||
"keycloak-core",
|
||||
"keycloak-policy-enforcer"
|
||||
]
|
||||
|
||||
test = [
|
||||
"assertj",
|
||||
"junit-jupiter",
|
||||
|
|
|
@ -4,7 +4,7 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
implementation libs.mapstruct.base
|
||||
implementation libs.spring.boot.security
|
||||
implementation libs.bundles.spring.boot.security
|
||||
annotationProcessor libs.mapstruct.processor
|
||||
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package de.twomartens.wahlrecht.configuration;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.OAuthFlow;
|
||||
import io.swagger.v3.oas.annotations.security.OAuthFlows;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
@ -8,9 +10,21 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@SecurityScheme(
|
||||
name = "basicAuth",
|
||||
name = "bearerAuth",
|
||||
type = SecuritySchemeType.HTTP,
|
||||
scheme = "basic"
|
||||
scheme = "bearer",
|
||||
bearerFormat = "JWT"
|
||||
)
|
||||
@SecurityScheme(
|
||||
name = "oauth2",
|
||||
type = SecuritySchemeType.OAUTH2,
|
||||
flows = @OAuthFlows(
|
||||
implicit = @OAuthFlow(
|
||||
authorizationUrl = "https://id.2martens.de/realms/wahlrecht/protocol/openid-connect/auth",
|
||||
tokenUrl = "https://id.2martens.de/realms/wahlrecht/protocol/openid-connect/token",
|
||||
scopes = {}
|
||||
)
|
||||
)
|
||||
)
|
||||
@Configuration
|
||||
public class OpenApiConfiguration {
|
||||
|
|
|
@ -1,57 +1,101 @@
|
|||
package de.twomartens.wahlrecht.configuration;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.keycloak.adapters.authorization.integration.jakarta.ServletPolicyEnforcerFilter;
|
||||
import org.keycloak.adapters.authorization.spi.ConfigurationResolver;
|
||||
import org.keycloak.adapters.authorization.spi.HttpRequest;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfiguration {
|
||||
|
||||
public static final String ROLE_USER = "USER";
|
||||
private static final Collection<String> PERMITTED_PATHS = List.of(
|
||||
"/wahlrecht/v1/getToken",
|
||||
"/wahlrecht/v1/doc/**",
|
||||
"/wahlrecht/v1/api-docs/**",
|
||||
"/error");
|
||||
|
||||
private static final List<PathConfig> PATHS = buildPathConfigs();
|
||||
|
||||
|
||||
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
|
||||
private String jwkSetUri;
|
||||
|
||||
@Value("#{environment.CLIENT_SECRET}")
|
||||
private String clientSecret;
|
||||
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(@NonNull HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf().disable()
|
||||
.authorizeHttpRequests()
|
||||
.requestMatchers(HttpMethod.PUT, "/wahlrecht/v1/**")
|
||||
.authenticated()
|
||||
.and()
|
||||
.authorizeHttpRequests()
|
||||
.requestMatchers("/wahlrecht/v1/**", "/wahlrecht/version",
|
||||
"/error")
|
||||
.permitAll()
|
||||
.and()
|
||||
.authorizeHttpRequests()
|
||||
.requestMatchers("/actuator/**")
|
||||
.requestMatchers(PERMITTED_PATHS.toArray(new String[0]))
|
||||
.permitAll()
|
||||
.and()
|
||||
.authorizeHttpRequests()
|
||||
.requestMatchers("/resources/**")
|
||||
.permitAll()
|
||||
.and()
|
||||
.httpBasic();
|
||||
.anyRequest()
|
||||
.authenticated()
|
||||
.and()
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
|
||||
.addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
UserDetails user = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles(ROLE_USER)
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
JwtDecoder jwtDecoder() {
|
||||
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
|
||||
}
|
||||
|
||||
private ServletPolicyEnforcerFilter createPolicyEnforcerFilter() {
|
||||
return new ServletPolicyEnforcerFilter(new ConfigurationResolver() {
|
||||
@Override
|
||||
public PolicyEnforcerConfig resolve(HttpRequest request) {
|
||||
try {
|
||||
PolicyEnforcerConfig policyEnforcerConfig = JsonSerialization.readValue(
|
||||
getClass().getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig.class);
|
||||
policyEnforcerConfig.setCredentials(Map.of("secret", clientSecret));
|
||||
policyEnforcerConfig.setPaths(PATHS);
|
||||
|
||||
return policyEnforcerConfig;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static List<PathConfig> buildPathConfigs() {
|
||||
List<PathConfig> paths = new ArrayList<>();
|
||||
for (String path : PERMITTED_PATHS) {
|
||||
PathConfig pathConfig = new PathConfig();
|
||||
pathConfig.setPath(path.replace("**", "*"));
|
||||
pathConfig.setEnforcementMode(EnforcementMode.DISABLED);
|
||||
paths.add(pathConfig);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
@ -46,6 +47,8 @@ public class CalculationController {
|
|||
}
|
||||
)
|
||||
@PostMapping(value = "/calculate", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
public ResponseEntity<ElectedCandidates> calculateResult(
|
||||
@RequestBody ElectionResult electionResult
|
||||
) {
|
||||
|
|
|
@ -46,6 +46,8 @@ public class ElectionController {
|
|||
}
|
||||
)
|
||||
@GetMapping(value = "/elections", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
public ResponseEntity<Collection<Election>> getElections() {
|
||||
List<Election> elections = service.getElections().stream().map(mapper::mapToExternal).toList();
|
||||
return ResponseEntity.ok()
|
||||
|
@ -68,7 +70,8 @@ public class ElectionController {
|
|||
}
|
||||
)
|
||||
@PutMapping("/election")
|
||||
@SecurityRequirement(name = "basicAuth")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
public ResponseEntity<?> putElection(@RequestBody Election election) {
|
||||
boolean createdNew = service.storeElection(mapper.mapToDB(election));
|
||||
return createdNew
|
||||
|
|
|
@ -55,6 +55,8 @@ public class ElectionResultController {
|
|||
}
|
||||
)
|
||||
@GetMapping(value = "/electionResult/by-election-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
public ResponseEntity<ElectionResult> getElectionByName(
|
||||
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) {
|
||||
ElectionResult result = mapper.mapToExternal(service.getElectionResult(electionName));
|
||||
|
@ -78,7 +80,8 @@ public class ElectionResultController {
|
|||
}
|
||||
)
|
||||
@PutMapping("/electionResult")
|
||||
@SecurityRequirement(name = "basicAuth")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
public ResponseEntity<?> putElection(@RequestBody ElectionResult electionResult) {
|
||||
boolean createdNew = service.storeResult(mapper.mapToDB(electionResult));
|
||||
return createdNew
|
||||
|
|
|
@ -58,6 +58,8 @@ public class PartyController {
|
|||
}
|
||||
)
|
||||
@GetMapping(value = "/parties/by-election-name/{electionName}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
public ResponseEntity<Collection<PartyInElection>> getPartiesByElectionName(
|
||||
@PathVariable @Parameter(description = "the election name", example = "Bezirkswahl 2019") String electionName) {
|
||||
List<PartyInElection> parties = service.getPartiesByElectionName(electionName).stream()
|
||||
|
@ -82,7 +84,8 @@ public class PartyController {
|
|||
}
|
||||
)
|
||||
@PutMapping("/party")
|
||||
@SecurityRequirement(name = "basicAuth")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
@SecurityRequirement(name = "oauth2")
|
||||
public ResponseEntity<?> putParty(@RequestBody PartyInElection party) {
|
||||
boolean createdNew = service.storeParty(mapper.mapToDB(party));
|
||||
return createdNew
|
||||
|
|
|
@ -30,6 +30,12 @@ spring:
|
|||
data.mongodb:
|
||||
uri: ${MONGODB_CONNECTION_STRING}
|
||||
auto-index-creation: true
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
jwk-set-uri: https://id.2martens.de/realms/wahlrecht/protocol/openid-connect/certs
|
||||
|
||||
|
||||
# properties for application
|
||||
de.twomartens.wahlrecht:
|
||||
|
@ -49,6 +55,16 @@ springdoc:
|
|||
openapi:
|
||||
description: |
|
||||
Open API Documentation for the Wahlrecht API
|
||||
|
||||
## Authenticate
|
||||
|
||||
The API is secured by the need to provide bearer tokens. Anonymous access is supported
|
||||
with the user "anonymous" with password "anonymous". It grants access to all GET
|
||||
operations as well as the calculation of results.
|
||||
|
||||
For changes to the database-stored data, you need a proper authorized user.
|
||||
|
||||
The client_id is "wahlrecht", if you are on the Swagger UI and want to authorize there.
|
||||
|
||||
## Calculate election results
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"realm": "wahlrecht",
|
||||
"auth-server-url": "https://id.2martens.de",
|
||||
"resource": "wahlrecht",
|
||||
"http-method-as-scope": true
|
||||
}
|
Loading…
Reference in New Issue