diff --git a/bruno/propify/Authenticate/Auth.bru b/bruno/propify/Authenticate/Auth.bru
new file mode 100644
index 0000000..2b1b156
--- /dev/null
+++ b/bruno/propify/Authenticate/Auth.bru
@@ -0,0 +1,48 @@
+meta {
+ name: Auth
+ type: http
+ seq: 2
+}
+
+post {
+ url: {{KEYCLOAK_BASE_URL}}/realms/propify/protocol/openid-connect/token
+ body: formUrlEncoded
+ auth: inherit
+}
+
+headers {
+ Content-Type: application/x-www-form-urlencoded
+}
+
+body:form-urlencoded {
+ grant_type: password
+ client_id: {{KEYCLOAK_CLIENT_ID}}
+ username: {{ADMIN_USERNAME}}
+ password: {{ADMIN_PASSWORD}}
+}
+
+script:post-response {
+ // Parse die JSON-Antwort
+ const jsonResponse = res.body;
+
+
+ if (jsonResponse.access_token) {
+ // Schreibe den access_token in eine Umgebungsvariable
+ // oder in eine collection-Variable
+
+ // Option 1: In eine Umgebungsvariable schreiben
+ // (z.B. für eine bestimmte Umgebung wie "Development")
+ bru.setEnvVar("BEARER_TOKEN", jsonResponse.access_token);
+
+ // Option 2: In eine Collection-Variable schreiben
+ // (Diese Variable ist global für alle Anfragen in deiner Collection)
+ // bru.setVar("bearerToken", "Bearer " + jsonResponse.access_token);
+ } else {
+ // optional: Error Handling, falls der Token nicht in der Antwort ist
+ console.log("Error: access_token not found in the response.");
+ }
+}
+
+settings {
+ encodeUrl: false
+}
diff --git a/bruno/propify/Authenticate/folder.bru b/bruno/propify/Authenticate/folder.bru
new file mode 100644
index 0000000..196f590
--- /dev/null
+++ b/bruno/propify/Authenticate/folder.bru
@@ -0,0 +1,8 @@
+meta {
+ name: Authenticate
+ seq: 2
+}
+
+auth {
+ mode: inherit
+}
diff --git a/bruno/propify/Properties/Create new.bru b/bruno/propify/Properties/Create new.bru
index d2ad449..a79b3d5 100644
--- a/bruno/propify/Properties/Create new.bru
+++ b/bruno/propify/Properties/Create new.bru
@@ -5,9 +5,13 @@ meta {
}
post {
- url: {{BASE_URL}}/api/{{API_VERSION}}/properties
+ url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties
body: json
- auth: inherit
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{BEARER_TOKEN}}
}
body:json {
diff --git a/bruno/propify/Properties/Delete one by ID.bru b/bruno/propify/Properties/Delete one by ID.bru
index d71ff16..c5057b9 100644
--- a/bruno/propify/Properties/Delete one by ID.bru
+++ b/bruno/propify/Properties/Delete one by ID.bru
@@ -5,9 +5,13 @@ meta {
}
delete {
- url: {{BASE_URL}}/api/{{API_VERSION}}/properties/e64d5e53-cd45-45fb-9237-46078077bf22
+ url: API_BASE_URL}}/api/{{API_VERSION}}/properties/e64d5e53-cd45-45fb-9237-46078077bf22
body: json
- auth: inherit
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{BEARER_TOKEN}}
}
body:json {
diff --git a/bruno/propify/Properties/Get all.bru b/bruno/propify/Properties/Get all.bru
index 919b56a..9ae00d5 100644
--- a/bruno/propify/Properties/Get all.bru
+++ b/bruno/propify/Properties/Get all.bru
@@ -5,9 +5,13 @@ meta {
}
get {
- url: {{BASE_URL}}/api/{{API_VERSION}}/properties
+ url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties
body: none
- auth: inherit
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{BEARER_TOKEN}}
}
settings {
diff --git a/bruno/propify/Properties/Get one by ID.bru b/bruno/propify/Properties/Get one by ID.bru
index 018a2a9..8631471 100644
--- a/bruno/propify/Properties/Get one by ID.bru
+++ b/bruno/propify/Properties/Get one by ID.bru
@@ -5,9 +5,13 @@ meta {
}
get {
- url: {{BASE_URL}}/api/{{API_VERSION}}/properties/b6ab79d8-f7eb-4fb7-acde-e3310cb0166c
+ url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties/5dba067e-d7fd-4d79-a08a-ec379834938a
body: none
- auth: inherit
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{BEARER_TOKEN}}
}
settings {
diff --git a/bruno/propify/environments/local.bru b/bruno/propify/environments/local.bru
index 1d1d7b3..de3d934 100644
--- a/bruno/propify/environments/local.bru
+++ b/bruno/propify/environments/local.bru
@@ -1,4 +1,11 @@
vars {
- BASE_URL: http://localhost:8080
+ API_BASE_URL: http://localhost:8080
API_VERSION: v1
+ BEARER_TOKEN: -
+ KEYCLOAK_BASE_URL: http://localhost:8280
+ DEV_USERNAME: dev@example.com
+ DEV_PASSWORD: dev
+ ADMIN_USERNAME: admin@example.com
+ ADMIN_PASSWORD: admin
+ KEYCLOAK_CLIENT_ID: propify-app
}
diff --git a/pom.xml b/pom.xml
index 2e2fc54..d3cb5cf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,6 +75,15 @@
2.6.0
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
diff --git a/src/main/java/de/iwomm/propify_api/controller/PropertyController.java b/src/main/java/de/iwomm/propify_api/controller/PropertyController.java
index 01b5489..809bdfa 100644
--- a/src/main/java/de/iwomm/propify_api/controller/PropertyController.java
+++ b/src/main/java/de/iwomm/propify_api/controller/PropertyController.java
@@ -3,6 +3,7 @@ package de.iwomm.propify_api.controller;
import de.iwomm.propify_api.entity.Property;
import de.iwomm.propify_api.service.PropertyService;
import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -18,24 +19,34 @@ public class PropertyController {
this.propertyService = propertyService;
}
+ @GetMapping("/info")
+ @PreAuthorize("isAuthenticated()")
+ public String infoEndpoint() {
+ return "Hello, you are authenticated!";
+ }
+
@GetMapping
+ @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
public List getAllProperties() {
return propertyService.findAll();
}
@GetMapping("/{id}")
+ @PreAuthorize("hasAnyRole('ADMIN', 'USER')")
public Property getPropertyById(@PathVariable UUID id) {
return propertyService.findById(id).orElse(null);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
+ @PreAuthorize("hasRole('ADMIN')")
public Property createProperty(@RequestBody Property property) {
return propertyService.save(property);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @PreAuthorize("hasRole('ADMIN')")
public void deleteProperty(@PathVariable UUID id) {
Optional property = propertyService.findById(id);
if (property.isEmpty()) {
diff --git a/src/main/java/de/iwomm/propify_api/security/CorsConfig.java b/src/main/java/de/iwomm/propify_api/security/CorsConfig.java
new file mode 100644
index 0000000..6705614
--- /dev/null
+++ b/src/main/java/de/iwomm/propify_api/security/CorsConfig.java
@@ -0,0 +1,28 @@
+package de.iwomm.propify_api.security;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+ private final CorsProperties corsProperties;
+
+ public CorsConfig(CorsProperties corsProperties) {
+ this.corsProperties = corsProperties;
+ }
+
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**") // Apply rules to all endpoints
+ .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) // This targets the frontend app's URLs (you can allow multiple URLs, e.g. "http://localhost:4200,http://example.com"
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+ .allowedHeaders(corsProperties.getAllowedHeaders())
+ .allowCredentials(true); // Allow cookies and authentication headers
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/iwomm/propify_api/security/CorsProperties.java b/src/main/java/de/iwomm/propify_api/security/CorsProperties.java
new file mode 100644
index 0000000..fa18893
--- /dev/null
+++ b/src/main/java/de/iwomm/propify_api/security/CorsProperties.java
@@ -0,0 +1,29 @@
+package de.iwomm.propify_api.security;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+@Configuration
+@ConfigurationProperties(prefix = "cors")
+public class CorsProperties {
+ private List allowedOrigins;
+ private String allowedHeaders;
+
+ public List getAllowedOrigins() {
+ return allowedOrigins;
+ }
+
+ public void setAllowedOrigins(List allowedOrigins) {
+ this.allowedOrigins = allowedOrigins;
+ }
+
+ public String getAllowedHeaders() {
+ return allowedHeaders;
+ }
+
+ public void setAllowedHeaders(String allowedHeaders) {
+ this.allowedHeaders = allowedHeaders;
+ }
+}
diff --git a/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java b/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java
new file mode 100644
index 0000000..d09984e
--- /dev/null
+++ b/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java
@@ -0,0 +1,33 @@
+package de.iwomm.propify_api.security;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class KeycloakRoleConverter implements Converter> {
+
+ @Override
+ public Collection convert(Jwt jwt) {
+ // Holen Sie sich das "realm_access" Feld aus dem Token
+ Map realmAccess = (Map) jwt.getClaims().get("realm_access");
+
+ if (realmAccess == null || realmAccess.isEmpty()) {
+ return List.of();
+ }
+
+ // Holen Sie sich die Liste der Rollen
+ List roles = (List) realmAccess.get("roles");
+
+ // Konvertieren Sie die Rollen in Spring Security GrantedAuthority-Objekte
+ return roles.stream()
+ .map(roleName -> "ROLE_" + roleName.toUpperCase()) // Empfohlene Namenskonvention
+ .map(SimpleGrantedAuthority::new)
+ .collect(Collectors.toList());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java b/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java
index 10f2339..361dde4 100644
--- a/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java
+++ b/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java
@@ -2,27 +2,44 @@ package de.iwomm.propify_api.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
+@EnableMethodSecurity // Wichtig für die Verwendung von @PreAuthorize
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
- .csrf(csrf -> csrf.disable()) // CSRF deaktivieren für APIs
- .authorizeHttpRequests(auth -> auth
- .requestMatchers("/api/**").permitAll() // API ist offen
- .anyRequest().permitAll() // Alles andere auch offen
+ .csrf(AbstractHttpConfigurer::disable)
+ .cors(cors -> {})
+
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers("/api/**").authenticated()
+ .anyRequest().permitAll()
)
- .httpBasic(httpBasic -> httpBasic.disable()) // Kein Basic Auth
- .formLogin(form -> form.disable()); // Kein Login-Formular
+
+ .oauth2ResourceServer(oauth2 -> oauth2
+ .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
+ )
+
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
-
-}
+ // Konvertiert die Keycloak-Rollen (im JWT) in Spring Security Authorities
+ private JwtAuthenticationConverter jwtAuthenticationConverter() {
+ JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
+ jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
+ return jwtAuthenticationConverter;
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index ec39737..872f6f7 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -13,3 +13,20 @@ spring:
h2:
console:
enabled: true
+ security:
+ oauth2:
+ resourceserver:
+ jwt:
+ issuer-uri: http://localhost:8280/realms/propify
+ jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
+logging:
+ level:
+ org:
+ springframework:
+ security: DEBUG
+
+cors:
+ allowed-origins:
+ - http://localhost:4200
+ - http://localhost:8080
+ allowed-headers: "*"
\ No newline at end of file