From fc79e8e8ecc6a69c6f715dd7ac171c9033980c28 Mon Sep 17 00:00:00 2001 From: lukaskabc Date: Wed, 4 Sep 2024 09:49:19 +0200 Subject: [PATCH] fix JwtAuthenticationProvider and missing authorities --- doc/setup.md | 37 ++++++++++++++++--- .../termit/config/OAuth2SecurityConfig.java | 8 +++- .../kbss/termit/config/SecurityConfig.java | 17 +++++++-- .../termit/security/TermitJwtDecoder.java | 15 ++++++++ .../WebSocketJwtAuthorizationInterceptor.java | 7 +++- 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/doc/setup.md b/doc/setup.md index f5555e8fe..81dad0f81 100644 --- a/doc/setup.md +++ b/doc/setup.md @@ -172,10 +172,35 @@ termit: TermIt can operate in two authentication modes: -1. Internal authentication means -2. [Keycloak](https://www.keycloak.org/) -based +1. Internal authentication +2. OAuth2 based (e.g. [Keycloak](https://www.keycloak.org/)) + +By default, OAuth2 is disabled and internal authentication is used +To enable it, set termit security provider to `oidc` +and provide issuer-uri and jwk-set-uri. + +**`application.yml` example:** +```yml +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://keycloak.lan/realms/termit + jwk-set-uri: http://keycloak.lan/realms/termit/protocol/openid-connect/certs +termit: + security: + provider: "oidc" +``` + +**Environmental variables example:** +``` +SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI=http://keycloak.lan/realms/termit +SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWKSETURI=http://keycloak.lan/realms/termit/protocol/openid-connect/certs +TERMIT_SECURITY_PROVIDER=oidc +``` + +TermIt will automatically configure its security accordingly +(it is using Spring's [`ConditionalOnProperty`](https://www.baeldung.com/spring-conditionalonproperty)). -By default, Keycloak is disabled (see `keycloak.enabled` in `application.yml`). To enable it, set `keycloak.enabled` to `true` and -provide additional required Keycloak parameters - see the [Keycloak Spring Boot integration docs](https://www.keycloak.org/docs/latest/securing_apps/#_spring_boot_adapter). -TermIt will automatically configure its security (it is using Spring's [`ConditionalOnProperty`](https://www.baeldung.com/spring-conditionalonproperty)) -accordingly. +**Note that termit-ui needs to be configured for mathcing authentication mode.** diff --git a/src/main/java/cz/cvut/kbss/termit/config/OAuth2SecurityConfig.java b/src/main/java/cz/cvut/kbss/termit/config/OAuth2SecurityConfig.java index 32623b9a2..cb5081690 100644 --- a/src/main/java/cz/cvut/kbss/termit/config/OAuth2SecurityConfig.java +++ b/src/main/java/cz/cvut/kbss/termit/config/OAuth2SecurityConfig.java @@ -86,9 +86,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } + /** + * An attempt to replicate auth provider from HttpSecurity + * @see cz.cvut.kbss.termit.security.WebSocketJwtAuthorizationInterceptor + */ @Bean public JwtAuthenticationProvider jwtAuthenticationProvider(JwtDecoder jwtDecoder) { - return new JwtAuthenticationProvider(jwtDecoder); + final JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwtDecoder); + provider.setJwtAuthenticationConverter(grantedAuthoritiesExtractor()); + return provider; } private CorsConfigurationSource corsConfigurationSource() { diff --git a/src/main/java/cz/cvut/kbss/termit/config/SecurityConfig.java b/src/main/java/cz/cvut/kbss/termit/config/SecurityConfig.java index d08c3fa71..a8d22070b 100644 --- a/src/main/java/cz/cvut/kbss/termit/config/SecurityConfig.java +++ b/src/main/java/cz/cvut/kbss/termit/config/SecurityConfig.java @@ -41,10 +41,10 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.HttpStatusEntryPoint; @@ -126,9 +126,20 @@ private JwtAuthenticationFilter authenticationFilter(AuthenticationManager authe return authenticationFilter; } + /** + * @see cz.cvut.kbss.termit.security.WebSocketJwtAuthorizationInterceptor + */ @Bean public JwtAuthenticationProvider jwtAuthenticationProvider(JwtDecoder jwtDecoder) { - return new JwtAuthenticationProvider(jwtDecoder); + final JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); + authoritiesConverter.setAuthorityPrefix(""); // this removes default "SCOPE_" prefix + // otherwise, all granted authorities would have this prefix + // (like "SCOPE_ROLE_RESTRICTED_USER", we want just ROLE_...) + final JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + final JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwtDecoder); + provider.setJwtAuthenticationConverter(converter); + return provider; } private CorsConfigurationSource corsConfigurationSource() { diff --git a/src/main/java/cz/cvut/kbss/termit/security/TermitJwtDecoder.java b/src/main/java/cz/cvut/kbss/termit/security/TermitJwtDecoder.java index e4b0b5241..4cbd64db4 100644 --- a/src/main/java/cz/cvut/kbss/termit/security/TermitJwtDecoder.java +++ b/src/main/java/cz/cvut/kbss/termit/security/TermitJwtDecoder.java @@ -5,11 +5,17 @@ import cz.cvut.kbss.termit.service.security.TermItUserDetailsService; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; +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.jwt.JwtException; import java.util.Objects; +import java.util.stream.Collectors; +/** + * @see #decode(String) + */ public class TermitJwtDecoder implements org.springframework.security.oauth2.jwt.JwtDecoder { private final JwtUtils jwtUtils; @@ -21,6 +27,11 @@ public TermitJwtDecoder(JwtUtils jwtUtils, TermItUserDetailsService userDetailsS this.userDetailsService = userDetailsService; } + /** + * Decodes JWT token (without the {@code Bearer} prefix) + * and ensures its validity. + * @throws JwtException with cause, when token could not be decoded or verified + */ @Override public Jwt decode(String token) throws JwtException { try { @@ -36,6 +47,10 @@ public Jwt decode(String token) throws JwtException { SecurityUtils.verifyAccountStatus(existingDetails.getUser()); + claims.put("scope", existingDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet())); + claims.putIfAbsent(JwtClaimNames.SUB, existingDetails); + return new Jwt(token, claims.getIssuedAt().toInstant(), claims.getExpiration() .toInstant(), expanded.getHeader(), claims); } catch (cz.cvut.kbss.termit.exception.JwtException | NullPointerException e) { diff --git a/src/main/java/cz/cvut/kbss/termit/security/WebSocketJwtAuthorizationInterceptor.java b/src/main/java/cz/cvut/kbss/termit/security/WebSocketJwtAuthorizationInterceptor.java index b1fffd73a..026c218a3 100644 --- a/src/main/java/cz/cvut/kbss/termit/security/WebSocketJwtAuthorizationInterceptor.java +++ b/src/main/java/cz/cvut/kbss/termit/security/WebSocketJwtAuthorizationInterceptor.java @@ -1,5 +1,7 @@ package cz.cvut.kbss.termit.security; +import cz.cvut.kbss.termit.security.model.TermItUserDetails; +import cz.cvut.kbss.termit.service.security.SecurityUtils; import org.jetbrains.annotations.NotNull; import org.springframework.http.HttpHeaders; import org.springframework.messaging.Message; @@ -12,7 +14,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; @@ -23,7 +24,9 @@ /** * Authenticates STOMP CONNECT messages *

- * Retrieves token from the {@code Authorization} header and authenticates the session. + * Retrieves token from the {@code Authorization} header + * and uses {@link JwtAuthenticationProvider} to authenticate the token. + * @see Consult this Stackoverflow answer */ @Component public class WebSocketJwtAuthorizationInterceptor implements ChannelInterceptor {