diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..27d6d76c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Use an official OpenJDK runtime as a parent image +FROM debian:latest + +USER root + +# Set the working directory inside the container +WORKDIR /app + +COPY ./ ./ + +RUN apt-get update && apt-get install -y maven openjdk-21-jdk + +RUN mvn clean install +EXPOSE 8080 + +ENTRYPOINT ["mvn", "spring-boot:run"] \ No newline at end of file diff --git a/README.md b/README.md index b0c77d9f..c14d32c9 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,28 @@ The project uses the following key dependencies: - `spring-boot-starter-security`: For securing the application with basic authentication. - `springdoc-openapi-ui`: For generating OpenAPI documentation and Swagger UI. - `spring-boot-starter-actuator`: For monitoring and managing the application. + + +## Security && rbac +Endpoint Access/Security is configured in package com.ase.userservice.security via SecurityConfig.java. + +To add a new rule, you need to fill the function parameters for this snippet: +```java +http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/demo").hasRole("DEFAULT-ROLES-SAU") + .requestMatchers("/admin/**").hasRole("admin") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter)) + ); +``` + +Specifically you need to add the following line for each protected route and role: +```java +.requestMatchers("/").hasRole("") +``` +Glob pattern matching is supported. + +If you need more infos regarding secuirty, visit our documentation page: [placeholder](http://example.com) \ No newline at end of file diff --git a/pom.xml b/pom.xml index e26b2f8a..145a1755 100644 --- a/pom.xml +++ b/pom.xml @@ -64,5 +64,13 @@ h2 runtime + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-security + diff --git a/src/main/java/com/ase/userservice/controllers/DemoController.java b/src/main/java/com/ase/userservice/controllers/DemoController.java new file mode 100644 index 00000000..36062429 --- /dev/null +++ b/src/main/java/com/ase/userservice/controllers/DemoController.java @@ -0,0 +1,15 @@ +package com.ase.userservice.controllers; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class DemoController { + + // to manage access, add route rules in security/SecurityConfig.java like in the + // examples + @GetMapping("/demo") + public String demo() { + return "Hello from DemoController!"; + } +} \ No newline at end of file diff --git a/src/main/java/com/ase/userservice/entities/User.java b/src/main/java/com/ase/userservice/entities/User.java new file mode 100644 index 00000000..1e695ec5 --- /dev/null +++ b/src/main/java/com/ase/userservice/entities/User.java @@ -0,0 +1,26 @@ +package com.ase.userservice.entities; + +import java.util.ArrayList; + +public class User { + public int exp; + public int iat; + public int auth_time; + public String jti; + public String iss; + public String aud; + public String sub; + public String typ; + public String azp; + public String sid; + public String at_hash; + public String acr; + public String upn; + public boolean email_verified; + public String name; + public ArrayList groups; + public String preferred_username; + public String given_name; + public String family_name; + public String email; +} diff --git a/src/main/java/com/ase/userservice/security/JwtAuthConverter.java b/src/main/java/com/ase/userservice/security/JwtAuthConverter.java new file mode 100644 index 00000000..6cebcdd3 --- /dev/null +++ b/src/main/java/com/ase/userservice/security/JwtAuthConverter.java @@ -0,0 +1,27 @@ +// based on this tutorial: https://www.javacodegeeks.com/2025/07/spring-boot-keycloak-role-based-authorization.html + +package com.ase.userservice.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 org.springframework.lang.NonNull; +import java.util.Collection; +import java.util.stream.Collectors; + +public class JwtAuthConverter implements Converter> { + + @Override + public Collection convert(@NonNull Jwt jwt) { + var roles = jwt.getClaimAsStringList("groups"); + + // you can check the roles here if you want to + //for (String role : roles) { + //System.out.println("Role from JWT: " + role); + // } + return roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/ase/userservice/security/SecurityConfig.java b/src/main/java/com/ase/userservice/security/SecurityConfig.java new file mode 100644 index 00000000..8c2122dd --- /dev/null +++ b/src/main/java/com/ase/userservice/security/SecurityConfig.java @@ -0,0 +1,31 @@ +// based on this tutorial: https://www.javacodegeeks.com/2025/07/spring-boot-keycloak-role-based-authorization.html + +package com.ase.userservice.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); + jwtConverter.setJwtGrantedAuthoritiesConverter(new JwtAuthConverter()); + + // the role always has to be capatalized + http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/demo").hasRole("DEFAULT-ROLES-SAU") + .requestMatchers("/admin/**").hasRole("admin") + .anyRequest().authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter))); + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4b5711b9..f7a5442c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -14,6 +14,13 @@ spring: driverClassName: org.h2.Driver username: sa password: password + security: + oauth2: + resourceserver: + jwt: + issuer-uri: https://keycloak.sau-portal.de/realms/sau + jwk-set-uri: https://keycloak.sau-portal.de/realms/sau/protocol/openid-connect/certs server: + port: 8080 error: include-message: always