Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Option 2 - FlowExecutionListener Injected ViewScoped CSRF Token

Overview

Similar to option 1, but inserting and validating anti-CSRF tokens using a SWF FlowExecutionListener. In summary, using a listener 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 by configuration.
  • CSRF defence could easily be controlled (turned on/off) by a property in idp.properties - not implemented.
  • 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.
  • only checks CSRF token validity on ‘proceed’ events from view-states, although this can be configured by a condition Predicate. It is possible therefore to bypass this e.g. if an event ID other than ‘proceed’ was used to transition out of a view-state. As this is possible, it needs checking.
    • In addition, how this interacts with external servlets and MFA flows needs further investigation.

Implementation

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

New Classes

  • A CsrfToken API interface . This defines token implementations that return token values and HTTP parameter names.
    • packaged inside idp-session-api. Although this maybe better inside of java-support, or idp-ui?
  • A default implementation of CsrfToken, namely SimpleCsrfToken . Encapsulating both token value and HTTP parameter name as a non-empty Strings. Has no business logic.
    • packaged inside idp-session-api. Although this maybe better inside of java-support, or idp-ui?
  • A CsrfTokenManager to provide helper methods for generating and validating CSRF tokens .
    • packaged inside idp-session-impl. Although this maybe better inside of java-support, or idp-ui?
    • uses a configurable SecureRandomIdentifierGenerationStrategy to generate anti-csrf tokens - even though suitable, is generating a token and not an identifier..
    • currently fixed to a single implementation.
  • 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 ‘proceed’ event occurs.
    • Can be configured to ignore or exclude certain view-states by ID.
  • An InvalidCsrfTokenException, subtype of FlowExecutionException, thrown when an invalid CSRF token is found.

Global config changes

The CsrfTokenManager is added to the set of global system beans (global-system.xml):

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

The flow listener is added to the flowExecutor in the webflow-config.xml:

Code Block
 <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"/>  <!--NEW-->       
        </webflow:flow-execution-listeners>
    </webflow:flow-executor>

 <bean id="csrfTokenFlowExecutionListener"
          class="net.shibboleth.idp.session.impl.CsrfTokenFlowExecutionListener" p:csrfTokenManager-ref="shibboleth.CsrfTokenManager">
      		<property name="excludedViewstateIds">
      			<list>
      				<value>LocalStorageRead</value>
      				<value>LocalStorageWrite</value>
      			</list>
      		</property>
      </bean>

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

Code Block
<entry key="InvalidCsrfToken" value="false" />

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 end-state. For example, in the authn-abstract-flow.xml:

Code Block
<end-state id="InvalidCsrfToken" />

 <global-transitions>
    <transition on-exception="net.shibboleth.idp.session.InvalidCsrfTokenException" to="InvalidCsrfToken" /> 
   <transition on="InvalidCsrfToken" to="InvalidCsrfToken" />
   ...

Note, if you do not do this, a generic uncaught exception error will be shown.

View Changes

Any HTML Forms must include a hidden input field with both the anti-csrf token ( value attribute), and the HTTP parameter name (name attribute).

Code Block
 <form action="$flowExecutionUrl" method="post">            
             <input type="hidden" name="${csrfToken.parameterName}" value="${csrfToken.token}" id="csrf_token">
	...
</form>

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

Image Removed

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:

...


...

Implementation of this has moved to here.


Option 3 - Add a CSRF token to a new CSRFUI context

Overview

Add a cryptographically secure anti-csrf token to a CSRF subcontext of the ProfileRequestContext (initialised early in the authentication flow at present). The returned CSRF token from a view-state in the HTTP request is checked against that stored in the context by a suitable profile action. The conversation ends and a suitable error message is displayed if an invalid CSRF token is found.

...

  • per conversation token is marginally less safe than per view - although this does not seem significant for the IdP, as it has a limited number of view-states, and limited conversation durations.
  • initialisation and validation actions would need to be configured/integrated slightly differently for different flows e.g.the admin flow .
    • has to be incorporated, where needed, into each flow with view-state.
    • not clear at this stage where to place the CSRF validation action. In its own action-state, or inside another etc.
  • no guarantees this approach will be adhered too if custom flows with custom views are created.
  • small overhead of creating/managing a context and holding onto state.
    • Although a single token can safely be used for a number of view-states if required.

Implementation 

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

New Classes

All new classes have been packaged inside the idp-ui module.

  • A CsrfToken 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 String. Has no business logic.
  • A new IdP context for holding per conversation CSRF tokens, CsrfUIContext.
  • An action, InitializeCsrfUIContext, to initlialise the CsrfUIContext, generate a CsrfToken using a SecureRandomIdentifierGenerationStrategy and configured HTTP Parameter name, and set it onto the context.
  • A ValidateCsrfToken action class. Takes the CsrfToken from the CsrfUIContext and compares it against that extracted from the HTTP request.
    • is-a AbstractProfileAction, is not an AbstractValidationAction - it does not produce an AuthenticationResult.
    • return a new IdPEventIds.INVALID_CSRF_TOKEN eventID if token validation fails, else return nothing (null) to allow other actions to proceed.
    • map that eventID to an end-state and the default error view with custom error message.


Global config changes

Signify an InvalidCsrfToken event is ‘local’ by adding it to the shibboleth.LocalEventMap in errors.xml.

...

Code Block
    <bean id="ValidateCsrfToken" 
      	class="net.shibboleth.idp.ui.csrf.impl.ValidateCsrfToken" scope="prototype"
    	p:httpServletRequest-ref="shibboleth.HttpServletRequest"/>


Changes To Flows

CsrfUIContext Initialisation

...

Importantly, it needs more thought as to which action-state to use, and whether modification to the flow (e.g. new action-state) would be more appropriate.

View Changes

Any HTML Forms must include a hidden input field with both the anti-csrf token ( value attribute), and the HTTP parameter name (name attribute).

...

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


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:

...