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) {} diff --git a/build.gradle b/build.gradle index 9109909..2e8f686 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 { @@ -140,12 +140,12 @@ 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' } } dependencies { + 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 @@ -222,3 +222,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/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/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/security/OpalJwtAuthenticationProvider.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java new file mode 100644 index 0000000..cac3ada --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationProvider.java @@ -0,0 +1,79 @@ +package uk.gov.hmcts.opal.common.spring.security; + +import lombok.extern.slf4j.Slf4j; +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.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; + +@Slf4j +public class OpalJwtAuthenticationProvider implements AuthenticationProvider { + + private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter; + private final JwtDecoder jwtDecoder; + private final 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; + } + + /** + * 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 { + //This can only ever be BearerTokenAuthenticationToken due to the supports() method + BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; + Jwt jwt = getJwt(bearer); + UserState userState = userStateClientService.getUserStateByAuthenticationToken(jwt) + .orElseThrow(() -> new InvalidBearerTokenException("User state not found for authenticated user")); + + Collection authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt); + + OpalJwtAuthenticationToken token = new OpalJwtAuthenticationToken(userState, jwt, authorities); + token.setDetails(bearer.getDetails()); + + return token; + } + + + 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/security/OpalJwtAuthenticationToken.java b/src/main/java/uk/gov/hmcts/opal/common/spring/security/OpalJwtAuthenticationToken.java new file mode 100644 index 0000000..abdd3a2 --- /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 -> + permission.getPermissionName() + .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 + .getOrDefault(Short.parseShort(businessUnitId), Set.of()) + .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 new file mode 100644 index 0000000..c25b5e1 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/common/user/authentication/SecurityUtil.java @@ -0,0 +1,36 @@ +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 = 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; + } + 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/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); + + } +} 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..04c894d --- /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..6a9e8a0 --- /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_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"); + + 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/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/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); + } +} 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();