...
Login forms can be susceptible to CSRF[2] attacks. In these cases the attacker attempts to trick the victim into login into logging in as themselves (the attacker). The attacker could then:
- Track a victims activity because it is happening in their own session.
- Trick the victim into entering sensitive information into an attackers session/account e.g. bank account details etc.
Appendix A describes an example Login CSRF attack on the IdP.
...
Add a cryptographically secure anti-csrf token to the request context viewScope
, on-entry
to any view-state. Then check the returned token (in the HTTP request) matches that stored in the viewScope
on proceed
a proceed event/
transition. If the token is invalid, prevent execution of the transition and re-render the view, else proceed.
...
- per-view (as on-entry, per request if on-render) synchroniser token - marginally safer than per-session or conversation etc. but more expensive. Although with the IdP only typically having one view a few views per flow, the efficiency/safety tradeoff is probably less of an issue.
- the token only needs to be stored for the lifetime it is required e.g. in the viewscopeof the view state.
- the token is only generated on view states, and is not generated when not needed e.g. when using a previous AuthenticationResult.
...
- 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 ofjava-support, or idp-ui
?
- packaged inside
- 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 ofjava-support, or idp-ui
?
- packaged inside
- A
CsrfTokenManager
to provide helper methods for generating and validating anti-csrf tokens .- packaged inside
idp-session-impl
. Although this maybe better inside ofjava-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.
- contains predicate logic for token validation, but is not a Predicate type.
- packaged inside
- A
ValidateCsrfToken
action class. Extracts the CsrfToken from therequestContext
viewScope
and the CSRF token String value from the HTTP request. Delegates to theCsrfTokenManager
to compare them, if equal (String comparison) signals a ‘success’ event, if different signals a ‘failure’ event. Note here, as this action is (see later sections) nested within a transaction, any event with ID other than ‘success’, ‘yes’, or ‘true’ will prevent the transaction from executing.- is-a
AbstractProfileAction
, is not anAbstractValidationAction
- it does not produce anAuthenticationResult
.
- is-a
...
The anti-csrf token is generated on-entry
(or on-render) to a view state, and placed inside the SWF viewScope. For example in the DisplayUsernamePasswordPage view-state of the authn-password-flow.xml:
Code Block |
---|
<on-entry> <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CsrfTokenManager').generateCsrfToken()" result="viewScope.csrfToken" /> </on-entry> |
Still inside the DisplayUsernamePasswordPage view-state, the anti-csrf token stored in the viewScope is then must be compared to that returned as a HTTP parameter from the viewclient. This needs to happen (because the token is bound to the viewScope) inside the view-state
before the proceed
transition is executed and the state exited. Hence, a ValidateCsrfToken
action is nested inside the proceed
transition:
...
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:
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 be turned on/off as 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 ofjava-support, or idp-ui
?
- packaged inside
- 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 ofjava-support, or idp-ui
?
- packaged inside
- A
CsrfTokenManager
to provide helper methods for generating and validating anti-csrf tokens .- packaged inside
idp-session-impl
. Although this maybe better inside ofjava-support, or idp-ui
? - uses a configurable
SecureRandomIdentifierGenerationStrategy
to generate anti-csrf tokens - even though suitable, maybe an abuse of its definition. - currently fixed to a single implementation.
- contains predicate logic for token validation, but is not a Predicate type.
- packaged inside
- 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 ofFlowExecutionException
, 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:
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
:
Code Block |
---|
InvalidCsrfToken = invalid-csrf-token ... invalid-csrf-token.title= Login Failed invalid-csrf-token.message= CSRF token verification failed ... </form> |
This will then be rendered, for example, in the output as:
Option 2 - FlowExecutionListener Injected ViewScoped CSRF Token
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
, namelySimpleCsrfToken
. 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 theCsrfUIContext
, generate aCsrfToken
using aSecureRandomIdentifierGenerationStrategy
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
...
To correctly transition the IdPEventIds.INVALID_CSRF_TOKEN
eventID event ID to an error page, a transition needs to be placed in the action-state where the validation profile actions action is evaluated (see next section), and a global transition needs to be defined that maps to an end-state.
...
Changes to password authn flow
The anti-csrf CSRF token is taken from the CsrfUIContext
and placed inside the SWF viewScope on-entry to render of the DisplayUsernamePasswordPage view-state (or any view-state as required):
...
Note, as the profile request context is already passed into the view, the CsrfUIContext and then token could be extracted by the view templating engine e.g. as is the case with the RelyingPartyContext in the login view.
...
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
:
...
- The options above are not mutually exclusive. For example, a flow execution lister could be used to inject tokens into the SWF viewScope, while a profile action could be used to check it. Similarly, a profile action could be used to initialise a CSRF Context, an SWF action be used to add it to the viewScope, and a flow execution listener used to check it on ‘proceed’ transition.
Anchor | ||||
---|---|---|---|---|
|
Login CSRF Example
The following HTML form is an example of one which can be used by an attacker to post a username and password to the IdP. This is essentially that which is used by the IdP's login view, but pre-populated with a username and password of the attacker - whom must have an active account with the IdP's authentication source.
...