diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index 998c7bc1..96517766 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -40,6 +40,7 @@ jobs: format: "HTML" args: > --enableRetired + --disableOssIndex true - name: Upload Test results uses: actions/upload-artifact@master with: diff --git a/.github/workflows/imageBuild.yml b/.github/workflows/imageBuild.yml index 1055fee4..2cc66d59 100644 --- a/.github/workflows/imageBuild.yml +++ b/.github/workflows/imageBuild.yml @@ -41,6 +41,7 @@ jobs: format: "HTML" args: > --enableRetired + --disableOssIndex true - name: Upload Test results uses: actions/upload-artifact@master with: diff --git a/.gitignore b/.gitignore index 39c6819b..ee7f37d0 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ build/ ### VS Code ### .vscode/ +application-local.properties \ No newline at end of file diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/config/BeanConfig.java b/src/main/java/gov/cabinetoffice/gap/applybackend/config/BeanConfig.java index 92657ac1..d1ab6851 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/config/BeanConfig.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/config/BeanConfig.java @@ -1,9 +1,5 @@ package gov.cabinetoffice.gap.applybackend.config; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider; -import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProviderClientBuilder; import com.google.i18n.phonenumbers.PhoneNumberUtil; import gov.cabinetoffice.gap.applybackend.config.properties.GovNotifyProperties; import lombok.RequiredArgsConstructor; @@ -21,7 +17,6 @@ @Configuration public class BeanConfig { - private final CognitoConfigProperties cognitoProps; private final GovNotifyProperties notifyProperties; @Bean @@ -53,16 +48,6 @@ public PhoneNumberUtil getPhoneNumberUtil() { return PhoneNumberUtil.getInstance(); } - @Bean - public AWSCognitoIdentityProvider getCognitoClientBuilder() { - final BasicAWSCredentials awsCredentials = new BasicAWSCredentials(cognitoProps.getAccessKey(), cognitoProps.getSecretKey()); - return AWSCognitoIdentityProviderClientBuilder - .standard() - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .withRegion(cognitoProps.getRegion()) - .build(); - } - @Bean public NotificationClient notificationClient() { return new NotificationClient(notifyProperties.getApiKey()); diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/config/CognitoConfigProperties.java b/src/main/java/gov/cabinetoffice/gap/applybackend/config/UserServiceConfig.java similarity index 54% rename from src/main/java/gov/cabinetoffice/gap/applybackend/config/CognitoConfigProperties.java rename to src/main/java/gov/cabinetoffice/gap/applybackend/config/UserServiceConfig.java index 5ee69d13..5f0f0b8a 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/config/CognitoConfigProperties.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/config/UserServiceConfig.java @@ -13,29 +13,12 @@ @Builder @AllArgsConstructor @NoArgsConstructor -@Configuration("cognitoConfigurationProperties") -@ConfigurationProperties(prefix = "cognito") -public class CognitoConfigProperties { - - @NotNull - private String secretKey; - - @NotNull - private String accessKey; - - @NotNull - private String region; - - @NotNull - private String userPoolId; - - @NotNull - private String userPassword; - +@Configuration("userServiceConfigurationProperties") +@ConfigurationProperties(prefix = "user-service") +public class UserServiceConfig { @NotNull private String domain; @NotNull - private String appClientId; - + private String cookieName; } diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/dto/api/JwtPayload.java b/src/main/java/gov/cabinetoffice/gap/applybackend/dto/api/JwtPayload.java index 32f78906..dbd7b8a6 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/dto/api/JwtPayload.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/dto/api/JwtPayload.java @@ -20,7 +20,6 @@ public class JwtPayload { private String eventId; private String tokenUse; private String phoneNumber; - private int authTime; private int exp; private int iat; private String familyName; diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/dto/api/UpdateGrantApplicantOrganisationProfileDto.java b/src/main/java/gov/cabinetoffice/gap/applybackend/dto/api/UpdateGrantApplicantOrganisationProfileDto.java index edc290d3..d4ee4360 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/dto/api/UpdateGrantApplicantOrganisationProfileDto.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/dto/api/UpdateGrantApplicantOrganisationProfileDto.java @@ -6,19 +6,54 @@ import lombok.Data; import lombok.NoArgsConstructor; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + @Data @NoArgsConstructor @AllArgsConstructor @Builder public class UpdateGrantApplicantOrganisationProfileDto { + @Size(max = 250, message = "Organisation name must be 250 characters or less") + @Pattern(regexp = "^(?![\\s\\S])|^[a-zA-Z0-9\\s',-]+$", + message = "Organisation name must only use letters, numbers, and special characters such as hyphens, spaces and apostrophes") private String legalName; + private GrantApplicantOrganisationType type; + + @Size(max = 250, message = "Address line 1 must be 250 characters or less") + @Pattern(regexp = "^(?![\\s\\S])|^[a-zA-Z0-9\\s',-]+$", + message = "Address line 1 must only use letters, numbers, and special characters such as hyphens, spaces and apostrophes") private String addressLine1; + + @Size(max = 250, message = "Address line 2 must be 250 characters or less") + @Pattern(regexp = "^(?![\\s\\S])|^[a-zA-Z0-9\\s',-]+$", + message = "Address line 2 must only use letters, numbers, and special characters such as hyphens, spaces and apostrophes") private String addressLine2; + + @Size(max = 250, message = "Town or City must be 250 characters or less") + @Pattern(regexp = "^(?![\\s\\S])|^[a-zA-Z0-9\\s',-]+$", + message = "Town or City must only use letters, numbers, and special characters such as hyphens, spaces and apostrophes") private String town; + + @Size(max = 250, message = "County must be 250 characters or less") + @Pattern(regexp = "^(?![\\s\\S])|^[a-zA-Z0-9\\s',-]+$", + message = "County must only use letters, numbers, and special characters such as hyphens, spaces and apostrophes") private String county; + + @Size(max = 8, message = "Postcode must be 8 characters or less") + @Pattern(regexp = "^(?![\\s\\S])|^[a-zA-Z0-9\\s',-]+$", + message = "Postcode must only use letters, numbers, and special characters such as hyphens, spaces and apostrophes") private String postcode; + + @Size(max = 250, message = "Charity commission number must be 250 characters or less") + @Pattern(regexp = "^(?![\\s\\S])|^[a-zA-Z0-9\\s',-]+$", + message = "Charity commission number must only use letters, numbers, and special characters such as hyphens, spaces and apostrophes") private String charityCommissionNumber; + + @Size(max = 250, message = "Companies house number must be 250 characters or less") + @Pattern(regexp = "^(?![\\s\\S])|^[a-zA-Z0-9\\s',-]+$", + message = "Companies house must only use letters, numbers, and special characters such as hyphens, spaces and apostrophes") private String companiesHouseNumber; } diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/security/JwtTokenFilter.java b/src/main/java/gov/cabinetoffice/gap/applybackend/security/JwtTokenFilter.java index a1525037..111aee2a 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/security/JwtTokenFilter.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/security/JwtTokenFilter.java @@ -1,9 +1,7 @@ package gov.cabinetoffice.gap.applybackend.security; -import com.auth0.jwk.JwkException; import com.auth0.jwt.interfaces.DecodedJWT; import gov.cabinetoffice.gap.applybackend.dto.api.JwtPayload; -import gov.cabinetoffice.gap.applybackend.exception.JwkNotValidTokenException; import gov.cabinetoffice.gap.applybackend.service.JwtService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; @@ -44,15 +42,12 @@ protected void doFilterInternal(HttpServletRequest request, } //verify the token String normalisedJwt = header.split(" ")[1]; - DecodedJWT decodedJWT = jwtService.decodedJwt(normalisedJwt); - try { - if (!jwtService.verifyToken(decodedJWT)) { - chain.doFilter(request, response); - return; - } - } catch (JwkException e) { - throw new JwkNotValidTokenException("Token not valid"); + if (!jwtService.verifyToken(normalisedJwt)) { + chain.doFilter(request, response); + return; } + + DecodedJWT decodedJWT = jwtService.decodedJwt(normalisedJwt); //set the Security context, so we can access it everywhere JwtPayload jwtPayload = jwtService.decodeTheTokenPayloadInAReadableFormat(decodedJWT); diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/service/CreateGrantApplicantService.java b/src/main/java/gov/cabinetoffice/gap/applybackend/service/CreateGrantApplicantService.java deleted file mode 100644 index 78fed950..00000000 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/service/CreateGrantApplicantService.java +++ /dev/null @@ -1,89 +0,0 @@ -package gov.cabinetoffice.gap.applybackend.service; - -import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider; -import com.amazonaws.services.cognitoidp.model.*; -import gov.cabinetoffice.gap.applybackend.config.CognitoConfigProperties; -import gov.cabinetoffice.gap.applybackend.model.RegisterApplicant; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class CreateGrantApplicantService { - - private final CognitoConfigProperties cognitoProps; - private final AWSCognitoIdentityProvider cognitoClient; - - private String createCognitoFormattedPhoneNumberFromString(final String unformattedNumber) { - final String telephoneCountryCode = "+44"; - - if (!unformattedNumber.contains(telephoneCountryCode)) { - final String telephoneWithoutLeadingZero = unformattedNumber.substring(1); - return telephoneCountryCode.concat(telephoneWithoutLeadingZero); - } - - return unformattedNumber; - } - - private AdminCreateUserResult addNewUserToCognito(final AWSCognitoIdentityProvider cognitoClient, final RegisterApplicant applicantInformation, final String temporaryPassword) { - final String phoneNumber = createCognitoFormattedPhoneNumberFromString(applicantInformation.getTelephone()); - - final AttributeType[] attributeTypes = new AttributeType[]{ - // The first two attributes are hardcoded for specific applicant roles. - new AttributeType().withName("custom:features").withValue("user=ordinary_user"), - new AttributeType().withName("custom:isAdmin").withValue("false"), - new AttributeType().withName("custom:phoneNumber").withValue(phoneNumber), - new AttributeType().withName("email").withValue(applicantInformation.getEmail()), - new AttributeType().withName("family_name").withValue(applicantInformation.getLastName()), - new AttributeType().withName("given_name").withValue(applicantInformation.getFirstName()), - // This is set to conform with cola. - new AttributeType().withName("custom:lastLogin").withValue("1970-01-01T00:00:00Z") - }; - - final AdminCreateUserRequest userRequest = - new AdminCreateUserRequest() - .withUserPoolId(cognitoProps.getUserPoolId()) - .withUsername(applicantInformation.getEmail()) - .withUserAttributes(attributeTypes) - .withTemporaryPassword(temporaryPassword) - .withMessageAction(MessageActionType.SUPPRESS); - - return cognitoClient.adminCreateUser(userRequest); - } - - private void verifyEmailAddress(final AWSCognitoIdentityProvider cognitoClient, final UserType user) { - final AttributeType userAttributeEmailVerified = new AttributeType() - .withName("email_verified") - .withValue("true"); - - final AdminUpdateUserAttributesRequest adminUpdateUserAttributesRequest = new AdminUpdateUserAttributesRequest() - .withUsername(user.getUsername()) - .withUserPoolId(cognitoProps.getUserPoolId()) - .withUserAttributes(userAttributeEmailVerified); - - cognitoClient.adminUpdateUserAttributes(adminUpdateUserAttributesRequest); - } - - private void setPermanentPassword(final AWSCognitoIdentityProvider cognitoClient, final UserType user, final String password) { - final AdminSetUserPasswordRequest adminSetUserPasswordRequest = - new AdminSetUserPasswordRequest() - .withUsername(user.getUsername()) - .withUserPoolId(cognitoProps.getUserPoolId()) - .withPassword(password) - .withPermanent(true); - - cognitoClient.adminSetUserPassword(adminSetUserPasswordRequest); - } - - public void createNewUser(RegisterApplicant applicantInformation) { - // create our new user - final AdminCreateUserResult createUserResult = addNewUserToCognito(cognitoClient, applicantInformation, cognitoProps.getUserPassword()); - - // verify the email address - verifyEmailAddress(cognitoClient, createUserResult.getUser()); - - // set the account password - setPermanentPassword(cognitoClient, createUserResult.getUser(), cognitoProps.getUserPassword()); - } - -} \ No newline at end of file diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/service/JwtService.java b/src/main/java/gov/cabinetoffice/gap/applybackend/service/JwtService.java index 85f91530..187fe961 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/service/JwtService.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/service/JwtService.java @@ -1,49 +1,45 @@ package gov.cabinetoffice.gap.applybackend.service; -import com.auth0.jwk.Jwk; -import com.auth0.jwk.JwkException; -import com.auth0.jwk.JwkProvider; -import com.auth0.jwk.UrlJwkProvider; import com.auth0.jwt.JWT; -import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; -import gov.cabinetoffice.gap.applybackend.config.CognitoConfigProperties; +import gov.cabinetoffice.gap.applybackend.config.UserServiceConfig; import gov.cabinetoffice.gap.applybackend.dto.api.JwtPayload; -import gov.cabinetoffice.gap.applybackend.exception.JwkNotValidTokenException; +import static java.lang.Boolean.TRUE; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.StringUtils; import org.json.JSONObject; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; -import java.security.interfaces.RSAPublicKey; import java.util.Calendar; +@Slf4j @RequiredArgsConstructor @Service public class JwtService { - private final CognitoConfigProperties cognitoProps; + + private final UserServiceConfig userServiceConfig; + private final RestTemplate restTemplate; public DecodedJWT decodedJwt(String normalisedJWT) { return JWT.decode(normalisedJWT); } - public boolean verifyToken(DecodedJWT jwt) throws JwkException { - if (isTokenExpired(jwt)) { - return false; - } - - boolean isExpectedIssuer = jwt.getIssuer().equals(cognitoProps.getDomain()); - boolean isExpectedAud = jwt.getAudience().get(0).equals(cognitoProps.getAppClientId()); - if (!isExpectedAud || !isExpectedIssuer) { - throw new JwkNotValidTokenException("Token is not valid"); - } - JwkProvider provider = new UrlJwkProvider(cognitoProps.getDomain()); - Jwk jwk = provider.get(jwt.getKeyId()); - Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null); - algorithm.verify(jwt); + public boolean verifyToken(final String jwt) { + final String url = userServiceConfig.getDomain() + "/is-user-logged-in"; + final HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add("Cookie", userServiceConfig.getCookieName() + "=" + jwt); + final HttpEntity requestEntity = new HttpEntity<>(null, requestHeaders); + final ResponseEntity isJwtValid = restTemplate.exchange(url, HttpMethod.GET, requestEntity, Boolean.class); + log.info("is user logged in: " + isJwtValid); - return true; + return TRUE.equals(isJwtValid.getBody()); } public String decodeBase64ToJson(final String base64) { @@ -68,7 +64,6 @@ public JwtPayload decodeTheTokenPayloadInAReadableFormat(DecodedJWT jwt) { final String eventId = jsonObject.getString("event_id"); final String tokenUse = jsonObject.getString("token_use"); final String phoneNumber = jsonObject.getString("custom:phoneNumber"); - final int authTime = jsonObject.getInt("auth_time"); final int exp = jsonObject.getInt("exp"); final int iat = jsonObject.getInt("iat"); final String familyName = jsonObject.getString("family_name"); @@ -85,7 +80,6 @@ public JwtPayload decodeTheTokenPayloadInAReadableFormat(DecodedJWT jwt) { .eventId(eventId) .tokenUse(tokenUse) .phoneNumber(phoneNumber) - .authTime(authTime) .exp(exp) .iat(iat) .familyName(familyName) diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantController.java b/src/main/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantController.java index 9ab56270..92b34371 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantController.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantController.java @@ -5,8 +5,6 @@ import gov.cabinetoffice.gap.applybackend.exception.NotFoundException; import gov.cabinetoffice.gap.applybackend.model.GrantApplicant; import gov.cabinetoffice.gap.applybackend.model.GrantApplicantOrganisationProfile; -import gov.cabinetoffice.gap.applybackend.model.RegisterApplicant; -import gov.cabinetoffice.gap.applybackend.service.CreateGrantApplicantService; import gov.cabinetoffice.gap.applybackend.service.GrantApplicantOrganisationProfileService; import gov.cabinetoffice.gap.applybackend.service.GrantApplicantService; import io.swagger.v3.oas.annotations.media.Content; @@ -20,7 +18,6 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; -import javax.validation.Valid; import java.util.UUID; @RequiredArgsConstructor @@ -29,7 +26,6 @@ public class GrantApplicantController { private final GrantApplicantService grantApplicantService; - private final CreateGrantApplicantService createGrantApplicantService; private final GrantApplicantOrganisationProfileService grantApplicantOrganisationProfileService; private final ModelMapper modelMapper; @@ -83,12 +79,4 @@ public ResponseEntity createApplicant(){ return ResponseEntity.ok("User has been created"); } - - - @PostMapping("/register") - public ResponseEntity registerApplicant(@Valid @RequestBody RegisterApplicant registerApplicant) { - createGrantApplicantService.createNewUser(registerApplicant); - String response = "User has been created"; - return ResponseEntity.ok(response); - } } diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantOrganisationProfileController.java b/src/main/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantOrganisationProfileController.java index c512437c..ac01a144 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantOrganisationProfileController.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantOrganisationProfileController.java @@ -17,6 +17,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; import java.util.UUID; @RequiredArgsConstructor @@ -43,7 +44,7 @@ public ResponseEntity getOrganisationBy @ApiResponse(responseCode = "404", description = "No Organisation found", content = @Content(mediaType = "application/json")), }) public ResponseEntity updateOrganisation(@PathVariable long organisationId, - @RequestBody UpdateGrantApplicantOrganisationProfileDto organisation) { + @RequestBody @Valid UpdateGrantApplicantOrganisationProfileDto organisation) { GrantApplicantOrganisationProfile grantApplicantOrganisationProfile = grantApplicantOrganisationProfileService.getProfileById(organisationId); modelMapper.map(organisation, grantApplicantOrganisationProfile); grantApplicantOrganisationProfile.setId(organisationId); diff --git a/src/main/java/gov/cabinetoffice/gap/applybackend/web/JwtController.java b/src/main/java/gov/cabinetoffice/gap/applybackend/web/JwtController.java index e8ed6673..b35e1606 100644 --- a/src/main/java/gov/cabinetoffice/gap/applybackend/web/JwtController.java +++ b/src/main/java/gov/cabinetoffice/gap/applybackend/web/JwtController.java @@ -8,9 +8,11 @@ import gov.cabinetoffice.gap.applybackend.exception.JwtTokenUndefinedException; import gov.cabinetoffice.gap.applybackend.service.JwtService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +@Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/jwt") @@ -26,7 +28,8 @@ public ResponseEntity validateToken(@RequestHeader("Authoriz final String normalisedJwt = jwtToken.split(" ")[1]; final DecodedJWT jwt = jwtService.decodedJwt(normalisedJwt); - final boolean isValid = jwtService.verifyToken(jwt); + final boolean isValid = jwtService.verifyToken(normalisedJwt); + log.info("is token valid: " + isValid); return ResponseEntity.ok( IsJwtValidResponse.builder() @@ -59,7 +62,7 @@ public ResponseEntity isAdmin(@RequestHeader("Authorization" final String normalisedJwt = jwtToken.split(" ")[1]; final DecodedJWT jwt = jwtService.decodedJwt(normalisedJwt); - final boolean isValid = jwtService.verifyToken(jwt); + final boolean isValid = jwtService.verifyToken(normalisedJwt); final JwtPayload payload = jwtService.decodeTheTokenPayloadInAReadableFormat(jwt); final boolean isAdministrator = payload.getFeatures().contains("user=administrator"); diff --git a/src/test/java/gov/cabinetoffice/gap/applybackend/service/CreateGrantApplicantServiceTest.java b/src/test/java/gov/cabinetoffice/gap/applybackend/service/CreateGrantApplicantServiceTest.java deleted file mode 100644 index 4e950e42..00000000 --- a/src/test/java/gov/cabinetoffice/gap/applybackend/service/CreateGrantApplicantServiceTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package gov.cabinetoffice.gap.applybackend.service; - -import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider; -import com.amazonaws.services.cognitoidp.model.*; -import gov.cabinetoffice.gap.applybackend.config.CognitoConfigProperties; -import gov.cabinetoffice.gap.applybackend.model.RegisterApplicant; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Date; -import java.util.List; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class CreateGrantApplicantServiceTest { - - private CognitoConfigProperties cognitoProps; - @Mock - private AWSCognitoIdentityProvider cognitoClient; - private CreateGrantApplicantService serviceUnderTest; - - @Captor - private ArgumentCaptor adminUserRequestCaptor; - - @Captor - private ArgumentCaptor emailVerificationCaptor; - - @Captor - private ArgumentCaptor passwordResetCaptor; - - @BeforeEach - void setup() { - cognitoProps = CognitoConfigProperties.builder() - .accessKey("an-access-key") - .secretKey("a-secret-key") - .userPoolId("a-user-pool-id") - .region("eu-west-2") - .userPassword("a-user-password") - .build(); - - serviceUnderTest = new CreateGrantApplicantService(cognitoProps, cognitoClient); - } - - @Test - void createNewUser_CreatesUserInCognito() { - - final String unformattedTelephoneNumber = "00000000000"; - final RegisterApplicant applicantToCreate = RegisterApplicant.builder() - .firstName("John") - .lastName("Smith") - .email("john.smith.test@cabinetoffice.gov.uk") - .emailConfirmed("john.smith.test@cabinetoffice.gov.uk") - .telephone(unformattedTelephoneNumber) - .build(); - - final UserType createdUser = new UserType(); - createdUser.setUserCreateDate(new Date()); - createdUser.setUsername("john.smith.test@cabinetoffice.gov.uk"); - - final AdminCreateUserResult createUserResult = new AdminCreateUserResult() - .withUser(createdUser); - - when(cognitoClient.adminCreateUser(Mockito.any())) - .thenReturn(createUserResult); - - serviceUnderTest.createNewUser(applicantToCreate); - - assertUserIsCreated(applicantToCreate, unformattedTelephoneNumber); - assertEmailAddressIsVerified(createdUser); - assertPasswordIsReset(createdUser); - } - - private void assertPasswordIsReset(UserType createdUser) { - - verify(cognitoClient).adminSetUserPassword(passwordResetCaptor.capture()); - - final AdminSetUserPasswordRequest attributes = passwordResetCaptor.getValue(); - assertThat(attributes.getUserPoolId()).isEqualTo(cognitoProps.getUserPoolId()); - assertThat(attributes.getUsername()).isEqualTo(createdUser.getUsername()); - assertThat(attributes.getPermanent()).isTrue(); - assertThat(attributes.getPassword()).isEqualTo(cognitoProps.getUserPassword()); - } - - private void assertEmailAddressIsVerified(UserType user) { - - verify(cognitoClient).adminUpdateUserAttributes(emailVerificationCaptor.capture()); - - final AdminUpdateUserAttributesRequest attributes = emailVerificationCaptor.getValue(); - assertThat(attributes.getUsername()).isEqualTo(user.getUsername()); - assertThat(attributes.getUserPoolId()).isEqualTo(cognitoProps.getUserPoolId()); - - assertContainsAttributeWithValue(attributes.getUserAttributes(), "email_verified", "true"); - } - - void assertUserIsCreated(RegisterApplicant applicantToCreate, String unformattedTelephoneNumber) { - // make sure we actually send the request to create a user and capture the result of it - verify(cognitoClient).adminCreateUser(adminUserRequestCaptor.capture()); - - // test those values against the data we passed in - AdminCreateUserRequest newUserRequest = adminUserRequestCaptor.getValue(); - - assertThat(newUserRequest.getUserPoolId()).isEqualTo(cognitoProps.getUserPoolId()); - assertThat(newUserRequest.getUsername()).isEqualTo(applicantToCreate.getEmail()); - - // check the data contained inside the custom attributes sent to Cognito - final List attributes = newUserRequest.getUserAttributes(); - assertContainsAttributeWithValue(attributes, "custom:features", "user=ordinary_user"); - assertContainsAttributeWithValue(attributes, "custom:isAdmin", "false"); - assertContainsAttributeWithValue(attributes, "custom:phoneNumber", "+44".concat(unformattedTelephoneNumber.substring(1))); - assertContainsAttributeWithValue(attributes, "email", applicantToCreate.getEmail()); - assertContainsAttributeWithValue(attributes, "family_name", applicantToCreate.getLastName()); - assertContainsAttributeWithValue(attributes, "given_name", applicantToCreate.getFirstName()); - assertContainsAttributeWithValue(attributes, "custom:lastLogin", "1970-01-01T00:00:00Z"); - } - - private void assertContainsAttributeWithValue(List attributes, String attributeName, String value) { - attributes.stream() - .filter(a -> a.getName().equals(attributeName)) - .findAny() - .ifPresentOrElse( - a -> assertThat(a.getValue()).isEqualTo(value), - () -> Assertions.fail(String.format("No attribute with name '%s' found", attributeName)) - ); - } -} diff --git a/src/test/java/gov/cabinetoffice/gap/applybackend/service/JwtServiceTest.java b/src/test/java/gov/cabinetoffice/gap/applybackend/service/JwtServiceTest.java index 89953ee6..13ca4f04 100644 --- a/src/test/java/gov/cabinetoffice/gap/applybackend/service/JwtServiceTest.java +++ b/src/test/java/gov/cabinetoffice/gap/applybackend/service/JwtServiceTest.java @@ -1,9 +1,8 @@ package gov.cabinetoffice.gap.applybackend.service; import com.auth0.jwt.interfaces.DecodedJWT; -import gov.cabinetoffice.gap.applybackend.config.CognitoConfigProperties; +import gov.cabinetoffice.gap.applybackend.config.UserServiceConfig; import gov.cabinetoffice.gap.applybackend.dto.api.JwtPayload; -import gov.cabinetoffice.gap.applybackend.exception.JwkNotValidTokenException; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; @@ -12,35 +11,40 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; -import java.util.UUID; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class JwtServiceTest { - private CognitoConfigProperties cognitoProps; + private UserServiceConfig userServiceConfig; + + @Mock + private RestTemplate restTemplate; @InjectMocks private JwtService serviceUnderTest; + @BeforeEach void setup() { - cognitoProps = CognitoConfigProperties.builder() - .accessKey("an-access-key") - .secretKey("a-secret-key") - .userPoolId("a-user-pool-id") - .region("eu-west-2") - .userPassword("a-user-password") - .domain("domain") - .appClientId("appClientId") + userServiceConfig = UserServiceConfig.builder() + .domain("http://localhost:8082") + .cookieName("user-service-token") .build(); - serviceUnderTest = new JwtService(cognitoProps); + serviceUnderTest = new JwtService(userServiceConfig, restTemplate); } @Test @@ -60,21 +64,37 @@ void isTokenExpired_returnFalse() { } @Test - void verifyToken_ThrowErrorWhenNotExpectedIssuer() { - final String encodedJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NWFiNWZiZC0wNjgyLTRkM2QtYTQ2Ny0wMWM3YTQ0N2YwN2MiLCJjdXN0b206ZmVhdHVyZXMiOiJ0ZXN0ZXIiLCJpc3MiOiJ3cm9uZ19kb21haW4iLCJjb2duaXRvOnVzZXJuYW1lIjoidGVzdEB0ZXN0LmNvbSIsImdpdmVuX25hbWUiOiJUZXN0IiwiYXVkIjoiYXBwQ2xpZW50SWQiLCJldmVudF9pZCI6ImxramRsa2pzZmxraiIsInRva2VuX3VzZSI6ImlkIiwiY3VzdG9tOnBob25lTnVtYmVyIjoiMDAwMDAwMDAwMDAwMCIsImF1dGhfdGltZSI6MTY2MTQxODk5MywiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjE2NjE0MTg5OTMsImZhbWlseV9uYW1lIjoiVGVzdGVyIiwiZW1haWwiOiJ0ZXN0QHRlc3QuY29tIn0.1AqU7qQ_KZ0ID7Pym_jsAcHV7w4h3dfC-7Ht-UlTvyo"; - final DecodedJWT decodedJWT = serviceUnderTest.decodedJwt(encodedJwt); - Exception result = assertThrows(JwkNotValidTokenException.class, - () -> serviceUnderTest.verifyToken(decodedJWT)); - assertTrue(result.getMessage().contains("Token is not valid")); + void verifyToken_ReturnsTrue() { + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(), eq(Boolean.class))) + .thenReturn(ResponseEntity.of(Optional.of(Boolean.TRUE))); + + final boolean response = serviceUnderTest.verifyToken("testToken"); + + assertTrue(response); } @Test - void verifyToken_ThrowErrorWhenNotExpectedAud() { - final String encodedJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3NWFiNWZiZC0wNjgyLTRkM2QtYTQ2Ny0wMWM3YTQ0N2YwN2MiLCJjdXN0b206ZmVhdHVyZXMiOiJ0ZXN0ZXIiLCJpc3MiOiJkb21haW4iLCJjb2duaXRvOnVzZXJuYW1lIjoidGVzdEB0ZXN0LmNvbSIsImdpdmVuX25hbWUiOiJUZXN0IiwiYXVkIjoid3JvbmdfYXBwQ2xpZW50SWQiLCJldmVudF9pZCI6ImxramRsa2pzZmxraiIsInRva2VuX3VzZSI6ImlkIiwiY3VzdG9tOnBob25lTnVtYmVyIjoiMDAwMDAwMDAwMDAwMCIsImF1dGhfdGltZSI6MTY2MTQxODk5MywiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjE2NjE0MTg5OTMsImZhbWlseV9uYW1lIjoiVGVzdGVyIiwiZW1haWwiOiJ0ZXN0QHRlc3QuY29tIn0.mHNSL1QXuMaXM_ZYnEdwt_5oBAui-yGajRU1ddId3mM"; - final DecodedJWT decodedJWT = serviceUnderTest.decodedJwt(encodedJwt); - Exception result = assertThrows(JwkNotValidTokenException.class, - () -> serviceUnderTest.verifyToken(decodedJWT)); - assertTrue(result.getMessage().contains("Token is not valid")); + void verifyToken_ReturnsFalse() { + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(), eq(Boolean.class))) + .thenReturn(ResponseEntity.of(Optional.of(Boolean.FALSE))); + + final boolean response = serviceUnderTest.verifyToken("testToken"); + + assertFalse(response); + } + + @Test + void verifyToken_CallsUserService() { + final HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add("Cookie", userServiceConfig.getCookieName() + "=" + "testToken"); + final HttpEntity requestEntity = new HttpEntity<>(null, requestHeaders); + + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(), eq(Boolean.class))) + .thenReturn(ResponseEntity.of(Optional.of(Boolean.FALSE))); + + serviceUnderTest.verifyToken("testToken"); + + verify(restTemplate).exchange(userServiceConfig.getDomain() + "/is-user-logged-in", HttpMethod.GET, requestEntity, Boolean.class); } @Test @@ -133,7 +153,6 @@ void decodeTheTokenPayloadInAReadableFormat() throws JSONException { .eventId(eventId) .tokenUse(tokenUse) .phoneNumber(phoneNumber) - .authTime(authTime) .exp(exp) .iat(iat) .familyName(familyName) diff --git a/src/test/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantControllerTest.java b/src/test/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantControllerTest.java index 5f950192..ef7ea133 100644 --- a/src/test/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantControllerTest.java +++ b/src/test/java/gov/cabinetoffice/gap/applybackend/web/GrantApplicantControllerTest.java @@ -7,7 +7,6 @@ import gov.cabinetoffice.gap.applybackend.model.GrantApplicant; import gov.cabinetoffice.gap.applybackend.model.GrantApplicantOrganisationProfile; import gov.cabinetoffice.gap.applybackend.model.RegisterApplicant; -import gov.cabinetoffice.gap.applybackend.service.CreateGrantApplicantService; import gov.cabinetoffice.gap.applybackend.service.GrantApplicantOrganisationProfileService; import gov.cabinetoffice.gap.applybackend.service.GrantApplicantService; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -42,8 +41,6 @@ class GrantApplicantControllerTest { @Mock private GrantApplicantService grantApplicantService; @Mock - private CreateGrantApplicantService createGrantApplicantService; - @Mock private ModelMapper modelMapper; @Mock private GrantApplicantOrganisationProfileService grantApplicantOrganisationProfileService; @@ -112,23 +109,6 @@ void getApplicantById_ReturnsTheCorrectApplicant() { assertEquals(response.getBody(), getGrantApplicantDto); } - @Test - void RegisterApplicant_ReturnsTheCorrectResponse() { - RegisterApplicant applicantInformation = RegisterApplicant.builder() - .firstName("Test") - .lastName("Tester") - .email("test@test.com") - .emailConfirmed("test@test.com") - .telephone("07123456789") - .build(); - - ResponseEntity response = controllerUnderTest.registerApplicant(applicantInformation); - - verify(createGrantApplicantService, times(1)).createNewUser(applicantInformation); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals("User has been created", response.getBody()); - } - @Test void doesApplicantExist_ReturnTrue(){ when(securityContext.getAuthentication()).thenReturn(authentication); diff --git a/src/test/java/gov/cabinetoffice/gap/applybackend/web/JwtControllerTest.java b/src/test/java/gov/cabinetoffice/gap/applybackend/web/JwtControllerTest.java index 6f12238f..8dfa5261 100644 --- a/src/test/java/gov/cabinetoffice/gap/applybackend/web/JwtControllerTest.java +++ b/src/test/java/gov/cabinetoffice/gap/applybackend/web/JwtControllerTest.java @@ -8,15 +8,11 @@ import gov.cabinetoffice.gap.applybackend.exception.JwtTokenUndefinedException; import gov.cabinetoffice.gap.applybackend.models.TestDecodedJwt; import gov.cabinetoffice.gap.applybackend.service.JwtService; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; -import static org.mockito.Mockito.when; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -28,6 +24,11 @@ import java.time.ZonedDateTime; import java.util.Date; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class JwtControllerTest { @Mock @@ -47,7 +48,7 @@ void validateToken_ReturnsExpectedResponse_ReturnTrue() throws JwkException { when(jwtService.decodedJwt("testTest")) .thenReturn(decodeJwt); - when(jwtService.verifyToken(decodeJwt)) + when(jwtService.verifyToken("testTest")) .thenReturn(true); ResponseEntity response = controllerUnderTest.validateToken("bearer testTest"); @@ -69,7 +70,7 @@ void validateToken_ReturnsExpectedResponse_ReturnFalse() throws JwkException, Pa when(jwtService.decodedJwt("testTest")) .thenReturn(decodeJwt); - when(jwtService.verifyToken(decodeJwt)) + when(jwtService.verifyToken("testTest")) .thenReturn(false); ResponseEntity response = controllerUnderTest.validateToken("bearer testTest"); @@ -123,7 +124,7 @@ void isAdmin_ReturnsExpectedResponse() throws JwkException { when(jwtService.decodedJwt(normalisedJwt)) .thenReturn(new TestDecodedJwt()); - when(jwtService.verifyToken(Mockito.any())) + when(jwtService.verifyToken("test")) .thenReturn(true); when(jwtService.decodeTheTokenPayloadInAReadableFormat(Mockito.any()))