def test_authn_2(): authn = AuthnBroker() target = "https://example.org/login" authn.add(AUTHNCTXT, target, 10, "https://example.org") result = authn.pick(REQAUTHNCTXT) assert len(result) == 1 method, reference = result[0] assert target == method
def test_authn_1(): ac = authn_context_class_ref(PASSWORDPROTECTEDTRANSPORT) rac = requested_authn_context(PASSWORDPROTECTEDTRANSPORT) authn = AuthnBroker() target = "https://example.org/login" authn.add(ac, target, 1, "http://www.example.com") result = authn.pick(rac) assert len(result) == 1 method, reference = result[0] assert target == method
def main(): global IDP global AUTHN_BROKER global LOOKUP global args sys.path.insert(0, os.getcwd()) from wsgiref.simple_server import make_server parser = argparse.ArgumentParser() parser.add_argument('-p', dest='path', help='Path to configuration file.') parser.add_argument('-v', dest='valid', help="How long, in days, the metadata is valid from the time of creation") parser.add_argument('-c', dest='cert', help='certificate') parser.add_argument('-i', dest='id', help="The ID of the entities descriptor") parser.add_argument('-k', dest='keyfile', help="A file with a key to sign the metadata with") parser.add_argument('-n', dest='name') parser.add_argument('-s', dest='sign', action='store_true', help="sign the metadata") parser.add_argument('-m', dest='mako_root', default="./") parser.add_argument(dest="config") args = parser.parse_args() AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), username_password_authn, 10, "http://%s" % socket.gethostname()) AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED), "", 0, "http://%s" % socket.gethostname()) CONFIG = importlib.import_module(args.config) IDP = server.Server(args.config, cache=Cache()) IDP.ticket = {} _rot = args.mako_root LOOKUP = TemplateLookup(directories=[_rot + 'templates', _rot + 'htdocs'], module_directory=_rot + 'modules', input_encoding='utf-8', output_encoding='utf-8') HOST = CONFIG.HOST PORT = CONFIG.PORT SRV = make_server(HOST, PORT, application) print "IdP listening on %s:%s" % (HOST, PORT) SRV.serve_forever()
def test_authn_3(): authn = AuthnBroker() level = 0 for ref in [AL1, AL2, AL3, AL4]: level += 4 ac = authn_context_class_ref(ref) authn.add(ac, REF2METHOD[ref], level, "https://www.example.com/%s" % "al%d" % level) rac = requested_authn_context(AL1, "minimum") info = authn.pick(rac) assert len(info) == 4 method, ref = info[0] assert REF2METHOD[AL1] == method rac = requested_authn_context(AL2, "minimum") info = authn.pick(rac) assert len(info) == 3 method, ref = info[0] assert REF2METHOD[AL2] == method rac = requested_authn_context(AL3, "minimum") info = authn.pick(rac) assert len(info) == 2 method, ref = info[0] assert REF2METHOD[AL3] == method rac = requested_authn_context(AL4, "minimum") info = authn.pick(rac) assert len(info) == 1 method, ref = info[0] assert REF2METHOD[AL4] == method rac = requested_authn_context(AL1, "exact") info = authn.pick(rac) assert len(info) == 1 method, ref = info[0] assert REF2METHOD[AL1] == method rac = requested_authn_context(AL1, "better") info = authn.pick(rac) assert len(info) == 3
def handle_authn_request(self, saml_request, relay_state, binding, userid): self.authn_req = self.idp.parse_authn_request(saml_request, binding) _encrypt_cert = encrypt_cert_from_item(self.authn_req.message) self.binding_out, self.destination = self.idp.pick_binding( "assertion_consumer_service", bindings=None, entity_id=self.authn_req.message.issuer.text, request=self.authn_req.message) resp_args = self.idp.response_args(self.authn_req.message) AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), username_password_authn_dummy, 10, "http://test.idp.se") AUTHN_BROKER.get_authn_by_accr(PASSWORD) resp_args["authn"] = AUTHN_BROKER.get_authn_by_accr(PASSWORD) _resp = self.idp.create_authn_response(TestIdP.USERS[userid], userid=userid, encrypt_cert=_encrypt_cert, encrypt_assertion_self_contained=True, encrypted_advice_attributes=True, **resp_args) kwargs = {} http_args = self.idp.apply_binding(BINDING_HTTP_POST, "%s" % _resp, self.destination, relay_state, response=True, **kwargs) action, body = get_post_action_body(http_args["data"][3]) return action, urllib.urlencode(body)
def outgoing(self, response, org_response, instance): """ An authentication response has been received and now an authentication response from this server should be constructed. :param response: The Authentication response :param instance: SP instance that received the authentication response :return: response """ _idp = self.create_SamlIDP(instance.environ, instance.start_response, self.outgoing) _state = instance.sp.state[response.in_response_to] orig_authn_req, relay_state, req_args = instance.sp.state[_state] # The Subject NameID try: subject = response.get_subject() except: pass resp_args = _idp.idp.response_args(orig_authn_req) try: _authn_info = response.authn_info()[0] AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(_authn_info[0]), username_password_authn_dummy, 0, self.issuer) _authn = AUTHN_BROKER.get_authn_by_accr(_authn_info[0]) #_authn = {"class_ref": _authn_info[0], "authn_auth": self.issuer} except: AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED), username_password_authn_dummy, 0, self.issuer) _authn = AUTHN_BROKER.get_authn_by_accr(UNSPECIFIED) identity = response.ava if identity is None and response.response.encrypted_assertion is not None: #Add dummy value identity = {"uid": "dummyuser"} # Will signed the response by default resp = _idp.construct_authn_response(identity, userid="dummyuser", authn=_authn, name_id=None, resp_args=resp_args, relay_state=relay_state, sign_response=True, org_resp=response, org_xml_response=org_response) return resp
def handle_auth_req(self, saml_request, relay_state, binding, userid, response_binding=BINDING_HTTP_POST): """ Handles a SAML request, validates and creates a SAML response. :type saml_request: str :type relay_state: str :type binding: str :type userid: str :rtype: :param saml_request: :param relay_state: RelayState is a parameter used by some SAML protocol implementations to identify the specific resource at the resource provider in an IDP initiated single sign on scenario. :param binding: :param userid: The user identification. :return: A tuple with """ auth_req = self.parse_authn_request(saml_request, binding) binding_out, destination = self.pick_binding( 'assertion_consumer_service', bindings=[response_binding], entity_id=auth_req.message.issuer.text, request=auth_req.message) resp_args = self.response_args(auth_req.message) authn_broker = AuthnBroker() authn_broker.add(authn_context_class_ref(PASSWORD), lambda: None, 10, 'unittest_idp.xml') authn_broker.get_authn_by_accr(PASSWORD) resp_args['authn'] = authn_broker.get_authn_by_accr(PASSWORD) _resp = self.create_authn_response(self.user_db[userid], userid=userid, **resp_args) if response_binding == BINDING_HTTP_POST: saml_response = base64.b64encode(str(_resp).encode("utf-8")) resp = {"SAMLResponse": saml_response, "RelayState": relay_state} elif response_binding == BINDING_HTTP_REDIRECT: http_args = self.apply_binding(response_binding, '%s' % _resp, destination, relay_state, response=True) resp = dict(parse_qsl(urlparse(dict(http_args["headers"])["Location"]).query)) return destination, resp
def __create_authn_response(self, saml_request, relay_state, binding, userid, response_binding=BINDING_HTTP_POST): """ Handles a SAML request, validates and creates a SAML response but does not apply the binding to encode it. :type saml_request: str :type relay_state: str :type binding: str :type userid: str :rtype: tuple [string, saml2.samlp.Response] :param saml_request: :param relay_state: RelayState is a parameter used by some SAML protocol implementations to identify the specific resource at the resource provider in an IDP initiated single sign on scenario. :param binding: :param userid: The user identification. :return: A tuple containing the destination and instance of saml2.samlp.Response """ auth_req = self.parse_authn_request(saml_request, binding) binding_out, destination = self.pick_binding( 'assertion_consumer_service', bindings=[response_binding], entity_id=auth_req.message.issuer.text, request=auth_req.message) resp_args = self.response_args(auth_req.message) authn_broker = AuthnBroker() authn_broker.add(authn_context_class_ref(PASSWORD), lambda: None, 10, 'unittest_idp.xml') authn_broker.get_authn_by_accr(PASSWORD) resp_args['authn'] = authn_broker.get_authn_by_accr(PASSWORD) resp = self.create_authn_response(self.user_db[userid], userid=userid, **resp_args) return destination, resp
def handle_auth_req(self, saml_request, relay_state, binding, userid): auth_req = self.parse_authn_request(saml_request, binding) binding_out, destination = self.pick_binding( 'assertion_consumer_service', entity_id=auth_req.message.issuer.text, request=auth_req.message) resp_args = self.response_args(auth_req.message) authn_broker = AuthnBroker() authn_broker.add(authn_context_class_ref(PASSWORD), lambda: None, 10, 'unittest_idp.xml') authn_broker.get_authn_by_accr(PASSWORD) resp_args['authn'] = authn_broker.get_authn_by_accr(PASSWORD) _resp = self.create_authn_response(self.user_db[userid], userid=userid, **resp_args) http_args = self.apply_binding(BINDING_HTTP_POST, '%s' % _resp, destination, relay_state, response=True) url = http_args['url'] saml_response = base64.b64encode(str(_resp).encode("utf-8")) resp = {'SAMLResponse': saml_response, 'RelayState': relay_state} return url, resp
def test_authn_3(): authn = AuthnBroker() level = 0 for ref in [AL1, AL2, AL3, AL4]: level += 4 ac = authn_context_class_ref(ref) authn.add(ac, REF2METHOD[ref], level, "https://www.example.com/%s" % "al%d" % level) rac = requested_authn_context(AL1, "minimum") info = authn.pick(rac) assert len(info) == 4 method, ref = info[0] assert REF2METHOD[AL1] == method rac = requested_authn_context(AL2, "minimum") info = authn.pick(rac) assert len(info) == 3 method, ref = info[0] assert REF2METHOD[AL2] == method rac = requested_authn_context(AL3, "minimum") info = authn.pick(rac) assert len(info) == 2 method, ref = info[0] assert REF2METHOD[AL3] == method rac = requested_authn_context(AL4, "minimum") info = authn.pick(rac) assert len(info) == 1 method, ref = info[0] assert REF2METHOD[AL4] == method rac = requested_authn_context(AL1, "exact") info = authn.pick(rac) assert len(info) == 1 method, ref = info[0] assert REF2METHOD[AL1] == method rac = requested_authn_context([AL1, AL2], "exact") info = authn.pick(rac) assert len(info) == 2 method, ref = info[0] assert REF2METHOD[AL1] == method method, ref = info[1] assert REF2METHOD[AL2] == method rac = requested_authn_context([AL3, AL2], "exact") info = authn.pick(rac) assert len(info) == 2 method, ref = info[0] assert REF2METHOD[AL3] == method method, ref = info[1] assert REF2METHOD[AL2] == method rac = requested_authn_context(AL1, "better") info = authn.pick(rac) assert len(info) == 3
def get(self, request, *args, **kwargs): # pylint: disable=missing-function-docstring, too-many-locals, unused-argument passed_data = request.POST if request.method == 'POST' else request.GET # get sp information from the parameters try: sp_entity_id = passed_data['sp'] except KeyError as excp: return self.handle_error(request, exception=excp, status=400) try: # sp_config = SAML_IDP_SPCONFIG[sp_entity_id] sp_config = { 'processor': 'djangosaml2idp.processors.BaseProcessor', 'attribute_mapping': { # DJANGO: SAML 'username': '******', 'email': 'email', 'name': 'first_name', 'is_boss': 'is_admin', 'token': 'token', } } except Exception: # pylint: disable=broad-except return self.handle_error( request, exception=ImproperlyConfigured( "No config for SP %s defined in SAML_IDP_SPCONFIG" % sp_entity_id), status=400) binding_out, destination = self.IDP.pick_binding( service="assertion_consumer_service", entity_id=sp_entity_id) processor = self.get_processor(sp_entity_id, sp_config) # Check if user has access to the service of this SP if not processor.has_access(request): return self.handle_error( request, exception=PermissionDenied( "You do not have access to this resource"), status=403) identity = self.get_identity(processor, request.user, sp_config) req_authn_context = PASSWORD AUTHN_BROKER = AuthnBroker() # pylint: disable=invalid-name AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "") user_id = processor.get_user_id(request.user) # Construct SamlResponse messages try: name_id_formats = self.IDP.config.getattr( "name_id_format", "idp") or [NAMEID_FORMAT_UNSPECIFIED] name_id = NameID(format=name_id_formats[0], text=user_id) authn = AUTHN_BROKER.get_authn_by_accr(req_authn_context) sign_response = self.IDP.config.getattr("sign_response", "idp") or False sign_assertion = self.IDP.config.getattr("sign_assertion", "idp") or False authn_resp = self.IDP.create_authn_response( identity=identity, in_response_to=None, destination=destination, sp_entity_id=sp_entity_id, userid=user_id, name_id=name_id, authn=authn, sign_response=sign_response, sign_assertion=sign_assertion, **passed_data) except Exception as excp: # pylint: disable=broad-except return self.handle_error(request, exception=excp, status=500) # Return as html with self-submitting form. http_args = self.IDP.apply_binding( binding=binding_out, msg_str="%s" % authn_resp, destination=destination, relay_state=passed_data['RelayState'], response=True) return HttpResponse(http_args['data'])
def get(self, request, *args, **kwargs): # pylint: disable=missing-function-docstring, unused-argument, too-many-locals binding = request.session.get('Binding', BINDING_HTTP_POST) # Parse incoming request try: req_info = self.IDP.parse_authn_request( request.session['SAMLRequest'], binding) except Exception as excp: # pylint: disable=broad-except return self.handle_error(request, exception=excp) # Signed request for HTTP-REDIRECT if "SigAlg" in request.session and "Signature" in request.session: _certs = self.IDP.metadata.certs(req_info.message.issuer.text, "any", "signing") verified_ok = False for cert in _certs: # pylint: disable=unused-variable # TODO implement # if verify_redirect_signature(_info, self.IDP.sec.sec_backend, cert): # verified_ok = True # break pass if not verified_ok: return self.handle_error( request, extra_message="Message signature verification failure", status=400) # Gather response arguments try: resp_args = self.IDP.response_args(req_info.message) except (UnknownPrincipal, UnsupportedBinding) as excp: return self.handle_error(request, exception=excp, status=400) try: # sp_config = SAML_IDP_SPCONFIG[resp_args['sp_entity_id']] sp_config = { 'processor': 'djangosaml2idp.processors.BaseProcessor', 'attribute_mapping': { # DJANGO: SAML 'email': 'email', 'private_email': 'private_email', 'username': '******', 'is_staff': 'is_staff', 'is_superuser': '******', 'token': 'token', }, } except Exception: # pylint: disable=broad-except return self.handle_error( request, exception=ImproperlyConfigured( "No config for SP %s defined in SAML_IDP_SPCONFIG" % resp_args['sp_entity_id']), status=400) processor = self.get_processor(resp_args['sp_entity_id'], sp_config) # Check if user has access to the service of this SP if not processor.has_access(request): return self.handle_error( request, exception=PermissionDenied( "You do not have access to this resource"), status=403) cookie_user = self.cookie_user(request) identity = self.get_identity(processor, cookie_user, sp_config) req_authn_context = req_info.message.requested_authn_context or PASSWORD AUTHN_BROKER = AuthnBroker() # pylint: disable=invalid-name AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "") user_id = processor.get_user_id(cookie_user) # Construct SamlResponse message try: authn_resp = self.IDP.create_authn_response( identity=identity, userid=user_id, name_id=NameID(format=resp_args['name_id_policy'].format, sp_name_qualifier=resp_args['sp_entity_id'], text=user_id), authn=AUTHN_BROKER.get_authn_by_accr(req_authn_context), sign_response=self.IDP.config.getattr("sign_response", "idp") or False, sign_assertion=self.IDP.config.getattr("sign_assertion", "idp") or False, **resp_args) except Exception as excp: # pylint: disable=broad-except return self.handle_error(request, exception=excp, status=500) http_args = self.IDP.apply_binding( binding=resp_args['binding'], msg_str="%s" % authn_resp, destination=resp_args['destination'], relay_state=request.session['RelayState'], response=True) logger.debug('http args are: %s' % http_args) # pylint: disable=logging-not-lazy return self.render_response(request, processor, http_args)
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)
def get_authn(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 get(self, request, *args, **kwargs): # Parse incoming request try: req_info = self.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 = self.IDP.metadata.certs(req_info.message.issuer.text, "any", "signing") verified_ok = False for cert in _certs: # TODO implement #if verify_redirect_signature(_info, self.IDP.sec.sec_backend, cert): # verified_ok = True # break pass if not verified_ok: return HttpResponseBadRequest("Message signature verification failure") binding_out, destination = self.IDP.pick_binding(service="assertion_consumer_service", entity_id=req_info.message.issuer.text) # Gather response arguments try: resp_args = self.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']) processor = self.get_processor(sp_config) # 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") identity = self.get_identity(processor, request.user, sp_config) # 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 = self.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=self.IDP.config.getattr("sign_response", "idp") or False, sign_assertion=self.IDP.config.getattr("sign_assertion", "idp") or False, **resp_args) except Exception as excp: return HttpResponseServerError(excp) http_args = self.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) return self.render_response(request, processor, http_args)
def main(): global IDP global AUTHN_BROKER global LOOKUP global args global CONFIG global USERS global PASSWD global EXTRA sys.path.insert(0, os.getcwd()) from wsgiref.simple_server import make_server parser = argparse.ArgumentParser() parser.add_argument('-p', dest='path', help='Path to configuration file.') parser.add_argument('-v', dest='valid', help="How long, in days, the metadata is valid from the time of creation") parser.add_argument('-c', dest='cert', help='certificate') parser.add_argument('-i', dest='id', help="The ID of the entities descriptor") parser.add_argument('-k', dest='keyfile', help="A file with a key to sign the metadata with") parser.add_argument('-n', dest='name') parser.add_argument('-s', dest='sign', action='store_true', help="sign the metadata") parser.add_argument('-m', dest='mako_root', default="./") parser.add_argument(dest="config") args = parser.parse_args() AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), username_password_authn, 10, "http://%s" % socket.gethostname()) AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED), "", 0, "http://%s" % socket.gethostname()) CONFIG = importlib.import_module(args.config) USERS = CONFIG.USERS PASSWD = CONFIG.PASSWD EXTRA = CONFIG.EXTRA IDP = server.Server(args.config, cache=Cache()) IDP.ticket = {} _rot = args.mako_root LOOKUP = TemplateLookup(directories=[_rot + 'htdocs', _rot + 'htdocs'], module_directory=_rot + 'modules', input_encoding='utf-8', output_encoding='utf-8') HOST = CONFIG.HOST PORT = CONFIG.PORT #SRV = make_server(HOST, PORT, application) SRV = wsgiserver.CherryPyWSGIServer(('0.0.0.0', PORT), application) make_server if CONFIG.HTTPS: SRV.ssl_adapter = ssl_pyopenssl.pyOpenSSLAdapter(CONFIG.SERVER_CERT, CONFIG.SERVER_KEY, CONFIG.CERT_CHAIN) print "IdP listening on %s:%s" % (HOST, PORT) try: SRV.start() except KeyboardInterrupt: SRV.stop()
def setup_authn_broker(self, base_url, sphandler, authorization): ab = AuthnBroker() sphandler.sp_authentication = SpAuthentication(self, sphandler) cas_auth = CasAuth(self, self.cas_server, self.service_url) password_auth = PasswordYubikeyAuth(self, self.passwd, password=True, yubikey=False) yubikey_auth = PasswordYubikeyAuth(self, self.passwd, password=False, yubikey=True) password_yubikey_auth = PasswordYubikeyAuth(self, self.passwd, password=True, yubikey=True) for authkey, value in authorization.items(): level = str(value[IdPHandler.AUTHORIZATION_WEIGHT]) url = value[IdPHandler.AUTHORIZATION_URL] acr = value[IdPHandler.AUTHORIZATION_ACR] user_info = value[IdPHandler.AUTHORIZATION_USER_INFO] if authkey == IdPHandler.AUTHORIZATION_SAML: sphandler.sp_authentication.user_info(user_info) ab.add(acr, sphandler.sp_authentication, level, url) elif authkey == IdPHandler.AUTHORIZATION_CAS: cas_auth.user_info(user_info) ab.add(acr, cas_auth, level, url) elif authkey == IdPHandler.AUTHORIZATION_PASSWORD_YUBIKEY: password_yubikey_auth.user_info(user_info) ab.add(acr, password_yubikey_auth, level, url) elif authkey == IdPHandler.AUTHORIZATION_PASSWORD: password_auth.user_info(user_info) ab.add(acr, password_auth, level, url) elif authkey == IdPHandler.AUTHORIZATION_YUBIKEY: yubikey_auth.user_info(user_info) ab.add(acr, yubikey_auth, level, url) elif authkey == IdPHandler.AUTHORIZATION_MULTIPLEAUTHN: authn_list = [] for m_items in value[IdPHandler.AUTHENTICATION_AUTHNLIST]: m_authkey = m_items[IdPHandler.AUTHORIZATION_ACR] if m_authkey == IdPHandler.AUTHORIZATION_SAML: authn_list.append(sphandler.sp_authentication) elif m_authkey == IdPHandler.AUTHORIZATION_CAS: authn_list.append(cas_auth) elif m_authkey == IdPHandler.AUTHORIZATION_PASSWORD_YUBIKEY: authn_list.append(password_yubikey_auth) elif m_authkey == IdPHandler.AUTHORIZATION_PASSWORD: authn_list.append(password_auth) elif m_authkey == IdPHandler.AUTHORIZATION_YUBIKEY: authn_list.append(yubikey_auth) ab.add(acr, MultipleAuthentication(self, authn_list, user_info), level, url) else: ab.add(authn_context_class_ref(UNSPECIFIED), UnspecifiedAuth(self), level, url) return ab
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.email, name_id=NameID(format=resp_args['name_id_policy'].format, sp_name_qualifier=destination, text=request.user.email), 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'])
class SSIXASAMLProvider(IdPServer): def __init__(self, config): baseurl = config['protocol'] + "://" + config['url'] log.info("Application baseurl: " + baseurl) # Update URLs with base for pro in SSIXASAMLProvider_SERVICE_EP: for serv in SSIXASAMLProvider_SERVICE_EP[pro]['endpoints']: if 'service' in serv: for ep in SSIXASAMLProvider_SERVICE_EP[pro]['endpoints'][ serv]: SSIXASAMLProvider_SERVICE_EP[pro]['endpoints'][serv][ 0] = baseurl + ep[0] # Update SAML config config['saml_config'].update({'service': SSIXASAMLProvider_SERVICE_EP}) cfg = SAMLIdpConfig(config['saml_config']) # Create SAML IdP server - Init super class after config preparation super(SSIXASAMLProvider, self).__init__(config=cfg, cache=Cache()) # Add authn broker self.authn_broker = AuthnBroker() self.userinfodb = SAMLUserInfoDB() i = BlockchainAuthMethod(baseurl + "/saml", config['proxymode'], config, self.userinfodb, config['trustmodel']) self.authn_broker.add(authn_context_class_ref(UNSPECIFIED), i, 3, {i.acr}) # Add attribute converters self.config.attribute_converters = ac_factory() # Add metadata self.metadata = self.create_metadata(config['saml_config']['metadata']) # Response bindings that are offered self.response_bindings = [BINDING_HTTP_POST, BINDING_HTTP_REDIRECT] # Default claims self.defaul_claims = copy.deepcopy( config['saml_config']['default_claims']) def create_metadata(self, metadata): # For database implementation #mds = MetaDataStoreDB(db, self.config.attribute_converters, self.config) #mds.load_from_database() mds = MetadataStore(self.config.attribute_converters, self.config) mds.imp(metadata) return mds def store_request(self, msg): log.debug("Message: " + str(msg)) key = sha1(msg["SAMLRequest"].encode()).hexdigest() log.debug("Key: " + str(key)) self.ticket[key] = msg return key def get_entityid_fromkey(self, key): log.debug("Key: " + str(key)) msg = self.ticket[key] req = self.parse_authn_request(msg["SAMLRequest"]) return req.message.issuer.text def get_claims_forkey(self, key): # ToDo entity specific claim retrieval return self.defaul_claims def sso_redirect_or_post(self, request, binding): assert type(request) == dict if "key" in request: msg = self.ticket[request["key"]] req = self.parse_authn_request(msg["SAMLRequest"], binding) del self.ticket[request["key"]] if req.message.force_authn is not None and req.message.force_authn.lower( ) == "true": key = self.store_request(msg) auth_info = self.authn_broker.pick(req.requested_authn_context) if len(auth_info) > 0: method, reference = auth_info[0] return method(key=key) else: log.debug("No authentication method found") return Unauthorized("No usable authentication method") return self.sso_operation(msg, binding, uid=request["pysaml"]) else: req_info = self.parse_authn_request(request["SAMLRequest"], binding) req = req_info.message key = self.store_request(request) auth_info = self.authn_broker.pick(req.requested_authn_context) if len(auth_info) > 0: method, reference = auth_info[0] log.debug("Authn chosen: " + str(method.acr)) return method(key=key) else: log.debug("No authentication method found") return Unauthorized("No authentication method found") def sso_operation(self, msg, binding, uid=None): log.debug("Msg: " + str(msg)) log.debug("Inbound binding: " + str(binding)) if not (msg and "SAMLRequest" in msg): return BadRequest("Error parsing request or no request") else: if "Signature" in msg: try: kwargs = { "signature": msg["Signature"], "sigalg": msg["SigAlg"] } except KeyError: return BadRequest( "Signature Algorithm specification is missing") else: kwargs = {} try: kwargs["encrypt_cert"] = encrypt_cert_from_item( msg["req_info"].message) except KeyError: pass try: kwargs["relay_state"] = msg["RelayState"] except KeyError: pass if not uid is None: kwargs["uid"] = uid return self.do(msg["SAMLRequest"], binding, **kwargs) def do(self, query, binding_in, relay_state="", encrypt_cert=None, **kwargs): log.debug("Query: " + str(query)) log.debug("Inbound binding: " + str(binding_in)) log.debug("Relay state: " + str(relay_state)) log.debug("Encrypt_cert: " + str(encrypt_cert)) log.debug("Kwargs: " + str(kwargs)) try: resp_args = self.verify_request(query, binding_in) except Exception as excp: log.exception("SSO request verification failed.") return ServiceError("Request verification failed.") # Get user data try: userid = self.cache.uid2user[kwargs["uid"]] identity = self.userinfodb[userid] log.debug("Identity: " + str(identity)) except Exception as excp: log.exception("Identity attributes retrieval failed") return ServiceError("Exception occurred") try: method = self.authn_broker.pick()[0] resp_args["authn"] = dict(blockchain=method) resp = self.create_authn_response( identity, userid=userid, encrypt_cert_assertion=encrypt_cert, **resp_args) except Exception as excp: log.exception("User data retrieval failed") return ServiceError("Exception occurred") kwargs = {} binding_out = resp_args['binding'] destination = resp_args['destination'] http_args = self.apply_binding(binding_out, "%s" % resp, destination, relay_state, response=True, **kwargs) return Response(http_args) def verify_request(self, query, binding): log.debug("Query: " + str(query)) log.debug("Binding: " + str(binding)) request = self.parse_authn_request(query, binding) authn_request = None if not request is None: authn_request = request.message resp_args = self.response_args(authn_request) log.debug("Response arguments: " + str(resp_args)) return resp_args def slo_redirect_or_post(self, query, binding): log.debug("Query: " + query) log.debug("Binding: " + binding) try: req_info = self.parse_logout_request(query, binding) except Exception as exc: log.exception("Message parsing failed.") return BadRequest("Message parsing failed") msg = req_info.message if msg.name_id: lid = self.ident.find_local_id(msg.name_id) if lid in self.cache.user2uid: uid = self.cache.user2uid[lid] if uid in self.cache.uid2user: del self.cache.uid2user[uid] del self.cache.user2uid[lid] try: self.session_db.remove_authn_statements(msg.name_id) except KeyError as exc: log.exception("Session removal failed") resp = self.create_logout_response(msg, [binding]) binding, destination = self.pick_binding("single_logout_service", [binding], "spsso", req_info) response = True try: hinfo = self.apply_binding(binding, "%s" % resp, destination, query['relay_state'], response=response) except Exception as exc: log.exception("ServiceError: %s", exc) return ServiceError("%s" % exc) if binding == BINDING_HTTP_REDIRECT: for key, value in hinfo["headers"]: if key.lower() == "location": return Redirect(value, headers=hinfo["headers"]) return ServiceError("missing Location header") else: return Response(hinfo["data"], headers=hinfo["headers"]) def stop(self): pass
def get(self, request, *args, **kwargs): passed_data = request.POST if request.method == 'POST' else request.GET # get sp information from the parameters try: sp_entity_id = passed_data['sp'] except KeyError as excp: return self.handle_error(request, exception=excp, status=400) try: sp_config = settings.SAML_IDP_SPCONFIG[sp_entity_id] except Exception: return self.handle_error( request, exception=ImproperlyConfigured( "No config for SP %s defined in SAML_IDP_SPCONFIG" % sp_entity_id), status=400) binding_out, destination = self.IDP.pick_binding( service="assertion_consumer_service", entity_id=sp_entity_id) processor = self.get_processor(sp_entity_id, sp_config) # Check if user has access to the service of this SP if not processor.has_access(request): return self.handle_error( request, exception=PermissionDenied( "You do not have access to this resource"), status=403) identity = self.get_identity(processor, request.user, sp_config) req_authn_context = PASSWORD AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "") user_id = processor.get_user_id(request.user) # Construct SamlResponse messages try: name_id_formats = self.IDP.config.getattr( "name_id_format", "idp") or [NAMEID_FORMAT_UNSPECIFIED] name_id = NameID(format=name_id_formats[0], text=user_id) authn = AUTHN_BROKER.get_authn_by_accr(req_authn_context) sign_response = self.IDP.config.getattr("sign_response", "idp") or False sign_assertion = self.IDP.config.getattr("sign_assertion", "idp") or False authn_resp = self.IDP.create_authn_response( identity=identity, in_response_to="IdP_Initiated_Login", destination=destination, sp_entity_id=sp_entity_id, userid=user_id, name_id=name_id, authn=authn, sign_response=sign_response, sign_assertion=sign_assertion, **passed_data) except Exception as excp: return self.handle_error(request, exception=excp, status=500) # Return as html with self-submitting form. http_args = self.IDP.apply_binding( binding=binding_out, msg_str="%s" % authn_resp, destination=destination, relay_state=passed_data['RelayState'], response=True) return HttpResponse(http_args['data'])
def get(self, request, *args, **kwargs): binding = request.session.get('Binding', BINDING_HTTP_POST) # Parse incoming request try: req_info = self.IDP.parse_authn_request( request.session['SAMLRequest'], binding) except Exception as excp: return self.handle_error(request, exception=excp) # Signed request for HTTP-REDIRECT if "SigAlg" in request.session and "Signature" in request.session: _certs = self.IDP.metadata.certs(req_info.message.issuer.text, "any", "signing") verified_ok = False for cert in _certs: # TODO implement # if verify_redirect_signature(_info, self.IDP.sec.sec_backend, cert): # verified_ok = True # break pass if not verified_ok: return self.handle_error( request, extra_message="Message signature verification failure", status=400) # Gather response arguments try: resp_args = self.IDP.response_args(req_info.message) except (UnknownPrincipal, UnsupportedBinding) as excp: return self.handle_error(request, exception=excp, status=400) try: sp_config = settings.SAML_IDP_SPCONFIG[resp_args['sp_entity_id']] except Exception: return self.handle_error( request, exception=ImproperlyConfigured( "No config for SP %s defined in SAML_IDP_SPCONFIG" % resp_args['sp_entity_id']), status=400) processor = self.get_processor(resp_args['sp_entity_id'], sp_config) # Check if user has access to the service of this SP if not processor.has_access(request): return self.handle_error( request, exception=PermissionDenied( "You do not have access to this resource"), status=403) identity = self.get_identity(processor, request.user, sp_config) req_authn_context = req_info.message.requested_authn_context or PASSWORD AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(req_authn_context), "") user_id = processor.get_user_id(request.user) # Construct SamlResponse message try: authn_resp = self.IDP.create_authn_response( identity=identity, userid=user_id, name_id=NameID(format=resp_args['name_id_policy'].format, sp_name_qualifier=resp_args['sp_entity_id'], text=user_id), authn=AUTHN_BROKER.get_authn_by_accr(req_authn_context), sign_response=self.IDP.config.getattr("sign_response", "idp") or False, sign_assertion=self.IDP.config.getattr("sign_assertion", "idp") or False, **resp_args) except Exception as excp: return self.handle_error(request, exception=excp, status=500) http_args = self.IDP.apply_binding( binding=resp_args['binding'], msg_str="%s" % authn_resp, destination=resp_args['destination'], relay_state=request.session['RelayState'], response=True) logger.debug('http args are: %s' % http_args) return self.render_response(request, processor, http_args)
help="The ID of the entities descriptor") parser.add_argument('-k', dest='keyfile', help="A file with a key to sign the metadata with") parser.add_argument('-n', dest='name') parser.add_argument('-s', dest='sign', action='store_true', help="sign the metadata") parser.add_argument('-m', dest='mako_root', default="./") parser.add_argument(dest="config") args = parser.parse_args() CONFIG = importlib.import_module(args.config) AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), username_password_authn, 10, CONFIG.BASE) AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED), "", 0, CONFIG.BASE) IDP = server.Server(args.config, cache=Cache()) IDP.ticket = {} _rot = args.mako_root LOOKUP = TemplateLookup(directories=[_rot + 'templates', _rot + 'htdocs'], module_directory=_rot + 'modules', input_encoding='utf-8', output_encoding='utf-8') HOST = CONFIG.HOST HOST = '0.0.0.0'
"the time of creation") parser.add_argument('-c', dest='cert', help='certificate') parser.add_argument('-i', dest='id', help="The ID of the entities descriptor") parser.add_argument('-k', dest='keyfile', help="A file with a key to sign the metadata with") parser.add_argument('-n', dest='name') parser.add_argument('-s', dest='sign', action='store_true', help="sign the metadata") parser.add_argument('-m', dest='mako_root', default="./") parser.add_argument(dest="config") args = parser.parse_args() CONFIG = importlib.import_module(args.config) AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), username_password_authn, 10, CONFIG.BASE) AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED), "", 0, CONFIG.BASE) IDP = server.Server(args.config, cache=Cache()) IDP.ticket = {} _rot = args.mako_root LOOKUP = TemplateLookup(directories=[_rot + 'templates', _rot + 'htdocs'], module_directory=_rot + 'modules', input_encoding='utf-8', output_encoding='utf-8') HOST = CONFIG.HOST
# allow uwsgi or gunicorn mount # by moving some initialization out of __name__ == '__main__' section. # uwsgi -s 0.0.0.0:8088 --protocol http --callable application --module idp args = type('Config', (object, ), {}) args.config = 'idp_conf' args.mako_root = './' args.path = None import socket from idp_user import USERS from idp_user import EXTRA from mako.lookup import TemplateLookup AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), username_password_authn, 10, "http://%s" % socket.gethostname()) AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED), "", 0, "http://%s" % socket.gethostname()) CONFIG = importlib.import_module(args.config) IDP = server.Server(args.config, cache=Cache()) IDP.ticket = {} # ---------------------------------------------------------------------------- if __name__ == '__main__': from wsgiref.simple_server import make_server parser = argparse.ArgumentParser() parser.add_argument('-p', dest='path', help='Path to configuration file.')
def get(self, request, *args, **kwargs): # pylint: disable=missing-function-docstring, unused-argument, too-many-locals resp_args = { 'in_response_to': self.IN_RESPONSE_TO, 'sp_entity_id': self.SP_ENTITY_ID, 'name_id_policy': saml.NAMEID_FORMAT_PERSISTENT, 'binding': BINDING_HTTP_POST, 'destination': self.DESTINATION, } sp_config = { 'processor': 'djangosaml2idp.processors.BaseProcessor', 'attribute_mapping': { 'username': '******', 'token': 'token', 'aliyun_sso_roles': self.CUSTOM_CONFIG['role'], 'uid': self.CUSTOM_CONFIG['role_session_name'], 'aliyun_sso_session_duration': self.CUSTOM_CONFIG['session_duration'], }, } processor = self.get_processor(resp_args['sp_entity_id'], sp_config) # Check if user has access to the service of this SP if not processor.has_access(request): return self.handle_error( request, exception=PermissionDenied( "You do not have access to this resource"), status=403) cookie_user = self.cookie_user(request) if not cookie_user.aliyun_sso_role.is_active: # 用户的角色SSO被禁用 return self.handle_error( request, exception=PermissionDenied("Your role SSO has been disabled"), status=403) identity = self.get_identity(processor, cookie_user, sp_config) # print('identity is', identity) AUTHN_BROKER = AuthnBroker() # pylint: disable=invalid-name AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), "") user_id = processor.get_user_id(cookie_user) # Construct SamlResponse message try: app = SAMLAPP.valid_objects.get( entity_id=resp_args['sp_entity_id']) _spsso_descriptor = entity_descriptor_from_string( app.xmldata).spsso_descriptor.pop() # pylint: disable=no-member authn_resp = self.IDP.create_authn_response( identity=identity, userid=user_id, name_id=NameID(format=resp_args['name_id_policy'], sp_name_qualifier=resp_args['sp_entity_id'], text=user_id), authn=AUTHN_BROKER.get_authn_by_accr(PASSWORD), sign_response=getattr(_spsso_descriptor, 'want_response_signed', '') == 'true', sign_assertion=getattr(_spsso_descriptor, 'want_assertions_signed', '') == 'true', **resp_args) except Exception as excp: # pylint: disable=broad-except return self.handle_error(request, exception=excp, status=500) # print('authn_resp is', authn_resp) http_args = self.IDP.apply_binding( binding=resp_args['binding'], msg_str="%s" % authn_resp, destination=resp_args['destination'], response=True) return HttpResponse(http_args['data'])
# allow uwsgi or gunicorn mount # by moving some initialization out of __name__ == '__main__' section. # uwsgi -s 0.0.0.0:8088 --protocol http --callable application --module idp args = type('Config', (object,), { }) args.config = 'idp_conf' args.mako_root = './' args.path = None import socket from idp_user import USERS from idp_user import EXTRA from mako.lookup import TemplateLookup AUTHN_BROKER = AuthnBroker() AUTHN_BROKER.add(authn_context_class_ref(PASSWORD), username_password_authn, 10, "http://%s" % socket.gethostname()) AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED), "", 0, "http://%s" % socket.gethostname()) IDP = server.Server(args.config, cache=Cache()) IDP.ticket = {} # ---------------------------------------------------------------------------- if __name__ == '__main__': from wsgiref.simple_server import make_server parser = argparse.ArgumentParser()
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 = SPID_LEVELS _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' } } # digitalAddress => PEC CHALLENGES_TIMEOUT = 30 # seconds SAML_VERSION = '2.0' 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(config=config) # 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() self.spid_parser = SpidParser() @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 [] if metadata: for typ in ['local', 'remote']: if metadata.get(typ) is None: metadata[typ] = [] 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('base_url') if not self.entity_id: raise BadConfiguration('base_url non impostato!') 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, '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, ep_type, getattr(self, ep_type), methods=['GET', 'POST']) self.app.add_url_rule('/', 'index', self.index, methods=['GET']) self.app.add_url_rule('/login', 'login', self.login, methods=['POST', 'GET']) # Endpoint for user add action self.app.add_url_rule('/users', 'users', self.users, 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()) setattr( self.idp_config, 'attribute_converters', ac_factory('testenv/attributemaps', **{'override_types': self._all_attributes})) self.server = SpidServer(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 _is_expired = total_seconds > self.CHALLENGES_TIMEOUT if self.challenges[key][0] != otp or _is_expired: 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( render_template("error.html", **{ 'msg': msg, 'extra': extra or "" }), 200)) def _check_spid_restrictions(self, msg, action, binding, **kwargs): parsed_msg, errors = self.spid_parser.parse(msg.message, action, binding, **kwargs) self.app.logger.debug('parsed authn_request: {}'.format(parsed_msg)) return parsed_msg, errors 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 _handle_errors(self, errors, xmlstr): _escaped_xml = escape(prettify_xml(xmlstr.decode())) rendered_error_response = render_template_string( spid_error_table, **{ 'lines': _escaped_xml.splitlines(), 'errors': errors }) return rendered_error_response def _verify_redirect(self, saml_msg, issuer_name): """ Verify Http-Redirect signature :param saml_msg: request parameters :param issuer_name: issuer name (Service Provider) """ if "SigAlg" in saml_msg and "Signature" in saml_msg: # Signed request self.app.logger.debug('Messaggio SAML firmato.') _sig_alg = saml_msg['SigAlg'] if _sig_alg not in ALLOWED_SIG_ALGS: self._raise_error( 'L\'Algoritmo {} non è supportato.'.format(_sig_alg)) try: _certs = self.server.metadata.certs(issuer_name, "any", "signing") except KeyError: self._raise_error( 'entity ID {} non registrato, impossibile ricavare'\ ' un certificato valido.'.format(issuer_name) ) 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.') else: self._raise_error( 'I parametri Signature e SigAlg sono entrambi'\ ' necessari per le richieste di tipo HTTP-REDIRECT' ) def _parse_message(self, action='login'): """ Parse an AuthnRequest or a LogoutRequest using pysaml2 API :param saml_msg: request parameters :param method: request method :param action: type of request """ method = request.method if method == 'GET': _binding = BINDING_HTTP_REDIRECT saml_msg = self.unpack_args(request.args) elif method == 'POST': _binding = BINDING_HTTP_POST saml_msg = self.unpack_args(request.form) else: self._raise_error( 'I metodi consentiti sono'\ ' GET (Http-Redirect) o POST (Http-Post)' ) if 'SAMLRequest' not in saml_msg: self._raise_error('Parametro SAMLRequest assente.') if action == 'login': _func = 'parse_authn_request' elif action == 'logout': _func = 'parse_logout_request' try: req_info = getattr(self.server, _func)(saml_msg['SAMLRequest'], _binding) except IncorrectlySigned as err: self.app.logger.debug(str(err)) self._raise_error( 'Messaggio corrotto o non firmato correttamente.') return req_info, _binding def single_sign_on_service(self): """ Process Http-Redirect or Http-POST request :param request: Flask request object """ # Unpack parameters saml_msg = self.unpack_args(request.args) try: req_info, binding = self._parse_message(action='login') authn_req = req_info.message self.app.logger.debug('AuthnRequest: \n{}'.format( prettify_xml(str(authn_req)))) extra = {} sp_id = authn_req.issuer.text issuer_name = authn_req.issuer.text if issuer_name and issuer_name not in self.server.metadata.service_providers( ): raise UnknownSystemEntity # TODO: refactor a bit fetching this kind of data from pysaml2 atcss = [] for k, _md in self.server.metadata.items(): if k == sp_id: _srvs = _md.get('spsso_descriptor', []) for _srv in _srvs: for _acs in _srv.get('attribute_consuming_service', []): atcss.append(_acs) try: ascss = self.server.metadata.assertion_consumer_service(sp_id) except UnknownSystemEntity as err: ascss = [] except UnsupportedBinding as err: ascss = [] atcss_indexes = [str(el.get('index')) for el in atcss] ascss_indexes = [str(el.get('index')) for el in ascss] extra['issuer'] = issuer_name extra['attribute_consuming_service_indexes'] = atcss_indexes extra['assertion_consumer_service_indexes'] = ascss_indexes extra['receivers'] = req_info.receiver_addrs _, errors = self._check_spid_restrictions(req_info, 'login', binding, **extra) except UnknownBinding as err: self.app.logger.debug(str(err)) self._raise_error( 'Binding non supportato. Formati supportati ({}, {})'.format( BINDING_HTTP_POST, BINDING_HTTP_REDIRECT)) except UnknownSystemEntity as err: self.app.logger.debug(str(err)) self._raise_error( 'entity ID {} non registrato.'.format(issuer_name)) except IncorrectlySigned as err: self.app.logger.debug(str(err)) self._raise_error( 'Messaggio corrotto o non firmato correttamente.'.format( issuer_name)) if errors: return self._handle_errors(errors, req_info.xmlstr) if not req_info: self._raise_error('Processo di parsing del messaggio fallito.') # Check if it is signed if binding == BINDING_HTTP_REDIRECT: self._verify_redirect(saml_msg, issuer_name) # 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')) @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() @property def _all_attributes(self): _dct = self._spid_attributes['primary'].copy() _dct.update(self._spid_attributes['secondary']) return _dct def users(self): """ Add user endpoint """ spid_main_fields = self._spid_main_fields spid_secondary_fields = self._spid_secondary_fields rendered_form = render_template( "users.html", **{ 'action': '/users', 'primary_attributes': spid_main_fields, 'secondary_attributes': spid_secondary_fields, 'users': self.user_manager.all(), 'sp_list': self.server.metadata.service_providers() }) if request.method == 'GET': return rendered_form, 200 elif request.method == 'POST': username = request.form.get('username') password = request.form.get('password') sp = request.form.get('service_provider') if not sp: sp = None if not username or not password: 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 index(self): rendered_form = render_template( "home.html", **{ 'sp_list': [{ "name": sp, "spId": sp } for sp in self.server.metadata.service_providers()], }) return rendered_form, 200 def get_destination(self, req, sp_id): destination = None acs_index = req.message.assertion_consumer_service_index if acs_index is not None: acss = self.server.metadata.assertion_consumer_service( sp_id, req.message.protocol_binding) for acs in acss: if acs.get('index') == acs_index: destination = acs.get('location') break self.app.logger.debug( 'AssertionConsumerServiceIndex Location: {}'.format( destination)) if destination is None: destination = req.message.assertion_consumer_service_url self.app.logger.debug( 'AssertionConsumerServiceURL: {}'.format(destination)) return destination def login(self): """ Login endpoint (verify user credentials) """ def from_session(key): return session[key] if key in session else None key = from_session('request_key') relay_state = from_session('relay_state') self.app.logger.debug('Request key: {}'.format(key)) if key and key in self.ticket: authn_request = self.ticket[key] message = authn_request.message sp_id = message.issuer.text destination = self.get_destination(authn_request, sp_id) authn_context = message.requested_authn_context spid_level = authn_context.authn_context_class_ref[0].text authn_info = self.authn_broker.pick(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}) rendered_form = render_template( 'login.html', **{ 'action': url_for('login'), 'request_key': key, 'relay_state': relay_state, 'extra_challenge': extra_challenge }) return rendered_form, 200 if 'confirm' in request.form: # verify optional challenge based on spid level verified = 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 identity = user['attrs'].copy() AUTHN = { "class_ref": spid_level, "authn_auth": spid_level } self.app.logger.debug( 'Unfiltered data: {}'.format(identity)) atcs_idx = message.attribute_consuming_service_index self.app.logger.debug( 'attribute_consuming_service_index: {}'.format( atcs_idx)) if atcs_idx: attrs = self.server.wants(sp_id, atcs_idx) required = [ Attribute(name=el.get('name'), friendly_name=None, name_format=NAME_FORMAT_BASIC) for el in attrs.get('required') ] optional = [ Attribute(name=el.get('name'), friendly_name=None, name_format=NAME_FORMAT_BASIC) for el in attrs.get('optional') ] acs = ac_factory( './testenv/attributemaps', **{'override_types': self._all_attributes}) rava = list_to_local(acs, required) oava = list_to_local(acs, optional) else: rava = {} oava = {} self.app.logger.debug( 'Required attributes: {}'.format(rava)) self.app.logger.debug( 'Optional attributes: {}'.format(oava)) identity = filter_on_demands(identity, rava, oava) self.app.logger.debug( 'Filtered data: {}'.format(identity)) _data = dict(identity=identity, userid=user_id, in_response_to=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, release_policy=SpidPolicy(restrictions={ 'default': { 'name_form': NAME_FORMAT_BASIC, } }, index=atcs_idx)) response = self.server.create_authn_response(**_data) self.app.logger.debug( 'Response: \n{}'.format(response)) http_args = self.server.apply_binding( BINDING_HTTP_POST, response, destination, response=True, sign=True, relay_state=relay_state) # Setup confirmation page data self.responses[key] = http_args['data'] rendered_response = render_template( 'confirm.html', **{ 'destination_service': sp_id, 'lines': escape(prettify_xml(response)).splitlines(), 'attrs': identity.keys(), 'action': '/continue-response', 'request_key': key }) return rendered_response, 200 elif 'delete' in request.form: error_response = self.server.create_error_response( in_response_to=authn_request.message.id, destination=destination, info=get_spid_error(AUTH_NO_CONSENT)) self.app.logger.debug('Error response: \n{}'.format( prettify_xml(str(error_response)))) http_args = self.server.apply_binding(BINDING_HTTP_POST, error_response, destination, response=True, sign=True, relay_state=relay_state) del self.ticket[key] return http_args['data'], 200 return render_template('403.html'), 403 def continue_response(self): key = request.form['request_key'] if key and key in self.responses and key in self.responses: _response = self.responses.pop(key) auth_req = self.ticket.pop(key) if 'confirm' in request.form: return _response, 200 elif 'delete' in request.form: destination = self.get_destination( auth_req, auth_req.message.issuer.text) error_response = self.server.create_error_response( in_response_to=auth_req.message.id, destination=destination, info=get_spid_error(AUTH_NO_CONSENT)) self.app.logger.debug('Error response: \n{}'.format( prettify_xml(str(error_response)))) http_args = self.server.apply_binding( BINDING_HTTP_POST, error_response, destination, response=True, sign=True, ) return http_args['data'], 200 return render_template('403.html'), 403 def _sp_single_logout_service(self, issuer_name): _slo = None for binding in [BINDING_HTTP_POST, BINDING_HTTP_REDIRECT]: try: _slo = self.server.metadata.single_logout_service( issuer_name, binding=binding, typ='spsso') except UnsupportedBinding: pass return _slo def single_logout_service(self): """ SLO endpoint """ self.app.logger.debug("req: '%s'", request) saml_msg = self.unpack_args(request.args) try: req_info, _binding = self._parse_message(action='logout') msg = req_info.message self.app.logger.debug('LogoutRequest: \n{}'.format( prettify_xml(str(msg)))) issuer_name = req_info.issuer.text extra = {} extra['receivers'] = req_info.receiver_addrs _, errors = self._check_spid_restrictions(req_info, 'logout', _binding, **extra) except UnknownBinding as err: self.app.logger.debug(str(err)) self._raise_error( 'Binding non supportato. Formati supportati ({}, {})'.format( BINDING_HTTP_POST, BINDING_HTTP_REDIRECT)) except UnknownSystemEntity as err: self.app.logger.debug(str(err)) self._raise_error( 'entity ID {} non registrato.'.format(issuer_name)) except IncorrectlySigned as err: self.app.logger.debug(str(err)) self._raise_error( 'Messaggio corrotto o non firmato correttamente.') if errors: return self._handle_errors(errors, req_info.xmlstr) # Check if it is signed if _binding == BINDING_HTTP_REDIRECT: self._verify_redirect(saml_msg, issuer_name) _slo = self._sp_single_logout_service(issuer_name) if _slo is None: self._raise_error( 'Impossibile trovare un servizio di'\ ' Single Logout per il service provider {}'.format( issuer_name ) ) response_binding = _slo[0].get('binding') self.app.logger.debug( 'Response binding: \n{}'.format(response_binding)) _signing = True if response_binding == BINDING_HTTP_POST else False self.app.logger.debug( 'Signature inside response: \n{}'.format(_signing)) response = self.server.create_logout_response(msg, [response_binding], sign_alg=SIGN_ALG, digest_alg=DIGEST_ALG, sign=_signing) self.app.logger.debug('Response: \n{}'.format(response)) binding, destination = self.server.pick_binding( "single_logout_service", [response_binding], "spsso", req_info) self.app.logger.debug('Destination {}'.format(destination)) if response_binding == BINDING_HTTP_POST: _sign = False extra = {} else: _sign = True extra = {'sigalg': SIGN_ALG} relay_state = saml_msg.get('RelayState', '') http_args = self.server.apply_binding(binding, "%s" % response, destination, response=True, sign=_sign, relay_state=relay_state, **extra) if response_binding == BINDING_HTTP_POST: self.app.logger.debug('Form post {}'.format(http_args['data'])) return http_args['data'], 200 elif response_binding == BINDING_HTTP_REDIRECT: headers = dict(http_args['headers']) self.app.logger.debug('Headers {}'.format(headers)) location = headers.get('Location') self.app.logger.debug('Location {}'.format(location)) if location: return redirect(location) abort(400) 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)