Beispiel #1
0
    def _parse_assertion(self, response: LightResponse, assertion: Element,
                         auth_class_map: Optional[Dict[str, LevelOfAssurance]]) -> None:
        attributes = response.attributes = OrderedDict()
        name_id_elm = assertion.find('./{}/{}'.format(Q_NAMES['saml2:Subject'], Q_NAMES['saml2:NameID']))
        if name_id_elm is not None:
            response.subject = name_id_elm.text
            response.subject_name_id_format = NameIdFormat(name_id_elm.get('Format'))

        attribute_elms = assertion.findall('./{}/{}'.format(Q_NAMES['saml2:AttributeStatement'],
                                                            Q_NAMES['saml2:Attribute']))
        for attribute in attribute_elms:
            attributes[attribute.get('Name')] = [
                elm.text for elm in attribute.findall('./{}'.format(Q_NAMES['saml2:AttributeValue']))]

        stm_elm = assertion.find('./{}'.format(Q_NAMES['saml2:AuthnStatement']))
        if stm_elm is not None:
            locality_elm = stm_elm.find('./{}'.format(Q_NAMES['saml2:SubjectLocality']))
            if locality_elm is not None:
                response.ip_address = locality_elm.get('Address')

            authn_class_elm = stm_elm.find('./{}/{}'.format(Q_NAMES['saml2:AuthnContext'],
                                                            Q_NAMES['saml2:AuthnContextClassRef']))
            if authn_class_elm is not None:
                try:
                    response.level_of_assurance = LevelOfAssurance(authn_class_elm.text)
                except ValueError:
                    if auth_class_map and authn_class_elm.text in auth_class_map:
                        response.level_of_assurance = auth_class_map[authn_class_elm.text]
                    else:
                        raise ValidationError({get_element_path(authn_class_elm): authn_class_elm.text}) from None
 def test_create_light_response_strip_prefix_needed(self):
     response = LightResponse(**self.get_light_response_data('CZ/CZ/ff70c9dd-6a05-4068-aaa2-b57be4f328e9'))
     self.maxDiff = None
     with patch.object(IdentityProviderResponseView, 'create_light_response', return_value=response):
         view = CzNiaResponseView()
         self.assertEqual(view.create_light_response(),
                          LightResponse(**self.get_light_response_data('ff70c9dd-6a05-4068-aaa2-b57be4f328e9')))
    def test_create_light_response_do_not_strip_prefix(self):
        data = self.get_light_response_data('CZ/CZ/ff70c9dd-6a05-4068-aaa2-b57be4f328e9')
        response = LightResponse(**deepcopy(data))

        with patch.object(IdentityProviderResponseView, 'create_light_response', return_value=response):
            view = CzNiaResponseView()
            self.assertEqual(view.create_light_response(), LightResponse(**data))
