Anti-CSRF FlowExecutionListener Implementation

Overview

Using a spring webflow FlowExecutionListener to add a CSRF token to the viewScope on `#viewRendering(..)` and check it against that returned in the HTTP request on `#eventSignaled(..)`.


Advantages of Approach:

  • system wide enforcement of a synchroniser token based CSRF defence for SWF view-states.
    • For internal views and any custom views created as part of IdP flow extensions.
    • View-states can be excluded from CSRF protection by adding an annotation metadata attribute to the view-state definition in the flow XML configuration files.
  • CSRF defence can easily be turned on/off globally by a property in idp.properties.
  • can, if configured correctly (e.g. catching invalid token exceptions in the flow), be used to show the default (or other) IdP error page with a custom message. Otherwise the generic uncaught exception message will be shown.
  • CSRF tokens are refreshed per view render. Arguably slightly stronger than, for example, per-session tokens, as they could be used along with the session ID in a session fixation type attack.

Disadvantages of Approach:

  • is tightly coupled to SWF and the lifecycle of a flow.
  • does not fit very well with the general IdP architecture:
    • is, mostly, hidden from flow definitions.
    • can only signal error states i.e. an invalid CSRF token, by throwing RuntimeExceptions or subclasses thereof e.g. InvalidCSRFTokenException. Whereas nearly all other error states in the IdP are encoded directly as SWF Events.
      • Although these can be caught and turned into an error-state by SWF transitions.
  • the listener will be called for every SWF lifecycle event, which may incur a small performance penalty.
    • this can be limited if required by specifying which flows to observe as criteria in the configuration.
  • by default, only checks CSRF token validity on certain events e.g. ‘proceed’, from view-states, although this can be configured by a condition Predicate. It is possible therefore to bypass validation if a different event ID was used to transition out of a view-state.


Implementation

The proof of concept can be found on my personal git repository [git@git.shibboleth.net:philsmart/java-identity-provider] (branch feature/anti-csrf-flowlistener).


New Classes

All new classes and test classes have been packaged inside idp-ui. 

  • A CSRFToken API interface . This defines token implementations that return token values and HTTP parameter names.
  • A default implementation of CSRFToken, namely SimpleCSRFToken . Encapsulating both token value and HTTP parameter name as a non-empty Strings. Has no business logic.
  • A CSRFTokenManager to provide helper methods for generating and validating CSRF tokens .
    • uses a configurable SecureRandomIdentifierGenerationStrategy to generate anti-csrf tokens - even though suitable, is used to generate a token and not an identifier..
    • Uses a configurable predicate for validating tokens. 
    • Note, the logic of this class could be pushed into the CSRFTokenFlowExecutionListener class.
  • A CSRFTokenFlowExecutionListener which reacts to SWF lifecycle events.
    • adds CSRF tokens to the viewScope when views are rendering.
    • checks the CSRF token in the request matches that stored in the view scope when a suitable event occurs e.g. ‘proceed’.
    • Can be configured to include or exclude certain view-states by their state identifier, or other facets of the request context and event, using configurable and replaceable Java Predicates.
      • DefaultEventRequiresCSRFTokenValidationPredicate - A BiPredicate for checking the request context and event require CSRF token validation i.e. checks current state is not  excluded from CSRF protection.
      • DefaultViewRequiresCSRFTokenPredicate - A Predicate for checking the request context requires a CSRF token to be set into the viewScope i.e. checks current view-state is not  excluded from CSRF protection.
      • BaseCSRFTokenPredicate - Encodes common logic between both previous predicates. 

    • Can be enabled and disabled.
  • An InvalidCSRFTokenException, subtype of FlowExecutionException, thrown when an invalid CSRF token is found.

Global config changes

The flow listener is added to the flowExecutor in the webflow-config.xml. Note, the csrfTokenFlowExecutionListener defines an init-method and destroy-method to guarantee nonnull fields are set. The webflow config is also where the supporting beans, namely; CsrfTokenManager,  DefaultEventRequiresCSRFTokenValidationPredicate, and DefaultViewRequiresCSRFTokenPredicate are defined.

 <webflow:flow-executor id="flowExecutor">
        <webflow:flow-execution-repository max-execution-snapshots="0" conversation-manager="conversationManager" />
        <webflow:flow-execution-listeners>
            <webflow:listener ref="profileRequestContextFlowExecutionListener"
                              criteria="%{idp.profile.exposeProfileRequestContextInServletRequest:*}" />
            <webflow:listener ref="csrfTokenFlowExecutionListener"/>       
        </webflow:flow-execution-listeners>
    </webflow:flow-executor>

	<bean id="csrfTokenFlowExecutionListener" init-method="initialize" destroy-method="destroy"
          class="net.shibboleth.idp.ui.csrf.impl.CSRFTokenFlowExecutionListener" p:csrfTokenManager-ref="shibboleth.CsrfTokenManager"
          p:enabled="%{idp.csrf.enabled:false}"
          p:viewRequiresCSRFTokenPredicate-ref="shibboleth.DefaultViewRequiresCsrfTokenPredicate"
          p:eventRequiresCSRFTokenValidationPredicate-ref="shibboleth.DefaultEventRequiresCSRFTokenValidationPredicate"
          />

 	<!-- Cross Site Request Forgery token manager -->
	<bean id="shibboleth.CsrfTokenManager" class="net.shibboleth.idp.ui.csrf.CSRFTokenManager"
       p:csrfParameterName="%{idp.csrf.token.parameter:csrf_token}"/>

	
	<bean id="shibboleth.DefaultViewRequiresCsrfTokenPredicate" 
		class="net.shibboleth.idp.ui.csrf.impl.DefaultViewRequiresCSRFTokenPredicate"
		parent="shibboleth.BaseCSRFTokenPredicate"/>

	
	<bean id="shibboleth.DefaultEventRequiresCSRFTokenValidationPredicate" 
		class="net.shibboleth.idp.ui.csrf.impl.DefaultEventRequiresCSRFTokenValidationPredicate"
		parent="shibboleth.BaseCSRFTokenPredicate"/>

