Work In Progress |
Here we look at three options for integrating a synchroniser token pattern[1] Cross Site Request Forgery (CSRF) defence into the IdP's Password authentication flow (although it could be configured more widely e.g. other authn views or admin views).
Login forms can be susceptible to CSRF[2] attacks. In these cases the attacker attempts to trick the victim into logging in as themselves (the attacker). The attacker could then:
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 a proceed event/
transition. If the token is invalid, prevent execution of the transition and re-render the view, else proceed.
Advantages of Approach:
Disadvantages of Approach:
Possible high level implementation issues
AbstractProfileAction
. Which under every other instance, produces a meaningful SWF event that triggers a new state via a transition - this therefore seems like a missue of this type of action, or its contract.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-viewscope
).
CsrfToken
API interface . This defines token implementations that return token values and HTTP parameter names.idp-session-api
. Although this maybe better inside of java-support, or idp-ui
?SimpleCsrfToken
. Encapsulating both token value and HTTP parameter name as a non-empty Strings. Has no business logic.idp-session-api
. Although this maybe better inside of java-support, or idp-ui
?CsrfTokenManager
to provide helper methods for generating and validating anti-csrf tokens .idp-session-impl
. Although this maybe better inside of java-support, or idp-ui
?SecureRandomIdentifierGenerationStrategy
to generate anti-csrf tokens - even though suitable, is generating a token and not an identifier.ValidateCsrfToken
action class. Extracts the CsrfToken from the requestContext
viewScope
and the CSRF token String value from the HTTP request. Delegates to the CsrfTokenManager
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.AbstractProfileAction
, is not an AbstractValidationAction
- it does not produce an AuthenticationResult
.The CsrfTokenManager is added to the set of global system beans (global-system.xml):
<!-- 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 ValidateCsrfToken
prototype bean must be declared where it is used (e.g. password-authn-beans.xml, or perhaps more globally when used across flow types)
<bean id="ValidateCsrfToken" class="net.shibboleth.idp.session.impl.ValidateCsrfToken" scope="prototype" p:httpServletRequest-ref="shibboleth.HttpServletRequest" p:csrfTokenManager-ref="shibboleth.CsrfTokenManager" /> |
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:
<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 must be compared to that returned as a HTTP parameter from the client. 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:
<transition on="proceed" to="ExtractUsernamePasswordFromFormRequest"> <evaluate expression="ValidateCsrfToken"/> <!-- CSRF Validation Action --> <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).setAttemptedFlow(thisFlow)" /> </transition> |
If token validation fails, SWF will not execute the transition, and the view will be re-rendered with appropriate error message.
Any HTML Forms must include a hidden input field with both the anti-csrf token ( value
attribute), and the HTTP parameter name (name
attribute).
<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:
Implementation of this has moved to here.
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.
Advantages of Approach:
Disadvantages of Approach:
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
).
All new classes have been packaged inside the idp-ui
module.
CsrfToken
interface. This defines token implementations that return token values and HTTP parameter names.CsrfToken
, namely SimpleCsrfToken
. Encapsulating both token value and HTTP parameter name as a non-empty String. Has no business logic.CsrfUIContext
.InitializeCsrfUIContext
, to initlialise the CsrfUIContext
, generate a CsrfToken
using a SecureRandomIdentifierGenerationStrategy
and configured HTTP Parameter name, and set it onto the context.ValidateCsrfToken
action class. Takes the CsrfToken from the CsrfUIContext and compares it against that extracted from the HTTP request.IdPEventIds.INVALID_CSRF_TOKEN
eventID if token validation fails, else return nothing (null) to allow other actions to proceed.Signify an InvalidCsrfToken
event is ‘local’ by adding it to the shibboleth.LocalEventMap
in errors.xml
.
<entry key="InvalidCSRFToken" value="false" /> |
The InitializeCsrfUIContext
prototype bean is declared in the authn-beans.xml
so it can be used to initalise the CsrfUIContext in the AuthenticationSetup action-state of the authn-flow.xml
. e.g. in authn-beans.xml
<bean id="InitializeCsrfUIContext" class="net.shibboleth.idp.ui.csrf.impl.InitializeCsrfUIContext" scope="prototype" p:csrfParameterName="csrf_token"/> |
The ValidateCsrfToken prototype bean must be declared where it is used (e.g. password-authn-beans.xml)
<bean id="ValidateCsrfToken" class="net.shibboleth.idp.ui.csrf.impl.ValidateCsrfToken" scope="prototype" p:httpServletRequest-ref="shibboleth.HttpServletRequest"/> |
The InitializeCsrfUIContext
action is added to the AuthenticationSetup action-state in the authn-flow.xml
.
<action-state id="AuthenticationSetup"> <evaluate expression="PopulateAuthenticationContext" /> <evaluate expression="PopulateSessionContext" /> <evaluate expression="SetRPUIInformation" /> <evaluate expression="InitializeCsrfUIContext"/> <evaluate expression="'proceed'" /> <transition on="proceed" to="TestForSession" /> </action-state> |
To correctly transition the IdPEventIds.INVALID_CSRF_TOKEN
event ID to an error page, a transition needs to be placed in the action-state where the validation profile action is evaluated (see next section), and a global transition needs to be defined that maps to an end-state.
For example, in the authn-abstract-flow.xml
:
<end-state id="InvalidCsrfToken" /> <global-transitions> ... <transition on="InvalidCsrfToken" to="InvalidCsrfToken" /> ... </global-transitions> |
The CSRF token is taken from the CsrfUIContext
and placed inside the SWF viewScope on-render of the DisplayUsernamePasswordPage view-state (or any view-state as required):
<view-state id="DisplayUsernamePasswordPage" view="login"> <on-render> ... <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.ui.csrf.context.CsrfUIContext)).getCsrfToken()" result="viewScope.csrfToken" /> </on-render> ... </view-state> |
Note, as the profile request context is already passed into the view, the CsrfUIContext and token could be extracted by the view templating engine e.g. as is the case with the RelyingPartyContext in the login view.
The CSRF token in the HTTP request is validated against that stored in the CSRF UI context using the ValidateCsrfToken
profile action. This action could, as with option 1, be placed inside the ‘proceed’ transition. However, in order to cleanly transition to an end-state and default error view, it has been placed outside the view-state, and inside an action-state - in this case the ExtractUsernamePasswordFromFormRequest
action-state.
<action-state id="ExtractUsernamePasswordFromFormRequest"> <evaluate expression="ValidateCsrfToken" /> <evaluate expression="ExtractUsernamePasswordFromFormRequest" /> <evaluate expression="'proceed'" /> <transition on="InvalidCSRFToken" to="InvalidCSRFToken" /> <!-- Let the validate action handle any problems later. --> <transition to="ValidateUsernamePassword" /> </action-state> |
An additional transition is required to handle the invalid CSRF token event - this can not be made global in this case, as the *->ValidateUsernamePassword
transition would catch it first.
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.
Any HTML Forms must include a hidden input field with both the anti-csrf token ( value
attribute), and the HTTP parameter name (name
attribute).
<form action="$flowExecutionUrl" method="post"> <input type="hidden" name="${csrfToken.parameterName}" value="${csrfToken.token}" id="csrf_token"> ... |
This will then be rendered, for example, in the output as:
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. |
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.
<form action="https://localhost:8443/idp/profile/SAML2/Unsolicited/SSO?execution=e1s2" method="post"> <input id="username" name="j_username" type="text" value="jdoe"/> <input id="password" name="j_password" type="password" value="PASSWORD"/> <button type="submit" name="_eventId_proceed">Login</button> </form> |
Such a form could be auto-submitted without the user knowing using JavaScript.
For the Password IdP flow, the victim using a web browser user agent must:
Risk (low):
2 - https://seclab.stanford.edu/websec/csrf/csrf.pdf