diff --git a/.github/workflows/dockerImage.yml b/.github/workflows/dockerImage.yml
index 653c052..c6bee83 100644
--- a/.github/workflows/dockerImage.yml
+++ b/.github/workflows/dockerImage.yml
@@ -27,7 +27,7 @@ jobs:
- name: Setup JVM
uses: actions/setup-java@v1
with:
- java-version: 11.0.10
+ java-version: 17.0.7
java-package: jdk
architecture: x64
- name: Caching maven dependencies
@@ -62,7 +62,7 @@ jobs:
push_to_registry:
strategy:
matrix:
- registry: ["docker.pkg.github.com", "ghcr.io"]
+ registry: ["ghcr.io"]
needs: [test]
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
diff --git a/.github/workflows/feature-branch.yml b/.github/workflows/feature-branch.yml
index 9aaa919..502d943 100644
--- a/.github/workflows/feature-branch.yml
+++ b/.github/workflows/feature-branch.yml
@@ -17,7 +17,7 @@ jobs:
- name: Setup JVM
uses: actions/setup-java@v2
with:
- java-version: "11"
+ java-version: 17.0.7
distribution: "adopt"
architecture: x64
diff --git a/Dockerfile b/Dockerfile
index 0210d5d..4f1a568 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
-FROM adoptopenjdk/openjdk11
+FROM openjdk:17-oracle
VOLUME ["/tmp","/log"]
EXPOSE 8080
ARG JAR_FILE
COPY ./LiveService.jar app.jar
-ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
+ENTRYPOINT ["java","-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
diff --git a/pom.xml b/pom.xml
index 0e066af..a397423 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,21 +14,25 @@
org.springframework.boot
spring-boot-starter-parent
- 2.5.14
+ 3.0.6
UTF-8
UTF-8
- 11
+ 17
17.0.0
- 2.17.1
+ 2.19.0
0.2.3
- 5.7.5
- 5.1.1
+ 6.0.5
+ 6.6.0
3.0.0
+ 17
+ 17
+ 2.10.9.2
+ 8.0.0.Final
@@ -41,17 +45,14 @@
org.springframework.security
spring-security-messaging
- 5.3.5.RELEASE
org.springframework.security
spring-security-web
- 5.3.9.RELEASE
org.springframework.security
spring-security-config
- 5.3.5.RELEASE
@@ -66,6 +67,22 @@
spring-boot-starter-actuator
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+
+ net.sf.ehcache
+ ehcache
+ ${ehcache.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
org.codehaus.plexus
@@ -105,12 +122,24 @@
${jackson-databind-nullable.version}
+
+ io.swagger.core.v3
+ swagger-annotations
+ 2.2.15
+
+
io.springfox
springfox-boot-starter
${springfox-boot-starter.version}
+
+ org.hibernate.validator
+ hibernate-validator
+ ${hibernate.validator.version}
+
+
org.keycloak
@@ -175,6 +204,11 @@
2.0
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
org.springframework.boot
@@ -241,7 +275,7 @@
org.openapitools
openapi-generator-maven-plugin
- 5.1.1
+ 6.6.0
@@ -251,6 +285,7 @@
true
/
+ true
${project.basedir}/api/liveservice.yaml
spring
diff --git a/src/main/java/de/caritas/cob/liveservice/api/auth/AuthorisationService.java b/src/main/java/de/caritas/cob/liveservice/api/auth/AuthorisationService.java
new file mode 100644
index 0000000..ab2ce16
--- /dev/null
+++ b/src/main/java/de/caritas/cob/liveservice/api/auth/AuthorisationService.java
@@ -0,0 +1,48 @@
+package de.caritas.cob.liveservice.api.auth;
+
+import com.google.common.collect.Lists;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AuthorisationService {
+
+ private final RoleAuthorizationAuthorityMapper roleAuthorizationAuthorityMapper =
+ new RoleAuthorizationAuthorityMapper();
+
+ public Object getUsername() {
+ return getPrincipal().getClaims().get("username");
+ }
+
+ private Authentication getAuthentication() {
+ return SecurityContextHolder.getContext().getAuthentication();
+ }
+
+ private Jwt getPrincipal() {
+ return (Jwt) getAuthentication().getPrincipal();
+ }
+
+ public Collection extractRealmAuthorities(Jwt jwt) {
+ var roles = extractRealmRoles(jwt);
+ return roleAuthorizationAuthorityMapper.mapAuthorities(
+ roles.stream().collect(Collectors.toSet()));
+ }
+
+ public Collection extractRealmRoles(Jwt jwt) {
+ Map realmAccess = (Map) jwt.getClaims().get("realm_access");
+ if (realmAccess != null) {
+ var roles = (List) realmAccess.get("roles");
+ if (roles != null) {
+ return roles;
+ }
+ }
+ return Lists.newArrayList();
+ }
+}
diff --git a/src/main/java/de/caritas/cob/liveservice/api/auth/Authority.java b/src/main/java/de/caritas/cob/liveservice/api/auth/Authority.java
new file mode 100644
index 0000000..91f2e9e
--- /dev/null
+++ b/src/main/java/de/caritas/cob/liveservice/api/auth/Authority.java
@@ -0,0 +1,46 @@
+package de.caritas.cob.liveservice.api.auth;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.stream.Stream;
+import lombok.Getter;
+
+/** Definition of all authorities and of the role-authority-mapping. */
+@Getter
+public enum Authority {
+ CONSULTANT(UserRole.CONSULTANT, "AUTHORIZATION_CONSULTANT_DEFAULT"),
+ USER(UserRole.USER, "AUTHORIZATION_USER_DEFAULT"),
+
+ JITSI_TECHNICAL(UserRole.JITSI_TECHNICAL, "AUTHORIZATION_JITSI_TECHNICAL_DEFAULT");
+
+ private final UserRole role;
+ private final List authorities;
+
+ Authority(final UserRole role, final String authorityName) {
+ this.role = role;
+ this.authorities = Lists.newArrayList(authorityName);
+ }
+
+ /**
+ * Finds a {@link Authority} instance by given roleName.
+ *
+ * @param roleName the role name to search for
+ * @return the {@link Authority} instance
+ */
+ public static Authority fromRoleName(String roleName) {
+ return Stream.of(values())
+ .filter(authority -> authority.role.getValue().equals(roleName))
+ .findFirst()
+ .orElse(null);
+ }
+
+ public static class AuthorityValue {
+
+ private AuthorityValue() {}
+
+ public static final String PREFIX = "AUTHORIZATION_";
+ public static final String CONSULTANT = PREFIX + "CONSULTANT_DEFAULT";
+ public static final String USER = PREFIX + "USER_DEFAULT";
+ public static final String JITSI_TECHNICAL = PREFIX + "JITSI_TECHNICAL_DEFAULT";
+ }
+}
diff --git a/src/main/java/de/caritas/cob/liveservice/api/auth/JwtAuthConverter.java b/src/main/java/de/caritas/cob/liveservice/api/auth/JwtAuthConverter.java
new file mode 100644
index 0000000..17093e2
--- /dev/null
+++ b/src/main/java/de/caritas/cob/liveservice/api/auth/JwtAuthConverter.java
@@ -0,0 +1,61 @@
+package de.caritas.cob.liveservice.api.auth;
+
+import de.caritas.cob.liveservice.config.security.JwtAuthConverterProperties;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthConverter implements Converter {
+
+ private final @NonNull AuthorisationService authorisationService;
+
+ private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
+ new JwtGrantedAuthoritiesConverter();
+
+ private final JwtAuthConverterProperties properties;
+
+ public JwtAuthConverter(
+ JwtAuthConverterProperties properties, AuthorisationService authorisationService) {
+ this.properties = properties;
+ this.authorisationService = authorisationService;
+ }
+
+ @Override
+ public AbstractAuthenticationToken convert(Jwt jwt) {
+ var authorities = getGrantedAuthorities(jwt);
+ return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
+ }
+
+ private Collection getGrantedAuthorities(Jwt jwt) {
+ Collection convertedGrantedAuthorities =
+ jwtGrantedAuthoritiesConverter.convert(jwt);
+ if (convertedGrantedAuthorities != null) {
+ return Stream.concat(
+ convertedGrantedAuthorities.stream(),
+ authorisationService.extractRealmAuthorities(jwt).stream())
+ .collect(Collectors.toSet());
+ } else {
+ return authorisationService.extractRealmAuthorities(jwt);
+ }
+ }
+
+ private String getPrincipalClaimName(Jwt jwt) {
+ String claimName = JwtClaimNames.SUB;
+ if (properties.getPrincipalAttribute() != null) {
+ claimName = properties.getPrincipalAttribute();
+ }
+ return jwt.getClaim(claimName);
+ }
+}
diff --git a/src/main/java/de/caritas/cob/liveservice/api/auth/RoleAuthorizationAuthorityMapper.java b/src/main/java/de/caritas/cob/liveservice/api/auth/RoleAuthorizationAuthorityMapper.java
new file mode 100644
index 0000000..1169b19
--- /dev/null
+++ b/src/main/java/de/caritas/cob/liveservice/api/auth/RoleAuthorizationAuthorityMapper.java
@@ -0,0 +1,37 @@
+package de.caritas.cob.liveservice.api.auth;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.stereotype.Component;
+
+/** Own implementation of the Spring GrantedAuthoritiesMapper. */
+@Component
+public class RoleAuthorizationAuthorityMapper implements GrantedAuthoritiesMapper {
+
+ @Override
+ public Collection extends GrantedAuthority> mapAuthorities(
+ Collection extends GrantedAuthority> authorities) {
+ Set roleNames =
+ authorities.stream()
+ .map(GrantedAuthority::getAuthority)
+ .map(String::toLowerCase)
+ .collect(Collectors.toSet());
+
+ return mapAuthorities(roleNames);
+ }
+
+ public Set mapAuthorities(Set roleNames) {
+ return roleNames.stream()
+ .map(Authority::fromRoleName)
+ .filter(Objects::nonNull)
+ .map(Authority::getAuthorities)
+ .flatMap(Collection::parallelStream)
+ .map(SimpleGrantedAuthority::new)
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/de/caritas/cob/liveservice/api/auth/UserRole.java b/src/main/java/de/caritas/cob/liveservice/api/auth/UserRole.java
new file mode 100644
index 0000000..df55e6a
--- /dev/null
+++ b/src/main/java/de/caritas/cob/liveservice/api/auth/UserRole.java
@@ -0,0 +1,14 @@
+package de.caritas.cob.liveservice.api.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum UserRole {
+ USER("user"),
+ CONSULTANT("consultant"),
+ JITSI_TECHNICAL("jitsi-technical");
+
+ private final String value;
+}
diff --git a/src/main/java/de/caritas/cob/liveservice/api/controller/CustomSwaggerApiResourceController.java b/src/main/java/de/caritas/cob/liveservice/api/controller/CustomSwaggerApiResourceController.java
deleted file mode 100644
index 75221be..0000000
--- a/src/main/java/de/caritas/cob/liveservice/api/controller/CustomSwaggerApiResourceController.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package de.caritas.cob.liveservice.api.controller;
-
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RequestMapping;
-import springfox.documentation.annotations.ApiIgnore;
-import springfox.documentation.swagger.web.ApiResourceController;
-import springfox.documentation.swagger.web.SwaggerResourcesProvider;
-
-@Controller
-@ApiIgnore
-@RequestMapping(value = "${springfox.docuPath}" + "/swagger-resources")
-public class CustomSwaggerApiResourceController extends ApiResourceController {
-
- public static final String SWAGGER_UI_BASE_URL = "/liveevent/docs";
-
- public CustomSwaggerApiResourceController(SwaggerResourcesProvider swaggerResources) {
- super(swaggerResources, SWAGGER_UI_BASE_URL);
- }
-
-}
diff --git a/src/main/java/de/caritas/cob/liveservice/config/security/JwtAuthConverterProperties.java b/src/main/java/de/caritas/cob/liveservice/config/security/JwtAuthConverterProperties.java
new file mode 100644
index 0000000..e3ff610
--- /dev/null
+++ b/src/main/java/de/caritas/cob/liveservice/config/security/JwtAuthConverterProperties.java
@@ -0,0 +1,16 @@
+package de.caritas.cob.liveservice.config.security;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.validation.annotation.Validated;
+
+@Data
+@Validated
+@Configuration
+@ConfigurationProperties(prefix = "jwt.auth.converter")
+public class JwtAuthConverterProperties {
+
+ private String resourceId;
+ private String principalAttribute;
+}
diff --git a/src/main/java/de/caritas/cob/liveservice/config/security/WebSecurityConfig.java b/src/main/java/de/caritas/cob/liveservice/config/security/WebSecurityConfig.java
index bb566e7..42e491a 100644
--- a/src/main/java/de/caritas/cob/liveservice/config/security/WebSecurityConfig.java
+++ b/src/main/java/de/caritas/cob/liveservice/config/security/WebSecurityConfig.java
@@ -1,16 +1,20 @@
package de.caritas.cob.liveservice.config.security;
+import de.caritas.cob.liveservice.api.auth.AuthorisationService;
+import de.caritas.cob.liveservice.api.auth.JwtAuthConverter;
import de.caritas.cob.liveservice.api.config.SpringFoxConfig;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
+import org.keycloak.adapters.springboot.KeycloakSpringBootProperties;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
-import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
-import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
@@ -18,36 +22,38 @@
* Configuration class to provide the keycloak security configuration.
*/
@KeycloakConfiguration
-public class WebSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
+public class WebSecurityConfig {
- /**
- * Configures the basic http security behavior.
- *
- * @param http springs http security
- */
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http
+ @Autowired
+ AuthorisationService authorisationService;
+ @Autowired
+ JwtAuthConverterProperties jwtAuthConverterProperties;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
+
+ httpSecurity
.csrf().disable()
- .authenticationProvider(keycloakAuthenticationProvider())
- .addFilterBefore(keycloakAuthenticationProcessingFilter(), BasicAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.and()
.authorizeRequests()
- .antMatchers(SpringFoxConfig.WHITE_LIST).permitAll()
+ .requestMatchers(SpringFoxConfig.WHITE_LIST).permitAll()
.requestMatchers(new NegatedRequestMatcher(new AntPathRequestMatcher("/live"))).permitAll()
.requestMatchers(new NegatedRequestMatcher(new AntPathRequestMatcher("/live/**")))
.permitAll();
+
+ httpSecurity.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthConverter());
+ return httpSecurity.build();
}
- /**
- * Provides the authentication strategy.
- *
- * @return the configured {@link SessionAuthenticationStrategy}
- */
- @Override
+ @Bean
+ public JwtAuthConverter jwtAuthConverter() {
+ return new JwtAuthConverter(jwtAuthConverterProperties, authorisationService);
+ }
+
+
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@@ -62,4 +68,10 @@ public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
+ @Bean
+ @ConfigurationProperties(prefix = "keycloak", ignoreUnknownFields = false)
+ public KeycloakSpringBootProperties keycloakSpringBootProperties() {
+ return new KeycloakSpringBootProperties();
+ }
+
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 973dc64..9ebf1d2 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,7 +1,7 @@
spring.main.allow-bean-definition-overriding=true
keycloak.auth-server-url=
keycloak.realm=
-keycloak.resource=
+keycloak.resource=resource1221
keycloak.disable-trust-manager=true
app.base.url=
@@ -28,4 +28,8 @@ logging.level.root=WARN
management.endpoint.health.enabled=true
management.endpoint.health.show-details=never
management.endpoints.web.exposure.include=health
-management.health.probes.enabled=true
\ No newline at end of file
+management.health.probes.enabled=true
+spring.security.oauth2.resourceserver.jwt.issuer-uri: https://localhost/auth/realms/onlineberatung
+spring.security.oauth2.resourceserver.jwt.jwk-set-uri: https://localhost/auth/realms/onlineberatung/protocol/openid-connect/certs
+spring.jwt.auth.converter.resource-id: app
+spring.jwt.auth.converter.principal-attribute: preferred_username
\ No newline at end of file
diff --git a/src/test/java/de/caritas/cob/liveservice/LiveServiceApplicationIT.java b/src/test/java/de/caritas/cob/liveservice/LiveServiceApplicationIT.java
index fec4b72..e4b69f0 100644
--- a/src/test/java/de/caritas/cob/liveservice/LiveServiceApplicationIT.java
+++ b/src/test/java/de/caritas/cob/liveservice/LiveServiceApplicationIT.java
@@ -20,7 +20,6 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
-import de.caritas.cob.liveservice.api.controller.CustomSwaggerApiResourceController;
import de.caritas.cob.liveservice.api.model.LiveEventMessage;
import de.caritas.cob.liveservice.api.model.VideoCallRequestDTO;
import de.caritas.cob.liveservice.websocket.model.WebSocketUserSession;
@@ -43,9 +42,6 @@
@TestPropertySource(properties = "spring.profiles.active=testing")
class LiveServiceApplicationIT extends StompClientIntegrationTest {
- @MockBean
- private CustomSwaggerApiResourceController customSwaggerApiResourceController;
-
@Autowired
private SocketUserRegistry socketUserRegistry;
diff --git a/src/test/java/de/caritas/cob/liveservice/StompClientIntegrationTest.java b/src/test/java/de/caritas/cob/liveservice/StompClientIntegrationTest.java
index f042c83..7baeb6e 100644
--- a/src/test/java/de/caritas/cob/liveservice/StompClientIntegrationTest.java
+++ b/src/test/java/de/caritas/cob/liveservice/StompClientIntegrationTest.java
@@ -21,7 +21,7 @@
import org.junit.runner.RunWith;
import org.keycloak.common.VerificationException;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -58,7 +58,7 @@ public abstract class StompClientIntegrationTest extends AbstractJUnit4SpringCon
};
@LocalServerPort
- private Integer port;
+ private int port;
private final WebSocketStompClient socketStompClient = new WebSocketStompClient(
new SockJsClient(singletonList(new WebSocketTransport(new StandardWebSocketClient()))));