def test_1(self): server = Server("idp_slo_redirect_conf") req_id, request = _logout_request("sp_slo_redirect_conf") print(request) bindings = [BINDING_HTTP_REDIRECT] response = server.create_logout_response(request, bindings) binding, destination = server.pick_binding("single_logout_service", bindings, "spsso", request) http_args = server.apply_binding(binding, "%s" % response, destination, "relay_state", response=True) assert len(http_args) == 4 assert http_args["headers"][0][0] == "Location" assert http_args["data"] == []
def test_1(self): server = Server("idp_slo_redirect_conf") req_id, request = _logout_request("sp_slo_redirect_conf") print(request) bindings = [BINDING_HTTP_REDIRECT] response = server.create_logout_response(request, bindings) binding, destination = server.pick_binding("single_logout_service", bindings, "spsso", request) http_args = server.apply_binding(binding, "%s" % response, destination, "relay_state", response=True) assert len(http_args) == 4 assert http_args["headers"][0][0] == "Location" assert http_args["data"] == []
def test_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") binding, destination = sp.pick_binding("manage_name_id_service", entity_id=idp.config.entityid) nameid = NameID(format=NAMEID_FORMAT_TRANSIENT, text="foobar") newid = NewID(text="Barfoo") mid, mreq = sp.create_manage_name_id_request(destination, name_id=nameid, new_id=newid) print(mreq) rargs = sp.apply_binding(binding, "%s" % mreq, destination, "") # --------- @IDP -------------- _req = idp.parse_manage_name_id_request(rargs["data"], binding) print((_req.message)) mnir = idp.create_manage_name_id_response(_req.message, None) if binding != BINDING_SOAP: binding, destination = idp.pick_binding("manage_name_id_service", entity_id=sp.config.entityid) else: destination = "" respargs = idp.apply_binding(binding, "%s" % mnir, destination, "") print(respargs) # ---------- @SP --------------- _response = sp.parse_manage_name_id_request_response( respargs["data"], binding) print((_response.response)) assert _response.response.id == mnir.id
def test_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") binding, destination = sp.pick_binding("manage_name_id_service", entity_id=idp.config.entityid) nameid = NameID(format=NAMEID_FORMAT_TRANSIENT, text="foobar") newid = NewID(text="Barfoo") mid, midq = sp.create_manage_name_id_request(destination, name_id=nameid, new_id=newid) print midq rargs = sp.apply_binding(binding, "%s" % midq, destination, "") # --------- @IDP -------------- _req = idp.parse_manage_name_id_request(rargs["data"], binding) print _req.message mnir = idp.create_manage_name_id_response(_req.message, [binding]) if binding != BINDING_SOAP: binding, destination = idp.pick_binding("manage_name_id_service", entity_id=sp.config.entityid) else: destination = "" respargs = idp.apply_binding(binding, "%s" % mnir, destination, "") print respargs # ---------- @SP --------------- _response = sp.parse_manage_name_id_request_response(respargs["data"], binding) print _response.response assert _response.response.id == mnir.id
def test_handle_logout_soap(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") policy = NameIDPolicy(format=NAMEID_FORMAT_TRANSIENT, sp_name_qualifier=sp.config.entityid, allow_create="true") name_id = idp.ident.construct_nameid("subject", name_id_policy=policy) binding, destination = idp.pick_binding("single_logout_service", [BINDING_SOAP], entity_id=sp.config.entityid) rid, request = idp.create_logout_request(destination, idp.config.entityid, name_id=name_id) args = idp.apply_binding(BINDING_SOAP, "%s" % request, destination) # register the user session_info = { "name_id": name_id, "issuer": idp.config.entityid, "not_on_or_after": in_a_while(minutes=15), "ava": { "givenName": "Anders", "surName": "Andersson", "mail": "*****@*****.**" } } sp.users.add_information_about_person(session_info) reply_args = sp.handle_logout_request(args["data"], name_id, binding, sign=False) print(reply_args) assert reply_args["method"] == "POST" assert reply_args["headers"] == [('content-type', 'application/soap+xml')] #if __name__ == "__main__": # test_handle_logout_soap()
def test_basic_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") # -------- @IDP ------------- relay_state = "FOO" # -- dummy request --- orig_req = AuthnRequest(issuer=sp._issuer(), name_id_policy=NameIDPolicy( allow_create="true", format=NAMEID_FORMAT_TRANSIENT)) # == Create an AuthnRequest response name_id = idp.ident.transient_nameid("id12", sp.config.entityid) binding, destination = idp.pick_binding("assertion_consumer_service", entity_id=sp.config.entityid) resp = idp.create_authn_response( { "eduPersonEntitlement": "Short stop", "surName": "Jeter", "givenName": "Derek", "mail": "*****@*****.**", "title": "The man" }, "id-123456789", destination, sp.config.entityid, name_id=name_id, authn=AUTHN) hinfo = idp.apply_binding(binding, "%s" % resp, destination, relay_state) # --------- @SP ------------- xmlstr = get_msg(hinfo, binding) aresp = sp.parse_authn_request_response(xmlstr, binding, {resp.in_response_to: "/"}) # == Look for assertion X asid = aresp.assertion.id binding, destination = sp.pick_binding("assertion_id_request_service", entity_id=idp.config.entityid) hinfo = sp.apply_binding(binding, asid, destination) # ---------- @IDP ------------ aid = get_msg(hinfo, binding, response=False) # == construct response resp = idp.create_assertion_id_request_response(aid) hinfo = idp.apply_binding(binding, "%s" % resp, None, "", response=True) # ----------- @SP ------------- xmlstr = get_msg(hinfo, binding, response=True) final = sp.parse_assertion_id_request_response(xmlstr, binding) print final.response assert isinstance(final.response, Assertion)
class IdpServer(object): ticket = {} responses = {} challenges = {} _binding_mapping = { 'http-redirect': BINDING_HTTP_REDIRECT, 'http-post': BINDING_HTTP_POST } _endpoint_types = ['single_sign_on_service', 'single_logout_service'] _spid_levels = [ 'https://www.spid.gov.it/SpidL1', 'https://www.spid.gov.it/SpidL2', 'https://www.spid.gov.it/SpidL3' ] _spid_attributes = { 'primary': { 'spidCode': 'xs:string', 'name': 'xs:string', 'familyName': 'xs:string', 'placeOfBirth': 'xs:string', 'countryOfBirth': 'xs:string', 'dateOfBirth': 'xs:date', 'gender': 'xs:string', 'companyName': 'xs:string', 'registeredOffice': 'xs:string', 'fiscalNumber': 'xs:string', 'ivaCode': 'xs:string', 'idCard': 'xs:string', }, 'secondary': { 'mobilePhone': 'xs:string', 'email': 'xs:string', 'address': 'xs:string', 'expirationDate': 'xs:date', 'digitalAddress': 'xs:string' # PEC } } CHALLENGES_TIMEOUT = 30 # seconds def __init__(self, app, config, *args, **kwargs): """ :param app: Flask instance :param config: dictionary containing the configuration :param args: :param kwargs: """ # bind Flask app self.app = app self.user_manager = JsonUserManager() # setup self._config = config self.app.secret_key = 'sosecret' handler = RotatingFileHandler('spid.log', maxBytes=500000, backupCount=1) self.app.logger.addHandler(handler) self._prepare_server() @property def _mode(self): return 'https' if self._config.get('https', False) else 'http' def _idp_config(self): """ Process pysaml2 configuration """ key_file_path = self._config.get('key_file') cert_file_path = self._config.get('cert_file') metadata = self._config.get('metadata') metadata = metadata if metadata else [] existing_key = os.path.isfile(key_file_path) if key_file_path else None existing_cert = os.path.isfile( cert_file_path) if cert_file_path else None if not existing_key: raise BadConfiguration( 'Chiave privata dell\'IdP di test non trovata: {} non trovato'. format(key_file_path)) if not existing_cert: raise BadConfiguration( 'Certificato dell\'IdP di test non trovato: {} non trovato'. format(cert_file_path)) self.entity_id = self._config.get('hostname') if not self.entity_id: self.entity_id = self._config.get('host') self.entity_id = '{}://{}'.format(self._mode, self.entity_id) port = self._config.get('port') if port: self.entity_id = '{}:{}'.format(self.entity_id, port) idp_conf = { "entityid": self.entity_id, "description": "Spid Test IdP", "service": { "idp": { "name": "Spid Testenv", "endpoints": { "single_sign_on_service": [], "single_logout_service": [], }, "policy": { "default": { "name_form": NAME_FORMAT_BASIC, }, }, "name_id_format": [ NAMEID_FORMAT_TRANSIENT, ] }, }, "debug": 1, "key_file": self._config.get('key_file'), "cert_file": self._config.get('cert_file'), "metadata": metadata, "organization": { "display_name": "Spid testenv", "name": "Spid testenv", "url": "http://www.example.com", }, "contact_person": [ { "contact_type": "technical", "given_name": "support", "sur_name": "support", "email_address": "*****@*****.**" }, ], "logger": { "rotating": { "filename": "idp.log", "maxBytes": 500000, "backupCount": 1, }, "loglevel": "debug", } } # setup services url for _service_type in self._endpoint_types: endpoint = self._config['endpoints'][_service_type] idp_conf['service']['idp']['endpoints'][_service_type].append( ('{}{}'.format(self.entity_id, endpoint), BINDING_HTTP_REDIRECT)) idp_conf['service']['idp']['endpoints'][_service_type].append( ('{}{}'.format(self.entity_id, endpoint), BINDING_HTTP_POST)) return idp_conf def _setup_app_routes(self): """ Setup Flask routes """ # Setup SSO and SLO endpoints endpoints = self._config.get('endpoints') if endpoints: for ep_type in self._endpoint_types: _url = endpoints.get(ep_type) if _url: if not _url.startswith('/'): raise BadConfiguration( 'Errore nella configurazione delle url, i path devono essere relativi ed iniziare con "/" (slash) - url {}' .format(_url)) for _binding in self._binding_mapping.keys(): self.app.add_url_rule(_url, '{}_{}'.format( ep_type, _binding), getattr(self, ep_type), methods=[ 'GET', ]) self.app.add_url_rule('/login', 'login', self.login, methods=[ 'POST', 'GET', ]) # Endpoint for user add action self.app.add_url_rule('/add-user', 'add_user', self.add_user, methods=[ 'GET', 'POST', ]) self.app.add_url_rule('/continue-response', 'continue_response', self.continue_response, methods=[ 'POST', ]) self.app.add_url_rule('/metadata', 'metadata', self.metadata, methods=['POST', 'GET']) def _prepare_server(self): """ Setup server """ self.idp_config = Saml2Config() self.BASE = '{}://{}:{}'.format(self._mode, self._config.get('host'), self._config.get('port')) if 'entityid' not in self._config: # as fallback for entityid use host:port string self._config['entityid'] = self.BASE self.idp_config.load(cnf=self._idp_config()) self.server = Server(config=self.idp_config) self._setup_app_routes() # setup custom methods in order to # prepare the login form and verify the challenge (optional) # for every spid level (1-2-3) self.authn_broker = AuthnBroker() for index, _level in enumerate(self._spid_levels): self.authn_broker.add( authn_context_class_ref(_level), getattr(self, '_verify_spid_{}'.format(index + 1))) def _verify_spid_1(self, verify=False, **kwargs): self.app.logger.debug('spid level 1 - verifica ({})'.format(verify)) return self._verify_spid(1, verify, **kwargs) def _verify_spid_2(self, verify=False, **kwargs): self.app.logger.debug('spid level 2 - verifica ({})'.format(verify)) return self._verify_spid(2, verify, **kwargs) def _verify_spid_3(self, verify=False, **kwargs): self.app.logger.debug('spid level 3 - verifica ({})'.format(verify)) return self._verify_spid(3, verify, **kwargs) def _verify_spid(self, level=1, verify=False, **kwargs): """ :param level: integer, SPID level :param verify: boolean, if True verify spid extra challenge (otp etc.), if False prepare the challenge :param kwargs: dictionary, extra arguments """ if verify: # Verify the challenge if level == 2: # spid level 2 otp = kwargs.get('data').get('otp') key = kwargs.get('key') if key and key not in self.challenges or not otp: return False total_seconds = (datetime.now() - self.challenges[key][1]).total_seconds() # Check that opt value is equal and not expired if self.challenges[key][ 0] != otp or total_seconds > self.CHALLENGES_TIMEOUT: del self.challenges[key] return False return True else: # Prepare the challenge if level == 2: # spid level 2 # very simple otp implementation, while opt is a random 6 digits string # with a lifetime setup in the server instance key = kwargs.get('key') otp = ''.join(random.choice(string.digits) for _ in range(6)) self.challenges[key] = [otp, datetime.now()] extra_challenge = '<span>Otp ({})</span><input type="text" name="otp" />'.format( otp) else: extra_challenge = '' return extra_challenge def unpack_args(self, elems): """ Unpack arguments from request """ return dict([(k, v) for k, v in elems.items()]) def _raise_error(self, msg, extra=None): """ Raise some error using 'abort' function from Flask :param msg: string for error type :param extra: optional string for error details """ abort(Response(error_table.format(msg, extra), 200)) def _check_saml_message_restrictions(self, obj): # TODO: Implement here or somewhere (e.g. mixin on pysaml2 subclasses) # the logic to validate spid rules on saml entities raise NotImplementedError def _store_request(self, authnreq): """ Store authnrequest in a dictionary :param authnreq: authentication request string """ self.app.logger.debug('store_request: {}'.format(authnreq)) key = sha1(authnreq.xmlstr).hexdigest() # store the AuthnRequest self.ticket[key] = authnreq return key def single_sign_on_service(self): """ Process Http-Redirect or Http-POST request :param request: Flask request object """ self.app.logger.info("Http-Redirect") # Unpack parameters saml_msg = self.unpack_args(request.args) try: _key = session['request_key'] req_info = self.ticket[_key] except KeyError as e: try: binding = self._get_binding('single_sign_on_service', request) # Parse AuthnRequest req_info = self.server.parse_authn_request( saml_msg["SAMLRequest"], binding) authn_req = req_info.message except KeyError as err: self.app.logger.debug(str(err)) self._raise_error('Parametro SAMLRequest assente.') if not req_info: self._raise_error('Processo di parsing del messaggio fallito.') self.app.logger.debug('AuthnRequest: {}'.format(authn_req)) # Check if it is signed if "SigAlg" in saml_msg and "Signature" in saml_msg: # Signed request self.app.logger.debug('Messaggio SAML firmato.') issuer_name = authn_req.issuer.text _certs = self.server.metadata.certs(issuer_name, "any", "signing") verified_ok = False for cert in _certs: self.app.logger.debug('security backend: {}'.format( self.server.sec.sec_backend.__class__.__name__)) # Check signature if verify_redirect_signature(saml_msg, self.server.sec.sec_backend, cert): verified_ok = True break if not verified_ok: self._raise_error( 'Verifica della firma del messaggio fallita.') # Perform login key = self._store_request(req_info) relay_state = saml_msg.get('RelayState', '') session['request_key'] = key session['relay_state'] = relay_state return redirect(url_for('login')) def _get_binding(self, endpoint_type, request): try: endpoint = request.endpoint binding = endpoint.split('{}_'.format(endpoint_type))[1] return self._binding_mapping.get(binding) except IndexError: pass @property def _spid_main_fields(self): """ Returns a list of spid main attributes """ return self._spid_attributes['primary'].keys() @property def _spid_secondary_fields(self): """ Returns a list of spid secondary attributes """ return self._spid_attributes['secondary'].keys() def add_user(self): """ Add user endpoint """ spid_main_fields = self._spid_main_fields spid_secondary_fields = self._spid_secondary_fields _fields = '<br><b>{}</b><br>'.format('Primary attributes') for _field_name in spid_main_fields: _fields = '{}<span>{}</span> <input type="text" name={} /><br>'.format( _fields, _field_name, _field_name) _fields = '{}<br><b>{}</b><br>'.format(_fields, 'Secondary attributes') for _field_name in spid_secondary_fields: _fields = '{}<span>{}</span> <input type="text" name={} /><br>'.format( _fields, _field_name, _field_name) if request.method == 'GET': return FORM_ADD_USER.format('/add-user', _fields), 200 elif request.method == 'POST': username = request.form.get('username') password = request.form.get('password') sp = request.form.get('service_provider') if not username or not password or not sp: abort(400) extra = {} for spid_field in spid_main_fields: spid_value = request.form.get(spid_field) if spid_value: extra[spid_field] = spid_value for spid_field in spid_secondary_fields: spid_value = request.form.get(spid_field) if spid_value: extra[spid_field] = spid_value self.user_manager.add(username, password, sp, extra) return 'Added a new user', 200 def login(self): """ Login endpoint (verify user credentials) """ key = session['request_key'] if 'request_key' in session else None relay_state = session['relay_state'] if 'relay_state' in session else '' self.app.logger.debug('Request key: {}'.format(key)) if key and key in self.ticket: authn_request = self.ticket[key] sp_id = authn_request.message.issuer.text destination = authn_request.message.assertion_consumer_service_url spid_level = authn_request.message.requested_authn_context.authn_context_class_ref[ 0].text authn_info = self.authn_broker.pick( authn_request.message.requested_authn_context) callback, reference = authn_info[0] if request.method == 'GET': # inject extra data in form login based on spid level extra_challenge = callback(**{'key': key}) return FORM_LOGIN.format(url_for('login'), key, relay_state, extra_challenge), 200 # verify optional challenge based on spid level verified = callback(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 attribute_statement_on_response = self._config.get( 'attribute_statement_on_response') identity = user['attrs'] AUTHN = {"class_ref": spid_level, "authn_auth": spid_level} _data = dict(identity=identity, userid=user_id, in_response_to=authn_request.message.id, destination=destination, sp_entity_id=sp_id, authn=AUTHN, issuer=self.server.config.entityid, sign_alg=SIGN_ALG, digest_alg=DIGEST_ALG, sign_assertion=True) response = self.server.create_authn_response(**_data) http_args = self.server.apply_binding( BINDING_HTTP_POST, response, destination, response=True, sign=True, relay_state=relay_state) # Setup confirmation page data ast = Assertion(identity) policy = self.server.config.getattr("policy", "idp") ast.acs = self.server.config.getattr( "attribute_converters", "idp") res = ast.apply_policy(sp_id, policy, self.server.metadata) attrs = res.keys() attrs_list = '' for _attr in attrs: attrs_list = '{}<tr><td>{}</td></tr>'.format( attrs_list, _attr) self.responses[key] = http_args['data'] return CONFIRM_PAGE.format(attrs_list, '/continue-response', key), 200 abort(403) def continue_response(self): key = request.form['request_key'] if key and key in self.ticket and key in self.responses: return self.responses[key], 200 abort(403) def single_logout_service(self): """ SLO endpoint :param binding: 'redirect' is http-redirect, 'post' is http-post binding """ self.app.logger.debug("req: '%s'", request) saml_msg = self.unpack_args(request.args) _binding = self._get_binding('single_logout_service', request) req_info = self.server.parse_logout_request(saml_msg['SAMLRequest'], _binding) msg = req_info.message response = self.server.create_logout_response( msg, [BINDING_HTTP_POST, BINDING_HTTP_REDIRECT], sign_alg=SIGN_ALG, digest_alg=DIGEST_ALG, sign=True) binding, destination = self.server.pick_binding( "single_logout_service", [BINDING_HTTP_POST, BINDING_HTTP_REDIRECT], "spsso", req_info) http_args = self.server.apply_binding(binding, "%s" % response, destination, response=True, sign=True) return http_args['data'], 200 def metadata(self): metadata = create_metadata_string( __file__, self.server.config, ) return Response(metadata, mimetype='text/xml') @property def _wsgiconf(self): _cnf = { 'host': self._config.get('host', '0.0.0.0'), 'port': self._config.get('port', '8000'), 'debug': self._config.get('debug', True), } if self._config.get('https', False): key = self._config.get('https_key_file') cert = self._config.get('https_cert_file') if not key or not cert: raise KeyError( 'Errore modalità https: Chiave e/o certificato assenti!') _cnf['ssl_context'] = ( cert, key, ) return _cnf def start(self): """ Start the server instance """ self.app.run(**self._wsgiconf)
class IdPHandlerViewMixin(ErrorHandler): """ Contains some methods used by multiple views """ def dispatch(self, request, *args, **kwargs): """ Construct IDP server with config from settings dict """ conf = IdPConfig() try: conf.load(copy.deepcopy(settings.SAML_IDP_CONFIG)) self.IDP = Server(config=conf) except Exception as e: return self.handle_error(request, exception=e) return super().dispatch(request, *args, **kwargs) def set_sp(self, sp_entity_id): """ Saves SP info to instance variable Raises an exception if sp matching the given entity id cannot be found. """ self.sp = {'id': sp_entity_id} try: self.sp['config'] = settings.SAML_IDP_SPCONFIG[sp_entity_id] except KeyError: msg = _("No config for SP {} defined in SAML_IDP_SPCONFIG").format(sp_entity_id) raise ImproperlyConfigured(msg) def set_processor(self): """ Instantiate user-specified processor or default to an all-access base processor. Raises an exception if the configured processor class can not be found or initialized. """ processor_string = self.sp['config'].get('processor', None) if processor_string: try: self.processor = import_string(processor_string)(self.sp['id']) return except Exception as e: msg = _("Failed to instantiate processor: {} - {}") logger.error(msg.format(processor_string,e), exc_info=True) raise ImproperlyConfigured(_(msg.format(processor_string, e), exc_info=True)) self.processor = BaseProcessor(self.sp['id']) def verify_request_signature(self, req_info): """ Signature verification for authn request signature_check is at saml2.sigver.SecurityContext.correctly_signed_authn_request """ # TODO: Add unit tests for this if not req_info.signature_check(req_info.xmlstr): raise ValueError(_("Message signature verification failure")) def check_access(self, request): """ Check if user has access to the service of this SP """ if not self.processor.has_access(request): raise PermissionDenied(_("You do not have access to this resource")) def get_authn(self, req_info=None): if req_info: req_authn_context = req_info.message.requested_authn_context else: req_authn_context = PASSWORD broker = AuthnBroker() broker.add(authn_context_class_ref(req_authn_context), "") return broker.get_authn_by_accr(req_authn_context) def build_authn_response(self, user, authn, resp_args): """ pysaml2 server.Server.create_authn_response wrapper """ self.sp['name_id_format'] = resp_args.get('name_id_policy').format idp_name_id_format_list = self.IDP.config.getattr("name_id_format", "idp") # name_id format availability if idp_name_id_format_list and not self.sp['name_id_format']: name_id_format = idp_name_id_format_list[0] elif self.sp['name_id_format'] and not idp_name_id_format_list: name_id_format = self.sp['name_id_format'] elif self.sp['name_id_format'] not in idp_name_id_format_list: return self.handle_error(request, exception=_('SP requested a name_id_format ' 'that is not supported in the IDP')) elif self.sp['name_id_format'] in idp_name_id_format_list: name_id_format = self.sp['name_id_format'] else: name_id_format = NAMEID_FORMAT_UNSPECIFIED # if SP doesn't request a specific name_id_format... if not self.sp['name_id_format']: self.sp['name_id_format'] = name_id_format user_id = self.processor.get_user_id(user, self.sp, self.IDP.config) name_id = NameID(format=self.sp['name_id_format'], sp_name_qualifier=self.sp['id'], text=user_id) user_attrs = self.processor.create_identity(user, self.sp) # ASSERTION ENCRYPTED enrypt_response = getattr(settings, 'SAML_ENCRYPT_AUTHN_RESPONSE', False) if 'encrypt_saml_responses' in self.sp['config'].keys(): enrypt_response = self.sp['config'].get('encrypt_saml_responses') authn_resp = self.IDP.create_authn_response( authn=authn, identity=user_attrs, userid=user_id, name_id=name_id, # signature sign_response=self.sp['config'].get("sign_response") or \ self.IDP.config.getattr("sign_response", "idp") or \ False, sign_assertion=self.sp['config'].get("sign_assertion") or \ self.IDP.config.getattr("sign_assertion", "idp") or \ False, # default will be sha1 in pySAML2 sign_alg=self.sp['config'].get("signing_algorithm") or \ getattr(settings, 'SAML_AUTHN_SIGN_ALG', False), digest_alg=self.sp['config'].get("digest_algorithm") or \ getattr(settings, 'SAML_AUTHN_DIGEST_ALG', False), # Encryption encrypt_assertion=enrypt_response, encrypted_advice_attributes=enrypt_response, **resp_args ) return authn_resp def create_html_response(self, request, binding, authn_resp, destination, relay_state): """ Login form for SSO """ if binding == BINDING_HTTP_POST: context = { "acs_url": destination, "saml_response": base64.b64encode(authn_resp.encode()).decode(), "relay_state": relay_state, } template = "saml_login.html" html_response = render_to_string(template, context=context, request=request) else: http_args = self.IDP.apply_binding( binding=binding, msg_str=authn_resp, destination=destination, relay_state=relay_state, response=True) logger.debug('http args are: %s' % http_args) html_response = http_args['data'] return html_response def render_response(self, request, html_response): """ Return either as redirect to MultiFactorView or as html with self-submitting form. """ if not hasattr(self, 'processor'): # In case of SLO, where processor isn't relevant return HttpResponse(html_response) request.session['saml_data'] = html_response # Generate request session stuff needed for user agreement screen attrs_to_exclude = self.sp['config'].get('user_agreement_attr_exclude', []) + \ getattr(settings, "SAML_IDP_USER_AGREEMENT_ATTR_EXCLUDE", []) request.session['identity'] = { k: v for k, v in self.processor.create_identity(request.user, self.sp).items() if k not in attrs_to_exclude } request.session['sp_display_info'] = { 'display_name': self.sp['config'].get('display_name', self.sp['id']), 'display_description': self.sp['config'].get('display_description'), 'display_agreement_message': self.sp['config'].get('display_agreement_message') } request.session['sp_entity_id'] = self.sp['id'] # Conditions for showing user agreement screen user_agreement_enabled_for_sp = self.sp['config'].get('show_user_agreement_screen', getattr(settings, "SAML_IDP_SHOW_USER_AGREEMENT_SCREEN")) try: agreement_for_sp = AgreementRecord.objects.get(user=request.user, sp_entity_id=self.sp['id']) if agreement_for_sp.is_expired() or \ agreement_for_sp.wants_more_attrs(request.session['identity'].keys()): agreement_for_sp.delete() already_agreed = False else: already_agreed = True except AgreementRecord.DoesNotExist: already_agreed = False # Multifactor goes before user agreement because might result in user not being authenticated if self.processor.enable_multifactor(request.user): logger.debug("Redirecting to process_multi_factor") return HttpResponseRedirect(reverse('djangosaml2idp:saml_multi_factor')) # If we are here, there's no multifactor. Check whether to show user agreement if user_agreement_enabled_for_sp and not already_agreed: logger.debug("Redirecting to process_user_agreement") return HttpResponseRedirect(reverse('djangosaml2idp:saml_user_agreement')) # No multifactor or user agreement logger.debug("Performing SAML redirect") return HttpResponse(html_response)
class SamlIDP(service.Service): def __init__(self, environ, start_response, conf, cache, incomming): """ Constructor for the class. :param environ: WSGI environ :param start_response: WSGI start response function :param conf: The SAML configuration :param cache: Cache with active sessions """ service.Service.__init__(self, environ, start_response) self.response_bindings = None self.idp = Server(config=conf, cache=cache) self.incomming = incomming def verify_request(self, query, binding): """ Parses and verifies the SAML Authentication Request :param query: The SAML authn request, transport encoded :param binding: Which binding the query came in over :returns: dictionary """ if not query: logger.info("Missing QUERY") resp = Unauthorized('Unknown user') return {"response": resp(self.environ, self.start_response)} req_info = self.idp.parse_authn_request(query, binding) logger.info("parsed OK") _authn_req = req_info.message logger.debug("%s" % _authn_req) # Check that I know where to send the reply to try: binding_out, destination = self.idp.pick_binding( "assertion_consumer_service", bindings=self.response_bindings, entity_id=_authn_req.issuer.text, request=_authn_req) except Exception as err: logger.error("Couldn't find receiver endpoint: %s" % err) raise logger.debug("Binding: %s, destination: %s" % (binding_out, destination)) resp_args = {} try: resp_args = self.idp.response_args(_authn_req) _resp = None except UnknownPrincipal as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) except UnsupportedBinding as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) req_args = {} for key in [ "subject", "name_id_policy", "conditions", "requested_authn_context", "scoping", "force_authn", "is_passive" ]: try: val = getattr(_authn_req, key) except AttributeError: pass else: req_args[key] = val return { "resp_args": resp_args, "response": _resp, "authn_req": _authn_req, "req_args": req_args } def handle_authn_request(self, binding_in): """ Deal with an authentication request :param binding_in: Which binding was used when receiving the query :return: A response if an error occurred or session information in a dictionary """ _request = self.unpack(binding_in) _binding_in = service.INV_BINDING_MAP[binding_in] try: _dict = self.verify_request(_request["SAMLRequest"], _binding_in) except UnknownPrincipal as excp: logger.error("UnknownPrincipal: %s" % (excp, )) resp = ServiceError("UnknownPrincipal: %s" % (excp, )) return resp(self.environ, self.start_response) except UnsupportedBinding as excp: logger.error("UnsupportedBinding: %s" % (excp, )) resp = ServiceError("UnsupportedBinding: %s" % (excp, )) return resp(self.environ, self.start_response) _binding = _dict["resp_args"]["binding"] if _dict["response"]: # An error response http_args = self.idp.apply_binding( _binding, "%s" % _dict["response"], _dict["resp_args"]["destination"], _request["RelayState"], response=True) logger.debug("HTTPargs: %s" % http_args) return self.response(_binding, http_args) else: return self.incomming(_dict, self, self.environ, self.start_response, _request["RelayState"]) def construct_authn_response(self, identity, name_id, authn, resp_args, relay_state, sign_response=True): """ :param identity: :param name_id: :param authn: :param resp_args: :param relay_state: :param sign_response: :return: """ _resp = self.idp.create_authn_response(identity, name_id=name_id, authn=authn, sign_response=sign_response, **resp_args) http_args = self.idp.apply_binding(resp_args["binding"], "%s" % _resp, resp_args["destination"], relay_state, response=True) logger.debug("HTTPargs: %s" % http_args) resp = None if http_args["data"]: resp = Response(http_args["data"], headers=http_args["headers"]) else: for header in http_args["headers"]: if header[0] == "Location": resp = Redirect(header[1]) if not resp: resp = ServiceError("Don't know how to return response") return resp(self.environ, self.start_response) def register_endpoints(self): """ Given the configuration, return a set of URL to function mappings. """ url_map = [] for endp, binding in self.idp.config.getattr( "endpoints", "idp")["single_sign_on_service"]: p = urlparse(endp) url_map.append( ("^%s/(.*)$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) url_map.append( ("^%s$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) return url_map
class IdPHandlerViewMixin: """ Contains some methods used by multiple views """ error_view = import_string( getattr(settings, 'SAML_IDP_ERROR_VIEW_CLASS', 'djangosaml2idp.error_views.SamlIDPErrorView')) def handle_error(self, request, **kwargs): return self.error_view.as_view()(request, **kwargs) def dispatch(self, request, *args, **kwargs): """ Construct IDP server with config from settings dict """ conf = IdPConfig() try: conf.load(copy.deepcopy(settings.SAML_IDP_CONFIG)) self.IDP = Server(config=conf) except Exception as e: return self.handle_error(request, exception=e) return super().dispatch(request, *args, **kwargs) def set_sp(self, sp_entity_id): """ Saves SP info to instance variable Raises an exception if sp matching the given entity id cannot be found. """ self.sp = {'id': sp_entity_id} try: self.sp['config'] = settings.SAML_IDP_SPCONFIG[sp_entity_id] except KeyError: raise ImproperlyConfigured( "No config for SP {} defined in SAML_IDP_SPCONFIG".format( sp_entity_id)) def set_processor(self): """ Instantiate user-specified processor or default to an all-access base processor. Raises an exception if the configured processor class can not be found or initialized. """ processor_string = self.sp['config'].get('processor', None) if processor_string: try: self.processor = import_string(processor_string)(self.sp['id']) return except Exception as e: logger.error("Failed to instantiate processor: {} - {}".format( processor_string, e), exc_info=True) raise e self.processor = BaseProcessor(self.sp['id']) def get_authn(self, req_info=None): req_authn_context = req_info.message.requested_authn_context if req_info else PASSWORD broker = AuthnBroker() broker.add(authn_context_class_ref(req_authn_context), "") return broker.get_authn_by_accr(req_authn_context) def build_authn_response(self, user, authn, resp_args): name_id_formats = [resp_args.get('name_id_policy').format ] or self.IDP.config.getattr( "name_id_format", "idp") or [NAMEID_FORMAT_UNSPECIFIED] authn_resp = self.IDP.create_authn_response( authn=authn, identity=self.processor.create_identity(user, self.sp['config']), userid=self.processor.get_user_id(user, self.sp['config']), name_id=NameID(format=name_id_formats[0], sp_name_qualifier=self.sp['id'], text=self.processor.get_user_id( user, self.sp['config'])), sign_response=self.sp['config'].get("sign_response") or self.IDP.config.getattr("sign_response", "idp") or False, sign_assertion=self.sp['config'].get("sign_assertion") or self.IDP.config.getattr("sign_assertion", "idp") or False, **resp_args) return authn_resp def create_html_response(self, request, binding, authn_resp, destination, relay_state): """ Login form for SSO """ if binding == BINDING_HTTP_POST: context = { "acs_url": destination, "saml_response": base64.b64encode(authn_resp.encode()).decode(), "relay_state": relay_state, } html_response = render_to_string("djangosaml2idp/login.html", context=context, request=request) else: http_args = self.IDP.apply_binding(binding=binding, msg_str=authn_resp, destination=destination, relay_state=relay_state, response=True) logger.debug('http args are: %s' % http_args) html_response = http_args['data'] return html_response def render_response(self, request, html_response): """ Return either as redirect to MultiFactorView or as html with self-submitting form. """ if self.processor.enable_multifactor(request.user): # Store http_args in session for after multi factor is complete request.session['saml_data'] = html_response logger.debug("Redirecting to process_multi_factor") return HttpResponseRedirect(reverse('saml_multi_factor')) logger.debug("Performing SAML redirect") return HttpResponse(html_response)
class SamlIDP(service.Service): def __init__(self, environ, start_response, conf, cache, incomming, tid1_to_tid2, tid2_to_tid1, encmsg_to_iv, tid_handler, force_persistant_nameid, force_no_userid_subject_cacheing, idp=None): """ Constructor for the class. :param environ: WSGI environ :param start_response: WSGI start response function :param conf: The SAML configuration :param cache: Cache with active sessions """ service.Service.__init__(self, environ, start_response) self.response_bindings = None if idp is None: self.idp = Server(config=conf, cache=cache) else: self.idp = idp self.incomming = incomming self.tid1_to_tid2 = tid1_to_tid2 self.tid2_to_tid1 = tid2_to_tid1 self.encmsg_to_iv = encmsg_to_iv self.tid_handler = tid_handler self.force_persistant_nameid = force_persistant_nameid self.force_no_userid_subject_cacheing = force_no_userid_subject_cacheing def verify_request(self, query, binding): """ Parses and verifies the SAML Authentication Request :param query: The SAML authn request, transport encoded :param binding: Which binding the query came in over :returns: dictionary """ if not query: logger.info("Missing QUERY") resp = Unauthorized('Unknown user') return {"response": resp} req_info = self.idp.parse_authn_request(query, binding) encrypt_cert = encrypt_cert_from_item(req_info.message) logger.info("parsed OK") _authn_req = req_info.message logger.debug("%s" % _authn_req) # Check that I know where to send the reply to try: binding_out, destination = self.idp.pick_binding( "assertion_consumer_service", bindings=self.response_bindings, entity_id=_authn_req.issuer.text, request=_authn_req) except Exception as err: logger.error("Couldn't find receiver endpoint: %s" % err) raise logger.debug("Binding: %s, destination: %s" % (binding_out, destination)) resp_args = {} try: resp_args = self.idp.response_args(_authn_req) _resp = None except UnknownPrincipal as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) except UnsupportedBinding as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) req_args = {} for key in ["subject", "name_id_policy", "conditions", "requested_authn_context", "scoping", "force_authn", "is_passive"]: try: val = getattr(_authn_req, key) except AttributeError: pass else: if val is not None: req_args[key] = val return {"resp_args": resp_args, "response": _resp, "authn_req": _authn_req, "req_args": req_args, "encrypt_cert": encrypt_cert} def handle_authn_request(self, binding_in): """ Deal with an authentication request :param binding_in: Which binding was used when receiving the query :return: A response if an error occurred or session information in a dictionary """ _request = self.unpack(binding_in) _binding_in = service.INV_BINDING_MAP[binding_in] try: _dict = self.verify_request(_request["SAMLRequest"], _binding_in) except UnknownPrincipal as excp: logger.error("UnknownPrincipal: %s" % (excp,)) resp = ServiceError("UnknownPrincipal: %s" % (excp,)) return resp except UnsupportedBinding as excp: logger.error("UnsupportedBinding: %s" % (excp,)) resp = ServiceError("UnsupportedBinding: %s" % (excp,)) return resp _binding = _dict["resp_args"]["binding"] if _dict["response"]: # An error response http_args = self.idp.apply_binding( _binding, "%s" % _dict["response"], _dict["resp_args"]["destination"], _request["RelayState"], response=True) logger.debug("HTTPargs: %s" % http_args) return self.response(_binding, http_args) else: return self.incomming(_dict, self, self.environ, self.start_response, _request["RelayState"]) def get_tid1_resp(self, org_resp): tid1 = org_resp.assertion.subject.name_id.text return tid1 def get_sp_entityid(self, resp_args): sp_entityid = resp_args["destination"] return sp_entityid def get_tid2_enc(self, tid1, sp_entityid): iv = None if self.encmsg_to_iv is not None: iv = self.tid_handler.get_new_iv() tid2_enc = self.tid_handler.tid2_encrypt(tid1, sp_entityid, iv=iv) if self.encmsg_to_iv is not None: self.encmsg_to_iv[tid2_enc] = iv return tid2_enc def get_tid2_hash(self, tid1, sp_entityid): tid2_hash = self.tid_handler.tid2_hash(tid1, sp_entityid) return tid2_hash def handle_tid(self, tid1, tid2): if self.tid1_to_tid2 is not None: self.tid1_to_tid2[tid1] = tid2 if self.tid2_to_tid1 is not None: self.tid2_to_tid1[tid2] = tid1 def name_id_exists(self, userid, name_id_policy, sp_entity_id): try: snq = name_id_policy.sp_name_qualifier except AttributeError: snq = sp_entity_id if not snq: snq = sp_entity_id kwa = {"sp_name_qualifier": snq} try: kwa["format"] = name_id_policy.format except AttributeError: pass return self.idp.ident.find_nameid(userid, **kwa) def construct_authn_response(self, identity, userid, authn, resp_args, relay_state, name_id=None, sign_response=True, org_resp=None, org_xml_response=None): """ :param identity: :param name_id: :param authn: :param resp_args: :param relay_state: :param sign_response: :return: """ if self.force_persistant_nameid: if "name_id_policy" in resp_args: resp_args["name_id_policy"].format = NAMEID_FORMAT_PERSISTENT sp_entityid = self.get_sp_entityid(resp_args) tid1 = self.get_tid1_resp(org_resp) userid = self.tid_handler.uid_hash(tid1) if self.force_no_userid_subject_cacheing: self.idp.ident = IdentDB({}) name_id_exist = False if self.name_id_exists(userid, resp_args["name_id_policy"], resp_args["sp_entity_id"]): name_id_exist = True if not name_id_exist: if identity is not None: identity["uid"] = userid if self.tid2_to_tid1 is None: tid2 = self.get_tid2_enc(tid1, sp_entityid) else: tid2 = self.get_tid2_hash(tid1, sp_entityid) else: tid2 = None _resp = self.idp.create_authn_response(identity, userid=userid, name_id=name_id, authn=authn, sign_response=False, **resp_args) if not name_id_exist: #Fix for name_id so sid2 is used instead. _resp.assertion.subject.name_id.text = tid2 self.idp.ident.remove_local(userid) self.idp.ident.store(userid, _resp.assertion.subject.name_id) tid2 = _resp.assertion.subject.name_id.text if self.tid2_to_tid1 is not None: self.tid2_to_tid1[tid2] = tid1 if self.tid1_to_tid2 is not None: self.tid1_to_tid2[tid1] = tid2 advice = None for tmp_assertion in org_resp.response.assertion: if tmp_assertion.advice is not None: advice = tmp_assertion.advice break if advice is not None: _resp.assertion.advice = advice #_resp.assertion = [] if sign_response: _class_sign = class_name(_resp) _resp.signature = pre_signature_part(_resp.id, self.idp.sec.my_cert, 1) _resp = self.idp.sec.sign_statement(_resp, _class_sign, node_id=_resp.id) http_args = self.idp.apply_binding( resp_args["binding"], "%s" % _resp, resp_args["destination"], relay_state, response=True) logger.debug("HTTPargs: %s" % http_args) resp = None if http_args["data"]: resp = Response(http_args["data"], headers=http_args["headers"]) else: for header in http_args["headers"]: if header[0] == "Location": resp = Redirect(header[1]) if not resp: resp = ServiceError("Don't know how to return response") return resp def register_endpoints(self): """ Given the configuration, return a set of URL to function mappings. """ url_map = [] for endp, binding in self.idp.config.getattr("endpoints", "idp")[ "single_sign_on_service"]: p = urlparse(endp) url_map.append(("^%s/(.*)$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) url_map.append(("^%s$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) return url_map
def test_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") relay_state = "FOO" # -- dummy request --- orig_req = AuthnRequest(issuer=sp._issuer(), name_id_policy=NameIDPolicy( allow_create="true", format=NAMEID_FORMAT_TRANSIENT)) # == Create an AuthnRequest response name_id = idp.ident.transient_nameid(sp.config.entityid, "id12") binding, destination = idp.pick_binding("assertion_consumer_service", entity_id=sp.config.entityid) resp = idp.create_authn_response( { "eduPersonEntitlement": "Short stop", "surName": "Jeter", "givenName": "Derek", "mail": "*****@*****.**", "title": "The man" }, "id-123456789", destination, sp.config.entityid, name_id=name_id, authn=AUTHN) hinfo = idp.apply_binding(binding, "%s" % resp, destination, relay_state) # ------- @SP ---------- xmlstr = get_msg(hinfo, binding) aresp = sp.parse_authn_request_response(xmlstr, binding, {resp.in_response_to: "/"}) binding, destination = sp.pick_binding("authn_query_service", entity_id=idp.config.entityid) authn_context = requested_authn_context(INTERNETPROTOCOLPASSWORD) subject = aresp.assertion.subject aq_id, aq = sp.create_authn_query(subject, destination, authn_context) print(aq) assert isinstance(aq, AuthnQuery) binding = BINDING_SOAP hinfo = sp.apply_binding(binding, "%s" % aq, destination, "state2") # -------- @IDP ---------- xmlstr = get_msg(hinfo, binding) pm = idp.parse_authn_query(xmlstr, binding) msg = pm.message assert msg.id == aq.id p_res = idp.create_authn_query_response(msg.subject, msg.session_index, msg.requested_authn_context) print(p_res) hinfo = idp.apply_binding(binding, "%s" % p_res, "", "state2", response=True) # ------- @SP ---------- xmlstr = get_msg(hinfo, binding) final = sp.parse_authn_query_response(xmlstr, binding) print(final) assert final.response.id == p_res.id
def test_basic_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") # -------- @IDP ------------- relay_state = "FOO" # -- dummy request --- orig_req = AuthnRequest( issuer=sp._issuer(), name_id_policy=NameIDPolicy( allow_create="true", format=NAMEID_FORMAT_TRANSIENT)) # == Create an AuthnRequest response name_id = idp.ident.transient_nameid("id12", sp.config.entityid) binding, destination = idp.pick_binding("assertion_consumer_service", entity_id=sp.config.entityid) resp = idp.create_authn_response({"eduPersonEntitlement": "Short stop", "surName": "Jeter", "givenName": "Derek", "mail": "*****@*****.**", "title": "The man"}, "id-123456789", destination, sp.config.entityid, name_id=name_id, authn=AUTHN) hinfo = idp.apply_binding(binding, "%s" % resp, destination, relay_state) # --------- @SP ------------- xmlstr = get_msg(hinfo, binding) aresp = sp.parse_authn_request_response(xmlstr, binding, {resp.in_response_to: "/"}) # == Look for assertion X asid = aresp.assertion.id binding, destination = sp.pick_binding("assertion_id_request_service", entity_id=idp.config.entityid) hinfo = sp.apply_binding(binding, asid, destination) # ---------- @IDP ------------ aid = get_msg(hinfo, binding, response=False) # == construct response resp = idp.create_assertion_id_request_response(aid) hinfo = idp.apply_binding(binding, "%s" % resp, None, "", response=True) # ----------- @SP ------------- xmlstr = get_msg(hinfo, binding, response=True) final = sp.parse_assertion_id_request_response(xmlstr, binding) print((final.response)) assert isinstance(final.response, Assertion)
class IdPHandlerViewMixin: """ Contains some methods used by multiple views """ error_view = import_string(getattr(settings, 'SAML_IDP_ERROR_VIEW_CLASS', 'djangosaml2idp.error_views.SamlIDPErrorView')) def handle_error(self, request, exception, **kwargs): if not getattr(settings, 'SAML_IDP_HANDLE_ERRORS', True): raise exception # Log the exception and the statuscode kwargs['exception'] = exception logger.error(kwargs) return self.error_view.as_view()(request, **kwargs) def dispatch(self, request, *args, **kwargs): """ Construct IDP server with config from settings dict """ conf = IdPConfig() try: conf.load(copy.deepcopy(settings.SAML_IDP_CONFIG)) self.IDP = Server(config=conf) except Exception as e: return self.handle_error(request, exception=e) return super().dispatch(request, *args, **kwargs) def get_sp_config(self, sp_entity_id): """ Get a dict with the configuration for a SP according to the SAML_IDP_SPCONFIG settings. Raises an exception if no SP matching the given entity id can be found. """ d = {'id': sp_entity_id} try: d['config'] = settings.SAML_IDP_SPCONFIG[sp_entity_id] except KeyError: raise ImproperlyConfigured(_("No config for SP {} defined in SAML_IDP_SPCONFIG").format(sp_entity_id)) return d def get_processor(self, sp_entity_id: str, processor_class_path: str) -> BaseProcessor: """ Instantiate user-specified processor or default to an all-access base processor. Raises an exception if the processor class can not be found or initialized. """ if processor_class_path: try: processor_cls = import_string(processor_class_path) except ImportError as e: msg = _("Failed to import processor class {}").format(processor_class_path) logger.error(msg, exc_info=True) raise ImproperlyConfigured(msg) from e else: processor_cls = BaseProcessor try: processor_instance = processor_cls(sp_entity_id) except Exception as e: msg = _("Failed to instantiate processor: {} - {}").format(processor_cls, e) logger.error(msg, exc_info=True) raise return processor_instance def verify_request_signature(self, req_info): """ Signature verification for authn request signature_check is at saml2.sigver.SecurityContext.correctly_signed_authn_request """ # TODO: Add unit tests for this if not req_info.signature_check(req_info.xmlstr): raise ValueError(_("Message signature verification failure")) def check_access(self, processor, request): """ Check if user has access to the service of this SP. Raises a PermissionDenied exception if not. """ if not processor.has_access(request): raise PermissionDenied(_("You do not have access to this resource")) def get_authn(self, req_info=None): req_authn_context = req_info.message.requested_authn_context if req_info else PASSWORD broker = AuthnBroker() broker.add(authn_context_class_ref(req_authn_context), "") return broker.get_authn_by_accr(req_authn_context) def build_authn_response(self, user, authn, resp_args, processor: BaseProcessor, sp_config: dict): """ pysaml2 server.Server.create_authn_response wrapper """ policy = resp_args.get('name_id_policy', None) if policy is None: sp_config['name_id_format'] = NAMEID_FORMAT_UNSPECIFIED else: sp_config['name_id_format'] = policy.format idp_name_id_format_list = self.IDP.config.getattr("name_id_format", "idp") or [NAMEID_FORMAT_UNSPECIFIED] if sp_config['name_id_format'] not in idp_name_id_format_list: raise ImproperlyConfigured(_('SP requested a name_id_format that is not supported in the IDP')) user_id = processor.get_user_id(user, sp_config, self.IDP.config) name_id = NameID(format=sp_config['name_id_format'], sp_name_qualifier=sp_config['id'], text=user_id) authn_resp = self.IDP.create_authn_response( authn=authn, identity=processor.create_identity(user, sp_config.get('attribute_mapping')), name_id=name_id, userid=user_id, sp_entity_id=sp_config['id'], # Signing sign_response=sp_config['config'].get("sign_response") or self.IDP.config.getattr("sign_response", "idp") or False, sign_assertion=sp_config['config'].get("sign_assertion") or self.IDP.config.getattr("sign_assertion", "idp") or False, sign_alg=sp_config['config'].get("signing_algorithm") or getattr(settings, "SAML_AUTHN_SIGN_ALG", xmldsig.SIG_RSA_SHA256), digest_alg=sp_config['config'].get("digest_algorithm") or getattr(settings, "SAML_AUTHN_DIGEST_ALG", xmldsig.DIGEST_SHA256), # Encryption encrypt_assertion=sp_config['config'].get('encrypt_saml_responses') or getattr(settings, 'SAML_ENCRYPT_AUTHN_RESPONSE', False), encrypted_advice_attributes=sp_config['config'].get('encrypt_saml_responses') or getattr(settings, 'SAML_ENCRYPT_AUTHN_RESPONSE', False), **resp_args ) return authn_resp def render_login_html_to_string(self, context=None, request=None, using=None): """ Render the html response for the login action. Can be using a custom html template if set on the view. """ default_login_template_name = 'djangosaml2idp/login.html' custom_login_template_name = getattr(self, 'login_html_template', None) if custom_login_template_name: try: template = get_template(custom_login_template_name, using=using) except (TemplateDoesNotExist, TemplateSyntaxError) as e: logger.error('Specified template {} cannot be used due to: {}. Falling back to default login template'.format(custom_login_template_name, str(e))) template = get_template(default_login_template_name, using=using) else: template = get_template(default_login_template_name, using=using) return template.render(context, request) def create_html_response(self, request, binding, authn_resp, destination, relay_state): """ Login form for SSO """ if binding == BINDING_HTTP_POST: context = { "acs_url": destination, "saml_response": base64.b64encode(str(authn_resp).encode()).decode(), "relay_state": relay_state, } html_response = { "data": self.render_login_html_to_string(context=context, request=request), "type": "POST", } else: http_args = self.IDP.apply_binding( binding=binding, msg_str=authn_resp, destination=destination, relay_state=relay_state, response=True) logger.debug('http args are: %s' % http_args) html_response = { "data": http_args['headers'][0][1], "type": "REDIRECT", } return html_response def render_response(self, request, html_response, processor: BaseProcessor = None): """ Return either a response as redirect to MultiFactorView or as html with self-submitting form to log in. """ if not processor: # In case of SLO, where processor isn't relevant if html_response['type'] == 'POST': return HttpResponse(html_response['data']) else: return HttpResponseRedirect(html_response['data']) request.session['saml_data'] = html_response if processor.enable_multifactor(request.user): logger.debug("Redirecting to process_multi_factor") return HttpResponseRedirect(reverse('djangosaml2idp:saml_multi_factor')) # No multifactor logger.debug("Performing SAML redirect") if html_response['type'] == 'POST': return HttpResponse(html_response['data']) else: return HttpResponseRedirect(html_response['data'])
class SamlIDP(service.Service): def __init__(self, environ, start_response, conf, cache, incoming): """ Constructor for the class. :param environ: WSGI environ :param start_response: WSGI start response function :param conf: The SAML configuration :param cache: Cache with active sessions """ service.Service.__init__(self, environ, start_response) self.response_bindings = None self.idp = Server(config=conf, cache=cache) self.incoming = incoming def verify_request(self, query, binding): """ Parses and verifies the SAML Authentication Request :param query: The SAML authn request, transport encoded :param binding: Which binding the query came in over :returns: dictionary """ if not query: logger.info("Missing QUERY") resp = Unauthorized('Unknown user') return {"response": resp(self.environ, self.start_response)} req_info = self.idp.parse_authn_request(query, binding) logger.info("parsed OK") _authn_req = req_info.message logger.debug("%s" % _authn_req) # Check that I know where to send the reply to. try: binding_out, destination = self.idp.pick_binding( "assertion_consumer_service", bindings=self.response_bindings, entity_id=_authn_req.issuer.text, request=_authn_req) except Exception as err: logger.error("Couldn't find receiver endpoint: %s" % err) raise logger.debug("Binding: %s, destination: %s" % (binding_out, destination)) resp_args = {} try: resp_args = self.idp.response_args(_authn_req) _resp = None except UnknownPrincipal as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) except UnsupportedBinding as excp: _resp = self.idp.create_error_response(_authn_req.id, destination, excp) req_args = {} for key in ["subject", "name_id_policy", "conditions", "requested_authn_context", "scoping", "force_authn", "is_passive"]: try: val = getattr(_authn_req, key) except AttributeError: pass else: if val is not None: req_args[key] = val return {"resp_args": resp_args, "response": _resp, "authn_req": _authn_req, "req_args": req_args} def handle_authn_request(self, binding_in): """ Deal with an authentication request :param binding_in: Which binding was used when receiving the query :return: A response if an error occurred or session information in a dictionary """ _request = self.unpack(binding_in) _binding_in = service.INV_BINDING_MAP[binding_in] try: _dict = self.verify_request(_request["SAMLRequest"], _binding_in) except UnknownPrincipal as excp: logger.error("UnknownPrincipal: %s" % (excp,)) resp = ServiceError("UnknownPrincipal: %s" % (excp,)) return resp(self.environ, self.start_response) except UnsupportedBinding as excp: logger.error("UnsupportedBinding: %s" % (excp,)) resp = ServiceError("UnsupportedBinding: %s" % (excp,)) return resp(self.environ, self.start_response) _binding = _dict["resp_args"]["binding"] if _dict["response"]: # An error response. http_args = self.idp.apply_binding( _binding, "%s" % _dict["response"], _dict["resp_args"]["destination"], _request["RelayState"], response=True) logger.debug("HTTPargs: %s" % http_args) return self.response(_binding, http_args) else: return self.incoming(_dict, self.environ, self.start_response, _request["RelayState"]) def construct_authn_response(self, identity, name_id, authn, resp_args, relay_state, sign_response=True): """ :param identity: :param name_id: :param authn: :param resp_args: :param relay_state: :param sign_response: :return: """ _resp = self.idp.create_authn_response(identity, name_id=name_id, authn=authn, sign_response=sign_response, **resp_args) http_args = self.idp.apply_binding( resp_args["binding"], "%s" % _resp, resp_args["destination"], relay_state, response=True) logger.debug("HTTPargs: %s" % http_args) resp = None if http_args["data"]: resp = Response(http_args["data"], headers=http_args["headers"]) else: for header in http_args["headers"]: if header[0] == "Location": resp = Redirect(header[1]) if not resp: resp = ServiceError("Don't know how to return response") return resp(self.environ, self.start_response) def register_endpoints(self): """ Given the configuration, return a set of URL to function mappings. """ url_map = [] idp_endpoints = self.idp.config.getattr("endpoints", "idp") for endp, binding in idp_endpoints["single_sign_on_service"]: p = urlparse(endp) url_map.append(("^%s/(.*)$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) url_map.append(("^%s$" % p.path[1:], ("IDP", "handle_authn_request", service.BINDING_MAP[binding]))) return url_map
def login_process(request): """ View which processes the actual SAML request and returns a self-submitting form with the SAML response. The login_required decorator ensures the user authenticates first on the IdP using 'normal' ways. """ # Construct server with config from settings dict conf = IdPConfig() conf.load(copy.deepcopy(settings.SAML_IDP_CONFIG)) IDP = Server(config=conf) # Parse incoming request try: req_info = IDP.parse_authn_request(request.session['SAMLRequest'], BINDING_HTTP_POST) except Exception as excp: return HttpResponseBadRequest(excp) # TODO this is taken from example, but no idea how this works or whats it does. Check SAML2 specification? # Signed request for HTTP-REDIRECT if "SigAlg" in request.session and "Signature" in request.session: _certs = IDP.metadata.certs(req_info.message.issuer.text, "any", "signing") verified_ok = False for cert in _certs: # TODO implement #if verify_redirect_signature(_info, IDP.sec.sec_backend, cert): # verified_ok = True # break pass if not verified_ok: return HttpResponseBadRequest( "Message signature verification failure") binding_out, destination = IDP.pick_binding( service="assertion_consumer_service", entity_id=req_info.message.issuer.text) # Gather response arguments try: resp_args = IDP.response_args(req_info.message) except (UnknownPrincipal, UnsupportedBinding) as excp: return HttpResponseServerError(excp) try: sp_config = settings.SAML_IDP_SPCONFIG[resp_args['sp_entity_id']] except Exception: raise ImproperlyConfigured( "No config for SP %s defined in SAML_IDP_SPCONFIG" % resp_args['sp_entity_id']) # Create user-specified processor or fallback to all-access base processor processor_string = sp_config.get('processor', None) if processor_string is None: processor = BaseProcessor else: processor_class = import_string(processor_string) processor = processor_class() # Check if user has access to the service of this SP if not processor.has_access(request.user): raise PermissionDenied("You do not have access to this resource") # Create Identity dict (SP-specific) sp_mapping = sp_config.get('attribute_mapping', {'username': '******'}) identity = processor.create_identity(request.user, sp_mapping) # TODO investigate how this works, because I don't get it. Specification? req_authn_context = req_info.message.requested_authn_context or PASSWORD AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "") # Construct SamlResponse message try: authn_resp = IDP.create_authn_response( identity=identity, userid=request.user.username, name_id=NameID(format=resp_args['name_id_policy'].format, sp_name_qualifier=destination, text=request.user.username), authn=AUTHN_BROKER.get_authn_by_accr(req_authn_context), sign_response=IDP.config.getattr("sign_response", "idp") or False, sign_assertion=IDP.config.getattr("sign_assertion", "idp") or False, **resp_args) except Exception as excp: return HttpResponseServerError(excp) # Return as html with self-submitting form. http_args = IDP.apply_binding(binding=binding_out, msg_str="%s" % authn_resp, destination=destination, relay_state=request.session['RelayState'], response=True) logger.debug('http args are: %s' % http_args) if processor.enable_multifactor(request.user): # Store http_args in session for after multi factor is complete request.session['saml_data'] = http_args['data'] logger.debug("Redirecting to process_multi_factor") return HttpResponseRedirect(reverse('saml_multi_factor')) else: logger.debug("Performing SAML redirect") return HttpResponse(http_args['data'])
def test_flow(): sp = Saml2Client(config_file="servera_conf") idp = Server(config_file="idp_all_conf") relay_state = "FOO" # -- dummy request --- orig_req = AuthnRequest( issuer=sp._issuer(), name_id_policy=NameIDPolicy(allow_create="true", format=NAMEID_FORMAT_TRANSIENT)) # == Create an AuthnRequest response name_id = idp.ident.transient_nameid(sp.config.entityid, "id12") binding, destination = idp.pick_binding("assertion_consumer_service", entity_id=sp.config.entityid) resp = idp.create_authn_response({"eduPersonEntitlement": "Short stop", "surName": "Jeter", "givenName": "Derek", "mail": "*****@*****.**", "title": "The man"}, "id-123456789", destination, sp.config.entityid, name_id=name_id, authn=AUTHN) hinfo = idp.apply_binding(binding, "%s" % resp, destination, relay_state) # ------- @SP ---------- xmlstr = get_msg(hinfo, binding) aresp = sp.parse_authn_request_response(xmlstr, binding, {resp.in_response_to: "/"}) binding, destination = sp.pick_binding("authn_query_service", entity_id=idp.config.entityid) authn_context = requested_authn_context(INTERNETPROTOCOLPASSWORD) subject = aresp.assertion.subject aq = sp.create_authn_query(subject, destination, authn_context) print aq assert isinstance(aq, AuthnQuery) binding = BINDING_SOAP hinfo = sp.apply_binding(binding, "%s" % aq, destination, "state2") # -------- @IDP ---------- xmlstr = get_msg(hinfo, binding) pm = idp.parse_authn_query(xmlstr, binding) msg = pm.message assert msg.id == aq.id p_res = idp.create_authn_query_response(msg.subject, msg.session_index, msg.requested_authn_context) print p_res hinfo = idp.apply_binding(binding, "%s" % p_res, "", "state2", response=True) # ------- @SP ---------- xmlstr = get_msg(hinfo, binding) final = sp.parse_authn_query_response(xmlstr, binding) print final assert final.response.id == p_res.id