def test_issuer_in_response_and_assertion(self): # https://github.com/italia/spid-testenv2/issues/145 response = create_response( { 'response': { 'attrs': { 'in_response_to': 'test_12345', 'destination': 'http://some.dest.nation' } }, 'issuer': { 'attrs': { 'name_qualifier': 'http://test_id.entity', }, 'text': 'http://test_id.entity' }, 'name_id': { 'attrs': { 'name_qualifier': 'http://test_id.entity', } }, 'subject_confirmation_data': { 'attrs': { 'recipient': 'http://test_id.entity', } }, 'audience': { 'text': 'http://test_sp_id.entity', }, 'authn_context_class_ref': { 'text': SPID_LEVEL_1 } }, {'status_code': STATUS_SUCCESS}, {}) issuers = response._element.findall('.//{%s}Issuer' % SAML) self.assertEqual(len(issuers), 2) for issuer in issuers: self.assertEqual(issuer.get('NameQualifier'), 'http://test_id.entity') self.assertEqual(issuer.text, 'http://test_id.entity') self.assertEqual(issuers[0].getparent().tag, '{%s}Response' % SAMLP) self.assertEqual(issuers[1].getparent().tag, '{%s}Assertion' % SAML) self.assertTrue( validate_xml(response.to_xml(), 'testenv/xsd/saml-schema-protocol-2.0.xsd'), "The resulting XML is invalid")
def test_session_index_in_response(self): # https://github.com/italia/spid-testenv2/issues/160 response = create_response( { 'response': { 'attrs': { 'in_response_to': 'test_8989', 'destination': 'http://des.nation' } }, 'issuer': { 'attrs': { 'name_qualifier': 'http://test_id2.entity', }, 'text': 'http://test_id2.entity' }, 'name_id': { 'attrs': { 'name_qualifier': 'http://test_id2.entity', } }, 'subject_confirmation_data': { 'attrs': { 'recipient': 'http://test_id2.entity', } }, 'audience': { 'text': 'http://test_sp_id2.entity', }, 'authn_context_class_ref': { 'text': SPID_LEVEL_1 } }, {'status_code': STATUS_SUCCESS}, {}) authn_statements = response._element.findall('.//{%s}AuthnStatement' % SAML) self.assertEqual(len(authn_statements), 1) authn_statement = authn_statements[0] self.assertTrue(authn_statement.get('SessionIndex', False)) self.assertTrue( validate_xml(response.to_xml(), 'testenv/xsd/saml-schema-protocol-2.0.xsd'), "The resulting XML is invalid")
def test_no_authenticating_authority_in_assertion(self): # See issue https://github.com/italia/spid-testenv2/issues/68 response = create_response( { 'response': { 'attrs': { 'in_response_to': 'test_12345', 'destination': 'http://some.dest.nation' } }, 'issuer': { 'attrs': { 'name_qualifier': 'http://test_id.entity', }, 'text': 'http://test_id.entity' }, 'name_id': { 'attrs': { 'name_qualifier': 'http://test_id.entity', } }, 'subject_confirmation_data': { 'attrs': { 'recipient': 'http://test_id.entity', } }, 'audience': { 'text': 'http://test_sp_id.entity', }, 'authn_context_class_ref': { 'text': SPID_LEVEL_1 } }, {'status_code': STATUS_SUCCESS}, {}) authenticating_authorities = response._element.findall( './/{%s}AuthenticatingAuthority' % SAML) self.assertEqual(len(authenticating_authorities), 0)
def login(self): """ Login endpoint (verify user credentials) """ key = from_session('request_key') relay_state = from_session('relay_state') logger.info('Request key: {}'.format(key)) if key and key in self.ticket: authn_request = self.ticket[key] sp_id = authn_request.issuer.text.strip() destination = self.get_destination(authn_request, sp_id) authn_context = authn_request.requested_authn_context spid_level = authn_context.authn_context_class_ref.text.strip() if request.method == 'GET': # inject extra data in form login based on spid level extra_challenge = self._verify_spid(level=spid_level, **{'key': key}) rendered_form = render_template( 'login.html', **{ 'action': url_for('login'), 'request_key': key, 'relay_state': relay_state, 'extra_challenge': extra_challenge, 'show_response_options': self._config.show_response_options, }) return rendered_form, 200 if 'confirm' in request.form: # verify optional challenge based on spid level verified = self._verify_spid(level=spid_level, verify=True, **{ 'key': key, 'data': request.form }) if verified: # verify user credentials user_id, user = self.user_manager.get( request.form['username'], request.form['password'], sp_id) if user_id is not None: # setup response _audience = sp_id _destination = destination _recipient_subj = destination _issuer_text = self._config.entity_id _pkey = self._config.idp_key _cert = self._config.idp_certificate # setup custom response elements (if any) wrong_destination = request.form.get( 'wrong_destination', False) if wrong_destination: _destination = '{}wrong/bad/'.format(_destination) wrong_relay_state = request.form.get( 'wrong_relay_state', False) if wrong_relay_state: relay_state = '{}wrong'.format(relay_state) wrong_audience = request.form.get( 'wrong_audience', False) if wrong_audience: _audience = '{}/wrong/bad/'.format(_audience) wrong_recipient_subj = request.form.get( 'wrong_recipient_subj', False) if wrong_recipient_subj: _recipient_subj = 'badrecipient' wrong_issuer = request.form.get('wrong_issuer', False) if wrong_issuer: _issuer_text = 'wrongissuer123' has_assertion = not request.form.get( 'no_assertion', False) bad_status_code = request.form.get( 'bad_status_code', False) wrong_conditions_notbefore = request.form.get( 'wrong_conditions_notbefore') wrong_conditions_notonorafter = request.form.get( 'wrong_conditions_notonorafter') wrong_subj_notonorafter = request.form.get( 'wrong_subj_notonorafter') wrong_subj_inresponseto = request.form.get( 'wrong_subj_inresponseto') custom_spid_level = request.form.get('spid_level') if custom_spid_level: spid_level = self._spid_levels[int( custom_spid_level)] sign_assertion = request.form.get( 'sign_assertion', False) sign_message = request.form.get('sign_message', False) custom_private_key = request.form.get('private_key') if custom_private_key: _pkey = bytes(custom_private_key.encode('utf-8')) custom_certificate = request.form.get('certificate') if custom_certificate: _pkey = bytes(custom_certificate.encode('utf-8')) _conditions = {'conditions': {'attrs': {}}} _subj_extra = {} if wrong_subj_inresponseto: _subj_extra[ 'in_response_to'] = 'inresponsetowron134' if wrong_conditions_notbefore: _conditions['conditions']['attrs'][ 'not_before'] = wrong_conditions_notbefore + ':00Z' if wrong_conditions_notonorafter: _conditions['conditions']['attrs'][ 'not_on_or_after'] = wrong_conditions_notonorafter + ':00Z' if wrong_subj_notonorafter: _subj_extra[ 'not_on_or_after'] = wrong_subj_notonorafter + ':00Z' _status_code = STATUS_AUTHN_FAILED if bad_status_code else STATUS_SUCCESS identity = user['attrs'].copy() logger.debug('Unfiltered data: {}'.format(identity)) atcs_idx = getattr( authn_request, 'attribute_consuming_service_index', None) logger.info( 'AttributeConsumingServiceIndex: {}'.format( atcs_idx)) sp_metadata = self._registry.get(sp_id) required = [] optional = [] if atcs_idx and sp_metadata: attrs = sp_metadata.attributes(atcs_idx) required = [el for el in attrs.get('required')] optional = [el for el in attrs.get('optional')] for attr_name, val in list(identity.items()): _type = self._attribute_type(attr_name) identity[attr_name] = (_type, val) _identity = self._filter_attributes( identity, required, optional) logger.debug('Filtered data: {}'.format(_identity)) _response_data = { 'response': { 'attrs': { 'in_response_to': authn_request.id, 'destination': _destination } }, 'issuer': { 'attrs': { 'name_qualifier': self._config.entity_id, }, 'text': _issuer_text }, 'name_id': { 'attrs': { 'name_qualifier': self._config.entity_id, } }, 'subject_confirmation_data': { 'attrs': { 'recipient': _recipient_subj } }, 'audience': { 'text': _audience }, 'authn_context_class_ref': { 'text': spid_level } } _response_data.update(_conditions) if _subj_extra: _response_data['subject_confirmation_data'][ 'attrs'].update(_subj_extra) response = create_response( _response_data, {'status_code': _status_code}, _identity.copy(), has_assertion=has_assertion) response = sign_http_post(response.to_xml(), _pkey, _cert, message=sign_message, assertion=sign_assertion) logger.info('Response: \n{}'.format(response)) rendered_template = render_template( 'form_http_post.html', **{ 'action': destination, 'relay_state': relay_state, 'message': base64.b64encode(response).decode('ascii'), 'message_type': 'SAMLResponse' }) self.responses[key] = rendered_template # Setup confirmation page data rendered_response = render_template( 'confirm.html', **{ 'destination_service': sp_id, 'lines': escape(response.decode('utf-8')).splitlines(), 'attrs': list(_identity.keys()), 'action': '/continue-response', 'request_key': key, 'show_response_options': self._config.show_response_options, }) return rendered_response, 200 elif 'delete' in request.form: error_info = get_spid_error(AUTH_NO_CONSENT) response = create_error_response( { 'response': { 'attrs': { 'in_response_to': authn_request.id, 'destination': destination } }, 'issuer': { 'attrs': { 'name_qualifier': self._config.entity_id, }, 'text': self._config.entity_id }, }, { 'status_code': error_info[0], 'status_message': error_info[1] }).to_xml() logger.info('Error response: \n{}'.format(response)) response = sign_http_post( response, self._config.idp_key, self._config.idp_certificate, ) del self.ticket[key] rendered_template = render_template( 'form_http_post.html', **{ 'action': destination, 'relay_state': relay_state, 'message': base64.b64encode(response).decode('ascii'), 'message_type': 'SAMLResponse' }) return rendered_template, 200 return render_template('403.html'), 403
def test_sign_http_redirect(self): # https://github.com/italia/spid-testenv2/issues/175 response_xmlstr = create_response( { 'response': { 'attrs': { 'in_response_to': 'test_3210', 'destination': 'http://redirect' } }, 'issuer': { 'attrs': { 'name_qualifier': 'http://test_id.entity', }, 'text': 'http://test_id.entity' }, 'name_id': { 'attrs': { 'name_qualifier': 'http://test_id.entity', } }, 'subject_confirmation_data': { 'attrs': { 'recipient': 'http://test_id.entity', } }, 'audience': { 'text': 'http://test_sp_id.entity', }, 'authn_context_class_ref': { 'text': SPID_LEVEL_1 } }, { 'status_code': STATUS_SUCCESS }, {}).to_xml() with open(os.path.join(DATA_DIR, 'test.key'), 'r') as fp, open(os.path.join(DATA_DIR, 'test.crt'), 'r') as fp2: pkey = fp.read().encode('utf-8') cert = fp2.read().encode('utf-8') verifier = RSA_VERIFIERS[SIG_RSA_SHA256] # No relay state url = sign_http_redirect(response_xmlstr, pkey, relay_state=None) query = parse_qs(url) self.assertIn('Signature', query) self.assertIn('SigAlg', query) self.assertNotIn('RelayState', query) self.assertIn('SAMLResponse', query) saml_response = query.get('SAMLResponse')[0] sig_alg = query.get('SigAlg')[0] signature = query.get('Signature')[0] signature = b64decode(signature) signed_data = '&'.join([ urlencode({'SAMLResponse': saml_response}), urlencode({'SigAlg': sig_alg}) ]) signed_data = signed_data.encode('ascii') verified = verifier.verify( load_pem_x509_certificate( cert, backend=default_backend()).public_key(), bytes(signed_data), bytes(signature)) self.assertTrue(verified) # No relay state (2) url = sign_http_redirect(response_xmlstr, pkey, relay_state='') query = parse_qs(url) self.assertIn('Signature', query) self.assertIn('SigAlg', query) self.assertNotIn('RelayState', query) self.assertIn('SAMLResponse', query) saml_response = query.get('SAMLResponse')[0] sig_alg = query.get('SigAlg')[0] signature = query.get('Signature')[0] signature = b64decode(signature) signed_data = '&'.join([ urlencode({'SAMLResponse': saml_response}), urlencode({'SigAlg': sig_alg}) ]) signed_data = signed_data.encode('ascii') verified = verifier.verify( load_pem_x509_certificate( cert, backend=default_backend()).public_key(), bytes(signed_data), bytes(signature)) self.assertTrue(verified) # with relay state url = sign_http_redirect(response_xmlstr, pkey, relay_state='somevalue') query = parse_qs(url) self.assertIn('Signature', query) self.assertIn('SigAlg', query) self.assertIn('RelayState', query) self.assertIn('SAMLResponse', query) saml_response = query.get('SAMLResponse')[0] sig_alg = query.get('SigAlg')[0] signature = query.get('Signature')[0] relay_state = query.get('RelayState')[0] signature = b64decode(signature) signed_data = '&'.join([ urlencode({'SAMLResponse': saml_response}), urlencode({'RelayState': relay_state}), urlencode({'SigAlg': sig_alg}) ]) signed_data = signed_data.encode('ascii') verified = verifier.verify( load_pem_x509_certificate( cert, backend=default_backend()).public_key(), bytes(signed_data), bytes(signature)) self.assertTrue(verified)
def test_sign_http_post(self): # https://github.com/italia/spid-testenv2/issues/169 response_xmlstr = create_response( { 'response': { 'attrs': { 'in_response_to': 'test_12345', 'destination': 'http://post' } }, 'issuer': { 'attrs': { 'name_qualifier': 'http://test_id.entity', }, 'text': 'http://test_id.entity' }, 'name_id': { 'attrs': { 'name_qualifier': 'http://test_id.entity', } }, 'subject_confirmation_data': { 'attrs': { 'recipient': 'http://test_id.entity', } }, 'audience': { 'text': 'http://test_sp_id.entity', }, 'authn_context_class_ref': { 'text': SPID_LEVEL_1 } }, { 'status_code': STATUS_SUCCESS }, {}).to_xml() with open(os.path.join(DATA_DIR, 'test.key'), 'r') as fp, open(os.path.join(DATA_DIR, 'test.crt'), 'r') as fp2: pkey = fp.read().encode('utf-8') cert = fp2.read().encode('utf-8') # No signature at all decoded_response = sign_http_post(response_xmlstr, pkey, cert, message=False, assertion=False) tree = etree.fromstring(decoded_response) signature_values = tree.findall( './/{http://www.w3.org/2000/09/xmldsig#}SignatureValue') digest_values = tree.findall( './/{http://www.w3.org/2000/09/xmldsig#}DigestValue') self.assertEqual(len(signature_values), 0) self.assertEqual(len(digest_values), 0) with pytest.raises(InvalidInput) as excinfo: # noqa: F841 XMLVerifier().verify(tree, x509_cert=cert) # Signed response decoded_response = sign_http_post(response_xmlstr, pkey, cert, message=True, assertion=False) tree = etree.fromstring(decoded_response) signature_values = tree.findall( './/{http://www.w3.org/2000/09/xmldsig#}SignatureValue') digest_values = tree.findall( './/{http://www.w3.org/2000/09/xmldsig#}DigestValue') self.assertEqual(len(signature_values), 1) self.assertEqual(len(digest_values), 1) XMLVerifier().verify(tree, x509_cert=cert) # Signed assertion decoded_response = sign_http_post(response_xmlstr, pkey, cert, message=False, assertion=True) tree = etree.fromstring(decoded_response) signature_values = tree.findall( './/{http://www.w3.org/2000/09/xmldsig#}SignatureValue') digest_values = tree.findall( './/{http://www.w3.org/2000/09/xmldsig#}DigestValue') self.assertEqual(len(signature_values), 1) self.assertEqual(len(digest_values), 1) XMLVerifier().verify(tree, x509_cert=cert) # Signed response and assertion together decoded_response = sign_http_post(response_xmlstr, pkey, cert, message=True, assertion=True) tree = etree.fromstring(decoded_response) signature_values = tree.findall( './/{http://www.w3.org/2000/09/xmldsig#}SignatureValue') digest_values = tree.findall( './/{http://www.w3.org/2000/09/xmldsig#}DigestValue') self.assertEqual(len(signature_values), 2) self.assertEqual(len(digest_values), 2) XMLVerifier().verify(tree, x509_cert=cert)
def login(self): """ Login endpoint (verify user credentials) """ key = from_session('request_key') relay_state = from_session('relay_state') self.app.logger.debug('Request key: {}'.format(key)) if key and key in self.ticket: authn_request = self.ticket[key] sp_id = authn_request.issuer.text destination = self.get_destination(authn_request, sp_id) authn_context = authn_request.requested_authn_context spid_level = authn_context.authn_context_class_ref.text if request.method == 'GET': # inject extra data in form login based on spid level extra_challenge = self._verify_spid(level=spid_level, **{'key': key}) rendered_form = render_template( 'login.html', **{ 'action': url_for('login'), 'request_key': key, 'relay_state': relay_state, 'extra_challenge': extra_challenge } ) return rendered_form, 200 if 'confirm' in request.form: # verify optional challenge based on spid level verified = self._verify_spid( level=spid_level, verify=True, **{ 'key': key, 'data': request.form } ) if verified: # verify user credentials user_id, user = self.user_manager.get( request.form['username'], request.form['password'], sp_id ) if user_id is not None: # setup response identity = user['attrs'].copy() self.app.logger.debug( 'Unfiltered data: {}'.format(identity) ) atcs_idx = getattr(authn_request, 'attribute_consuming_service_index', None) self.app.logger.debug( 'AttributeConsumingServiceIndex: {}'.format( atcs_idx ) ) if atcs_idx: # TODO: Remove this pysaml2 dependency attrs = self.server.wants(sp_id, atcs_idx) required = [el.get('name') for el in attrs.get('required')] optional = [el.get('name') for el in attrs.get('optional')] else: required = [] optional = [] for k, v in identity.items(): if k in self._spid_main_fields: _type = self._spid_attributes['primary'][k] else: _type = self._spid_attributes['secondary'][k] identity[k] = (_type, v) _identity = {} for _key in required: _identity[_key] = identity[_key] for _key in optional: _identity[_key] = identity[_key] self.app.logger.debug( 'Filtered data: {}'.format(_identity) ) response_xmlstr = create_response( { 'response': { 'attrs': { 'in_response_to': authn_request.id, 'destination': destination } }, 'issuer': { 'attrs': { 'name_qualifier': self._config.entity_id, }, 'text': self._config.entity_id }, 'name_id': { 'attrs': { 'name_qualifier': self._config.entity_id, } }, 'subject_confirmation_data': { 'attrs': { 'recipient': destination } }, 'audience': { 'text': sp_id }, 'authn_context_class_ref': { 'text': spid_level } }, { 'status_code': STATUS_SUCCESS }, _identity.copy() ).to_xml() response = sign_http_post( response_xmlstr, self._config.idp_key, self._config.idp_certificate, ) self.app.logger.debug( 'Response: \n{}'.format(response) ) rendered_template = render_template( 'form_http_post.html', **{ 'action': destination, 'relay_state': relay_state, 'message': response, 'message_type': 'SAMLResponse' } ) self.responses[key] = rendered_template # Setup confirmation page data rendered_response = render_template( 'confirm.html', **{ 'destination_service': sp_id, 'lines': escape( response_xmlstr.decode('ascii') ).splitlines(), 'attrs': _identity.keys(), 'action': '/continue-response', 'request_key': key } ) return rendered_response, 200 elif 'delete' in request.form: error_info = get_spid_error( AUTH_NO_CONSENT ) response = create_error_response( { 'response': { 'attrs': { 'in_response_to': authn_request.id, 'destination': destination } }, 'issuer': { 'attrs': { 'name_qualifier': self._config.entity_id, }, 'text': self._config.entity_id }, }, { 'status_code': error_info[0], 'status_message': error_info[1] } ).to_xml() self.app.logger.debug( 'Error response: \n{}'.format(response) ) response = sign_http_post( response, self._config.idp_key, self._config.idp_certificate, ) del self.ticket[key] rendered_template = render_template( 'form_http_post.html', **{ 'action': destination, 'relay_state': relay_state, 'message': response, 'message_type': 'SAMLResponse' } ) return rendered_template, 200 return render_template('403.html'), 403
def _build_success_response(self, user, authn_request, spid_level, destination, sp_id, relay_state): """ Build and return a successful response """ identity = user['attrs'].copy() self.app.logger.debug('Unfiltered data: {}'.format(identity)) atcs_idx = getattr(authn_request, 'attribute_consuming_service_index', None) self.app.logger.debug( 'AttributeConsumingServiceIndex: {}'.format(atcs_idx)) sp_metadata = self._registry.get(sp_id) required = [] optional = [] if atcs_idx and sp_metadata: attrs = sp_metadata.attributes(atcs_idx) required = [el for el in attrs.get('required')] optional = [el for el in attrs.get('optional')] for attr_name, val in identity.items(): _type = self._attribute_type(attr_name) identity[attr_name] = (_type, val) _identity = {} # TODO: refactor a bit the following snippet for _key in required: try: _identity[_key] = identity[_key] except KeyError: _identity[_key] = ('', self._attribute_type(_key)) for _key in optional: try: _identity[_key] = identity[_key] except KeyError: _identity[_key] = ('', self._attribute_type(_key)) self.app.logger.debug('Filtered data: {}'.format(_identity)) response_xmlstr = create_response( { 'response': { 'attrs': { 'in_response_to': authn_request.id, 'destination': destination } }, 'issuer': { 'attrs': { 'name_qualifier': self._config.entity_id, }, 'text': self._config.entity_id }, 'name_id': { 'attrs': { 'name_qualifier': self._config.entity_id, } }, 'subject_confirmation_data': { 'attrs': { 'recipient': destination } }, 'audience': { 'text': sp_id }, 'authn_context_class_ref': { 'text': spid_level } }, { 'status_code': STATUS_SUCCESS }, _identity.copy()).to_xml() response = sign_http_post( response_xmlstr, self._config.idp_key, self._config.idp_certificate, ) rendered_template = render_template( 'form_http_post.html', **{ 'action': destination, 'relay_state': relay_state, 'message': response, 'message_type': 'SAMLResponse' }) return rendered_template, response_xmlstr, _identity