diff --git a/build.gradle b/build.gradle index d95d14730..fc7dacfb4 100644 --- a/build.gradle +++ b/build.gradle @@ -31,9 +31,9 @@ allprojects { sourceCompatibility = 11 targetCompatibility = 11 - def idamBomVersion = '2.3.9' + def idamBomVersion = '2.5.1' //TODO: Remove once spring boot have updated versions to match - ext['tomcat.version'] = '9.0.37' + ext['tomcat.version'] = '9.0.39' ext['log4j2.version'] = '2.13.3' ext['spring.boot.version'] = '2.2.10.RELEASE' @@ -116,6 +116,7 @@ allprojects { testCompileOnly("org.projectlombok:lombok") testAnnotationProcessor("org.projectlombok:lombok") + testCompile('pl.pragmatists:JUnitParams:1.1.1') testImplementation group: 'org.mockito', name: 'mockito-core' testImplementation group: 'org.springframework', name: 'spring-test' diff --git a/src/main/java/uk/gov/hmcts/reform/idam/web/sso/SSOZuulFilter.java b/src/main/java/uk/gov/hmcts/reform/idam/web/sso/SSOZuulFilter.java index 7b4201e4e..2a235e755 100644 --- a/src/main/java/uk/gov/hmcts/reform/idam/web/sso/SSOZuulFilter.java +++ b/src/main/java/uk/gov/hmcts/reform/idam/web/sso/SSOZuulFilter.java @@ -19,6 +19,8 @@ @Component public class SSOZuulFilter extends ZuulFilter { + public static final int FILTER_ORDER = 0; + private final ConfigurationProperties configurationProperties; private final SSOService ssoService; @@ -35,7 +37,7 @@ public String filterType() { @Override public int filterOrder() { - return 0; + return FILTER_ORDER; } @Override diff --git a/src/main/java/uk/gov/hmcts/reform/idam/web/strategic/SPIService.java b/src/main/java/uk/gov/hmcts/reform/idam/web/strategic/SPIService.java index 90f3b97e3..c2afbd480 100644 --- a/src/main/java/uk/gov/hmcts/reform/idam/web/strategic/SPIService.java +++ b/src/main/java/uk/gov/hmcts/reform/idam/web/strategic/SPIService.java @@ -128,15 +128,30 @@ public String uplift(final String username, final String password, final String } public ApiAuthResult authenticate(final String username, final String password, final String redirectUri, final String ipAddress) throws JsonProcessingException { + return authenticate(username, password, null, redirectUri, ipAddress); + } + + public ApiAuthResult authenticate(final String tokenId, final String redirectUri, final String ipAddress) throws JsonProcessingException { + return authenticate(null, null, tokenId, redirectUri, ipAddress); + } + + protected ApiAuthResult authenticate(final String username, final String password, final String tokenId, final String redirectUri, final String ipAddress) throws JsonProcessingException { MultiValueMap form = new LinkedMultiValueMap<>(4); - form.add("username", username); - form.add("password", password); + if (username != null) { + form.add("username", username); + } + if (password != null) { + form.add("password", password); + } form.add("redirectUri", redirectUri); form.add("originIp", ipAddress); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.add(X_FORWARDED_FOR, ipAddress); + if (tokenId != null) { + headers.add(configurationProperties.getStrategic().getSession().getIdamSessionCookie(), tokenId); + } final ApiAuthResult.ApiAuthResultBuilder resultBuilder = ApiAuthResult.builder(); @@ -175,11 +190,11 @@ public ApiAuthResult authenticate(final String username, final String password, * @should not send state and scope parameters in form if they are not send as parameter in the service * @should return null if api response code is not 302 */ - public String authorize(final Map params, final List cookie) { + public String authorize(final Map params, final List cookies) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - if (cookie != null) { - headers.add(HttpHeaders.COOKIE, StringUtils.join(cookie, ";")); + if (cookies != null) { + headers.add(HttpHeaders.COOKIE, StringUtils.join(cookies, ";")); } addUriHeaders(headers); MultiValueMap form = new LinkedMultiValueMap<>(14); diff --git a/src/main/java/uk/gov/hmcts/reform/idam/web/strategic/StepUpAuthenticationZuulFilter.java b/src/main/java/uk/gov/hmcts/reform/idam/web/strategic/StepUpAuthenticationZuulFilter.java new file mode 100644 index 000000000..720e7f319 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/idam/web/strategic/StepUpAuthenticationZuulFilter.java @@ -0,0 +1,126 @@ +package uk.gov.hmcts.reform.idam.web.strategic; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import uk.gov.hmcts.reform.idam.web.config.properties.ConfigurationProperties; +import uk.gov.hmcts.reform.idam.web.helper.MvcKeys; +import uk.gov.hmcts.reform.idam.web.sso.SSOZuulFilter; + +import javax.annotation.Nonnull; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.Optional; + +import static com.netflix.zuul.constants.ZuulHeaders.X_FORWARDED_FOR; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; + +@Slf4j +@Component +public class StepUpAuthenticationZuulFilter extends ZuulFilter { + + public static final String ZUUL_PROCESSING_ERROR = "Cannot process authentication response"; + public static final String OIDC_AUTHORIZE_ENDPOINT = "/o/authorize"; + + private final SPIService spiService; + private final String idamSessionCookieName; + + @Autowired + public StepUpAuthenticationZuulFilter(@Nonnull final ConfigurationProperties configurationProperties, @Nonnull final SPIService spiService) { + this.idamSessionCookieName = configurationProperties.getStrategic().getSession().getIdamSessionCookie(); + this.spiService = spiService; + } + + @Override + public String filterType() { + return PRE_TYPE; + } + + /** + * {@inheritDoc} + * + *

Makes sure it runs AFTER the {@link SSOZuulFilter}.

+ * + * @return + */ + @Override + public int filterOrder() { + return SSOZuulFilter.FILTER_ORDER + 1; + } + + @Override + public boolean shouldFilter() { + final HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); + return isAuthorizeRequest(request) && hasSessionCookie(request); + } + + + @Override + public Object run() { + final RequestContext ctx = RequestContext.getCurrentContext(); + final HttpServletRequest request = ctx.getRequest(); + + log.info("StepUp filter triggered."); + + final String tokenId = getSessionToken(request); + + try { + final String originIp = ObjectUtils.defaultIfNull(request.getHeader(X_FORWARDED_FOR), request.getRemoteAddr()); + final String redirectUri = request.getParameter(MvcKeys.REDIRECT_URI); + final ApiAuthResult authenticationResult = spiService.authenticate(tokenId, redirectUri, originIp); + if (!authenticationResult.isSuccess()) { + return unauthorizedResponse("AuthTree check for session token failed", ctx); + } + + if (authenticationResult.requiresMfa()) { + dropCookie(idamSessionCookieName, ctx); + } + + // continue as usual (delegate to idam-api) + ctx.setSendZuulResponse(true); + return null; + } catch (final JsonProcessingException e) { + return unauthorizedResponse(ZUUL_PROCESSING_ERROR, ctx); + } + } + + protected void dropCookie(@Nonnull final String cookieName, @Nonnull final RequestContext context) { + context.addZuulRequestHeader(HttpHeaders.COOKIE, cookieName + "="); + } + + protected String getSessionToken(@Nonnull final HttpServletRequest request) { + return Arrays.stream(getCookiesFromRequest(request)) + .filter(cookie -> idamSessionCookieName.equals(cookie.getName())) + .map(Cookie::getValue) + .findAny() + .orElseThrow(); + } + + protected boolean isAuthorizeRequest(@Nonnull final HttpServletRequest request) { + return request.getRequestURI().contains(OIDC_AUTHORIZE_ENDPOINT) && + ("post".equalsIgnoreCase(request.getMethod()) || "get".equalsIgnoreCase(request.getMethod())); + } + + protected boolean hasSessionCookie(@Nonnull final HttpServletRequest request) { + return Arrays.stream(getCookiesFromRequest(request)).anyMatch(cookie -> idamSessionCookieName.equals(cookie.getName())); + } + + protected Object unauthorizedResponse(@Nonnull final String errorCause, @Nonnull final RequestContext context) { + log.error("StepUp authentication failed: {}", errorCause); + context.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED); + context.setSendZuulResponse(false); + return null; + } + + @Nonnull + protected Cookie[] getCookiesFromRequest(@Nonnull final HttpServletRequest request) { + return Optional.ofNullable(request.getCookies()).orElse(new Cookie[]{}); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/idam/web/strategic/StepUpAuthenticationZuulFilterTest.java b/src/test/java/uk/gov/hmcts/reform/idam/web/strategic/StepUpAuthenticationZuulFilterTest.java new file mode 100644 index 000000000..eeb5cf941 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/idam/web/strategic/StepUpAuthenticationZuulFilterTest.java @@ -0,0 +1,95 @@ +package uk.gov.hmcts.reform.idam.web.strategic; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import uk.gov.hmcts.reform.idam.web.config.properties.ConfigurationProperties; +import uk.gov.hmcts.reform.idam.web.config.properties.StrategicConfigurationProperties; +import uk.gov.hmcts.reform.idam.web.sso.SSOZuulFilter; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; + +@RunWith(JUnitParamsRunner.class) +public class StepUpAuthenticationZuulFilterTest { + + private StepUpAuthenticationZuulFilter filter; + + @Before + public void setUp() { + final ConfigurationProperties config = new ConfigurationProperties(); + final StrategicConfigurationProperties strategicProperties = new StrategicConfigurationProperties(); + final StrategicConfigurationProperties.Session session = new StrategicConfigurationProperties.Session(); + session.setIdamSessionCookie("Idam.Session"); + strategicProperties.setSession(session); + config.setStrategic(strategicProperties); + this.filter = spy(new StepUpAuthenticationZuulFilter(config, null)); + } + + @Test + public void filterType() { + assertEquals(PRE_TYPE, filter.filterType()); + } + + @Test + public void filterOrder() { + assertTrue(filter.filterOrder() > SSOZuulFilter.FILTER_ORDER); + } + + @Test + public void shouldFilter() { + doReturn(true).when(filter).isAuthorizeRequest(any()); + doReturn(true).when(filter).hasSessionCookie(any()); + + filter.shouldFilter(); + + verify(filter, times(1)).isAuthorizeRequest(any()); + verify(filter, times(1)).hasSessionCookie(any()); + } + + private Object isAuthorizeRequestParams() { + return new Object[]{ + new Object[]{"http://localhost:1234/o/authorize", "POST", true}, + new Object[]{"http://localhost:1234/o/authorize", "GET", true}, + new Object[]{"http://localhost:1234/o/authorize", "PUT", false}, + new Object[]{"http://localhost:1234/login?param=1", "POST", false}, + new Object[]{"http://localhost:1234/login?param=1", "GET", false}, + }; + } + + @Test + @Parameters(method = "isAuthorizeRequestParams") + public void isAuthorizeRequest(String requestUrl, String httpMethod, Boolean expectedResult) { + final HttpServletRequest request = mock(HttpServletRequest.class); + doReturn(requestUrl).when(request).getRequestURI(); + doReturn(httpMethod).when(request).getMethod(); + assertEquals(expectedResult, filter.isAuthorizeRequest(request)); + } + + + @Test + public void hasSessionCookie() { + final HttpServletRequest request = mock(HttpServletRequest.class); + + doReturn(new Cookie[]{}).when(request).getCookies(); + assertFalse(filter.hasSessionCookie(request)); + + Cookie cookie = new Cookie("Idam.Session", "value"); + doReturn(new Cookie[]{cookie}).when(request).getCookies(); + assertTrue(filter.hasSessionCookie(request)); + } + +} \ No newline at end of file diff --git a/src/test/js/mfa_e2e_test.js b/src/test/js/mfa_e2e_test.js index 880d02cee..95800f397 100644 --- a/src/test/js/mfa_e2e_test.js +++ b/src/test/js/mfa_e2e_test.js @@ -10,66 +10,47 @@ const {expect} = chai; Feature('I am able to login with MFA'); +const scope="openid profile roles manage-user create-user"; let token; let mfaUserEmail; -let blockUserEmail; +let mfaDisabledUserEmail; let randomUserFirstName; -let serviceAdminRole; -let successMfaPolicyName; -let failMfaPolicyName; -let blockPolicyName; - -let userFirstNames = []; -let roleNames = []; -let serviceNames = []; - -const serviceName = randomData.getRandomServiceName(); +let mfaTurnedOnServiceRole; +let mfaTurnedOffServiceRole; +let mfaApplicationPolicyName; +let mfaTurnedOnService; +let mfaTurnedOffService; BeforeSuite(async (I) => { randomUserFirstName = randomData.getRandomUserName(); mfaUserEmail = randomData.getRandomEmailAddress(); - blockUserEmail = randomData.getRandomEmailAddress(); - - successMfaPolicyName = `SIDM_TEST_POLICY_SUCCESS_MFA_${randomData.getRandomString()}`; - failMfaPolicyName = `SIDM_TEST_POLICY_FAIL_MFA_${randomData.getRandomString()}`; - blockPolicyName = `SIDM_TEST_POLICY_BLOCK_${randomData.getRandomString()}`; + mfaDisabledUserEmail = randomData.getRandomEmailAddress(); token = await I.getAuthToken(); - let response; - response = await I.createRole(randomData.getRandomRoleName() + "_mfaotptest_beta", 'beta description', '', token); - const serviceBetaRole = response.name; - response = await I.createRole(randomData.getRandomRoleName() + "_mfaotptest_admin", 'admin description', serviceBetaRole, token); - serviceAdminRole = response.name; - response = await I.createRole(randomData.getRandomRoleName() + "_mfaotptest_super", 'super description', serviceAdminRole, token); - const serviceSuperRole = response.name; - const serviceRoles = [serviceBetaRole, serviceAdminRole, serviceSuperRole]; - roleNames.push(serviceRoles); - await I.createServiceWithRoles(serviceName, serviceRoles, serviceBetaRole, token, "openid profile roles create-user manage-user"); - serviceNames.push(serviceName); - - await I.createUserWithRoles(mfaUserEmail, randomUserFirstName, [serviceAdminRole, "IDAM_ADMIN_USER"]); - userFirstNames.push(randomUserFirstName); - - await I.createUserWithRoles(blockUserEmail, randomUserFirstName, [serviceBetaRole]); - userFirstNames.push(randomUserFirstName); - - await I.createPolicyForMfaTest(successMfaPolicyName, serviceAdminRole, token); - await I.createPolicyForMfaTest(failMfaPolicyName, serviceBetaRole, token); - await I.createPolicyForMfaBlockTest(blockPolicyName, serviceBetaRole, token); + + mfaTurnedOnServiceRole = await I.createRole(randomData.getRandomRoleName() + "_mfaotptest_admin", 'admin description', '', token); + mfaTurnedOnService = await I.createNewServiceWithRoles(randomData.getRandomServiceName(), [mfaTurnedOnServiceRole.id], '', token, "openid profile roles create-user manage-user"); + + mfaTurnedOffServiceRole = await I.createRole(randomData.getRandomRoleName() + "_mfaotptest", 'admin description', '', token); + mfaTurnedOffService = await I.createNewServiceWithRoles(randomData.getRandomServiceName(), [mfaTurnedOffServiceRole.id], '', token, "openid profile roles create-user manage-user"); + + await I.createUserWithRoles(mfaUserEmail, randomUserFirstName, [mfaTurnedOnServiceRole.name, mfaTurnedOffServiceRole.name]); + await I.createUserWithRoles(mfaDisabledUserEmail, randomUserFirstName + "mfadisabled", [mfaTurnedOnServiceRole.name, "idam-mfa-disabled"]); + + mfaApplicationPolicyName = `MfaByApplicationPolicy-${mfaTurnedOnService.oauth2ClientId}`; + await I.createPolicyForApplicationMfaTest(mfaApplicationPolicyName, mfaTurnedOnService.activationRedirectUrl, token); }); AfterSuite(async (I) => { return Promise.all([ I.deleteAllTestData(randomData.TEST_BASE_PREFIX), - I.deletePolicy(successMfaPolicyName, token), - I.deletePolicy(failMfaPolicyName, token), - I.deletePolicy(blockPolicyName, token) + I.deletePolicy(mfaApplicationPolicyName, token), ]); }); Scenario('@functional @mfaLogin I am able to login with MFA', async (I) => { - const nonce = "0km9sBrZfnXv8e_O7U-XmSR6vtIgsUVTGybVUdoLV7g"; - const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${TestData.SERVICE_REDIRECT_URI}&client_id=${serviceName}&state=44p4OfI5CXbdvMTpRYWfleNWIYm6qz0qNDgMOm2qgpU&nonce=${nonce}&response_type=code&scope=openid profile roles manage-user create-user&prompt=`; + const nonce = "0km9sBrZfnXv8e_O7U-XmSR6vtIhsUVTGybVUdoLV7g"; + const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${mfaTurnedOnService.activationRedirectUrl}&client_id=${mfaTurnedOnService.oauth2ClientId}&state=44p4OfI5CXbdvMTpRYWfleNWIYm6qz0qNDgMOm2qgpU&nonce=${nonce}&response_type=code&scope=openid profile roles manage-user create-user&prompt=`; I.amOnPage(loginUrl); I.waitForText('Sign in', 20, 'h1'); @@ -84,24 +65,25 @@ Scenario('@functional @mfaLogin I am able to login with MFA', async (I) => { I.fillField('code', otpCode); I.interceptRequestsAfterSignin(); I.click('Submit'); - I.waitForText(TestData.SERVICE_REDIRECT_URI); + I.waitForText(mfaTurnedOnService.activationRedirectUrl.toLowerCase()); I.see('code='); I.dontSee('error='); let pageSource = await I.grabSource(); let code = pageSource.match(/\?code=([^&]*)(.*)/)[1]; - let accessToken = await I.getAccessToken(code, serviceName, TestData.SERVICE_REDIRECT_URI, TestData.SERVICE_CLIENT_SECRET); + let accessToken = await I.getAccessToken(code, mfaTurnedOnService.oauth2ClientId, mfaTurnedOnService.activationRedirectUrl, TestData.SERVICE_CLIENT_SECRET); let jwtDecode = await jwt_decode(accessToken); assert.equal("access_token", jwtDecode.tokenName); assert.equal(nonce, jwtDecode.nonce); + assert.equal(1, jwtDecode.auth_level); //Webpublic OIDC userinfo const oidcUserInfo = await I.retry({retries: 3, minTimeout: 10000}).getWebpublicOidcUserInfo(accessToken); expect(oidcUserInfo.sub.toUpperCase()).to.equal(mfaUserEmail.toUpperCase()); expect(oidcUserInfo.uid).to.not.equal(null); - expect(oidcUserInfo.roles).to.deep.equalInAnyOrder([serviceAdminRole, 'IDAM_ADMIN_USER']); + expect(oidcUserInfo.roles).to.deep.equalInAnyOrder([mfaTurnedOnServiceRole.id, mfaTurnedOffServiceRole.id]); expect(oidcUserInfo.name).to.equal(randomUserFirstName + ' User'); expect(oidcUserInfo.given_name).to.equal(randomUserFirstName); expect(oidcUserInfo.family_name).to.equal('User'); @@ -110,8 +92,8 @@ Scenario('@functional @mfaLogin I am able to login with MFA', async (I) => { }).retry(TestData.SCENARIO_RETRY_LIMIT); Scenario('@functional @mfaLogin @welshLanguage I am able to login with MFA in Welsh', async (I) => { - const nonce = "0km9sBrZfnXv8e_O7U-XmSR6vtIgsUVTGybVUdoLV7g"; - const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${TestData.SERVICE_REDIRECT_URI}&client_id=${serviceName}&state=44p4OfI5CXbdvMTpRYWfleNWIYm6qz0qNDgMOm2qgpU&nonce=${nonce}&prompt=&response_type=code&scope=openid profile roles manage-user create-user${Welsh.urlForceCy}`; + const nonce = "0km9sBrZfnXv8e_O7U-XmSR6wtIgsUVTGybVUdoLV7g"; + const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${mfaTurnedOnService.activationRedirectUrl}&client_id=${mfaTurnedOnService.oauth2ClientId}&state=44p4OfI5CXbdvMTpRYWfleNWIYm6qz0qNDgMOm2qgpU&nonce=${nonce}&prompt=&response_type=code&scope=openid profile roles manage-user create-user${Welsh.urlForceCy}`; I.amOnPage(loginUrl); I.waitForText(Welsh.signInOrCreateAccount, 20, 'h1'); @@ -128,24 +110,25 @@ Scenario('@functional @mfaLogin @welshLanguage I am able to login with MFA in We I.fillField('code', otpCode); I.interceptRequestsAfterSignin(); I.click(Welsh.submitBtn); - I.waitForText(TestData.SERVICE_REDIRECT_URI); + I.waitForText(mfaTurnedOnService.activationRedirectUrl.toLowerCase()); I.see('code='); I.dontSee('error='); let pageSource = await I.grabSource(); let code = pageSource.match(/\?code=([^&]*)(.*)/)[1]; - let accessToken = await I.getAccessToken(code, serviceName, TestData.SERVICE_REDIRECT_URI, TestData.SERVICE_CLIENT_SECRET); + let accessToken = await I.getAccessToken(code, mfaTurnedOnService.oauth2ClientId, mfaTurnedOnService.activationRedirectUrl, TestData.SERVICE_CLIENT_SECRET); let jwtDecode = await jwt_decode(accessToken); assert.equal("access_token", jwtDecode.tokenName); assert.equal(nonce, jwtDecode.nonce); + assert.equal(1, jwtDecode.auth_level); //Webpublic OIDC userinfo const oidcUserInfo = await I.retry({retries: 3, minTimeout: 10000}).getWebpublicOidcUserInfo(accessToken); expect(oidcUserInfo.sub.toUpperCase()).to.equal(mfaUserEmail.toUpperCase()); expect(oidcUserInfo.uid).to.not.equal(null); - expect(oidcUserInfo.roles).to.deep.equalInAnyOrder([serviceAdminRole, 'IDAM_ADMIN_USER']); + expect(oidcUserInfo.roles).to.deep.equalInAnyOrder([mfaTurnedOnServiceRole.id, mfaTurnedOffServiceRole.id]); expect(oidcUserInfo.name).to.equal(randomUserFirstName + ' User'); expect(oidcUserInfo.given_name).to.equal(randomUserFirstName); expect(oidcUserInfo.family_name).to.equal('User'); @@ -153,20 +136,8 @@ Scenario('@functional @mfaLogin @welshLanguage I am able to login with MFA in We I.resetRequestInterception(); }).retry(TestData.SCENARIO_RETRY_LIMIT); -//TODO: revert back once the authtrees is fixed properly. -Scenario('@mfaLogin I am not able to login with MFA for the block policy ', async (I) => { - const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${TestData.SERVICE_REDIRECT_URI}&client_id=${serviceName}`; - - I.amOnPage(loginUrl); - I.waitForText('Sign in', 20, 'h1'); - I.fillField('#username', blockUserEmail); - I.fillField('#password', TestData.PASSWORD); - I.click('Sign in'); - I.waitForText('Policies check failed', 10, 'h2'); -}).retry(TestData.SCENARIO_RETRY_LIMIT); - Scenario('@functional @mfaLogin Validate verification code and 3 incorrect otp attempts should redirect user to the sign in page', async (I) => { - const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${TestData.SERVICE_REDIRECT_URI}&client_id=${serviceName}`; + const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${mfaTurnedOnService.activationRedirectUrl}&client_id=${mfaTurnedOnService.oauth2ClientId}`; I.amOnPage(loginUrl); I.waitForText('Sign in', 20, 'h1'); @@ -221,8 +192,125 @@ Scenario('@functional @mfaLogin Validate verification code and 3 incorrect otp a I.fillField('code', otpCodeLatest); I.interceptRequestsAfterSignin(); I.click('Submit'); - I.waitForText(TestData.SERVICE_REDIRECT_URI); + I.waitForText(mfaTurnedOnService.activationRedirectUrl.toLowerCase()); I.see('code='); I.dontSee('error='); I.resetRequestInterception(); -}).retry(TestData.SCENARIO_RETRY_LIMIT); \ No newline at end of file +}).retry(TestData.SCENARIO_RETRY_LIMIT); + +Scenario('@functional @mfaLogin @mfaDisabledUserLogin As a mfa disabled user I can login without mfa for the application with mfa turned on', async (I) => { + const nonce = "0km9sBrZfnXv8e_O7U-XmSR6vtIgtUVTGybVUdoLV7g"; + const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${mfaTurnedOnService.activationRedirectUrl}&client_id=${mfaTurnedOnService.oauth2ClientId}&state=44p4OfI5CXbdvMTpRYWfleNWIYm6qz0qNDgMOm2qgpU&nonce=${nonce}&response_type=code&scope=openid profile roles manage-user create-user&prompt=`; + + I.amOnPage(loginUrl); + I.waitForText('Sign in', 20, 'h1'); + I.fillField('#username', mfaDisabledUserEmail); + I.fillField('#password', TestData.PASSWORD); + I.interceptRequestsAfterSignin(); + I.click('Sign in'); + I.waitForText(mfaTurnedOnService.activationRedirectUrl.toLowerCase()); + I.see('code='); + I.dontSee('error='); + + const pageSource = await I.grabSource(); + const code = pageSource.match(/\?code=([^&]*)(.*)/)[1]; + const accessToken = await I.getAccessToken(code, mfaTurnedOnService.oauth2ClientId, mfaTurnedOnService.activationRedirectUrl, TestData.SERVICE_CLIENT_SECRET); + + let jwtDecode = await jwt_decode(accessToken); + + assert.equal("access_token", jwtDecode.tokenName); + assert.equal(nonce, jwtDecode.nonce); + assert.equal(0, jwtDecode.auth_level); + + //Details api + const userInfo = await I.retry({retries: 3, minTimeout: 10000}).getUserInfo(accessToken); + expect(userInfo.active).to.equal(true); + expect(userInfo.email).to.equal(mfaDisabledUserEmail); + expect(userInfo.forename).to.equal(randomUserFirstName + 'mfadisabled'); + expect(userInfo.id).to.not.equal(null); + expect(userInfo.surname).to.equal('User'); + expect(userInfo.roles).to.deep.equalInAnyOrder([mfaTurnedOnServiceRole.id, 'idam-mfa-disabled']); + + //Webpublic OIDC userinfo + const oidcUserInfo = await I.retry({retries: 3, minTimeout: 10000}).getWebpublicOidcUserInfo(accessToken); + expect(oidcUserInfo.sub.toUpperCase()).to.equal(mfaDisabledUserEmail.toUpperCase()); + expect(oidcUserInfo.uid).to.not.equal(null); + expect(oidcUserInfo.roles).to.deep.equalInAnyOrder([mfaTurnedOnServiceRole.id, 'idam-mfa-disabled']); + + expect(oidcUserInfo.name).to.equal(randomUserFirstName + 'mfadisabled' + ' User'); + expect(oidcUserInfo.given_name).to.equal(randomUserFirstName + 'mfadisabled'); + expect(oidcUserInfo.family_name).to.equal('User'); + + I.resetRequestInterception(); +}); + +Scenario('@functional @mfaLogin @mfaStepUpLogin As a user, I can login with client A MFA turned OFF and then step-up to client B with MFA turned ON', async (I) => { + const nonce = "0km9sBrZfnXv8e_O7U-XmSR6vtIgsUVTXybVUdoLV7g"; + const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${mfaTurnedOffService.activationRedirectUrl}&client_id=${mfaTurnedOffService.oauth2ClientId}&state=44p4OfI5CXbdvMTpRYWfleNWIYm6qz0qNDgMOm2qgpU&nonce=${nonce}&response_type=code&scope=${scope}&prompt=`; + + I.amOnPage(loginUrl); + I.waitForText('Sign in', 20, 'h1'); + I.fillField('#username', mfaUserEmail); + I.fillField('#password', TestData.PASSWORD); + I.interceptRequestsAfterSignin(); + I.click('Sign in'); + I.waitForText(mfaTurnedOffService.activationRedirectUrl.toLowerCase()); + I.see('code='); + I.dontSee('error='); + + I.amOnPage(loginUrl); + const idamSessionCookie = await I.grabCookie('Idam.Session'); + const cookie = idamSessionCookie.value; + + // try authorizing with the Idam session cookie for the client MFA turned ON + const location = await I.getWebPublicOidcAuthorize(mfaTurnedOnService.oauth2ClientId, mfaTurnedOnService.activationRedirectUrl, scope, nonce, cookie); + console.log("Location: " + location); + location.includes(`/login?client_id=${mfaTurnedOnService.oauth2ClientId}&redirect_uri=${mfaTurnedOnService.activationRedirectUrl}`); + + I.amOnPage(location); + I.waitForText('Sign in', 20, 'h1'); + I.fillField('#username', mfaUserEmail); + I.fillField('#password', TestData.PASSWORD); + I.click('Sign in'); + I.seeInCurrentUrl("/verification"); + I.waitForText('Verification required', 10, 'h1'); + I.wait(5); + const otpCode = await I.extractOtpFromEmail(mfaUserEmail); + + I.fillField('code', otpCode); + I.click('Submit'); + I.waitForText(mfaTurnedOnService.activationRedirectUrl.toLowerCase()); + I.see('code='); + I.dontSee('error='); + + const pageSource = await I.grabSource(); + const code = pageSource.match(/\?code=([^&]*)(.*)/)[1]; + const accessToken = await I.getAccessToken(code, mfaTurnedOnService.oauth2ClientId, mfaTurnedOnService.activationRedirectUrl, TestData.SERVICE_CLIENT_SECRET); + + let jwtDecode = await jwt_decode(accessToken); + + assert.equal("access_token", jwtDecode.tokenName); + assert.equal(nonce, jwtDecode.nonce); + assert.equal(1, jwtDecode.auth_level); + + //Details api + const userInfo = await I.retry({retries: 3, minTimeout: 10000}).getUserInfo(accessToken); + expect(userInfo.active).to.equal(true); + expect(userInfo.email).to.equal(mfaUserEmail); + expect(userInfo.forename).to.equal(randomUserFirstName); + expect(userInfo.id).to.not.equal(null); + expect(userInfo.surname).to.equal('User'); + expect(userInfo.roles).to.deep.equalInAnyOrder([mfaTurnedOnServiceRole.id, mfaTurnedOffServiceRole.id]); + + //Webpublic OIDC userinfo + const oidcUserInfo = await I.retry({retries: 3, minTimeout: 10000}).getWebpublicOidcUserInfo(accessToken); + expect(oidcUserInfo.sub.toUpperCase()).to.equal(mfaUserEmail.toUpperCase()); + expect(oidcUserInfo.uid).to.not.equal(null); + expect(oidcUserInfo.roles).to.deep.equalInAnyOrder([mfaTurnedOnServiceRole.id, mfaTurnedOffServiceRole.id]); + + expect(oidcUserInfo.name).to.equal(randomUserFirstName + ' User'); + expect(oidcUserInfo.given_name).to.equal(randomUserFirstName); + expect(oidcUserInfo.family_name).to.equal('User'); + + I.resetRequestInterception(); +}); \ No newline at end of file diff --git a/src/test/js/policy_check_functional_test.js b/src/test/js/policy_check_functional_test.js deleted file mode 100644 index c5b0b9104..000000000 --- a/src/test/js/policy_check_functional_test.js +++ /dev/null @@ -1,63 +0,0 @@ -const TestData = require('./config/test_data'); -const randomData = require('./shared/random_data'); - -Feature('Policy check'); - -let token; -let citizenEmail; -let policyName; -let userFirstNames = []; -let roleNames = []; -let serviceNames = []; - -const box = { - left: 100, - top: 200, - right: 200, - bottom: 600 -}; -const serviceName = randomData.getRandomServiceName(); - -BeforeSuite(async (I) => { - const randomUserFirstName = randomData.getRandomUserName(); - citizenEmail = 'citizen.' + randomData.getRandomEmailAddress(); - policyName = `SIDM_TEST_POLICY_${serviceName}`; - - token = await I.getAuthToken(); - let response; - response = await I.createRole(randomData.getRandomRoleName() + "_beta", 'beta description', '', token); - const serviceBetaRole = response.name; - response = await I.createRole(randomData.getRandomRoleName() + "_admin", 'admin description', serviceBetaRole, token); - const serviceAdminRole = response.name; - response = await I.createRole(randomData.getRandomRoleName() + "_super", 'super description', serviceAdminRole, token); - const serviceSuperRole = response.name; - const serviceRoles = [serviceBetaRole, serviceAdminRole, serviceSuperRole]; - roleNames.push(serviceRoles); - await I.createServiceWithRoles(serviceName, serviceRoles, serviceBetaRole, token); - serviceNames.push(serviceName); - await I.createUserWithRoles(citizenEmail, randomUserFirstName + 'Citizen', ["citizen"]); - userFirstNames.push(randomUserFirstName + 'Citizen'); - - await I.createPolicyToBlockUser(policyName, citizenEmail, token); -}); - -AfterSuite(async (I) => { - return Promise.all([ - I.deleteAllTestData(randomData.TEST_BASE_PREFIX), - I.deletePolicy(policyName, token), - ]); -}); - -//TODO: revert back once the authtrees is fixed properly. -Scenario('@policy As a citizen with policies blocking me from login I should see an error message', async (I) => { - const loginUrl = `${TestData.WEB_PUBLIC_URL}/login?redirect_uri=${TestData.SERVICE_REDIRECT_URI}&client_id=${serviceName}`; - - I.amOnPage(loginUrl); - I.waitForText('Sign in', 20, 'h1'); - I.fillField('#username', citizenEmail.toUpperCase()); - I.fillField('#password', TestData.PASSWORD); - I.click('Sign in'); - I.saveScreenshot( 'Polycheck-login.png') - I.waitForText('Policies check failed', 10, 'h2'); - -}).retry(TestData.SCENARIO_RETRY_LIMIT); \ No newline at end of file diff --git a/src/test/js/shared/idam_helper.js b/src/test/js/shared/idam_helper.js index af84cc6cb..456ae59e0 100644 --- a/src/test/js/shared/idam_helper.js +++ b/src/test/js/shared/idam_helper.js @@ -82,6 +82,21 @@ class IdamHelper extends Helper { }) } + getWebPublicOidcAuthorize(serviceName, serviceRedirect, oauth2Scope, nonce, cookie) { + return fetch(`${TestData.WEB_PUBLIC_URL}/o/authorize?redirect_uri=${serviceRedirect}&client_id=${serviceName}&state=44p4OfI5CXbdvMTpRYWfleNWIYm6qz0qNDgMOm2qgpU&nonce=${nonce}&response_type=code&scope=${oauth2Scope}&prompt=`, { + agent: agent, + method: 'GET', + headers: {'Cookie': `Idam.Session=${cookie}`}, + redirect: 'manual' + }).then(response => { + return response.headers.get('Location'); + }) + .catch(err => { + console.log(err); + let browser = this.helpers['Puppeteer'].browser; + browser.close(); + }); + } createService(serviceName, roleId, token, scope = '', ssoProviders = '') { let data; @@ -157,6 +172,35 @@ class IdamHelper extends Helper { .catch(err => err); } + createNewServiceWithRoles(serviceName, serviceRoles, betaRole, token, scope) { + if (scope == null) { + scope = '' + } + const data = { + label: serviceName, + description: serviceName, + oauth2ClientId: serviceName, + oauth2ClientSecret: TestData.SERVICE_CLIENT_SECRET, + oauth2RedirectUris: [`http://www.${serviceName}.com`], + oauth2Scope: scope, + onboardingEndpoint: '/autotest', + onboardingRoles: [betaRole], + allowedRoles: serviceRoles, + activationRedirectUrl: `http://www.${serviceName}.com`, + selfRegistrationAllowed: true + }; + return fetch(`${TestData.IDAM_API}/services`, { + agent: agent, + method: 'POST', + body: JSON.stringify(data), + headers: {'Content-Type': 'application/json', 'Authorization': 'AdminApiAuthToken ' + token}, + }).then(res => res.json()) + .then((json) => { + return json; + }) + .catch(err => err); + } + getAuthToken() { return fetch(`${TestData.IDAM_API}/loginUser?username=${TestData.SMOKE_TEST_USER_USERNAME}&password=${TestData.SMOKE_TEST_USER_PASSWORD}`, { agent: agent, @@ -267,143 +311,27 @@ class IdamHelper extends Helper { .catch(err => err); } - createPolicyToBlockUser(name, userEmail, api_auth_token) { + createPolicyForApplicationMfaTest(name, redirectUri, api_auth_token) { const data = { "name": name, - "applicationName": "HmctsPolicySet", - "description": "Blocks specific user", "active": true, - "actionValues": { - "GET": false, - "POST": false, - "DELETE": false, - "PATCH": false, - "PUT": false, - "OPTIONS": false, - "HEAD": false - }, - "resourceTypeUuid": "HmctsUrlResourceType", - "resources": [ - "*://*", - "*://*:*/*", - "*://*:*/*?*", - "*://*?*" - ], - "subject": { - "type": "AND", - "subjects": [ - { - "type": "Identity", - "subjectValues": [ - `id=${userEmail},ou=user,o=hmcts,ou=services,dc=reform,dc=hmcts,dc=net` - ] - } - ] - } - }; - return fetch(`${TestData.IDAM_API}/api/v1/policies`, { - agent: agent, - method: 'POST', - body: JSON.stringify(data), - headers: {'Content-Type': 'application/json', 'Authorization': 'AdminApiAuthToken ' + api_auth_token}, - }) - .then(res => res.json()) - .catch(err => err); - } - - createPolicyForMfaTest(name, roleName, api_auth_token) { - const data = { - "name": name, "applicationName": "HmctsPolicySet", - "description": "Require MFA for test user", - "active": true, - "actionValues": { - "GET": false, - "POST": false, - "DELETE": false, - "PATCH": false, - "PUT": false, - "OPTIONS": false, - "HEAD": false - }, "resourceTypeUuid": "HmctsUrlResourceType", "resources": [ - "*://*", - "*://*:*/*", - "*://*:*/*?*", - "*://*?*" + `${redirectUri}` ], - "resourceAttributes": [{ - "type": "Static", - "propertyName": "mfaRequired", - "propertyValues": ["true"] - }], - "subject": { - "type": "Identity", - "subjectValues": [ - `id=${roleName},ou=group,o=hmcts,ou=services,dc=reform,dc=hmcts,dc=net` - ] - }, - "condition": { - "type": "NOT", - "condition": { - "type": "AuthLevel", - "authLevel": 1 + "actionValues": {}, + "resourceAttributes": [ + { + "type": "Static", + "propertyName": "mfaRequired", + "propertyValues": [ + "true" + ] } - } - }; - return fetch(`${TestData.IDAM_API}/api/v1/policies`, { - agent: agent, - method: 'POST', - body: JSON.stringify(data), - headers: {'Content-Type': 'application/json', 'Authorization': 'AdminApiAuthToken ' + api_auth_token}, - }) - .then(res => res.json()) - .catch(err => err); - } - - createPolicyForMfaBlockTest(name, roleName, api_auth_token) { - const data = { - "name": name, - "applicationName": "HmctsPolicySet", - "description": "Require MFA for test user", - "active": true, - "actionValues": { - "GET": false, - "POST": false, - "DELETE": false, - "PATCH": false, - "PUT": false, - "OPTIONS": false, - "HEAD": false - }, - "resourceTypeUuid": "HmctsUrlResourceType", - "resources": [ - "*://*", - "*://*:*/*", - "*://*:*/*?*", - "*://*?*" ], - "resourceAttributes": [{ - "type": "Static", - "propertyName": "blocked", - "propertyValues": ["true"] - }], "subject": { - "type": "Identity", - "subjectValues": [ - `id=${roleName},ou=group,o=hmcts,ou=services,dc=reform,dc=hmcts,dc=net` - ] - }, - "condition": { - "type": "NOT", - "condition": { - "type": "IPv4", - "startIp": "0.0.0.1", - "endIp": "0.0.0.10", - "ipRange": [], - "dnsName": [] - } + "type": "AuthenticatedUsers" } }; return fetch(`${TestData.IDAM_API}/api/v1/policies`, { @@ -416,6 +344,7 @@ class IdamHelper extends Helper { .catch(err => err); } + deletePolicy(name, api_auth_token) { return fetch(`${TestData.IDAM_API}/api/v1/policies/${name}`, { agent: agent, diff --git a/yarn.lock b/yarn.lock index d749109e0..ecec737b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1593,6 +1593,11 @@ jws@^3.1.4: jwa "^1.1.5" safe-buffer "^5.0.1" +jwt-decode@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.0.0.tgz#d9a17ddf6d37c03bf037b0b27cf8756cfd01c5c7" + integrity sha512-RBQv2MTm3FNKQkdzhEyQwh5MbdNgMa+FyIJIK5RMWEn6hRgRHr7j55cRxGhRe6vGJDElyi6f6u/yfkP7AoXddA== + kew@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"