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