CSRF FlowExecutionListener testing for External Authentication

Introduction

Following on from CSRF Mitigation Options, here we explore how the CsrfFlowExecutionListener[1] CSRF mitigation technique can be applied to the IdP's external authentication mechanism, helping to prevent Login CSRF across ‘external’ authentication strategies.

There are of course any number of possible custom external authentication strategies, not all will benefit from or require CSRF mitigation. Broadly speaking, these can be split into four types:

  • (1) Server-side with no client-side user interaction
    • Server-side as part of a JSP scriplet inside a JSP compiled Servlet, or a generic custom Servlet. Either of which do not interact with the client e.g. do not write a response (HTML) back to the user-agent (browser mostly) to generate a UI which accepts user input.
  • (2) Server-side with client-side user interaction that does not deal with sensitive information.
    • Authentication that does require input from the user, but that input is not sensitive and could not be meaningfully manipulated by an attacker.
  • (3) Server-side with a single client-side user interaction where the user posts sensitive information back to the server e.g. username and password.
    • Authentication which interacts with the user via the browser, such that the user posts login information (or invokes some sensitive operation etc.) back to the servlet before controlled is returned to the IdP.
  • (4) Server-side with many sensitive client-side user interactions.
    • Similar to (3), but the authentication mechanism may be involved with multiple sensitive interactions with the user before control is returned to the IdP.

For (1) and (2), as there is no interaction with the user which could be forged by an attacker, simply passing a CSRF token in and out of the server-side authentication strategy provides no security benefit.

For (3), the developer must embed a CSRF token into any HTML forms that are written back to the client. Once POSTed back to the Servlet, the developer must (within that request/response cycle) call finishExternalAuthentication on the ExternalAuthentication API. The ExternalAuthenticationImpl then needs to guarantee the token is present in the redirect back to the IdP.

For (4), A CSRF token is redundant for any interaction with the user that is not the final interaction before the token is sent back to the IdP to be checked. That is unless the developer of the authentication strategy takes responsibility for checking the token during each request.

In implementation, consideration must also be given to situations where CSRF protection has been disabled, and how best to communicate that to the developer.

Implementation

If CSRF protection is enabled, and external view states are included by default, any external authentication servlet must adhere to the following process:

  • The CsrfFlowExecutionListener generates a CsrfToken when the ExternalTransfer view-state is reached.
    • The token is placed into the spring-webflow viewScope.
  • The token is then extracted from the spring-webflow viewScope and exposed to the external authentication servlet, either;
    • By injecting it into the ExternalAuthenticationImpl before the view is rendered in the view-scope, and placed inside the HTTP request attributes - see Option 1; or
    • The token is retrieved from the FlowExecution active session and exposed by the public ExternalAuthentication API - see Option 2.
  • The token should then be used, where sensible, inside any external authentication process flow that involves user interaction/input via the web browser. Nearly always, the token should be used inside HTML forms (and possibly AJAX post requests) that carry sensitive information or invoke sensitive operations in the authentication flow. However, as discussed in the introduction, not all external authentication strategies will benefit from this.
  • The token must then be present in the redirect back to the IdP and hence spring-webflow e.g. as a query parameter, where it can be evaluated by the CsrfTokenFlowExecutionListener against that stored in the viewScope.


Getting the CsrfToken from the viewScope.

Option 1

Extend ExternalAuthenticationImpl to take a CsrfToken in its constructor (or as a parameter), and inject it during the sub context creation in the view-state (on-render).

 <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).addSubcontext(new net.shibboleth.idp.authn.context.ExternalAuthenticationContext(new net.shibboleth.idp.authn.impl.ExternalAuthenticationImpl(calledAsExtendedFlow\?:false,viewScope.csrfToken)), true).setFlowExecutionUrl(flowExecutionUrl + '&_eventId_proceed=1')" />

Set the CsrfToken as an attribute of the HTTP request when starting authentication e.g. in ExternalAuthenticationImpl#doStart.

