From 788e653fe51818cb4a747c7c43d99d515e495534 Mon Sep 17 00:00:00 2001 From: benedwards Date: Wed, 17 Dec 2025 18:39:53 +0000 Subject: [PATCH 1/8] Added security utility methods and token providers --- build.gradle | 16 +-- .../opal/common/exception/OpalApiError.java | 23 ---- .../common/exception/OpalApiException.java | 62 ---------- .../spring/OpalJwtAuthenticationProvider.java | 107 ++++++++++++++++++ .../spring/OpalJwtAuthenticationToken.java | 23 ++++ .../user/authentication/SecurityUtil.java | 22 ++++ .../AuthProviderConfigurationProperties.java | 5 +- .../exception/AuthenticationError.java | 45 -------- .../exception/AuthenticationException.java | 18 --- .../MissingRequestHeaderException.java | 14 --- ...java => OpalAuthenticationExceptions.java} | 29 +++-- .../user/authentication/model/Permission.java | 4 + .../service/AccessTokenService.java | 5 +- .../user/authorisation/client/UserClient.java | 11 +- .../client/config/UserTokenRelayConfig.java | 2 - .../service/UserStateClientService.java | 42 ++++++- .../CustomAuthenticationExceptionsTest.java | 57 ---------- .../service/AccessTokenServiceTest.java | 4 +- .../service/UserStateClientServiceTest.java | 6 +- 19 files changed, 240 insertions(+), 255 deletions(-) delete mode 100644 src/main/java/uk/gov/hmcts/opal/common/exception/OpalApiError.java delete mode 100644 src/main/java/uk/gov/hmcts/opal/common/exception/OpalApiException.java create mode 100644 src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java create mode 100644 src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationToken.java create mode 100644 src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java delete mode 100644 src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/AuthenticationError.java delete mode 100644 src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/AuthenticationException.java delete mode 100644 src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/MissingRequestHeaderException.java rename src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/{CustomAuthenticationExceptions.java => OpalAuthenticationExceptions.java} (58%) create mode 100644 src/main/java/uk/gov/hmcts/opal/common/user/authentication/model/Permission.java delete mode 100644 src/test/java/uk/gov/hmcts/opal/common/user/authentication/exception/CustomAuthenticationExceptionsTest.java diff --git a/build.gradle b/build.gradle index 1c1c5ed..643cfec 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { } group = 'uk.gov.hmcts' -version = '0.1.9' +version = '0.2.0' java { toolchain { @@ -133,6 +133,7 @@ ext { logbackVersion = "1.5.22" lombokVersion = "1.18.42" mapstructVersion = "1.6.3" + springVersion = "3.5.8" } ext['snakeyaml.version'] = '2.0' @@ -145,11 +146,12 @@ dependencyManagement { } dependencies { - implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web' - implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator' - implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop' - implementation group: 'org.springframework.boot', name: 'spring-boot-starter-json' - implementation group: 'org.springframework.boot', name: 'spring-boot-starter-oauth2-resource-server' + implementation group: 'uk.gov.hmcts', name: 'spring-exception-handler', version: '0.0.3' + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: springVersion + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: springVersion + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: springVersion + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-json', version: springVersion + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-oauth2-resource-server', version: springVersion implementation('org.springframework.cloud:spring-cloud-starter-openfeign') { exclude group: 'commons-fileupload', module: 'commons-fileupload' } @@ -183,7 +185,7 @@ dependencies { smokeTestAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" smokeTestAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" - testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test' + testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springVersion testImplementation group: 'io.rest-assured', name: 'rest-assured', version: '5.5.6' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/uk/gov/hmcts/opal/common/exception/OpalApiError.java b/src/main/java/uk/gov/hmcts/opal/common/exception/OpalApiError.java deleted file mode 100644 index c3f682d..0000000 --- a/src/main/java/uk/gov/hmcts/opal/common/exception/OpalApiError.java +++ /dev/null @@ -1,23 +0,0 @@ -package uk.gov.hmcts.opal.common.exception; - -import org.springframework.http.HttpStatus; - -import java.net.URI; - -public interface OpalApiError { - - String getErrorTypePrefix(); - - String getErrorTypeNumeric(); - - HttpStatus getHttpStatus(); - - String getTitle(); - - default URI getType() { - return URI.create( - String.format("%s_%s", getErrorTypePrefix(), getErrorTypeNumeric()) - ); - } - -} diff --git a/src/main/java/uk/gov/hmcts/opal/common/exception/OpalApiException.java b/src/main/java/uk/gov/hmcts/opal/common/exception/OpalApiException.java deleted file mode 100644 index 5124d04..0000000 --- a/src/main/java/uk/gov/hmcts/opal/common/exception/OpalApiException.java +++ /dev/null @@ -1,62 +0,0 @@ -package uk.gov.hmcts.opal.common.exception; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.HashMap; -import java.util.Map; - -@Slf4j(topic = "opal.OpalApiException") -@Getter -@SuppressWarnings("PMD.NullAssignment") -public class OpalApiException extends RuntimeException { - - private final OpalApiError error; - private final String detail; - private final HashMap customProperties = new HashMap<>(); - - public OpalApiException(OpalApiError error) { - super(error.getTitle()); - - this.error = error; - this.detail = null; - } - - public OpalApiException(OpalApiError error, Throwable throwable) { - super(error.getTitle(), throwable); - - this.error = error; - this.detail = null; - } - - public OpalApiException(OpalApiError error, String detail) { - super(String.format("%s. %s", error.getTitle(), detail)); - - this.error = error; - this.detail = detail; - } - - public OpalApiException(OpalApiError error, Map customProperties) { - super(error.getTitle()); - - this.error = error; - this.detail = null; - this.customProperties.putAll(customProperties); - } - - public OpalApiException(OpalApiError error, String detail, Map customProperties) { - super(String.format("%s. %s", error.getTitle(), detail)); - - this.error = error; - this.detail = detail; - this.customProperties.putAll(customProperties); - } - - public OpalApiException(OpalApiError error, String detail, Throwable throwable) { - super(String.format("%s. %s", error.getTitle(), detail), throwable); - - this.error = error; - this.detail = detail; - } - -} diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java b/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java new file mode 100644 index 0000000..bbebbc1 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java @@ -0,0 +1,107 @@ +package uk.gov.hmcts.opal.common.spring; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.text.CaseUtils; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.BadJwtException; +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.InvalidBearerTokenException; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.util.Assert; +import uk.gov.hmcts.opal.common.user.authorisation.client.service.UserStateClientService; +import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +@Slf4j +public class OpalJwtAuthenticationProvider implements AuthenticationProvider { + + private final Converter> jwtGrantedAuthoritiesConverter = + new JwtGrantedAuthoritiesConverter(); + private final JwtDecoder jwtDecoder; + private final UserStateClientService userStateClientService; + + + public OpalJwtAuthenticationProvider(JwtDecoder jwtDecoder, UserStateClientService userStateClientService) { + Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); + this.jwtDecoder = jwtDecoder; + this.userStateClientService = userStateClientService; + } + + /** + * Decode and validate the + * Bearer + * Token. + * + * @param authentication the authentication request object. + * @return A successful authentication + * @throws AuthenticationException if authentication failed for some reason + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; + Jwt jwt = getJwt(bearer); + UserState userState = userStateClientService.getUserStateByAuthenticationToken(jwt) + .orElseThrow(() -> new InvalidBearerTokenException("User state not found for authenticated user")); + + Set authorities = new HashSet<>(this.jwtGrantedAuthoritiesConverter.convert(jwt)); + authorities.addAll(getOpalAuthorities(userState)); + + OpalJwtAuthenticationToken token = new OpalJwtAuthenticationToken(userState, jwt, authorities); + + if (token.getDetails() == null) { + token.setDetails(bearer.getDetails()); + } + log.debug("Authenticated token"); + return token; + } + + private Collection getOpalAuthorities(UserState userState) { + Set authorities = new HashSet<>(); + userState.getBusinessUnitUser() + .stream().filter(Objects::nonNull) + .forEach(businessUnitUser -> { + Short businessUnitId = businessUnitUser.getBusinessUnitId(); + authorities.add(new SimpleGrantedAuthority("BUSINESS_UNIT:" + businessUnitId)); + businessUnitUser.getPermissions() + .stream().filter(Objects::nonNull) + .forEach(permission -> { + String permissionName = permission.getPermissionName().toUpperCase().replace(" ", "_"); + authorities.add(new SimpleGrantedAuthority( + "PERMISSION:" + permissionName)); + authorities.add(new SimpleGrantedAuthority( + "BUSINESS_UNIT:" + businessUnitId + ":PERMISSION:" + permissionName)); + }); + }); + return authorities; + } + + private Jwt getJwt(BearerTokenAuthenticationToken bearer) { + try { + return this.jwtDecoder.decode(bearer.getToken()); + } catch (BadJwtException failed) { + log.debug("Failed to authenticate since the JWT was invalid"); + throw new InvalidBearerTokenException(failed.getMessage(), failed); + } catch (JwtException failed) { + throw new AuthenticationServiceException(failed.getMessage(), failed); + } + } + + @Override + public boolean supports(Class authentication) { + return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationToken.java b/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationToken.java new file mode 100644 index 0000000..7a034dd --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationToken.java @@ -0,0 +1,23 @@ +package uk.gov.hmcts.opal.common.spring; + +import lombok.Getter; +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 uk.gov.hmcts.opal.common.user.authorisation.model.UserState; + +import java.util.Collection; + +@Getter +public class OpalJwtAuthenticationToken extends JwtAuthenticationToken { + + private final UserState userState; + + + public OpalJwtAuthenticationToken(UserState userState, Jwt jwt, + Collection authorities) { + super(jwt, authorities, jwt.getClaimAsString(JwtClaimNames.SUB)); + this.userState = userState; + } +} diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java new file mode 100644 index 0000000..d5e0ce8 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java @@ -0,0 +1,22 @@ +package uk.gov.hmcts.opal.common.user.authentication; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import uk.gov.hmcts.opal.common.spring.OpalJwtAuthenticationToken; +import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; + +public final class SecurityUtil { + + public static OpalJwtAuthenticationToken getAuthenticationToken() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof OpalJwtAuthenticationToken opalJwtAuthenticationToken) { + return opalJwtAuthenticationToken; + } else { + throw new IllegalStateException("Authentication token is not of type OpalJwtAuthenticationToken"); + } + } + + public static UserState getUserState() { + return getAuthenticationToken().getUserState(); + } +} diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/config/AuthProviderConfigurationProperties.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/config/AuthProviderConfigurationProperties.java index c4cdf37..2534a39 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/config/AuthProviderConfigurationProperties.java +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/config/AuthProviderConfigurationProperties.java @@ -3,7 +3,7 @@ import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.JWKSourceBuilder; import com.nimbusds.jose.proc.SecurityContext; -import uk.gov.hmcts.opal.common.user.authentication.exception.AuthenticationException; +import uk.gov.hmcts.common.exceptions.standard.UnauthorizedException; import java.net.MalformedURLException; import java.net.URI; @@ -28,7 +28,8 @@ default JWKSource getJwkSource() { return JWKSourceBuilder.create(jwksUrl).build(); } catch (MalformedURLException | URISyntaxException malformedUrlException) { - throw new AuthenticationException("Sorry authentication jwks URL (" + getJwkSetUri() + ") is incorrect"); + throw new UnauthorizedException("Unauthorized", + "Sorry authentication jwks URL (" + getJwkSetUri() + ") is incorrect"); } } } diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/AuthenticationError.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/AuthenticationError.java deleted file mode 100644 index 9bae17c..0000000 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/AuthenticationError.java +++ /dev/null @@ -1,45 +0,0 @@ -package uk.gov.hmcts.opal.common.user.authentication.exception; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import uk.gov.hmcts.opal.common.exception.OpalApiError; - -@Getter -@RequiredArgsConstructor -public enum AuthenticationError implements OpalApiError { - - FAILED_TO_OBTAIN_ACCESS_TOKEN( - "100", - HttpStatus.UNAUTHORIZED, - "Failed to obtain access token" - ), - - FAILED_TO_VALIDATE_ACCESS_TOKEN( - "101", - HttpStatus.UNAUTHORIZED, - "Failed to validate access token" - ), - - FAILED_TO_PARSE_ACCESS_TOKEN( - "102", - HttpStatus.UNAUTHORIZED, - "Failed to parse access token" - ), - - FAILED_TO_OBTAIN_AUTHENTICATION_CONFIG("103", - HttpStatus.INTERNAL_SERVER_ERROR, - "Failed to find authentication configuration"); - - private static final String ERROR_TYPE_PREFIX = "AUTHENTICATION"; - - private final String errorTypeNumeric; - private final HttpStatus httpStatus; - private final String title; - - @Override - public String getErrorTypePrefix() { - return ERROR_TYPE_PREFIX; - } - -} diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/AuthenticationException.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/AuthenticationException.java deleted file mode 100644 index 46d4359..0000000 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/AuthenticationException.java +++ /dev/null @@ -1,18 +0,0 @@ -package uk.gov.hmcts.opal.common.user.authentication.exception; - -@SuppressWarnings("PMD.MissingSerialVersionUID") -public class AuthenticationException extends RuntimeException { - - public AuthenticationException(String message) { - super(message); - } - - public AuthenticationException(String message, Throwable cause) { - super(message, cause); - } - - public AuthenticationException(String message, String cause) { - super(String.format("%s: %s", message, cause)); - } - -} diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/MissingRequestHeaderException.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/MissingRequestHeaderException.java deleted file mode 100644 index da33cb3..0000000 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/MissingRequestHeaderException.java +++ /dev/null @@ -1,14 +0,0 @@ -package uk.gov.hmcts.opal.common.user.authentication.exception; - -import lombok.Getter; - -@Getter -public class MissingRequestHeaderException extends RuntimeException { - - private final String headerName; - - public MissingRequestHeaderException(String headerName) { - super("Missing request header named: %s".formatted(headerName)); - this.headerName = headerName; - } -} diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/CustomAuthenticationExceptions.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/OpalAuthenticationExceptions.java similarity index 58% rename from src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/CustomAuthenticationExceptions.java rename to src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/OpalAuthenticationExceptions.java index d1b6887..7a6d2ab 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/CustomAuthenticationExceptions.java +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/exception/OpalAuthenticationExceptions.java @@ -1,45 +1,58 @@ package uk.gov.hmcts.opal.common.user.authentication.exception; -import jakarta.servlet.ServletException; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; +import uk.gov.hmcts.common.exceptions.standard.ForbiddenException; +import uk.gov.hmcts.common.exceptions.standard.UnauthorizedException; import java.io.IOException; import java.io.PrintWriter; @Component -public class CustomAuthenticationExceptions implements AuthenticationEntryPoint, AccessDeniedHandler { +@RequiredArgsConstructor +public class OpalAuthenticationExceptions implements AuthenticationEntryPoint, AccessDeniedHandler { + + private final ObjectMapper objectMapper; @Override public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException, ServletException { + AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); try (PrintWriter writer = response.getWriter()) { - writer.write("{\"error\": \"Unauthorized\", \"message\":" - + authException.getMessage() + "}"); + writer.write( + objectMapper.writeValueAsString( + new UnauthorizedException("Unauthorized", authException.getMessage()) + .createProblemDetail() + ) + ); } } @Override public void handle(HttpServletRequest request, HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException, ServletException { + AccessDeniedException accessDeniedException) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json"); try (PrintWriter writer = response.getWriter()) { - writer.write("{\"error\": \"Forbidden\", \"message\": " - + "\"Forbidden: access is forbidden for this user\"}"); + writer.write( + objectMapper.writeValueAsString( + new ForbiddenException("Forbidden", "Forbidden: access is forbidden for this user") + .createProblemDetail()) + ); } } } diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/model/Permission.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/model/Permission.java new file mode 100644 index 0000000..dcee8ed --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/model/Permission.java @@ -0,0 +1,4 @@ +package uk.gov.hmcts.opal.common.user.authentication.model; + +public class Permission { +} diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/service/AccessTokenService.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/service/AccessTokenService.java index 0ad4ba8..10edec4 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/service/AccessTokenService.java +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/service/AccessTokenService.java @@ -5,8 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import uk.gov.hmcts.opal.common.exception.OpalApiException; -import uk.gov.hmcts.opal.common.user.authentication.exception.AuthenticationError; +import uk.gov.hmcts.common.exceptions.standard.UnauthorizedException; import java.text.ParseException; @@ -56,7 +55,7 @@ public JWTClaimsSet extractClaims(String accessToken) { return parsedJwt.getJWTClaimsSet(); } catch (ParseException e) { log.error(":extractClaim: Unable to extract claims from JWT Token: {}", e.getMessage()); - throw new OpalApiException(AuthenticationError.FAILED_TO_PARSE_ACCESS_TOKEN, e); + throw new UnauthorizedException("Unauthorized", "Failed to obtain access token"); } } diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/UserClient.java b/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/UserClient.java index 6deb78f..4a01397 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/UserClient.java +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/UserClient.java @@ -4,10 +4,11 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; - import uk.gov.hmcts.opal.common.user.authentication.model.SecurityToken; -import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateDto; import uk.gov.hmcts.opal.common.user.authorisation.client.config.UserTokenRelayConfig; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateDto; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; @FeignClient( name = "userService", @@ -16,10 +17,12 @@ ) public interface UserClient { - static final String X_USER_EMAIL = "X-User-Email"; + String X_USER_EMAIL = "X-User-Email"; @GetMapping("/users/{id}/state") - UserStateDto getUserStateById(@PathVariable("id") Long id); + UserStateDto getUserStateByIdWithAuthToken( + @RequestHeader(AUTHORIZATION) String bearerAuth, + @PathVariable("id") Long id); @GetMapping("/testing-support/token/test-user") SecurityToken getTestUserToken(); diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/config/UserTokenRelayConfig.java b/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/config/UserTokenRelayConfig.java index 7baf169..c8690c3 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/config/UserTokenRelayConfig.java +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/config/UserTokenRelayConfig.java @@ -17,8 +17,6 @@ public RequestInterceptor requestInterceptor() { if (authentication instanceof JwtAuthenticationToken jwtAuth) { String token = jwtAuth.getToken().getTokenValue(); requestTemplate.header("Authorization", "Bearer " + token); - } else { - log.warn(":requestInterceptor: Authentication not of type Jwt."); } }; } diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/service/UserStateClientService.java b/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/service/UserStateClientService.java index 0b32e4c..e5cd772 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/service/UserStateClientService.java +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authorisation/client/service/UserStateClientService.java @@ -4,11 +4,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Service; -import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; import uk.gov.hmcts.opal.common.user.authorisation.client.UserClient; import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateDto; import uk.gov.hmcts.opal.common.user.authorisation.client.mapper.UserStateMapper; +import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; import java.util.Optional; @@ -29,13 +32,22 @@ public Optional getUserState(Long userId) { } @Cacheable(value = "userState", - key = "T(org.springframework.security.core.context.SecurityContextHolder)" - + ".getContext()?.getAuthentication()?.getName() ?: 'anonymous'") + key = "T(org.springframework.security.core.context.SecurityContextHolder)" + + ".getContext()?.getAuthentication()?.getName() ?: 'anonymous'") public Optional getUserStateByAuthenticatedUser() { return fetchUserState(AUTHENTICATED_USER_SPECIAL_CODE); } + @Cacheable(value = "userStateJwt") + public Optional getUserStateByAuthenticationToken(Jwt jwt) { + return fetchUserState(jwt.getTokenValue(), AUTHENTICATED_USER_SPECIAL_CODE); + } + private Optional fetchUserState(Long userId) { + return fetchUserState(null, userId); + } + + private Optional fetchUserState(String authenticationToken, Long userId) { log.info(":getUserState: Fetching user state for specific userId: {}", userId); @@ -44,15 +56,35 @@ private Optional fetchUserState(Long userId) { try { log.info(":getUserState: Fetching user state for userId: {}", userId); - UserStateDto userStateDto = userClient.getUserStateById(userId); + final String authToken; + if (authenticationToken == null) { + authToken = getAuthTokenFromContext(); + } else { + authToken = authenticationToken; + } + + + UserStateDto userStateDto = userClient.getUserStateByIdWithAuthToken("Bearer " + authToken, userId); UserState userState = userStateMapper.toUserState(userStateDto); log.debug(":getUserState: Mapped UserState for userId {}: {}", userId, userState); - return Optional.of(userState); + return Optional.ofNullable(userState); } catch (FeignException.NotFound e) { log.warn(":getUserState: User not found in User Service for userId: {}", userId); return Optional.empty(); } } + + private String getAuthTokenFromContext() { + var authentication = SecurityContextHolder.getContext().getAuthentication(); + log.debug(":requestInterceptor: authentication: {}", authentication); + if (authentication instanceof JwtAuthenticationToken jwtAuth) { + String token = jwtAuth.getToken().getTokenValue(); + return token; + } else { + log.warn(":requestInterceptor: Authentication not of type Jwt."); + } + return null; + } } diff --git a/src/test/java/uk/gov/hmcts/opal/common/user/authentication/exception/CustomAuthenticationExceptionsTest.java b/src/test/java/uk/gov/hmcts/opal/common/user/authentication/exception/CustomAuthenticationExceptionsTest.java deleted file mode 100644 index dc9befa..0000000 --- a/src/test/java/uk/gov/hmcts/opal/common/user/authentication/exception/CustomAuthenticationExceptionsTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package uk.gov.hmcts.opal.common.user.authentication.exception; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.core.AuthenticationException; - -import java.io.IOException; -import java.io.PrintWriter; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class CustomAuthenticationExceptionsTest { - - private CustomAuthenticationExceptions customAuthenticationExceptions; - private HttpServletRequest request; - private HttpServletResponse response; - private PrintWriter writer; - - @BeforeEach - void setUp() throws IOException { - customAuthenticationExceptions = new CustomAuthenticationExceptions(); - request = mock(HttpServletRequest.class); - response = mock(HttpServletResponse.class); - writer = mock(PrintWriter.class); - when(response.getWriter()).thenReturn(writer); - } - - @Test - void commenceShouldReturnUnauthorizedResponse() throws IOException, ServletException { - AuthenticationException authException = mock(AuthenticationException.class); - - customAuthenticationExceptions.commence(request, response, authException); - - verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); - verify(response).setContentType("application/json"); - verify(writer).write("{\"error\": \"Unauthorized\", \"message\":" - + authException.getMessage() + "}"); - } - - @Test - void handleShouldReturnForbiddenResponse() throws IOException, ServletException { - AccessDeniedException accessDeniedException = mock(AccessDeniedException.class); - - customAuthenticationExceptions.handle(request, response, accessDeniedException); - - verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN); - verify(response).setContentType("application/json"); - verify(writer).write("{\"error\": \"Forbidden\", \"message\": " - + "\"Forbidden: access is forbidden for this user\"}"); - } -} diff --git a/src/test/java/uk/gov/hmcts/opal/common/user/authentication/service/AccessTokenServiceTest.java b/src/test/java/uk/gov/hmcts/opal/common/user/authentication/service/AccessTokenServiceTest.java index 65b093f..2b46062 100644 --- a/src/test/java/uk/gov/hmcts/opal/common/user/authentication/service/AccessTokenServiceTest.java +++ b/src/test/java/uk/gov/hmcts/opal/common/user/authentication/service/AccessTokenServiceTest.java @@ -8,7 +8,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import uk.gov.hmcts.opal.common.exception.OpalApiException; +import uk.gov.hmcts.common.exceptions.standard.UnauthorizedException; import java.text.ParseException; @@ -47,7 +47,7 @@ void testExtractPreferredUsername_invalidToken() throws Exception { when(tokenValidator.parse(invalidToken)).thenThrow(ParseException.class); assertThrows( - OpalApiException.class, + UnauthorizedException.class, () -> accessTokenService.extractPreferredUsername("Bearer " + invalidToken) ); } diff --git a/src/test/java/uk/gov/hmcts/opal/common/user/authorisation/client/service/UserStateClientServiceTest.java b/src/test/java/uk/gov/hmcts/opal/common/user/authorisation/client/service/UserStateClientServiceTest.java index 8d4b291..7f09f63 100644 --- a/src/test/java/uk/gov/hmcts/opal/common/user/authorisation/client/service/UserStateClientServiceTest.java +++ b/src/test/java/uk/gov/hmcts/opal/common/user/authorisation/client/service/UserStateClientServiceTest.java @@ -40,7 +40,7 @@ void getUserState_returnsUserWhenPresent() { .username("HMCTS User") .userId(777L) .build(); - when(userClient.getUserStateById(any())).thenReturn(dto); + when(userClient.getUserStateByIdWithAuthToken(any(), any())).thenReturn(dto); Optional userState = userStateClientService.getUserState(0L); @@ -52,7 +52,7 @@ void getUserState_returnsUserWhenPresent() { @Test void getUserState_returnsEmptyWhenNotFound() { Request request = Mockito.mock(Request.class); - when(userClient.getUserStateById(any())) + when(userClient.getUserStateByIdWithAuthToken(any(), any())) .thenThrow(new FeignException.NotFound("not found", request, null, null)); Optional userState = userStateClientService.getUserState(0L); @@ -66,7 +66,7 @@ void getUserStateByAuthenticatedUser_returnsUserWhenPresent() { .username("HMCTS User") .userId(777L) .build(); - when(userClient.getUserStateById(any())).thenReturn(dto); + when(userClient.getUserStateByIdWithAuthToken(any(), any())).thenReturn(dto); Optional userState = userStateClientService.getUserStateByAuthenticatedUser(); From eb625b6418d479e82edc34c05d8d76c9118965d4 Mon Sep 17 00:00:00 2001 From: benedwards Date: Wed, 17 Dec 2025 18:52:31 +0000 Subject: [PATCH 2/8] fixed styles --- build.gradle | 7 +++++++ .../opal/common/spring/OpalJwtAuthenticationProvider.java | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 643cfec..efd25b6 100644 --- a/build.gradle +++ b/build.gradle @@ -223,3 +223,10 @@ rootProject.tasks.named("processSmokeTestResources") { wrapper { distributionType = Wrapper.DistributionType.ALL } + +tasks.register('runAllStyleChecks') { + dependsOn 'checkstyleMain' + dependsOn 'checkstyleTest' + dependsOn 'checkstyleIntegrationTest' + dependsOn 'checkstyleFunctionalTest' +} diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java b/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java index bbebbc1..88a98a8 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java @@ -1,8 +1,6 @@ package uk.gov.hmcts.opal.common.spring; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.text.CaseUtils; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; From a6ccad1d6243371012b2fa1f453656dbde6d4e2a Mon Sep 17 00:00:00 2001 From: benedwards Date: Mon, 12 Jan 2026 13:02:17 +0000 Subject: [PATCH 3/8] Updated security --- build.gradle | 1 - .../spring/OpalJwtAuthenticationToken.java | 23 ------ .../spring/security/MethodSecurityConfig.java | 19 +++++ .../OpalJwtAuthenticationProvider.java | 28 +------- .../security/OpalJwtAuthenticationToken.java | 70 +++++++++++++++++++ .../OpalMethodSecurityExpressionHandler.java | 29 ++++++++ .../OpalMethodSecurityExpressionRoot.java | 68 ++++++++++++++++++ .../user/authentication/SecurityUtil.java | 6 +- 8 files changed, 193 insertions(+), 51 deletions(-) delete mode 100644 src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationToken.java create mode 100644 src/main/java/uk/gov/hmcts/opal/common/spring/security/MethodSecurityConfig.java rename src/main/java/uk/gov/hmcts/opal/common/spring/{ => security}/OpalJwtAuthenticationProvider.java (71%) create mode 100644 src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java create mode 100644 src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionHandler.java create mode 100644 src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRoot.java diff --git a/build.gradle b/build.gradle index efd25b6..3f42e4e 100644 --- a/build.gradle +++ b/build.gradle @@ -140,7 +140,6 @@ ext['snakeyaml.version'] = '2.0' dependencyManagement { imports { - mavenBom 'org.springframework.boot:spring-boot-dependencies:4.0.0' mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2025.1.0' } } diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationToken.java b/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationToken.java deleted file mode 100644 index 7a034dd..0000000 --- a/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationToken.java +++ /dev/null @@ -1,23 +0,0 @@ -package uk.gov.hmcts.opal.common.spring; - -import lombok.Getter; -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 uk.gov.hmcts.opal.common.user.authorisation.model.UserState; - -import java.util.Collection; - -@Getter -public class OpalJwtAuthenticationToken extends JwtAuthenticationToken { - - private final UserState userState; - - - public OpalJwtAuthenticationToken(UserState userState, Jwt jwt, - Collection authorities) { - super(jwt, authorities, jwt.getClaimAsString(JwtClaimNames.SUB)); - this.userState = userState; - } -} diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/security/MethodSecurityConfig.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/MethodSecurityConfig.java new file mode 100644 index 0000000..2143357 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/security/MethodSecurityConfig.java @@ -0,0 +1,19 @@ +package uk.gov.hmcts.opal.common.spring.security; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity +public class MethodSecurityConfig { + + @Bean + @Primary + public MethodSecurityExpressionHandler methodSecurityExpressionHandler() { + return new OpalMethodSecurityExpressionHandler(); + } +} diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java similarity index 71% rename from src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java rename to src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java index 88a98a8..8d97e2d 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/spring/OpalJwtAuthenticationProvider.java +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java @@ -1,4 +1,4 @@ -package uk.gov.hmcts.opal.common.spring; +package uk.gov.hmcts.opal.common.spring.security; import lombok.extern.slf4j.Slf4j; import org.springframework.core.convert.converter.Converter; @@ -7,7 +7,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.BadJwtException; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; @@ -20,9 +19,6 @@ import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; import java.util.Collection; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; @Slf4j public class OpalJwtAuthenticationProvider implements AuthenticationProvider { @@ -55,8 +51,7 @@ public Authentication authenticate(Authentication authentication) throws Authent UserState userState = userStateClientService.getUserStateByAuthenticationToken(jwt) .orElseThrow(() -> new InvalidBearerTokenException("User state not found for authenticated user")); - Set authorities = new HashSet<>(this.jwtGrantedAuthoritiesConverter.convert(jwt)); - authorities.addAll(getOpalAuthorities(userState)); + Collection authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt); OpalJwtAuthenticationToken token = new OpalJwtAuthenticationToken(userState, jwt, authorities); @@ -67,25 +62,6 @@ public Authentication authenticate(Authentication authentication) throws Authent return token; } - private Collection getOpalAuthorities(UserState userState) { - Set authorities = new HashSet<>(); - userState.getBusinessUnitUser() - .stream().filter(Objects::nonNull) - .forEach(businessUnitUser -> { - Short businessUnitId = businessUnitUser.getBusinessUnitId(); - authorities.add(new SimpleGrantedAuthority("BUSINESS_UNIT:" + businessUnitId)); - businessUnitUser.getPermissions() - .stream().filter(Objects::nonNull) - .forEach(permission -> { - String permissionName = permission.getPermissionName().toUpperCase().replace(" ", "_"); - authorities.add(new SimpleGrantedAuthority( - "PERMISSION:" + permissionName)); - authorities.add(new SimpleGrantedAuthority( - "BUSINESS_UNIT:" + businessUnitId + ":PERMISSION:" + permissionName)); - }); - }); - return authorities; - } private Jwt getJwt(BearerTokenAuthenticationToken bearer) { try { diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java new file mode 100644 index 0000000..db9a8c5 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java @@ -0,0 +1,70 @@ +package uk.gov.hmcts.opal.common.spring.security; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +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 uk.gov.hmcts.opal.common.user.authorisation.model.BusinessUnitUser; +import uk.gov.hmcts.opal.common.user.authorisation.model.Permission; +import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@EqualsAndHashCode(callSuper = true) +@Getter +public class OpalJwtAuthenticationToken extends JwtAuthenticationToken { + + private final UserState userState; + + private final Set permissionNames; + private final Map> businessUnitIdsToPermissionNames; + + public OpalJwtAuthenticationToken(UserState userState, Jwt jwt, + Collection authorities) { + super(jwt, authorities, jwt.getClaimAsString(JwtClaimNames.SUB)); + this.userState = userState; + + Function toPermissionNameString = permission -> + String.valueOf(permission.getPermissionId()) + .toUpperCase() + .replace(" ", "_"); + + this.permissionNames = userState.getBusinessUnitUser().stream() + .flatMap(buUser -> buUser.getPermissions().stream()) + .map(toPermissionNameString) + .collect(Collectors.toSet()); + + this.businessUnitIdsToPermissionNames = userState.getBusinessUnitUser().stream() + .collect(Collectors.toMap( + BusinessUnitUser::getBusinessUnitId, + buUser -> buUser.getPermissions().stream() + .map(toPermissionNameString) + .collect(Collectors.toSet()) + )); + } + + public boolean hasBusinessUnit(String businessUnitId) { + return businessUnitIdsToPermissionNames + .containsKey(Short.parseShort(businessUnitId)); + } + + public boolean hasPermission(String permission) { + return getPermissionNames().contains(permission); + } + + public boolean hasPermissionInBusinessUnit(String permission, String businessUnitId) { + List permissionsInBusinessUnit = businessUnitIdsToPermissionNames + .get(Short.parseShort(businessUnitId)) + .stream() + .toList(); + return permissionsInBusinessUnit.contains(permission); + } + +} diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionHandler.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionHandler.java new file mode 100644 index 0000000..e689aa4 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionHandler.java @@ -0,0 +1,29 @@ +package uk.gov.hmcts.opal.common.spring.security; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.core.Authentication; + +import java.util.function.Supplier; + +public class OpalMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler { + + @Override + public EvaluationContext createEvaluationContext(Supplier authentication, MethodInvocation mi) { + final StandardEvaluationContext ctx = + (StandardEvaluationContext) super.createEvaluationContext(authentication, mi); + + Authentication auth = authentication.get(); + OpalMethodSecurityExpressionRoot root = new OpalMethodSecurityExpressionRoot(auth); + + root.setPermissionEvaluator(getPermissionEvaluator()); + root.setTrustResolver(getTrustResolver()); + root.setRoleHierarchy(getRoleHierarchy()); + root.setThis(mi.getThis()); + + ctx.setRootObject(root); + return ctx; + } +} diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRoot.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRoot.java new file mode 100644 index 0000000..8417606 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRoot.java @@ -0,0 +1,68 @@ +package uk.gov.hmcts.opal.common.spring.security; + +import org.springframework.security.access.expression.SecurityExpressionRoot; +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; +import org.springframework.security.core.Authentication; +import uk.gov.hmcts.opal.common.user.authentication.SecurityUtil; + +public class OpalMethodSecurityExpressionRoot extends SecurityExpressionRoot + implements MethodSecurityExpressionOperations { + + + private Object filterObject; + private Object returnObject; + private Object target; + + public OpalMethodSecurityExpressionRoot(Authentication authentication) { + super(authentication); + } + + public boolean hasBusinessUnit(String businessUnitId) { + OpalJwtAuthenticationToken token = getAuthenticationToken(); + return token.hasBusinessUnit(businessUnitId); + } + + public boolean hasPermission(String permission) { + OpalJwtAuthenticationToken token = getAuthenticationToken(); + return token.hasPermission(permission); + } + + public boolean hasPermissionInBusinessUnit(String permission, String businessUnitId) { + OpalJwtAuthenticationToken token = getAuthenticationToken(); + return token.hasPermissionInBusinessUnit(permission, businessUnitId); + } + + private OpalJwtAuthenticationToken getAuthenticationToken() { + return SecurityUtil.getAuthenticationToken(getAuthentication()); + } + + @Override + public void setFilterObject(Object filterObject) { + this.filterObject = filterObject; + } + + @Override + public Object getFilterObject() { + return filterObject; + } + + @Override + public void setReturnObject(Object returnObject) { + this.returnObject = returnObject; + } + + @Override + public Object getReturnObject() { + return returnObject; + } + + @Override + public Object getThis() { + return target; + } + + public void setThis(Object target) { + this.target = target; + } +} + diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java index d5e0ce8..839c7c0 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java @@ -2,13 +2,17 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import uk.gov.hmcts.opal.common.spring.OpalJwtAuthenticationToken; +import uk.gov.hmcts.opal.common.spring.security.OpalJwtAuthenticationToken; import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; public final class SecurityUtil { public static OpalJwtAuthenticationToken getAuthenticationToken() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return getAuthenticationToken(authentication); + } + + public static OpalJwtAuthenticationToken getAuthenticationToken(Authentication authentication) { if (authentication instanceof OpalJwtAuthenticationToken opalJwtAuthenticationToken) { return opalJwtAuthenticationToken; } else { From b310ba4c6059a055793fc486f5432f154e00879d Mon Sep 17 00:00:00 2001 From: benedwards Date: Mon, 12 Jan 2026 15:57:05 +0000 Subject: [PATCH 4/8] Updated sonar config --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 3f42e4e..b1c9081 100644 --- a/build.gradle +++ b/build.gradle @@ -98,8 +98,8 @@ tasks.check.dependsOn(integration) sonarqube { properties { - property "sonar.projectName", "Opal :: opal-commonlib" - property "sonar.projectKey", "uk.gov.hmcts:opal-commonlib" + property "sonar.projectName", "HMCTS :: opal-common-lib" + property "sonar.projectKey", "uk.gov.hmcts:opal-common-lib" } } From 4ff9377979d2f5069fe7bc4db4706e933da8de3b Mon Sep 17 00:00:00 2001 From: benedwards Date: Mon, 12 Jan 2026 18:47:30 +0000 Subject: [PATCH 5/8] Added some tests --- .../security/OpalJwtAuthenticationToken.java | 4 +- .../user/authentication/SecurityUtil.java | 16 ++- .../OpalJwtAuthenticationTokenTest.java | 106 ++++++++++++++++++ .../OpalMethodSecurityExpressionRootTest.java | 59 ++++++++++ .../user/authorisation/SecurityUtilTest.java | 106 ++++++++++++++++++ 5 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationTokenTest.java create mode 100644 src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRootTest.java create mode 100644 src/test/java/uk/gov/hmcts/opal/common/user/authorisation/SecurityUtilTest.java diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java index db9a8c5..abdd3a2 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java @@ -32,7 +32,7 @@ public OpalJwtAuthenticationToken(UserState userState, Jwt jwt, this.userState = userState; Function toPermissionNameString = permission -> - String.valueOf(permission.getPermissionId()) + permission.getPermissionName() .toUpperCase() .replace(" ", "_"); @@ -61,7 +61,7 @@ public boolean hasPermission(String permission) { public boolean hasPermissionInBusinessUnit(String permission, String businessUnitId) { List permissionsInBusinessUnit = businessUnitIdsToPermissionNames - .get(Short.parseShort(businessUnitId)) + .getOrDefault(Short.parseShort(businessUnitId), Set.of()) .stream() .toList(); return permissionsInBusinessUnit.contains(permission); diff --git a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java index 839c7c0..c25b5e1 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java @@ -1,23 +1,33 @@ package uk.gov.hmcts.opal.common.user.authentication; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import uk.gov.hmcts.opal.common.spring.security.OpalJwtAuthenticationToken; import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; +import java.util.Optional; + public final class SecurityUtil { + private SecurityUtil() { + + } public static OpalJwtAuthenticationToken getAuthenticationToken() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Authentication authentication = Optional.of(SecurityContextHolder.getContext()) + .map(SecurityContext::getAuthentication) + .orElse(null); return getAuthenticationToken(authentication); } public static OpalJwtAuthenticationToken getAuthenticationToken(Authentication authentication) { + if (authentication == null) { + throw new IllegalStateException("Authentication token must be provided"); + } if (authentication instanceof OpalJwtAuthenticationToken opalJwtAuthenticationToken) { return opalJwtAuthenticationToken; - } else { - throw new IllegalStateException("Authentication token is not of type OpalJwtAuthenticationToken"); } + throw new IllegalStateException("Authentication token is not of type OpalJwtAuthenticationToken"); } public static UserState getUserState() { diff --git a/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationTokenTest.java b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationTokenTest.java new file mode 100644 index 0000000..1a7d1e0 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationTokenTest.java @@ -0,0 +1,106 @@ +package uk.gov.hmcts.opal.common.spring.security; + +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.jwt.Jwt; +import uk.gov.hmcts.opal.common.user.authorisation.model.BusinessUnitUser; +import uk.gov.hmcts.opal.common.user.authorisation.model.Permission; +import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class OpalJwtAuthenticationTokenTest { + + @Test + void hasBusinessUnit_shouldReturnTrue_WhenBusinessUnitExistsInToken() { + OpalJwtAuthenticationToken token = createOpalJwtAuthenticationToken(); + assertThat(token.hasBusinessUnit("1")).isTrue(); + assertThat(token.hasBusinessUnit("2")).isTrue(); + assertThat(token.hasBusinessUnit("3")).isTrue(); + } + + @Test + void hasBusinessUnit_shouldReturnFalse_WhenBusinessUnitDoesNotExistsInToken() { + OpalJwtAuthenticationToken token = createOpalJwtAuthenticationToken(); + assertThat(token.hasBusinessUnit("4")).isFalse(); + } + + @Test + void hasPermission_shouldReturnTrue_WhenBusinessUnitExistsInToken() { + OpalJwtAuthenticationToken token = createOpalJwtAuthenticationToken(); + assertThat(token.hasPermission("SOME_PERM1")).isTrue(); + assertThat(token.hasPermission("SOME_PERM2")).isTrue(); + assertThat(token.hasPermission("SOME_PERM3")).isTrue(); + assertThat(token.hasPermission("BU1_PERM2")).isTrue(); + assertThat(token.hasPermission("BU2_PERM2")).isTrue(); + assertThat(token.hasPermission("BU3_PERM1")).isTrue(); + assertThat(token.hasPermission("BU3_PERM2")).isTrue(); + } + + @Test + void hasPermission_shouldReturnFalse_WhenBusinessUnitDoesNotExistsInToken() { + OpalJwtAuthenticationToken token = createOpalJwtAuthenticationToken(); + assertThat(token.hasPermission("SOME_PERM4")).isFalse(); + } + + @Test + void hasPermissionInBusinessUnit_shouldReturnTrue_WhenBusinessUnitHasTheAssociatedPermissionForTheUser() { + OpalJwtAuthenticationToken token = createOpalJwtAuthenticationToken(); + assertThat(token.hasPermissionInBusinessUnit("BU1_PERM1", "1")).isTrue(); + assertThat(token.hasPermissionInBusinessUnit("BU1_PERM2", "1")).isTrue(); + assertThat(token.hasPermissionInBusinessUnit("BU2_PERM1", "2")).isTrue(); + } + + @Test + void hasPermissionInBusinessUnit_shouldReturnFalse_WhenBusinessUnitDoesNotHaveTheAssociatedPermissionForTheUser() { + OpalJwtAuthenticationToken token = createOpalJwtAuthenticationToken(); + assertThat(token.hasPermissionInBusinessUnit("BU2_PERM1", "1")).isFalse(); + assertThat(token.hasPermissionInBusinessUnit("BU2_PERM3", "1")).isFalse(); + } + + @Test + void hasPermissionInBusinessUnit_shouldReturnFalse_WhenBusinessUnitDoesNotExistForTheUser() { + OpalJwtAuthenticationToken token = createOpalJwtAuthenticationToken(); + assertThat(token.hasPermissionInBusinessUnit("BU2_PERM3", "4")).isFalse(); + + } + + private OpalJwtAuthenticationToken createOpalJwtAuthenticationToken(List permissions, + Map> businessUnitIdsToPermissionNames) { + + Set businessUnitUsers = new HashSet<>(); + + BusinessUnitUser permissionBusinessUnitUser = new BusinessUnitUser(null, null, + permissions.stream().map(s -> new Permission(null, s)).collect(Collectors.toSet())); + businessUnitUsers.add(permissionBusinessUnitUser); + + businessUnitIdsToPermissionNames.forEach((businessUnitId, strings) -> { + BusinessUnitUser businessUnitUser = new BusinessUnitUser(null, businessUnitId, + strings.stream().map(s -> new Permission(null, s)).collect(Collectors.toSet())); + businessUnitUsers.add(businessUnitUser); + }); + + UserState userState = new UserState(null, null, businessUnitUsers); + return createOpalJwtAuthenticationToken(userState); + } + + private OpalJwtAuthenticationToken createOpalJwtAuthenticationToken() { + return createOpalJwtAuthenticationToken( + List.of("SOME_PERM1", "SOME_PERM2", "SOME_PERM3"), + Map.of( + (short) 1, Set.of("BU1_PERM1", "BU1_PERM2"), + (short) 2, Set.of("BU2_PERM1", "BU2_PERM2"), + (short) 3, Set.of("BU3_PERM1", "BU3_PERM2") + )); + } + + private OpalJwtAuthenticationToken createOpalJwtAuthenticationToken(UserState userState) { + return new OpalJwtAuthenticationToken(userState, mock(Jwt.class), List.of()); + } +} diff --git a/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRootTest.java b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRootTest.java new file mode 100644 index 0000000..cb34158 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRootTest.java @@ -0,0 +1,59 @@ +package uk.gov.hmcts.opal.common.spring.security; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class OpalMethodSecurityExpressionRootTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void hasBusinessUnit_shouldReturnValueFromTokenIndicatingIfBusinessUnitExistsOrNot(boolean value) { + OpalJwtAuthenticationToken token = mock(OpalJwtAuthenticationToken.class); + OpalMethodSecurityExpressionRoot expressionRoot = spy(new OpalMethodSecurityExpressionRoot(token)); + doReturn(token).when(expressionRoot).getAuthentication(); + doReturn(value).when(token).hasBusinessUnit("BU123"); + + assertThat(expressionRoot.hasBusinessUnit("BU123")) + .isEqualTo(value); + + verify(token).hasBusinessUnit("BU123"); + verify(expressionRoot).getAuthentication(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void hasPermission_shouldReturnValueFromTokenIndicatingIfPermissionIsLinkedToTheUserOrNot(boolean value) { + OpalJwtAuthenticationToken token = mock(OpalJwtAuthenticationToken.class); + OpalMethodSecurityExpressionRoot expressionRoot = spy(new OpalMethodSecurityExpressionRoot(token)); + doReturn(token).when(expressionRoot).getAuthentication(); + doReturn(value).when(token).hasPermission("PERM123"); + + assertThat(expressionRoot.hasPermission("PERM123")) + .isEqualTo(value); + + verify(token).hasPermission("PERM123"); + verify(expressionRoot).getAuthentication(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void hasPermissionInBusinessUnit_shouldReturnValueFromTokenIndicatingIfBusinessUnitHasTheAssociatedPermissionForTheUser(boolean value) { + OpalJwtAuthenticationToken token = mock(OpalJwtAuthenticationToken.class); + OpalMethodSecurityExpressionRoot expressionRoot = spy(new OpalMethodSecurityExpressionRoot(token)); + doReturn(token).when(expressionRoot).getAuthentication(); + doReturn(value).when(token).hasPermissionInBusinessUnit("PERM123","BU123"); + + assertThat(expressionRoot.hasPermissionInBusinessUnit("PERM123","BU123")) + .isEqualTo(value); + + verify(token).hasPermissionInBusinessUnit("PERM123","BU123"); + verify(expressionRoot).getAuthentication(); + } + +} diff --git a/src/test/java/uk/gov/hmcts/opal/common/user/authorisation/SecurityUtilTest.java b/src/test/java/uk/gov/hmcts/opal/common/user/authorisation/SecurityUtilTest.java new file mode 100644 index 0000000..c182067 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/opal/common/user/authorisation/SecurityUtilTest.java @@ -0,0 +1,106 @@ +package uk.gov.hmcts.opal.common.user.authorisation; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import uk.gov.hmcts.opal.common.spring.security.OpalJwtAuthenticationToken; +import uk.gov.hmcts.opal.common.user.authentication.SecurityUtil; +import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@DisplayName("SecurityUtil Test") +class SecurityUtilTest { + + @Test + void getAuthenticationTokenWithParam_throwsExceptionWhenAuthenticationIsOfWrongType() { + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> SecurityUtil.getAuthenticationToken(mock(JwtAuthenticationToken.class))); + assertThat(exception.getMessage()) + .isEqualTo("Authentication token is not of type OpalJwtAuthenticationToken"); + } + + @Test + void getAuthenticationTokenWithParam_throwsExceptionWhenAuthenticationIsNull() { + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> SecurityUtil.getAuthenticationToken(null)); + assertThat(exception.getMessage()) + .isEqualTo("Authentication token must be provided"); + } + + @Test + void getAuthenticationTokenWithParam_returnsTokenWhenAuthenticationIsOfCorrectType() { + OpalJwtAuthenticationToken expectedToken = mock(OpalJwtAuthenticationToken.class); + OpalJwtAuthenticationToken actualToken = SecurityUtil.getAuthenticationToken(expectedToken); + assertThat(actualToken).isEqualTo(expectedToken); + } + + + @Test + void getAuthenticationTokenWithoutParam_throwsExceptionWhenAuthenticationIsOfWrongType() { + setSecurityContextAuthentication(mock(JwtAuthenticationToken.class)); + IllegalStateException exception = + assertThrows(IllegalStateException.class, SecurityUtil::getAuthenticationToken); + assertThat(exception.getMessage()) + .isEqualTo("Authentication token is not of type OpalJwtAuthenticationToken"); + } + + @Test + void getAuthenticationTokenWithoutParam_throwsExceptionWhenAuthenticationIsNull() { + setSecurityContextAuthentication(null); + IllegalStateException exception = + assertThrows(IllegalStateException.class, SecurityUtil::getAuthenticationToken); + assertThat(exception.getMessage()) + .isEqualTo("Authentication token must be provided"); + } + + @Test + void getAuthenticationTokenWithoutParam_returnsTokenWhenAuthenticationIsOfCorrectType() { + OpalJwtAuthenticationToken expectedToken = mock(OpalJwtAuthenticationToken.class); + setSecurityContextAuthentication(expectedToken); + OpalJwtAuthenticationToken actualToken = SecurityUtil.getAuthenticationToken(); + assertThat(actualToken).isEqualTo(expectedToken); + } + + @Test + void getUserState_returnsUserStateWhenAuthenticationIsOfCorrectType() { + OpalJwtAuthenticationToken expectedToken = mock(OpalJwtAuthenticationToken.class); + UserState expectedUserState = mock(UserState.class); + when(expectedToken.getUserState()).thenReturn(expectedUserState); + + setSecurityContextAuthentication(expectedToken); + UserState actualUserState = SecurityUtil.getUserState(); + assertThat(actualUserState).isEqualTo(expectedUserState); + + verify(expectedToken).getUserState(); + } + + @Test + void getUserState_throwsExceptionWhenAuthenticationIsOfWrongType() { + setSecurityContextAuthentication(mock(JwtAuthenticationToken.class)); + IllegalStateException exception = + assertThrows(IllegalStateException.class, SecurityUtil::getUserState); + assertThat(exception.getMessage()) + .isEqualTo("Authentication token is not of type OpalJwtAuthenticationToken"); + } + + @Test + void getUserState_throwsExceptionWhenAuthenticationIsNull() { + setSecurityContextAuthentication(null); + IllegalStateException exception = + assertThrows(IllegalStateException.class, SecurityUtil::getUserState); + assertThat(exception.getMessage()) + .isEqualTo("Authentication token must be provided"); + + } + + private void setSecurityContextAuthentication(Authentication token) { + SecurityContextHolder.getContext().setAuthentication(token); + } +} From 3c71021a1d44a16ded8d79c72f876abf69f9bdfe Mon Sep 17 00:00:00 2001 From: benedwards Date: Tue, 13 Jan 2026 13:26:08 +0000 Subject: [PATCH 6/8] Fixed style --- .../spring/security/OpalJwtAuthenticationTokenTest.java | 4 ++-- .../security/OpalMethodSecurityExpressionRootTest.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationTokenTest.java b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationTokenTest.java index 1a7d1e0..04c894d 100644 --- a/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationTokenTest.java +++ b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationTokenTest.java @@ -71,8 +71,8 @@ void hasPermissionInBusinessUnit_shouldReturnFalse_WhenBusinessUnitDoesNotExistF } - private OpalJwtAuthenticationToken createOpalJwtAuthenticationToken(List permissions, - Map> businessUnitIdsToPermissionNames) { + private OpalJwtAuthenticationToken createOpalJwtAuthenticationToken( + List permissions, Map> businessUnitIdsToPermissionNames) { Set businessUnitUsers = new HashSet<>(); diff --git a/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRootTest.java b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRootTest.java index cb34158..6a9e8a0 100644 --- a/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRootTest.java +++ b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalMethodSecurityExpressionRootTest.java @@ -43,16 +43,16 @@ void hasPermission_shouldReturnValueFromTokenIndicatingIfPermissionIsLinkedToThe @ParameterizedTest @ValueSource(booleans = {true, false}) - void hasPermissionInBusinessUnit_shouldReturnValueFromTokenIndicatingIfBusinessUnitHasTheAssociatedPermissionForTheUser(boolean value) { + void hasPermissionInBusinessUnit_shouldReturnValueFromToken(boolean value) { OpalJwtAuthenticationToken token = mock(OpalJwtAuthenticationToken.class); OpalMethodSecurityExpressionRoot expressionRoot = spy(new OpalMethodSecurityExpressionRoot(token)); doReturn(token).when(expressionRoot).getAuthentication(); - doReturn(value).when(token).hasPermissionInBusinessUnit("PERM123","BU123"); + doReturn(value).when(token).hasPermissionInBusinessUnit("PERM123", "BU123"); - assertThat(expressionRoot.hasPermissionInBusinessUnit("PERM123","BU123")) + assertThat(expressionRoot.hasPermissionInBusinessUnit("PERM123", "BU123")) .isEqualTo(value); - verify(token).hasPermissionInBusinessUnit("PERM123","BU123"); + verify(token).hasPermissionInBusinessUnit("PERM123", "BU123"); verify(expressionRoot).getAuthentication(); } From f62ec8770915788c60e66c4dcd0fdb2d9fa81a5b Mon Sep 17 00:00:00 2001 From: benedwards Date: Tue, 13 Jan 2026 15:59:36 +0000 Subject: [PATCH 7/8] Added tests --- .../OpalJwtAuthenticationProvider.java | 16 +- .../OpalJwtAuthenticationProviderTest.java | 170 ++++++++++++++++++ 2 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProviderTest.java diff --git a/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java index 8d97e2d..cac3ada 100644 --- a/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java @@ -1,7 +1,6 @@ package uk.gov.hmcts.opal.common.spring.security; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; @@ -23,16 +22,17 @@ @Slf4j public class OpalJwtAuthenticationProvider implements AuthenticationProvider { - private final Converter> jwtGrantedAuthoritiesConverter = - new JwtGrantedAuthoritiesConverter(); + private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter; private final JwtDecoder jwtDecoder; private final UserStateClientService userStateClientService; - public OpalJwtAuthenticationProvider(JwtDecoder jwtDecoder, UserStateClientService userStateClientService) { + public OpalJwtAuthenticationProvider(JwtDecoder jwtDecoder, UserStateClientService userStateClientService, + JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter) { Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); this.jwtDecoder = jwtDecoder; this.userStateClientService = userStateClientService; + this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter; } /** @@ -46,6 +46,7 @@ public OpalJwtAuthenticationProvider(JwtDecoder jwtDecoder, UserStateClientServi */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { + //This can only ever be BearerTokenAuthenticationToken due to the supports() method BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; Jwt jwt = getJwt(bearer); UserState userState = userStateClientService.getUserStateByAuthenticationToken(jwt) @@ -54,16 +55,13 @@ public Authentication authenticate(Authentication authentication) throws Authent Collection authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt); OpalJwtAuthenticationToken token = new OpalJwtAuthenticationToken(userState, jwt, authorities); + token.setDetails(bearer.getDetails()); - if (token.getDetails() == null) { - token.setDetails(bearer.getDetails()); - } - log.debug("Authenticated token"); return token; } - private Jwt getJwt(BearerTokenAuthenticationToken bearer) { + Jwt getJwt(BearerTokenAuthenticationToken bearer) { try { return this.jwtDecoder.decode(bearer.getToken()); } catch (BadJwtException failed) { diff --git a/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProviderTest.java b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProviderTest.java new file mode 100644 index 0000000..059b6cb --- /dev/null +++ b/src/test/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProviderTest.java @@ -0,0 +1,170 @@ +package uk.gov.hmcts.opal.common.spring.security; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.BadJwtException; +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.InvalidBearerTokenException; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import uk.gov.hmcts.opal.common.user.authorisation.client.service.UserStateClientService; +import uk.gov.hmcts.opal.common.user.authorisation.model.UserState; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class OpalJwtAuthenticationProviderTest { + + @Mock + private JwtDecoder jwtDecoder; + + @Mock + private UserStateClientService userStateClientService; + + @Mock + private JwtGrantedAuthoritiesConverter converter; + + @InjectMocks + @Spy + private OpalJwtAuthenticationProvider opalJwtAuthenticationProvider; + + + @Test + void supports_shouldReturnTrueIfTypeIsBearerTokenAuthenticationToken() { + assertThat(opalJwtAuthenticationProvider.supports(BearerTokenAuthenticationToken.class)) + .isTrue(); + } + + @Test + void supports_shouldReturnFalseIfTypeIsNotBearerTokenAuthenticationToken() { + assertThat(opalJwtAuthenticationProvider.supports(Object.class)) + .isFalse(); + } + + @Test + void getJwt_shouldReturnJwtWithValidToken() { + Jwt jwt = mock(Jwt.class); + String token = "my.token.value"; + + BearerTokenAuthenticationToken authenticationToken = mock(BearerTokenAuthenticationToken.class); + when(authenticationToken.getToken()).thenReturn(token); + when(jwtDecoder.decode(token)).thenReturn(jwt); + + assertThat(opalJwtAuthenticationProvider.getJwt(authenticationToken)).isEqualTo(jwt); + + verify(jwtDecoder).decode(token); + verify(authenticationToken).getToken(); + } + + @Test + void getJwt_shouldThrowErrorWhenBadToken() { + String token = "my.token.value"; + + BearerTokenAuthenticationToken authenticationToken = mock(BearerTokenAuthenticationToken.class); + when(authenticationToken.getToken()).thenReturn(token); + BadJwtException exception = mock(BadJwtException.class); + when(exception.getMessage()).thenReturn("some-bad-jwt-error"); + + when(jwtDecoder.decode(token)).thenThrow(exception); + + InvalidBearerTokenException actualException = assertThrows(InvalidBearerTokenException.class, () -> + opalJwtAuthenticationProvider.getJwt(authenticationToken) + ); + assertThat(actualException.getMessage()) + .isEqualTo("some-bad-jwt-error"); + assertThat(actualException.getCause()) + .isEqualTo(exception); + } + + @Test + void getJwt_shouldThrowErrorForAnyNonBadJwtException() { + String token = "my.token.value"; + + BearerTokenAuthenticationToken authenticationToken = mock(BearerTokenAuthenticationToken.class); + when(authenticationToken.getToken()).thenReturn(token); + JwtException exception = mock(JwtException.class); + when(exception.getMessage()).thenReturn("some-jwt-error"); + + when(jwtDecoder.decode(token)).thenThrow(exception); + + AuthenticationServiceException actualException = assertThrows(AuthenticationServiceException.class, () -> + opalJwtAuthenticationProvider.getJwt(authenticationToken) + ); + assertThat(actualException.getMessage()) + .isEqualTo("some-jwt-error"); + assertThat(actualException.getCause()) + .isEqualTo(exception); + } + + @Test + void authenticate_shouldErrorWhenUserStateNotFound() { + BearerTokenAuthenticationToken authenticationToken = mock(BearerTokenAuthenticationToken.class); + Jwt jwt = mock(Jwt.class); + doReturn(jwt).when(opalJwtAuthenticationProvider).getJwt(authenticationToken); + + when(userStateClientService.getUserStateByAuthenticationToken(jwt)) + .thenReturn(Optional.empty()); + InvalidBearerTokenException exception = assertThrows(InvalidBearerTokenException.class, () -> + opalJwtAuthenticationProvider.authenticate(authenticationToken) + ); + assertThat(exception.getMessage()) + .isEqualTo("User state not found for authenticated user"); + verify(userStateClientService).getUserStateByAuthenticationToken(jwt); + verify(opalJwtAuthenticationProvider).getJwt(authenticationToken); + } + + @Test + void authenticate_shouldReturnAuthenticationWhenUserStateFound() { + BearerTokenAuthenticationToken authenticationToken = mock(BearerTokenAuthenticationToken.class); + Jwt jwt = mock(Jwt.class); + UserState userState = mock(UserState.class); + when(authenticationToken.getDetails()).thenReturn("some detail"); + + doReturn(jwt).when(opalJwtAuthenticationProvider).getJwt(authenticationToken); + + when(userStateClientService.getUserStateByAuthenticationToken(jwt)) + .thenReturn(Optional.of(userState)); + + Collection authorities = Set.of( + mock(GrantedAuthority.class), mock(GrantedAuthority.class), mock(GrantedAuthority.class) + ); + + when(converter.convert(jwt)).thenReturn(authorities); + + Authentication authentication = opalJwtAuthenticationProvider.authenticate(authenticationToken); + assertThat(authentication) + .isInstanceOf(OpalJwtAuthenticationToken.class); + OpalJwtAuthenticationToken opalJwtAuthenticationToken = (OpalJwtAuthenticationToken) authentication; + + assertThat(opalJwtAuthenticationToken.getUserState()) + .isEqualTo(userState); + + assertThat(opalJwtAuthenticationToken.getAuthorities()) + .containsAll(authorities); + assertThat(opalJwtAuthenticationToken.getDetails()) + .isEqualTo("some detail"); + + verify(userStateClientService).getUserStateByAuthenticationToken(jwt); + verify(opalJwtAuthenticationProvider).getJwt(authenticationToken); + verify(converter).convert(jwt); + + } +} From 951792f45e96174e669d2fc4f05d50c76203d768 Mon Sep 17 00:00:00 2001 From: benedwards Date: Tue, 13 Jan 2026 16:45:41 +0000 Subject: [PATCH 8/8] Removed jenkins as there is no service to deploy. Will be tested via github actions --- Jenkinsfile_CNP | 11 ----------- Jenkinsfile_nightly | 14 -------------- 2 files changed, 25 deletions(-) delete mode 100644 Jenkinsfile_CNP delete mode 100644 Jenkinsfile_nightly diff --git a/Jenkinsfile_CNP b/Jenkinsfile_CNP deleted file mode 100644 index b5ae08d..0000000 --- a/Jenkinsfile_CNP +++ /dev/null @@ -1,11 +0,0 @@ -#!groovy -// If this is new microservice built on the template and you want it to run in Jenkins, you -// will need to follow these steps: https://hmcts.github.io/cloud-native-platform/new-component/github-repo.html - -@Library("Infrastructure") - -def type = "java" -def product = "opal" -def component = "commonlib" - -withPipeline(type, product, component) {} diff --git a/Jenkinsfile_nightly b/Jenkinsfile_nightly deleted file mode 100644 index 1b71f93..0000000 --- a/Jenkinsfile_nightly +++ /dev/null @@ -1,14 +0,0 @@ -#!groovy - -properties([ - // H allow predefined but random minute see https://en.wikipedia.org/wiki/Cron#Non-standard_characters - pipelineTriggers([cron('H 07 * * 1-5')]) -]) - -@Library("Infrastructure") - -def type = "java" -def product = "rpe" -def component = "spring-boot-template" - -withNightlyPipeline(type, product, component) {}