Beispiel #4
0
    def test_load_xml_with_response_status_failure(self):
        self.maxDiff = None
        response = self.create_response(False)
        with cast(BinaryIO, (DATA_DIR / 'light_response_failure.xml').open('r')) as f:
            data = f.read()

        self.assertEqual(LightResponse.load_xml(parse_xml(data)), response)
 def pop_light_response(self, uid: str) -> Optional[LightResponse]:
     """Look up a LightResponse by a unique id and then remove it."""
     data = self.get_cache(self.response_cache_name).get_and_remove(uid)
     LOGGER.debug('Got Light Response from cache: id=%r, data=%s', uid,
                  data)
     return LightResponse().load_xml(
         parse_xml(data)) if data is not None else None
    def test_rewrite_name_id_persistent(self):
        light_response_data = LIGHT_RESPONSE_DICT.copy()
        light_response_data['status'] = Status(**light_response_data['status'])
        light_response_data['subject_name_id_format'] = NameIdFormat.PERSISTENT
        view = IdentityProviderResponseView()
        view.light_response = LightResponse(**light_response_data)
        view.auxiliary_data = {'name_id_format': NameIdFormat.PERSISTENT.value}

        view.rewrite_name_id()
        self.assertEqual(view.light_response.subject_name_id_format, NameIdFormat.PERSISTENT)
        self.assertEqual(view.light_response.subject, 'CZ/CZ/ff70c9dd-6a05-4068-aaa2-b57be4f328e9')
    def test_rewrite_name_id_unspecified_to_transient(self, uuid_mock):
        light_response_data = LIGHT_RESPONSE_DICT.copy()
        light_response_data['status'] = Status(**light_response_data['status'])
        light_response_data['subject_name_id_format'] = NameIdFormat.UNSPECIFIED
        view = IdentityProviderResponseView()
        view.light_response = LightResponse(**light_response_data)
        view.auxiliary_data = {'name_id_format': NameIdFormat.TRANSIENT.value}

        view.rewrite_name_id()
        self.assertEqual(view.light_response.subject_name_id_format, NameIdFormat.TRANSIENT)
        self.assertEqual(view.light_response.subject, '0uuid4')
    def test_rewrite_name_id_failure(self):
        light_response_data = FAILED_LIGHT_RESPONSE_DICT.copy()
        light_response_data['status'] = Status(**light_response_data['status'])
        light_response_data['subject_name_id_format'] = NameIdFormat.PERSISTENT
        view = IdentityProviderResponseView()
        view.light_response = LightResponse(**light_response_data)
        view.auxiliary_data = {'name_id_format': NameIdFormat.TRANSIENT.value}

        view.rewrite_name_id()
        self.assertEqual(view.light_response.subject_name_id_format, NameIdFormat.PERSISTENT)
        self.assertIsNone(view.light_response.subject)
    def test_post_success(self, uuid_mock: MagicMock):
        with cast(BinaryIO, (DATA_DIR / 'saml_response.xml').open('rb')) as f:
            saml_request_xml = f.read()

        response = self.client.post(self.url, {'SAMLResponse': b64encode(saml_request_xml).decode('ascii'),
                                               'RelayState': 'relay123'})

        # Context
        self.assertIn('token', response.context)
        self.assertEqual(response.context['token_parameter'], 'test_token')
        self.assertEqual(response.context['eidas_url'], 'https://test.example.net/SpecificProxyServiceResponse')
        self.assertEqual(response.context['error'], None)

        # Token
        encoded_token = response.context['token']
        token = LightToken.decode(encoded_token, 'sha256', 'response-token-secret')
        self.assertEqual(token.id, 'T0uuid4')
        self.assertEqual(token.issuer, 'response-token-issuer')
        self.assertEqual(token.created, datetime(2017, 12, 11, 14, 12, 5))

        # Storing light response
        light_response_data = LIGHT_RESPONSE_DICT.copy()
        light_response_data.update({
            'status': Status(**light_response_data['status']),
            'id': 'test-saml-response-id',  # Preserved
            'in_response_to_id': 'test-saml-request-id',  # Preserved
            'issuer': 'https://test.example.net/node-proxy-service-response',  # Replaced
        })
        light_response = LightResponse(**light_response_data)
        self.assertEqual(self.client_class_mock.mock_calls, [call(timeout=66)])
        self.assertEqual(self.client_mock.mock_calls,
                         [call.connect('test.example.net', 1234),
                          call.get_cache('test-proxy-service-response-cache'),
                          call.get_cache().put('T0uuid4', dump_xml(light_response.export_xml()).decode('utf-8'))])

        # Rendering
        self.assertContains(response, 'Redirect to eIDAS Node is in progress')
        self.assertContains(response,
                            '<form class="auto-submit" action="https://test.example.net/SpecificProxyServiceResponse"')
        self.assertContains(response, '<input type="hidden" name="test_token" value="{}"'.format(encoded_token))
        self.assertNotIn(b'An error occurred', response.content)
Beispiel #10
0
    def test_put_light_response(self):
        with cast(TextIO, (DATA_DIR / 'light_response.xml').open('r')) as f:
            data = f.read()

        response = LightResponse.load_xml(parse_xml(data))
        self.storage.put_light_response('abc', response)
        self.assertEqual(self.client_class_mock.mock_calls, [call(timeout=33)])
        self.assertEqual(self.client_mock.mock_calls, [
            call.connect(self.HOST, self.PORT),
            call.get_cache(self.RESPONSE_CACHE_NAME),
            call.get_cache().put('abc', data)
        ])
Beispiel #11
0
    def test_pop_light_response_found(self):
        with cast(BinaryIO, (DATA_DIR / 'light_response.xml').open('rb')) as f:
            data = f.read()

        self.cache_mock.get_and_remove.return_value = data.decode('utf-8')
        self.assertEqual(LightResponse.load_xml(parse_xml(data)),
                         self.storage.pop_light_response('abc'))
        self.assertEqual(self.client_class_mock.mock_calls, [call(timeout=33)])
        self.assertEqual(self.client_mock.mock_calls, [
            call.connect(self.HOST, self.PORT),
            call.get_cache(self.RESPONSE_CACHE_NAME),
            call.get_cache().get_and_remove('abc')
        ])
    def test_create_light_token(self, uuid_mock: MagicMock):
        view = IdentityProviderResponseView()
        view.request = self.factory.post(self.url)
        light_response_data = LIGHT_RESPONSE_DICT.copy()
        light_response_data['status'] = Status(**light_response_data['status'])
        view.light_response = LightResponse(**light_response_data)

        token, encoded_token = view.create_light_token('test-token-issuer', 'sha256', 'test-secret')
        self.assertEqual(token.id, 'T0uuid4')
        self.assertEqual(token.issuer, 'test-token-issuer')
        self.assertEqual(token.created, datetime(2017, 12, 11, 16, 12, 5))
        self.assertEqual(token.encode('sha256', 'test-secret').decode('ascii'), encoded_token)
        self.assertEqual(uuid_mock.mock_calls, [call()])
