Repository: IdentityPython/pysaml2

Issue:
Response issuer can be None
<!--- Provide a general summary of the issue in the Title above -->
The SAML spec [sstc-saml-core-errata-2.0-wd-07.pdf](https://github.com/IdentityPython/pysaml2/files/6429604/sstc-saml-core-errata-2.0-wd-07.pdf) states that Issuer is optional on a response object. Specifically:
- 3.3.3 Element <Response> line 1999 --> `It has the complex type ResponseType, which extends StatusResponseType`
- 3.2.2 Complex Type StatusResponseType, line 1633 --> `<saml:Issuer> [Optional]`

## Code Version
<!-- What version and/or branch of the code are you running? -->
master

## Expected Behavior
<!--- Tell us what should happen -->
Response with None as issuer should not throw exception

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
Response with None as issuer throws exception

## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
PR incoming which adds support for this case

## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1. run the attached reproduction.py in `tests/` (since it depends on the conf files in there)
2. get traceback:

```
Traceback (most recent call last):
  File "pysaml2/tests/reproduction.py", line 47, in <module>
    run()
  File "pysaml2/tests/reproduction.py", line 43, in run
    resp.issuer()
  File "pysaml2/src/saml2/response.py", line 438, in issuer
    return self.response.issuer.text.strip()
AttributeError: 'NoneType' object has no attribute 'text'
```


Discussion:

Patch:
diff --git a/src/saml2/response.py b/src/saml2/response.py
index 9d4021ee..21abae9a 100644
--- a/src/saml2/response.py
+++ b/src/saml2/response.py
@@ -435,7 +435,12 @@ class StatusResponse(object):
         self.response = mold.response
 
     def issuer(self):
-        return self.response.issuer.text.strip()
+        issuer_value = (
+            self.response.issuer.text
+            if self.response.issuer is not None
+            else ""
+        ).strip()
+        return issuer_value
 
 
 class LogoutResponse(StatusResponse):
@@ -1116,7 +1121,7 @@ class AuthnResponse(StatusResponse):
             raise StatusInvalidAuthnResponseStatement(
                 "The Authn Response Statement is not valid"
             )
-        
+
     def __str__(self):
         return self.xmlstr
 


Test patch:
diff --git a/tests/test_41_response.py b/tests/test_41_response.py
index 9f1dab6b..9380ae10 100644
--- a/tests/test_41_response.py
+++ b/tests/test_41_response.py
@@ -48,28 +48,38 @@ class TestResponse:
 
             self._resp_ = server.create_authn_response(
                 IDENTITY,
-                "id12",  # in_response_to
-                "http://lingon.catalogix.se:8087/",
-                # consumer_url
-                "urn:mace:example.com:saml:roland:sp",
-                # sp_entity_id
-                name_id=name_id)
+                in_response_to="id12",
+                destination="http://lingon.catalogix.se:8087/",
+                sp_entity_id="urn:mace:example.com:saml:roland:sp",
+                name_id=name_id,
+            )
 
             self._sign_resp_ = server.create_authn_response(
                 IDENTITY,
-                "id12",  # in_response_to
-                "http://lingon.catalogix.se:8087/",  # consumer_url
-                "urn:mace:example.com:saml:roland:sp",  # sp_entity_id
+                in_response_to="id12",
+                destination="http://lingon.catalogix.se:8087/",
+                sp_entity_id="urn:mace:example.com:saml:roland:sp",
                 name_id=name_id,
-                sign_assertion=True)
+                sign_assertion=True,
+            )
 
             self._resp_authn = server.create_authn_response(
                 IDENTITY,
-                "id12",  # in_response_to
-                "http://lingon.catalogix.se:8087/",  # consumer_url
-                "urn:mace:example.com:saml:roland:sp",  # sp_entity_id
+                in_response_to="id12",
+                destination="http://lingon.catalogix.se:8087/",
+                sp_entity_id="urn:mace:example.com:saml:roland:sp",
+                name_id=name_id,
+                authn=AUTHN,
+            )
+
+            self._resp_issuer_none = server.create_authn_response(
+                IDENTITY,
+                in_response_to="id12",
+                destination="http://lingon.catalogix.se:8087/",
+                sp_entity_id="urn:mace:example.com:saml:roland:sp",
                 name_id=name_id,
-                authn=AUTHN)
+            )
+            self._resp_issuer_none.issuer = None
 
             conf = config.SPConfig()
             conf.load_file("server_conf")
@@ -77,27 +87,45 @@ class TestResponse:
 
     def test_1(self):
         xml_response = ("%s" % (self._resp_,))
-        resp = response_factory(xml_response, self.conf,
-                                return_addrs=[
-                                    "http://lingon.catalogix.se:8087/"],
-                                outstanding_queries={
-                                    "id12": "http://localhost:8088/sso"},
-                                timeslack=TIMESLACK, decode=False)
+        resp = response_factory(
+            xml_response, self.conf,
+            return_addrs=["http://lingon.catalogix.se:8087/"],
+            outstanding_queries={"id12": "http://localhost:8088/sso"},
+            timeslack=TIMESLACK,
+            decode=False,
+        )
 
         assert isinstance(resp, StatusResponse)
         assert isinstance(resp, AuthnResponse)
 
     def test_2(self):
         xml_response = self._sign_resp_
-        resp = response_factory(xml_response, self.conf,
-                                return_addrs=[
-                                    "http://lingon.catalogix.se:8087/"],
-                                outstanding_queries={
-                                    "id12": "http://localhost:8088/sso"},
-                                timeslack=TIMESLACK, decode=False)
+        resp = response_factory(
+            xml_response,
+            self.conf,
+            return_addrs=["http://lingon.catalogix.se:8087/"],
+            outstanding_queries={"id12": "http://localhost:8088/sso"},
+            timeslack=TIMESLACK,
+            decode=False,
+        )
+
+        assert isinstance(resp, StatusResponse)
+        assert isinstance(resp, AuthnResponse)
+
+    def test_issuer_none(self):
+        xml_response = ("%s" % (self._resp_issuer_none,))
+        resp = response_factory(
+            xml_response,
+            self.conf,
+            return_addrs=["http://lingon.catalogix.se:8087/"],
+            outstanding_queries={"id12": "http://localhost:8088/sso"},
+            timeslack=TIMESLACK,
+            decode=False,
+        )
 
         assert isinstance(resp, StatusResponse)
         assert isinstance(resp, AuthnResponse)
+        assert resp.issuer() == ""
 
     @mock.patch('saml2.time_util.datetime')
     def test_false_sign(self, mock_datetime):