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:
idp.properties.
Disadvantages of Approach:
InvalidCSRFTokenException
. Whereas nearly all other error states in the IdP are encoded directly as SWF Events.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
).
All new classes and test classes have been packaged inside idp-ui.
Token
API interface . This defines token implementations that return token values and HTTP parameter names.SimpleCSRFToken
. Encapsulating both token value and HTTP parameter name as a non-empty Strings. Has no business logic.TokenManager
to provide helper methods for generating and validating CSRF tokens .SecureRandomIdentifierGenerationStrategy
to generate anti-csrf tokens - even though suitable, is used to generate a token and not an identifier..CSRFTokenFlowExecutionListener
class.CSRFTokenFlowExecutionListener
which reacts to SWF lifecycle events.BaseCSRFTokenPredicate - Encodes common logic between both previous predicates.
InvalidCSRFTokenException
, subtype of FlowExecutionException
, thrown when an invalid CSRF token is found.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" /> |
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 |
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.
ViewStateID | view template (that needs CSRF token embedded) |
---|---|
DisplayUsernamePasswordPage | login.vm |
LocalStorageRead | client-storage-read.vm, read.vm |
LocalStorageWrite | local-storage-write.vm, write.vm |
DisplayTermsOfUsePage | terms-of-user.vm |
DisplayAttributeReleasePage | attribute-release.vm |
PromptForPasswords | unlock-keys.vm |
ImpersonateView | impersonate.vm |
DisplayDuoWebView | duo.vm |
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.
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:
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> |
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. |
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. |