Verifying solicited responses in PySAML

Overview

In most example around the web, PySAML is used with the configuration attribute allow_unsolicited=True. This disables a, even though optional, security feature of SAML, allowing unsolicited responses to be received and considered valid. Obviously this should not be done in production implementations.

In this article I will show you what solicited responses are and how you can validate that a response is solicited in PySAML.

Solicited and unsolicited responses

So what is a solicited or unsolicited response? Simply put, a solicited response is a response you asked for, as opposed to a unsolicited response that arrives without you asking for it.

In the context of SAML, a un solicited response is a SAML authentication response received from the IdP without the SP first having sent an authentication request.

This is allowed in SAML and is also known as IdP initiated authentication, as opposed to SP initiated authentication, when the SP initiated authenticate by sending the authentication request.

In order to keep track of this in SAML, when the SP sends a request to the IdP, SP saves the Id of the request. When the IdP send the authentication response back includes the id for the request it responds to in a an attribute, InResponseTo

1<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
2    ID="de8b79c37092487aa7f9c5cb6c5a542a" 
3    InResponseTo="id-5PGmWNaR9dLIoTGOf"
4    Version="2.0"
5    IssueInstant="2023-12-02T13:17:14.765Z" 
6    Destination="http://localhost:5000/acs">
7...
8</samlp:Response>

When the SP receives the response it verifies that the InResponseTo id in the response is the same as the request it sent, ensuring that the response is actually an response to the request that was sent.

This is a topic a dive deep into in my book on SAML.

Code

The prepare_for_authenticate function in PySAML client returns in addition to the authentication request also the request id for this purpose. Lets store this as key in a dictionary. The value of the entry should be the target for the user was heading to before starting authentication. I the case below we just hard code it to simplify. The dictionary is stored on the HTTP session of the user

1request_id, authn_request = sp.prepare_for_authenticate()
2session['saml2_outstanding_requests'] = {request_id: "/success"}

Next when we receive the response we parse it using the parse_authn_request_response function of the PySAML client. This function also verifies the InResponseTo id and takes the dictionary we made earlier as a argument of ids of sent requests.

1authn_response = sp.parse_authn_request_response(request.form['SAMLResponse'], saml_config.BINDING_HTTP_POST, session['saml2_outstanding_requests'])

Get it on Github and try it out!

The full sample code is available on Github at https://github.com/rasmusson/pysaml-samples under solicited_responses/

Just clone it, run it, go nuts!

1apt-get install xmlsec1
2pip install flask
3pip install pysaml2
4
5git clone https://github.com/rasmusson/pysaml-samples.git
6cd intro
7
8python sample.py