request.setAttribute(CSRF_TOKEN_PARAM, csrfToken);

The developer of an external authentication mechanism must then extract the token from the HTTP request, e.g.

final CsrfToken tokenFromRequest =  (CsrfToken)request.getAttribute(ExternalAuthentication.CSRF_TOKEN_PARAM);

Notes:

  • This needs good documentation, otherwise it is not obvious where the token is and how it should be used.


Option 2

Extend the ExternalAutthentication API to extract the CsrfToken from the spring-webflow active session viewScope (using the new external auth API mechanism):

@Nullable public static CsrfToken getCsrfToken(@Nonnull @NotEmpty final String key,
            @Nonnull final HttpServletRequest request) throws ExternalAuthenticationException {

        final Object obj = request.getServletContext().getAttribute(SWF_KEY);
        if (!(obj instanceof FlowExecutorImpl)) {
            throw new ExternalAuthenticationException("No FlowExecutor available in servlet context");
        }

        try {
            final FlowExecutionRepository repo = ((FlowExecutorImpl) obj).getExecutionRepository();
            ExternalContextHolder.setExternalContext(
                    new ServletExternalContext(request.getServletContext(), request, null));
            
            final FlowExecution execution = repo.getFlowExecution(repo.parseFlowExecutionKey(key));
            final MutableAttributeMap<Object> viewScopeMap = execution.getActiveSession().getViewScope();
            final Object tokenObject = viewScopeMap.get("csrfToken");
            //check csrf is enabled\?
            if (tokenObject==null || !(tokenObject instanceof CsrfToken)) {
                return null;
            }
            return (CsrfToken)tokenObject;
            
          
        } catch (final FlowExecutionRepositoryException | IllegalStateException e) {
            throw new ExternalAuthenticationException("Error retrieving flow conversation or view scope", e);
        } finally {
            ExternalContextHolder.setExternalContext(null);
        }
    }

The developer of an external authentication mechanism must then retrieve the token from the ExternalAuthentication API, e.g.

final String key = ExternalAuthentication.startExternalAuthentication(request);
final CsrfToken token =  ExternalAuthentication.getCsrfToken(key, request);

Embedding the CSRF Token in a suitable HTML response

To work as protection for a CSRF attack, the CSRF token parameter name and value must be embedded in HTTP forms sent in a response to the user-agent. For example, as a simple example in JSP:

<%@ page pageEncoding="UTF-8" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ page import="net.shibboleth.idp.authn.ExternalAuthentication" %>
<%@ page import="net.shibboleth.idp.authn.ExternalAuthenticationException" %>
<%@ page import="net.shibboleth.utilities.java.support.net.CsrfToken" %>
<%
CsrfToken csrfToken = null;
String redirect = null;
  try {
      final String key = ExternalAuthentication.startExternalAuthentication(request);
      csrfToken = ExternalAuthentication.getCsrfToken(key,request);
      redirect = ExternalAuthentication.getExternalRedirect("/idp/external.jsp",key);
 
  } catch (final ExternalAuthenticationException e) {
      throw new ServletException("Error processing external authentication request", e);
  }
%>
<form action="<%=redirect%>" method="post">
		<input name="username" id="username">
		 <input type="hidden" name="<%=csrfToken.getParameterName()%>" value="<%=csrfToken.getToken()%>" id="csrf_token">
		<input type="submit" value="Login">
</form>

Passing the token back to the IdP

External authentication servlets are ‘redirected’ back into the IdP external-auth webflow. The token is required in the GET request, and hence must be added as a query parameter to the URL e.g. as a very simple example, inside the ExternalAuthenticationImpl#doFinish method:

response.sendRedirect(extContext.getFlowExecutionUrl()+"&csrf_token="+request.getParameter("csrf_token"));


---

[1] The CsrfFlowExecutionListener provides a core cross-cutting security concern across IdP view states by injecting and validation CSRF tokens.