1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# 4 5"""Contains classes and functions that a SAML2.0 Identity provider (IdP) 6or attribute authority (AA) may use to conclude its tasks. 7""" 8import logging 9import os 10 11import importlib 12import dbm 13import shelve 14import six 15import threading 16 17import saml2.cryptography.symmetric 18from saml2 import saml 19from saml2 import element_to_extension_element 20from saml2 import class_name 21from saml2 import BINDING_HTTP_REDIRECT 22from saml2.argtree import add_path, is_set 23 24from saml2.entity import Entity 25from saml2.eptid import Eptid 26from saml2.eptid import EptidShelve 27from saml2.samlp import NameIDMappingResponse 28from saml2.sdb import SessionStorage 29from saml2.schema import soapenv 30 31from saml2.request import AuthnRequest 32from saml2.request import AssertionIDRequest 33from saml2.request import AttributeQuery 34from saml2.request import NameIDMappingRequest 35from saml2.request import AuthzDecisionQuery 36from saml2.request import AuthnQuery 37 38from saml2.s_utils import MissingValue 39from saml2.s_utils import rndstr 40from saml2.s_utils import Unknown 41 42from saml2.sigver import pre_signature_part 43from saml2.sigver import signed_instance_factory 44from saml2.sigver import CertificateError 45 46from saml2.assertion import Assertion 47from saml2.assertion import Policy 48from saml2.assertion import restriction_from_attribute_spec 49from saml2.assertion import filter_attribute_value_assertions 50 51from saml2.ident import IdentDB, decode 52from saml2.profile import ecp 53 54logger = logging.getLogger(__name__) 55 56AUTHN_DICT_MAP = { 57 "decl": "authn_decl", 58 "authn_auth": "authn_auth", 59 "class_ref": "authn_class", 60 "authn_instant": "authn_instant", 61 "subject_locality": "subject_locality" 62} 63 64 65def _shelve_compat(name, *args, **kwargs): 66 try: 67 return shelve.open(name, *args, **kwargs) 68 except dbm.error[0]: 69 # Python 3 whichdb needs to try .db to determine type 70 if name.endswith('.db'): 71 name = name.rsplit('.db', 1)[0] 72 return shelve.open(name, *args, **kwargs) 73 else: 74 raise 75 76 77class Server(Entity): 78 """ A class that does things that IdPs or AAs do """ 79 80 def __init__( 81 self, 82 config_file="", 83 config=None, 84 cache=None, 85 stype="idp", 86 symkey="", 87 msg_cb=None, 88 ): 89 Entity.__init__(self, stype, config, config_file, msg_cb=msg_cb) 90 self.eptid = None 91 self.init_config(stype) 92 self.cache = cache 93 self.ticket = {} 94 self.session_db = self.choose_session_storage() 95 if symkey: 96 self.symkey = symkey.encode() 97 else: 98 self.symkey = saml2.cryptography.symmetric.Default.generate_key() 99 self.seed = rndstr() 100 self.lock = threading.Lock() 101 102 def getvalid_certificate_str(self): 103 if self.sec.cert_handler is not None: 104 return self.sec.cert_handler._last_validated_cert 105 return None 106 107 def support_AssertionIDRequest(self): 108 return True 109 110 def support_AuthnQuery(self): 111 return True 112 113 def choose_session_storage(self): 114 _spec = self.config.getattr("session_storage", "idp") 115 if not _spec: 116 return SessionStorage() 117 elif isinstance(_spec, six.string_types): 118 if _spec.lower() == "memory": 119 return SessionStorage() 120 else: # Should be tuple 121 typ, data = _spec 122 if typ.lower() == "mongodb": 123 from saml2.mongo_store import SessionStorageMDB 124 return SessionStorageMDB(database=data, collection="session") 125 126 raise NotImplementedError("No such storage type implemented") 127 128 def init_config(self, stype="idp"): 129 """ Remaining init of the server configuration 130 131 :param stype: The type of Server ("idp"/"aa") 132 """ 133 if stype == "aa": 134 return 135 136 # subject information is stored in a database 137 # default database is in memory which is OK in some setups 138 dbspec = self.config.getattr("subject_data", "idp") 139 idb = None 140 typ = "" 141 if not dbspec: 142 idb = {} 143 elif isinstance(dbspec, six.string_types): 144 idb = _shelve_compat(dbspec, writeback=True, protocol=2) 145 else: # database spec is a a 2-tuple (type, address) 146 # print(>> sys.stderr, "DBSPEC: %s" % (dbspec,)) 147 (typ, addr) = dbspec 148 if typ == "shelve": 149 idb = _shelve_compat(addr, writeback=True, protocol=2) 150 elif typ == "memcached": 151 import memcache 152 idb = memcache.Client(addr) 153 elif typ == "dict": # in-memory dictionary 154 idb = {} 155 elif typ == "mongodb": 156 from saml2.mongo_store import IdentMDB 157 self.ident = IdentMDB(database=addr, collection="ident") 158 elif typ == "identdb": 159 mod, clas = addr.rsplit('.', 1) 160 mod = importlib.import_module(mod) 161 self.ident = getattr(mod, clas)() 162 163 if typ == "mongodb" or typ == "identdb": 164 pass 165 elif idb is not None: 166 self.ident = IdentDB(idb) 167 elif dbspec: 168 raise Exception("Couldn't open identity database: %s" % 169 (dbspec,)) 170 171 try: 172 _domain = self.config.getattr("domain", "idp") 173 if _domain: 174 self.ident.domain = _domain 175 176 self.ident.name_qualifier = self.config.entityid 177 178 dbspec = self.config.getattr("edu_person_targeted_id", "idp") 179 if not dbspec: 180 pass 181 else: 182 typ = dbspec[0] 183 addr = dbspec[1] 184 secret = dbspec[2] 185 if typ == "shelve": 186 self.eptid = EptidShelve(secret, addr) 187 elif typ == "mongodb": 188 from saml2.mongo_store import EptidMDB 189 self.eptid = EptidMDB(secret, database=addr, 190 collection="eptid") 191 else: 192 self.eptid = Eptid(secret) 193 except Exception: 194 self.ident.close() 195 raise 196 197 def wants(self, sp_entity_id, index=None): 198 """ Returns what attributes the SP requires and which are optional 199 if any such demands are registered in the Metadata. 200 201 :param sp_entity_id: The entity id of the SP 202 :param index: which of the attribute consumer services its all about 203 if index == None then all attribute consumer services are clumped 204 together. 205 :return: 2-tuple, list of required and list of optional attributes 206 """ 207 return self.metadata.attribute_requirement(sp_entity_id, index) 208 209 def verify_assertion_consumer_service(self, request): 210 _acs = request.assertion_consumer_service_url 211 _aci = request.assertion_consumer_service_index 212 _binding = request.protocol_binding 213 _eid = request.issuer.text 214 if _acs: 215 # look up acs in for that binding in the metadata given the issuer 216 # Assuming the format is entity 217 for acs in self.metadata.assertion_consumer_service(_eid, _binding): 218 if _acs == acs.text: 219 return True 220 elif _aci: 221 for acs in self.metadata.assertion_consumer_service(_eid, _binding): 222 if _aci == acs.index: 223 return True 224 225 return False 226 227 # ------------------------------------------------------------------------- 228 229 def parse_authn_request(self, enc_request, binding=BINDING_HTTP_REDIRECT, 230 relay_state=None, sigalg=None, signature=None): 231 """Parse a Authentication Request 232 233 :param enc_request: The request in its transport format 234 :param binding: Which binding that was used to transport the message 235 :param relay_state: RelayState, when binding=redirect 236 :param sigalg: Signature Algorithm, when binding=redirect 237 :param signature: Signature, when binding=redirect 238 to this entity. 239 :return: A request instance 240 """ 241 242 return self._parse_request(enc_request, AuthnRequest, 243 "single_sign_on_service", binding, 244 relay_state=relay_state, sigalg=sigalg, 245 signature=signature) 246 247 def parse_attribute_query(self, xml_string, binding): 248 """ Parse an attribute query 249 250 :param xml_string: The Attribute Query as an XML string 251 :param binding: Which binding that was used for the request 252 :return: A query instance 253 """ 254 255 return self._parse_request(xml_string, AttributeQuery, 256 "attribute_service", binding) 257 258 def parse_authz_decision_query(self, xml_string, binding): 259 """ Parse an authorization decision query 260 261 :param xml_string: The Authz decision Query as an XML string 262 :param binding: Which binding that was used when receiving this query 263 :return: Query instance 264 """ 265 266 return self._parse_request(xml_string, AuthzDecisionQuery, 267 "authz_service", binding) 268 269 def parse_assertion_id_request(self, xml_string, binding): 270 """ Parse an assertion id query 271 272 :param xml_string: The AssertionIDRequest as an XML string 273 :param binding: Which binding that was used when receiving this request 274 :return: Query instance 275 """ 276 277 return self._parse_request(xml_string, AssertionIDRequest, 278 "assertion_id_request_service", binding) 279 280 def parse_authn_query(self, xml_string, binding): 281 """ Parse an authn query 282 283 :param xml_string: The AuthnQuery as an XML string 284 :param binding: Which binding that was used when receiving this query 285 :return: Query instance 286 """ 287 288 return self._parse_request(xml_string, AuthnQuery, 289 "authn_query_service", binding) 290 291 def parse_name_id_mapping_request(self, xml_string, binding): 292 """ Parse a nameid mapping request 293 294 :param xml_string: The NameIDMappingRequest as an XML string 295 :param binding: Which binding that was used when receiving this request 296 :return: Query instance 297 """ 298 299 return self._parse_request(xml_string, NameIDMappingRequest, 300 "name_id_mapping_service", binding) 301 302 @staticmethod 303 def update_farg(in_response_to, consumer_url, farg=None): 304 if not farg: 305 farg = add_path( 306 {}, 307 ['assertion', 'subject', 'subject_confirmation', 'method', 308 saml.SCM_BEARER]) 309 add_path( 310 farg['assertion']['subject']['subject_confirmation'], 311 ['subject_confirmation_data', 'in_response_to', in_response_to]) 312 add_path( 313 farg['assertion']['subject']['subject_confirmation'], 314 ['subject_confirmation_data', 'recipient', consumer_url]) 315 else: 316 if not is_set(farg, 317 ['assertion', 'subject', 'subject_confirmation', 318 'method']): 319 add_path(farg, 320 ['assertion', 'subject', 'subject_confirmation', 321 'method', saml.SCM_BEARER]) 322 if not is_set(farg, 323 ['assertion', 'subject', 'subject_confirmation', 324 'subject_confirmation_data', 'in_response_to']): 325 add_path(farg, 326 ['assertion', 'subject', 'subject_confirmation', 327 'subject_confirmation_data', 'in_response_to', 328 in_response_to]) 329 if not is_set(farg, ['assertion', 'subject', 'subject_confirmation', 330 'subject_confirmation_data', 'recipient']): 331 add_path(farg, 332 ['assertion', 'subject', 'subject_confirmation', 333 'subject_confirmation_data', 'recipient', 334 consumer_url]) 335 return farg 336 337 def setup_assertion( 338 self, 339 authn, 340 sp_entity_id, 341 in_response_to, 342 consumer_url, 343 name_id, 344 policy, 345 _issuer, 346 authn_statement, 347 identity, 348 best_effort, 349 sign_response, 350 farg=None, 351 session_not_on_or_after=None, 352 sign_alg=None, 353 digest_alg=None, 354 **kwargs, 355 ): 356 """ 357 Construct and return the Assertion 358 359 :param authn: Authentication information 360 :param sp_entity_id: 361 :param in_response_to: The ID of the request this is an answer to 362 :param consumer_url: The recipient of the assertion 363 :param name_id: The NameID of the subject 364 :param policy: Assertion policies 365 :param _issuer: Issuer of the statement 366 :param authn_statement: An AuthnStatement instance 367 :param identity: Identity information about the Subject 368 :param best_effort: Even if not the SPs demands can be met send a 369 response. 370 :param sign_response: Sign the response, only applicable if 371 ErrorResponse 372 :param kwargs: Extra keyword arguments 373 :return: An Assertion instance 374 """ 375 376 ast = Assertion(identity) 377 ast.acs = self.config.getattr("attribute_converters") 378 if policy is None: 379 policy = Policy(mds=self.metadata) 380 try: 381 ast.apply_policy(sp_entity_id, policy) 382 except MissingValue as exc: 383 if not best_effort: 384 response = self.create_error_response( 385 in_response_to, 386 destination=consumer_url, 387 info=exc, 388 sign=sign_response, 389 sign_alg=sign_alg, 390 digest_alg=digest_alg, 391 ) 392 return str(response).split("\n") 393 394 farg = self.update_farg(in_response_to, consumer_url, farg) 395 396 if authn: # expected to be a dictionary 397 # Would like to use dict comprehension but ... 398 authn_args = dict( 399 [(AUTHN_DICT_MAP[k], v) for k, v in authn.items() if 400 k in AUTHN_DICT_MAP]) 401 authn_args.update(kwargs) 402 403 assertion = ast.construct( 404 sp_entity_id, self.config.attribute_converters, policy, 405 issuer=_issuer, farg=farg['assertion'], name_id=name_id, 406 session_not_on_or_after=session_not_on_or_after, 407 **authn_args) 408 409 elif authn_statement: # Got a complete AuthnStatement 410 assertion = ast.construct( 411 sp_entity_id, self.config.attribute_converters, policy, 412 issuer=_issuer, authn_statem=authn_statement, 413 farg=farg['assertion'], name_id=name_id, 414 **kwargs) 415 else: 416 assertion = ast.construct( 417 sp_entity_id, self.config.attribute_converters, policy, 418 issuer=_issuer, farg=farg['assertion'], name_id=name_id, 419 session_not_on_or_after=session_not_on_or_after, 420 **kwargs) 421 return assertion 422 423 # XXX DONE calls pre_signature_part 424 # XXX calls _response 425 def _authn_response( 426 self, 427 in_response_to, 428 consumer_url, 429 sp_entity_id, 430 identity=None, 431 name_id=None, 432 status=None, 433 authn=None, 434 issuer=None, 435 policy=None, 436 sign_assertion=None, 437 sign_response=None, 438 best_effort=False, 439 encrypt_assertion=False, 440 encrypt_cert_advice=None, 441 encrypt_cert_assertion=None, 442 authn_statement=None, 443 encrypt_assertion_self_contained=False, 444 encrypted_advice_attributes=False, 445 pefim=False, 446 sign_alg=None, 447 digest_alg=None, 448 farg=None, 449 session_not_on_or_after=None, 450 ): 451 """ Create a response. A layer of indirection. 452 453 :param in_response_to: The session identifier of the request 454 :param consumer_url: The URL which should receive the response 455 :param sp_entity_id: The entity identifier of the SP 456 :param identity: A dictionary with attributes and values that are 457 expected to be the bases for the assertion in the response. 458 :param name_id: The identifier of the subject 459 :param status: The status of the response 460 :param authn: A dictionary containing information about the 461 authn context. 462 :param issuer: The issuer of the response 463 :param policy: 464 :param sign_assertion: Whether the assertion should be signed or not 465 :param sign_response: Whether the response should be signed or not 466 :param best_effort: Even if not the SPs demands can be met send a 467 response. 468 :param encrypt_assertion: True if assertions should be encrypted. 469 :param encrypt_assertion_self_contained: True if all encrypted 470 assertions should have alla namespaces 471 selfcontained. 472 :param encrypted_advice_attributes: True if assertions in the advice 473 element should be encrypted. 474 :param encrypt_cert_advice: Certificate to be used for encryption of 475 assertions in the advice element. 476 :param encrypt_cert_assertion: Certificate to be used for encryption 477 of assertions. 478 :param authn_statement: Authentication statement. 479 :param pefim: True if a response according to the PEFIM profile 480 should be created. 481 :param farg: Argument to pass on to the assertion constructor 482 :return: A response instance 483 """ 484 485 if farg is None: 486 assertion_args = {} 487 488 # if identity: 489 _issuer = self._issuer(issuer) 490 491 # if encrypt_assertion and show_nameid: 492 # tmp_name_id = name_id 493 # name_id = None 494 # name_id = None 495 # tmp_authn = authn 496 # authn = None 497 # tmp_authn_statement = authn_statement 498 # authn_statement = None 499 500 if pefim: 501 encrypted_advice_attributes = True 502 encrypt_assertion_self_contained = True 503 assertion_attributes = self.setup_assertion( 504 None, sp_entity_id, None, None, None, policy, None, None, 505 identity, best_effort, sign_response, farg=farg) 506 assertion = self.setup_assertion( 507 authn, sp_entity_id, in_response_to, consumer_url, name_id, 508 policy, _issuer, authn_statement, [], True, sign_response, 509 farg=farg, session_not_on_or_after=session_not_on_or_after) 510 assertion.advice = saml.Advice() 511 512 # assertion.advice.assertion_id_ref.append(saml.AssertionIDRef()) 513 # assertion.advice.assertion_uri_ref.append(saml.AssertionURIRef()) 514 assertion.advice.assertion.append(assertion_attributes) 515 else: 516 assertion = self.setup_assertion( 517 authn, sp_entity_id, in_response_to, consumer_url, name_id, 518 policy, _issuer, authn_statement, identity, True, 519 sign_response, farg=farg, 520 session_not_on_or_after=session_not_on_or_after) 521 522 to_sign = [] 523 if not encrypt_assertion: 524 if sign_assertion: 525 # XXX self.signing_algorithm self.digest_algorithm defined by entity 526 # XXX this should be handled through entity.py 527 # XXX sig/digest-allowed should be configurable 528 sign_alg = sign_alg or self.signing_algorithm 529 digest_alg = digest_alg or self.digest_algorithm 530 531 assertion.signature = pre_signature_part( 532 assertion.id, 533 self.sec.my_cert, 534 2, 535 sign_alg=sign_alg, 536 digest_alg=digest_alg, 537 ) 538 to_sign.append((class_name(assertion), assertion.id)) 539 540 if (self.support_AssertionIDRequest() or self.support_AuthnQuery()): 541 self.session_db.store_assertion(assertion, to_sign) 542 543 return self._response( 544 in_response_to, 545 consumer_url, 546 status, 547 issuer, 548 sign_response, 549 to_sign, 550 sp_entity_id=sp_entity_id, 551 encrypt_assertion=encrypt_assertion, 552 encrypt_cert_advice=encrypt_cert_advice, 553 encrypt_cert_assertion=encrypt_cert_assertion, 554 encrypt_assertion_self_contained=encrypt_assertion_self_contained, 555 encrypted_advice_attributes=encrypted_advice_attributes, 556 sign_assertion=sign_assertion, 557 pefim=pefim, 558 sign_alg=sign_alg, 559 digest_alg=digest_alg, 560 assertion=assertion, 561 ) 562 563 # ------------------------------------------------------------------------ 564 565 # XXX DONE idp create > _response 566 def create_attribute_response( 567 self, 568 identity, 569 in_response_to, 570 destination, 571 sp_entity_id, 572 userid="", 573 name_id=None, 574 status=None, 575 issuer=None, 576 sign_assertion=None, 577 sign_response=None, 578 attributes=None, 579 sign_alg=None, 580 digest_alg=None, 581 farg=None, 582 **kwargs, 583 ): 584 """ Create an attribute assertion response. 585 586 :param identity: A dictionary with attributes and values that are 587 expected to be the bases for the assertion in the response. 588 :param in_response_to: The session identifier of the request 589 :param destination: The URL which should receive the response 590 :param sp_entity_id: The entity identifier of the SP 591 :param userid: A identifier of the user 592 :param name_id: The identifier of the subject 593 :param status: The status of the response 594 :param issuer: The issuer of the response 595 :param sign_assertion: Whether the assertion should be signed or not 596 :param sign_response: Whether the whole response should be signed 597 :param attributes: 598 :param kwargs: To catch extra keyword arguments 599 :return: A response instance 600 """ 601 602 policy = self.config.getattr("policy", "aa") 603 604 if not name_id and userid: 605 try: 606 name_id = self.ident.construct_nameid(userid, policy, sp_entity_id) 607 logger.warning("Unspecified NameID format") 608 except Exception: 609 pass 610 611 to_sign = [] 612 613 if identity: 614 farg = self.update_farg(in_response_to, sp_entity_id, farg=farg) 615 616 _issuer = self._issuer(issuer) 617 ast = Assertion(identity) 618 if policy: 619 ast.apply_policy(sp_entity_id, policy) 620 else: 621 policy = Policy(mds=self.metadata) 622 623 if attributes: 624 restr = restriction_from_attribute_spec(attributes) 625 ast = filter_attribute_value_assertions(ast) 626 627 assertion = ast.construct( 628 sp_entity_id, self.config.attribute_converters, policy, 629 issuer=_issuer, name_id=name_id, 630 farg=farg['assertion']) 631 632 return self._response( 633 in_response_to, 634 destination, 635 status, 636 issuer, 637 sign_response, 638 to_sign, 639 sign_assertion=sign_assertion, 640 sign_alg=sign_alg, 641 digest_alg=digest_alg, 642 assertion=assertion, 643 sp_entity_id=sp_entity_id, 644 **kwargs, 645 ) 646 647 def gather_authn_response_args( 648 self, sp_entity_id, name_id_policy, userid, **kwargs 649 ): 650 kwargs["policy"] = kwargs.get("release_policy") 651 652 # collect args and return them 653 args = {} 654 655 # XXX will be passed to _authn_response 656 param_defaults = { 657 'policy': None, 658 'best_effort': False, 659 'sign_assertion': False, 660 'sign_response': False, 661 'encrypt_assertion': False, 662 'encrypt_assertion_self_contained': True, 663 'encrypted_advice_attributes': False, 664 'encrypt_cert_advice': None, 665 'encrypt_cert_assertion': None, 666 # need to be named sign_alg and digest_alg 667 } 668 for param, val_default in param_defaults.items(): 669 val_kw = kwargs.get(param) 670 val_config = self.config.getattr(param, "idp") 671 args[param] = ( 672 val_kw 673 if val_kw is not None 674 else val_config 675 if val_config is not None 676 else val_default 677 ) 678 679 for arg, attr, eca, pefim in [ 680 ('encrypted_advice_attributes', 'verify_encrypt_cert_advice', 681 'encrypt_cert_advice', kwargs["pefim"]), 682 ('encrypt_assertion', 'verify_encrypt_cert_assertion', 683 'encrypt_cert_assertion', False)]: 684 685 if args[arg] or pefim: 686 _enc_cert = self.config.getattr(attr, "idp") 687 688 if _enc_cert is not None: 689 if kwargs[eca] is None: 690 raise CertificateError( 691 "No SPCertEncType certificate for encryption " 692 "contained in authentication " 693 "request.") 694 if not _enc_cert(kwargs[eca]): 695 raise CertificateError( 696 "Invalid certificate for encryption!") 697 698 if 'name_id' not in kwargs or not kwargs['name_id']: 699 nid_formats = [] 700 for _sp in self.metadata[sp_entity_id]["spsso_descriptor"]: 701 if "name_id_format" in _sp: 702 nid_formats.extend([n["text"] for n in 703 _sp["name_id_format"]]) 704 try: 705 snq = name_id_policy.sp_name_qualifier 706 except AttributeError: 707 snq = sp_entity_id 708 709 if not snq: 710 snq = sp_entity_id 711 712 kwa = {"sp_name_qualifier": snq} 713 714 try: 715 kwa["format"] = name_id_policy.format 716 except AttributeError: 717 pass 718 719 _nids = self.ident.find_nameid(userid, **kwa) 720 # either none or one 721 if _nids: 722 args['name_id'] = _nids[0] 723 else: 724 args['name_id'] = self.ident.construct_nameid( 725 userid, args['policy'], sp_entity_id, name_id_policy) 726 logger.debug("construct_nameid: %s => %s", userid, 727 args['name_id']) 728 else: 729 args['name_id'] = kwargs['name_id'] 730 731 for param in ['status', 'farg']: 732 try: 733 args[param] = kwargs[param] 734 except KeyError: 735 pass 736 737 return args 738 739 # XXX DONE idp create > _authn_response > _response 740 def create_authn_response( 741 self, 742 identity, 743 in_response_to, 744 destination, 745 sp_entity_id, 746 name_id_policy=None, 747 userid=None, 748 name_id=None, 749 authn=None, 750 issuer=None, 751 sign_response=None, 752 sign_assertion=None, 753 encrypt_cert_advice=None, 754 encrypt_cert_assertion=None, 755 encrypt_assertion=None, 756 encrypt_assertion_self_contained=True, 757 encrypted_advice_attributes=False, 758 pefim=False, 759 sign_alg=None, 760 digest_alg=None, 761 session_not_on_or_after=None, 762 **kwargs, 763 ): 764 """ Constructs an AuthenticationResponse 765 766 :param identity: Information about an user 767 :param in_response_to: The identifier of the authentication request 768 this response is an answer to. 769 :param destination: Where the response should be sent 770 :param sp_entity_id: The entity identifier of the Service Provider 771 :param name_id_policy: How the NameID should be constructed 772 :param userid: The subject identifier 773 :param name_id: The identifier of the subject. A saml.NameID instance. 774 :param authn: Dictionary with information about the authentication 775 context 776 :param issuer: Issuer of the response 777 :param sign_assertion: Whether the assertion should be signed or not. 778 :param sign_response: Whether the response should be signed or not. 779 :param encrypt_assertion: True if assertions should be encrypted. 780 :param encrypt_assertion_self_contained: True if all encrypted 781 assertions should have alla namespaces 782 selfcontained. 783 :param encrypted_advice_attributes: True if assertions in the advice 784 element should be encrypted. 785 :param encrypt_cert_advice: Certificate to be used for encryption of 786 assertions in the advice element. 787 :param encrypt_cert_assertion: Certificate to be used for encryption 788 of assertions. 789 :param pefim: True if a response according to the PEFIM profile 790 should be created. 791 :return: A response instance 792 """ 793 794 try: 795 args = self.gather_authn_response_args( 796 sp_entity_id, 797 name_id_policy=name_id_policy, 798 userid=userid, 799 name_id=name_id, 800 sign_response=sign_response, 801 sign_assertion=sign_assertion, 802 encrypt_cert_advice=encrypt_cert_advice, 803 encrypt_cert_assertion=encrypt_cert_assertion, 804 encrypt_assertion=encrypt_assertion, 805 encrypt_assertion_self_contained=encrypt_assertion_self_contained, 806 encrypted_advice_attributes=encrypted_advice_attributes, 807 pefim=pefim, 808 **kwargs, 809 ) 810 except IOError as exc: 811 response = self.create_error_response( 812 in_response_to, 813 destination=destination, 814 info=exc, 815 sign=sign_response, 816 sign_alg=sign_alg, 817 digest_alg=digest_alg, 818 ) 819 return str(response).split("\n") 820 821 try: 822 _authn = authn 823 return self._authn_response( 824 in_response_to, 825 destination, 826 sp_entity_id, 827 identity, 828 authn=_authn, 829 issuer=issuer, 830 pefim=pefim, 831 sign_alg=sign_alg, 832 digest_alg=digest_alg, 833 session_not_on_or_after=session_not_on_or_after, 834 **args, 835 ) 836 except MissingValue as exc: 837 return self.create_error_response( 838 in_response_to, 839 destination=destination, 840 info=exc, 841 sign=sign_response, 842 sign_alg=sign_alg, 843 digest_alg=digest_alg, 844 ) 845 846 # XXX DONE idp create > create_authn_response > _authn_response > _response 847 def create_authn_request_response( 848 self, 849 identity, 850 in_response_to, 851 destination, 852 sp_entity_id, 853 name_id_policy=None, 854 userid=None, 855 name_id=None, 856 authn=None, 857 authn_decl=None, 858 issuer=None, 859 sign_response=None, 860 sign_assertion=None, 861 session_not_on_or_after=None, 862 sign_alg=None, 863 digest_alg=None, 864 **kwargs, 865 ): 866 return self.create_authn_response( 867 identity, 868 in_response_to, 869 destination, 870 sp_entity_id, 871 name_id_policy, 872 userid, 873 name_id, 874 authn, 875 issuer, 876 sign_response, 877 sign_assertion, 878 authn_decl=authn_decl, 879 session_not_on_or_after=session_not_on_or_after, 880 sign_alg=sign_alg, 881 digest_alg=digest_alg, 882 ) 883 884 # XXX DONE calls pre_signature_part 885 # XXX DONE idp create > [...] 886 def create_assertion_id_request_response( 887 self, assertion_id, sign=None, sign_alg=None, digest_alg=None, **kwargs 888 ): 889 try: 890 (assertion, to_sign) = self.session_db.get_assertion(assertion_id) 891 except KeyError: 892 raise Unknown 893 894 if to_sign: 895 if assertion.signature is None: 896 # XXX self.signing_algorithm self.digest_algorithm defined by entity 897 # XXX this should be handled through entity.py 898 # XXX sig/digest-allowed should be configurable 899 sign_alg = sign_alg or self.signing_algorithm 900 digest_alg = digest_alg or self.digest_algorithm 901 902 assertion.signature = pre_signature_part( 903 assertion.id, 904 self.sec.my_cert, 905 1, 906 sign_alg=sign_alg, 907 digest_alg=digest_alg, 908 ) 909 return signed_instance_factory(assertion, self.sec, to_sign) 910 else: 911 return assertion 912 913 # XXX calls self.sign without ensuring sign 914 # XXX calls self.sign => should it call _message (which calls self.sign)? 915 # XXX idp create > NameIDMappingResponse & sign? 916 def create_name_id_mapping_response( 917 self, 918 name_id=None, 919 encrypted_id=None, 920 in_response_to=None, 921 issuer=None, 922 sign_response=None, 923 status=None, 924 sign_alg=None, 925 digest_alg=None, 926 **kwargs, 927 ): 928 """ 929 protocol for mapping a principal's name identifier into a 930 different name identifier for the same principal. 931 Done over soap. 932 933 :param name_id: 934 :param encrypted_id: 935 :param in_response_to: 936 :param issuer: 937 :param sign_response: 938 :param status: 939 :return: 940 """ 941 # Done over SOAP 942 943 ms_args = self.message_args() 944 945 _resp = NameIDMappingResponse( 946 name_id, encrypted_id, in_response_to=in_response_to, **ms_args 947 ) 948 949 if sign_response: 950 return self.sign(_resp, sign_alg=sign_alg, digest_alg=digest_alg) 951 else: 952 logger.info("Message: %s", _resp) 953 return _resp 954 955 # XXX DONE idp create > _response 956 def create_authn_query_response( 957 self, 958 subject, 959 session_index=None, 960 requested_context=None, 961 in_response_to=None, 962 issuer=None, 963 sign_response=None, 964 status=None, 965 sign_alg=None, 966 digest_alg=None, 967 **kwargs, 968 ): 969 """ 970 A successful <Response> will contain one or more assertions containing 971 authentication statements. 972 973 :return: 974 """ 975 976 margs = self.message_args() 977 asserts = [ 978 saml.Assertion(authn_statement=statement, subject=subject, **margs) 979 for statement in self.session_db.get_authn_statements( 980 subject.name_id, session_index, requested_context 981 ) 982 ] 983 984 if asserts: 985 args = {"assertion": asserts} 986 else: 987 args = {} 988 989 return self._response( 990 in_response_to, 991 "", 992 status, 993 issuer, 994 sign_response, 995 to_sign=[], 996 sign_alg=sign_alg, 997 digest_alg=digest_alg, 998 **args, 999 ) 1000 1001 # --------- 1002 1003 def parse_ecp_authn_request(self): 1004 pass 1005 1006 # XXX DONE idp create > create_authn_response > _authn_response > _response 1007 def create_ecp_authn_request_response( 1008 self, 1009 acs_url, 1010 identity, 1011 in_response_to, 1012 destination, 1013 sp_entity_id, 1014 name_id_policy=None, 1015 userid=None, 1016 name_id=None, 1017 authn=None, 1018 issuer=None, 1019 sign_response=None, 1020 sign_assertion=None, 1021 sign_alg=None, 1022 digest_alg=None, 1023 **kwargs, 1024 ): 1025 1026 # ---------------------------------------- 1027 # <ecp:Response 1028 # ---------------------------------------- 1029 1030 ecp_response = ecp.Response(assertion_consumer_service_url=acs_url) 1031 header = soapenv.Header() 1032 header.extension_elements = [element_to_extension_element(ecp_response)] 1033 1034 # ---------------------------------------- 1035 # <samlp:Response 1036 # ---------------------------------------- 1037 1038 response = self.create_authn_response( 1039 identity, 1040 in_response_to, 1041 destination, 1042 sp_entity_id, 1043 name_id_policy, 1044 userid, 1045 name_id, 1046 authn, 1047 issuer, 1048 sign_response, 1049 sign_assertion, 1050 sign_alg=sign_alg, 1051 digest_alg=digest_alg 1052 ) 1053 body = soapenv.Body() 1054 body.extension_elements = [element_to_extension_element(response)] 1055 1056 soap_envelope = soapenv.Envelope(header=header, body=body) 1057 1058 return str(soap_envelope) 1059 1060 def close(self): 1061 self.ident.close() 1062 1063 def clean_out_user(self, name_id): 1064 """ 1065 Remove all authentication statements that belongs to a user identified 1066 by a NameID instance 1067 1068 :param name_id: NameID instance 1069 :return: The local identifier for the user 1070 """ 1071 1072 lid = self.ident.find_local_id(name_id) 1073 logger.info("Clean out %s", lid) 1074 1075 # remove the authentications 1076 try: 1077 for _nid in [decode(x) for x in self.ident.db[lid].split(" ")]: 1078 try: 1079 self.session_db.remove_authn_statements(_nid) 1080 except KeyError: 1081 pass 1082 except KeyError: 1083 pass 1084 1085 return lid 1086