To signify an InvalidCSRFToken event is ‘local’, add it to the shibboleth.LocalEventMap in errors.xml

<entry key="InvalidCSRFToken" value="false" />


IdP Property Changes

The listener can be configured by the deployer inside idp.properties. By default the listener is disabled unless the property (idp.csrf.enabled) is uncommented and set to true. The name of the HTTP parameter used to transport the CSRF token in requests can also be changed.

# Enable global cross-site request forgery mitigation for views. Default is off. 
idp.csrf.enabled = true
# Name of the HTTP parameter that stores the CSRF token.
#idp.csrf.token.parameter = csrf_token

Excluding Views

Sometimes views do not require CSRF protection e.g. they do not submit sensitive information back to the IdP.  Other times, views are involved in user flows that can not meaningfully benefit from the synchroniser token CSRF protection pattern employed by the IdP e.g. an external authentication servlet (see CSRF FlowExecutionListener testing for External Authentication). Such views can be excluded by annotating the view-state with a CSRF excluded metadata attribute in the flow XML configuration file. For example, to exclude an external authentication servlet:


<view-state id="ExternalTransfer" view="externalRedirect:#{T(net.shibboleth.idp.authn.ExternalAuthentication).getExternalRedirect(flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.External.externalAuthnPathStrategy').apply(opensamlProfileRequestContext), flowExecutionContext.getKey().toString())}">
        <attribute name="csrf_excluded" value="true" type="boolean"/> <!-- excludes attribute here -->
  ...
</view-state>

It is recommended the following views (listed by view-state ID, of which there maybe more than one definition) are excluded:

ViewStateID
ExternalTransfer
RunSPNEGO
ExpiringPassword
DisplayExpiringPasswordView
LogoutPropagateView

ShowServiceLogoutView

LogoutView

It is recommended the following view-states are not excluded.

ViewStateIDview template (that needs CSRF token embedded)
DisplayUsernamePasswordPagelogin.vm
LocalStorageReadclient-storage-read.vm, read.vm
LocalStorageWritelocal-storage-write.vm, write.vm
DisplayTermsOfUsePageterms-of-user.vm
DisplayAttributeReleasePageattribute-release.vm
PromptForPasswordsunlock-keys.vm
ImpersonateViewimpersonate.vm
DisplayDuoWebViewduo.vm


Changes To Flows

To cleanly handle the InvalidCSRFTokenException the CSRFFlowExecutionListener throws, a global on-exception transition must be registered in appropriate flows, in addition to a corresponding action/end state.

In subflows, this can be added to the list of error events that are reflected back to the parent flow. For example, in the authn-abstract-flow.xml:

<end-state id="InvalidCSRFToken" />

 <global-transitions>
   <transition on-exception="net.shibboleth.idp.ui.csrf.InvalidCSRFTokenException" to="InvalidCSRFToken" /> 
<transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" /> <!-- must be declared before this transition -->
   <transition on="InvalidCSRFToken" to="InvalidCSRFToken" />
   ...

In abstract parent flows, this can be added to the global-transitions transitioning to a new action state e.g.

 <!-- action state to set the InvalidCSRFToken event and proceed to HandleError -->
	<action-state id="InvalidCSRFToken">
		<evaluate expression="'InvalidCSRFToken'" />
		<transition to="HandleError"/>
	</action-state>

<!-- Default is to turn non-proceed events into an error. -->
    <global-transitions>
    	<transition  on-exception="net.shibboleth.idp.ui.csrf.InvalidCSRFTokenException" to="InvalidCSRFToken"/> <!--HERE-->
        <transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" />
        <transition on="#{!'proceed'.equals(currentEvent.id)}" to="HandleError" />
    </global-transitions>

Note, if you do not do this, a generic uncaught exception error will be shown. In addition, the InvalidCSRFTokenException must be declared before the more generic RuntimeException.

View Changes

HTML Forms of protected views must include a hidden input field with both the anti-CSRF token ( value attribute), and the HTTP parameter name (name attribute). Putting it inside a Velocity IF conditional prevents the input from being rendered if CSRF protection is disabled.

 <form action="$flowExecutionUrl" method="post">            
            #if (${csrfToken})
            	<input type="hidden" name="${csrfToken.parameterName}" value="${csrfToken.token}" id="csrf_token">
			#end
	...
</form>


This will then be rendered, for example, in the output as:


IFrame Usage


iframes that invoke transitions on the IdP also require CSRF tokens (even as GET requests as SWF does not differentiate between GET and POST requests to initiate or resume flows). These can be added with conditional logic e.g.

<iframe style="display:none" src="$flowExecutionUrl&_eventId=proceed#if($csrfToken)&${csrfToken.parameterName}=${csrfToken.token}#{else}#end"></iframe>

Token security

This will expose the CSRF token in browser network inspectors and possibly server logs. However, it will not display in the navigation bar, and should not appear in browser history/bookmarks. In addition, a new token is generated for every GET request to a view, greatly limiting their reply value. 


Error View Message Customisation

The default error view can then be customised to show a more appropriate error message on an InvalidCSRFToken event. For example, in system/messages/messages.properties:

InvalidCSRFToken = invalid-csrf-token
...
invalid-csrf-token.title= Login Failed
invalid-csrf-token.message= CSRF token verification failed.