Beispiel #13
0
    def create_light_response(self, auth_class_map: Dict[str, LevelOfAssurance] = None) -> LightResponse:
        """Convert SAML response to light response."""
        response = LightResponse(attributes=OrderedDict())
        root = self.document.getroot()
        if root.tag != Q_NAMES['saml2p:Response']:
            raise ValidationError({
                get_element_path(root): 'Wrong root element: {!r}'.format(root.tag)})

        response.id = root.get('ID')
        response.in_response_to_id = root.get('InResponseTo')
        response.issuer = self.issuer

        response.status = status = Status()
        status_code_elm = root.find('./{}/{}'.format(Q_NAMES['saml2p:Status'], Q_NAMES['saml2p:StatusCode']))
        if status_code_elm is not None:
            sub_status_code_elm = status_code_elm.find('./{}'.format(Q_NAMES['saml2p:StatusCode']))
            status_message_elm = root.find('./{}/{}'.format(Q_NAMES['saml2p:Status'], Q_NAMES['saml2p:StatusMessage']))

            status_code = status_code_elm.get('Value')
            sub_status_code = sub_status_code_elm.get('Value') if sub_status_code_elm is not None else None

            if status_code == SubStatusCode.VERSION_MISMATCH.value:
                # VERSION_MISMATCH is a status code in SAML 2 but a sub status code in Light response!
                status.status_code = StatusCode.REQUESTER
                status.sub_status_code = SubStatusCode.VERSION_MISMATCH
            else:
                status.status_code = StatusCode(status_code)
                try:
                    status.sub_status_code = SubStatusCode(sub_status_code)
                except ValueError:
                    # None or a sub status codes not recognized by eIDAS
                    status.sub_status_code = None

            status.failure = status.status_code != StatusCode.SUCCESS
            if status_message_elm is not None:
                status.status_message = status_message_elm.text

        assertion_elm = self.assertion
        if assertion_elm is not None:
            try:
                self._parse_assertion(response, assertion_elm, auth_class_map)
            except ValidationError as e:
                return LightResponse(
                    id=root.get('ID'),
                    in_response_to_id=root.get('InResponseTo'),
                    issuer=self.issuer,
                    status=Status(
                        failure=True,
                        status_code=StatusCode.RESPONDER,
                        status_message='Invalid data of {!r}: {!r}'.format(*e.errors.popitem())))

        response.relay_state = self.relay_state
        return response
 def put_light_response(self, uid: str, response: LightResponse) -> None:
     """Store a LightResponse under a unique id."""
     data = dump_xml(response.export_xml()).decode('utf-8')
     LOGGER.debug('Store Light Response to cache: id=%r, data=%s', uid,
                  data)
     self.get_cache(self.response_cache_name).put(uid, data)
Beispiel #15
0
 def create_response(self, success: bool) -> LightResponse:
     data = (LIGHT_RESPONSE_DICT if success else FAILED_LIGHT_RESPONSE_DICT).copy()
     data['status'] = Status(**data['status'])
     return LightResponse(**data)
