1import logging 2import six 3import time 4from saml2 import SAMLError 5import saml2.cryptography.symmetric 6from saml2.httputil import Response 7from saml2.httputil import make_cookie 8from saml2.httputil import Redirect 9from saml2.httputil import Unauthorized 10from saml2.httputil import parse_cookie 11 12from six.moves.urllib.parse import urlencode, parse_qs, urlsplit 13 14__author__ = 'rolandh' 15 16logger = logging.getLogger(__name__) 17 18 19class AuthnFailure(SAMLError): 20 pass 21 22 23class EncodeError(SAMLError): 24 pass 25 26 27class UserAuthnMethod(object): 28 def __init__(self, srv): 29 self.srv = srv 30 31 def __call__(self, *args, **kwargs): 32 raise NotImplementedError 33 34 def authenticated_as(self, **kwargs): 35 raise NotImplementedError 36 37 def verify(self, **kwargs): 38 raise NotImplementedError 39 40 41def is_equal(a, b): 42 if len(a) != len(b): 43 return False 44 45 result = 0 46 for x, y in zip(a, b): 47 result |= x ^ y 48 return result == 0 49 50 51def url_encode_params(params=None): 52 if not isinstance(params, dict): 53 raise EncodeError("You must pass in a dictionary!") 54 params_list = [] 55 for k, v in params.items(): 56 if isinstance(v, list): 57 params_list.extend([(k, x) for x in v]) 58 else: 59 params_list.append((k, v)) 60 return urlencode(params_list) 61 62 63def create_return_url(base, query, **kwargs): 64 """ 65 Add a query string plus extra parameters to a base URL which may contain 66 a query part already. 67 68 :param base: redirect_uri may contain a query part, no fragment allowed. 69 :param query: Old query part as a string 70 :param kwargs: extra query parameters 71 :return: 72 """ 73 part = urlsplit(base) 74 if part.fragment: 75 raise ValueError("Base URL contained parts it shouldn't") 76 77 for key, values in parse_qs(query).items(): 78 if key in kwargs: 79 if isinstance(kwargs[key], six.string_types): 80 kwargs[key] = [kwargs[key]] 81 kwargs[key].extend(values) 82 else: 83 kwargs[key] = values 84 85 if part.query: 86 for key, values in parse_qs(part.query).items(): 87 if key in kwargs: 88 if isinstance(kwargs[key], six.string_types): 89 kwargs[key] = [kwargs[key]] 90 kwargs[key].extend(values) 91 else: 92 kwargs[key] = values 93 94 _pre = base.split("?")[0] 95 else: 96 _pre = base 97 98 logger.debug("kwargs: %s" % kwargs) 99 100 return "%s?%s" % (_pre, url_encode_params(kwargs)) 101 102 103class UsernamePasswordMako(UserAuthnMethod): 104 """Do user authentication using the normal username password form 105 using Mako as template system""" 106 cookie_name = "userpassmako" 107 108 def __init__(self, srv, mako_template, template_lookup, pwd, return_to): 109 """ 110 :param srv: The server instance 111 :param mako_template: Which Mako template to use 112 :param pwd: Username/password dictionary like database 113 :param return_to: Where to send the user after authentication 114 :return: 115 """ 116 UserAuthnMethod.__init__(self, srv) 117 self.mako_template = mako_template 118 self.template_lookup = template_lookup 119 self.passwd = pwd 120 self.return_to = return_to 121 self.active = {} 122 self.query_param = "upm_answer" 123 self.symmetric = saml2.cryptography.symmetric.Default(srv.symkey) 124 125 def __call__(self, cookie=None, policy_url=None, logo_url=None, 126 query="", **kwargs): 127 """ 128 Put up the login form 129 """ 130 if cookie: 131 headers = [cookie] 132 else: 133 headers = [] 134 135 resp = Response(headers=headers) 136 137 argv = {"login": "", 138 "password": "", 139 "action": "verify", 140 "policy_url": policy_url, 141 "logo_url": logo_url, 142 "query": query} 143 logger.info("do_authentication argv: %s" % argv) 144 mte = self.template_lookup.get_template(self.mako_template) 145 resp.message = mte.render(**argv) 146 return resp 147 148 def _verify(self, pwd, user): 149 if not is_equal(pwd, self.passwd[user]): 150 raise ValueError("Wrong password") 151 152 def verify(self, request, **kwargs): 153 """ 154 Verifies that the given username and password was correct 155 :param request: Either the query part of a URL a urlencoded 156 body of a HTTP message or a parse such. 157 :param kwargs: Catch whatever else is sent. 158 :return: redirect back to where ever the base applications 159 wants the user after authentication. 160 """ 161 162 #logger.debug("verify(%s)" % request) 163 if isinstance(request, six.string_types): 164 _dict = parse_qs(request) 165 elif isinstance(request, dict): 166 _dict = request 167 else: 168 raise ValueError("Wrong type of input") 169 170 # verify username and password 171 try: 172 self._verify(_dict["password"][0], _dict["login"][0]) 173 timestamp = str(int(time.mktime(time.gmtime()))) 174 msg = "::".join([_dict["login"][0], timestamp]) 175 info = self.symmetric.encrypt(msg.encode()) 176 self.active[info] = timestamp 177 cookie = make_cookie(self.cookie_name, info, self.srv.seed) 178 return_to = create_return_url(self.return_to, _dict["query"][0], 179 **{self.query_param: "true"}) 180 resp = Redirect(return_to, headers=[cookie]) 181 except (ValueError, KeyError): 182 resp = Unauthorized("Unknown user or wrong password") 183 184 return resp 185 186 def authenticated_as(self, cookie=None, **kwargs): 187 if cookie is None: 188 return None 189 else: 190 logger.debug("kwargs: %s" % kwargs) 191 try: 192 info, timestamp = parse_cookie(self.cookie_name, 193 self.srv.seed, cookie) 194 if self.active[info] == timestamp: 195 msg = self.symmetric.decrypt(info).decode() 196 uid, _ts = msg.split("::") 197 if timestamp == _ts: 198 return {"uid": uid} 199 except Exception: 200 pass 201 202 return None 203 204 def done(self, areq): 205 try: 206 _ = areq[self.query_param] 207 return False 208 except KeyError: 209 return True 210 211 212class SocialService(UserAuthnMethod): 213 def __init__(self, social): 214 UserAuthnMethod.__init__(self, None) 215 self.social = social 216 217 def __call__(self, server_env, cookie=None, sid="", query="", **kwargs): 218 return self.social.begin(server_env, cookie, sid, query) 219 220 def callback(self, server_env, cookie=None, sid="", query="", **kwargs): 221 return self.social.callback(server_env, cookie, sid, query, **kwargs) 222 223 224class AuthnMethodChooser(object): 225 def __init__(self, methods=None): 226 self.methods = methods 227 228 def __call__(self, **kwargs): 229 if not self.methods: 230 raise SAMLError("No authentication methods defined") 231 elif len(self.methods) == 1: 232 return self.methods[0] 233 else: 234 pass # TODO 235 236try: 237 import ldap 238 239 class LDAPAuthn(UsernamePasswordMako): 240 def __init__(self, srv, ldapsrv, return_to, 241 dn_pattern, mako_template, template_lookup): 242 """ 243 :param srv: The server instance 244 :param ldapsrv: Which LDAP server to us 245 :param return_to: Where to send the user after authentication 246 :return: 247 """ 248 UsernamePasswordMako.__init__(self, srv, mako_template, template_lookup, 249 None, return_to) 250 251 self.ldap = ldap.initialize(ldapsrv) 252 self.ldap.protocol_version = 3 253 self.ldap.set_option(ldap.OPT_REFERRALS, 0) 254 self.dn_pattern = dn_pattern 255 256 def _verify(self, pwd, user): 257 """ 258 Verifies the username and password agains a LDAP server 259 :param pwd: The password 260 :param user: The username 261 :return: AssertionError if the LDAP verification failed. 262 """ 263 _dn = self.dn_pattern % user 264 try: 265 self.ldap.simple_bind_s(_dn, pwd) 266 except Exception: 267 raise AssertionError() 268except ImportError: 269 class LDAPAuthn(UserAuthnMethod): 270 pass 271