Skip to content

Commit

Permalink
Sidm 5013 step up authentication (#513)
Browse files Browse the repository at this point in the history
* SIDM-5013 Stash.

* SIDM-5013 StepUp autn implementation.

* SIDM-5013 Build file fixes.

* SIDM-5013 Initial tests/

* add mfa tests

* SIDM-5013 More tests.

* update mfa tests

* SIDM-5013 Add support for GET method in StepUp authn.

* SIDM-5013 Make filter ordering more explicit.

* SIDM-5013 Attempt to fix a CVE by bumping tomcat version.

* CVE-2020-13943 Fix.

* CVE-2020-13943 Fix.

* SIDM-5013 Always delegate to idam-api.

* SIDM-5013 Remove unused code.

* mfa e2e tests

* fix mfa tests

Co-authored-by: shravan mechineni <shravanmechineni5@gmail.com>
  • Loading branch information
Radoslaw Orlowski and shravanmechineni authored Oct 27, 2020
1 parent a905834 commit 5209b79
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 263 deletions.
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -35,7 +37,7 @@ public String filterType() {

@Override
public int filterOrder() {
return 0;
return FILTER_ORDER;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> 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();

Expand Down Expand Up @@ -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<String, String> params, final List<String> cookie) {
public String authorize(final Map<String, String> params, final List<String> 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<String, String> form = new LinkedMultiValueMap<>(14);
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
*
* <p>Makes sure it runs AFTER the {@link SSOZuulFilter}.</p>
*
* @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[]{});
}
}
Original file line number Diff line number Diff line change
@@ -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));
}

}
Loading

0 comments on commit 5209b79

Please sign in to comment.