Beispiel #16
0
    def from_light_response(cls: Type[SAMLResponseType],
                            light_response: LightResponse,
                            audience: Optional[str],
                            destination: Optional[str], issued: datetime,
                            validity: timedelta) -> SAMLResponseType:
        """Convert light response to SAML response."""
        light_response.validate()
        issue_instant = datetime_iso_format_milliseconds(issued) + 'Z'  # UTC
        valid_until = datetime_iso_format_milliseconds(issued +
                                                       validity) + 'Z'  # UTC

        root_attributes = {
            'ID': light_response.id,
            'InResponseTo': light_response.in_response_to_id,
            'Version': '2.0',
            'IssueInstant': issue_instant,
        }
        confirmation_data = {
            'InResponseTo': light_response.in_response_to_id,
            'NotOnOrAfter': valid_until
        }
        conditions = {'NotBefore': issue_instant, 'NotOnOrAfter': valid_until}

        if destination is not None:
            root_attributes['Destination'] = destination
            confirmation_data['Recipient'] = destination

        root = etree.Element(Q_NAMES['saml2p:Response'],
                             attrib=root_attributes,
                             nsmap=EIDAS_NAMESPACES)
        # 1. StatusResponseType <saml2:Issuer> optional
        if light_response.issuer is not None:
            SubElement(root,
                       Q_NAMES['saml2:Issuer']).text = light_response.issuer
        # 2. StatusResponseType <ds:Signature> optional, skipped
        # 3. StatusResponseType <saml2p:Extensions> optional, skipped
        # 4. StatusResponseType <saml2p:Status> required
        status = light_response.status
        assert status is not None
        status_elm = SubElement(root, Q_NAMES['saml2p:Status'])
        # 4.1 <saml2p:Status> <saml2p:StatusCode> required
        status_code = status.status_code
        sub_status_code = status.sub_status_code
        if status_code is None:
            status_code = StatusCode.SUCCESS if not status.failure else StatusCode.RESPONDER

        # VERSION_MISMATCH is a status code in SAML 2 but a sub status code in Light response!
        if sub_status_code == SubStatusCode.VERSION_MISMATCH:
            status_code_value = SubStatusCode.VERSION_MISMATCH.value
            sub_status_code_value = None
        else:
            status_code_value = status_code.value
            sub_status_code_value = None if sub_status_code is None else sub_status_code.value

        status_code_elm = SubElement(status_elm, Q_NAMES['saml2p:StatusCode'],
                                     {'Value': status_code_value})
        if sub_status_code_value is not None:
            SubElement(status_code_elm, Q_NAMES['saml2p:StatusCode'],
                       {'Value': sub_status_code_value})
        # 4.2 <saml2p:Status> <saml2p:StatusMessage> optional
        if status.status_message is not None:
            SubElement(
                status_elm,
                Q_NAMES['saml2p:StatusMessage']).text = status.status_message
        # 4.3 <saml2p:Status> <saml2p:StatusDetail> optional, skipped
        if not status.failure:
            # 5. AssertionType
            assertion_elm = SubElement(
                root, Q_NAMES['saml2:Assertion'], {
                    'ID': '_' + light_response.id,
                    'Version': '2.0',
                    'IssueInstant': issue_instant,
                })
            # 5.1 AssertionType <saml2:Issuer> required
            SubElement(assertion_elm,
                       Q_NAMES['saml2:Issuer']).text = light_response.issuer
            # 5.2 AssertionType <ds:Signature> optional, skipped
            # 5.3 AssertionType <saml2:Subject> optional
            subject_elm = SubElement(assertion_elm, Q_NAMES['saml2:Subject'])
            SubElement(subject_elm, Q_NAMES['saml2:NameID'], {
                'Format': light_response.subject_name_id_format.value
            }).text = light_response.subject
            confirmation_elm = SubElement(
                subject_elm, Q_NAMES['saml2:SubjectConfirmation'],
                {'Method': 'urn:oasis:names:tc:SAML:2.0:cm:bearer'})
            SubElement(confirmation_elm,
                       Q_NAMES['saml2:SubjectConfirmationData'],
                       confirmation_data)
            # 5.4 AssertionType <saml2:Conditions> optional
            conditions_elm = SubElement(assertion_elm,
                                        Q_NAMES['saml2:Conditions'],
                                        conditions)
            if audience is not None:
                SubElement(
                    SubElement(conditions_elm,
                               Q_NAMES['saml2:AudienceRestriction']),
                    Q_NAMES['saml2:Audience']).text = audience
            # 5.5 AssertionType <saml2:Advice> optional, skipped
            # 5.6 AssertionType <saml2:AttributeStatement>
            attributes_elm = SubElement(assertion_elm,
                                        Q_NAMES['saml2:AttributeStatement'])
            for name, values in light_response.attributes.items():
                attribute = SubElement(
                    attributes_elm, Q_NAMES['saml2:Attribute'],
                    create_attribute_elm_attributes(name, None))
                for value in values:
                    SubElement(attribute,
                               Q_NAMES['saml2:AttributeValue']).text = value

            # 5.7 AssertionType <saml2:AuthnStatement>
            statement_elm = SubElement(assertion_elm,
                                       Q_NAMES['saml2:AuthnStatement'],
                                       {'AuthnInstant': issue_instant})
            if light_response.ip_address is not None:
                SubElement(statement_elm, Q_NAMES['saml2:SubjectLocality'],
                           {'Address': light_response.ip_address})
            SubElement(
                SubElement(statement_elm, Q_NAMES['saml2:AuthnContext']),
                Q_NAMES['saml2:AuthnContextClassRef']
            ).text = light_response.level_of_assurance.value

        return cls(ElementTree(root), light_response.relay_state)
 def get_light_response(self, **kwargs) -> LightResponse:
     light_response_data = LIGHT_RESPONSE_DICT.copy()
     light_response_data['status'] = Status(**light_response_data['status'])
     light_response_data.update(**kwargs)
     return LightResponse(**light_response_data)