IdentityPython / pysaml2

Python implementation of SAML2
Apache License 2.0
555 stars 422 forks source link

Status code checking in unsuccessful responses #943

Open knagy opened 10 months ago

knagy commented 10 months ago

Hi,

According to this documentation the second level of StatusCode inside the StatusCode tag is optional.

Currently pysaml2 is only checking the value of the second level of the StatusCode and throws an exception based on that value. Because of that it throws a general saml2.response.StatusError exception instead of a more specific one when only one level of StatusCode is present.

Possible Solution

This part could be something like this:

if status.status_code.status_code:
    err_code = status.status_code.status_code.value
elif status.status_code:
    err_code = status.status_code.value
else:
    err_code = None

Steps to Reproduce

A small test script to reproduce the behaviour. This currently throws a saml2.response.StatusError exception, but I think it should throw a saml2.response.StatusResponder exception instead.

from datetime import datetime

import pytz
from saml2 import BINDING_URI
from saml2.client import Saml2Client
from saml2.config import Config

now = datetime.now(pytz.UTC).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
response = f'''<Response Destination="http://localhost/login" ID="fooId" IssueInstant="{now}" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:protocol">
    <Status>
        <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Responder"/>
        <StatusMessage>Failed to authenticate user.</StatusMessage>
    </Status>
</Response>'''

config = Config()
config.load({
    'entityid': 'foo',
    'service': {
        'sp': {
            'endpoints': {
                'assertion_consumer_service': [('http://localhost/login', BINDING_URI)]
            },
            'allow_unsolicited': True,
            'want_response_signed': False,
        },
    },
})
client = Saml2Client(config=config)
client.parse_authn_request_response(response, BINDING_URI)