Parcourir la source

better auth digest code

Guénaël Muller il y a 6 ans
Parent
révision
974d72cac9

+ 10 - 0
tracim/tracim/lib/webdav/sql_domain_controller.py Voir le fichier

@@ -13,6 +13,16 @@ class TracimDomainController(object):
13 13
     def getDomainRealm(self, inputURL, environ):
14 14
         return '/'
15 15
 
16
+    def getRealmUserPassword(self, realmname, username, environ):
17
+        """
18
+        Warning ! This return hashed version of password !
19
+        """
20
+        try:
21
+            user = self._api.get_one_by_email(username)
22
+            return user.password
23
+        except:
24
+            return False
25
+
16 26
     def requireAuthentication(self, realmname, environ):
17 27
         return True
18 28
 

+ 65 - 145
tracim/tracim/lib/webdav/tracim_http_authenticator.py Voir le fichier

@@ -1,156 +1,76 @@
1
-import os
2
-import re
3
-
4 1
 from wsgidav.http_authenticator import HTTPAuthenticator
5 2
 from wsgidav import util
6
-import cherrypy
7
-
8
-from tracim.lib.user import CurrentUserGetterApi
9
-from tracim.lib.user import CURRENT_USER_WSGIDAV
10 3
 
11 4
 _logger = util.getModuleLogger(__name__, True)
12
-HOTFIX_WINXP_AcceptRootShareLogin = True
13 5
 
14 6
 
15 7
 class TracimHTTPAuthenticator(HTTPAuthenticator):
16
-    def __init__(self, application, config):
17
-        super(TracimHTTPAuthenticator, self).__init__(application, config)
18
-        self._headerfixparser = re.compile(r'([\w]+)=("[^"]*,[^"]*"),')
19
-
20
-    def authDigestAuthRequest(self, environ, start_response):
21
-        realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
22
-
23
-        isinvalidreq = False
24
-
25
-        authheaderdict = dict([])
26
-        authheaders = environ["HTTP_AUTHORIZATION"] + ","
27
-        if not authheaders.lower().strip().startswith("digest"):
28
-            isinvalidreq = True
29
-            # Hotfix for Windows file manager and OSX Finder:
30
-        # Some clients don't urlencode paths in auth header, so uri value may
31
-        # contain commas, which break the usual regex headerparser. Example:
32
-        # Digest username="user",realm="/",uri="a,b.txt",nc=00000001, ...
33
-        # -> [..., ('uri', '"a'), ('nc', '00000001'), ...]
34
-        # Override any such values with carefully extracted ones.
35
-        authheaderlist = self._headerparser.findall(authheaders)
36
-        authheaderfixlist = self._headerfixparser.findall(authheaders)
37
-        if authheaderfixlist:
38
-            _logger.info("Fixing authheader comma-parsing: extend %s with %s" \
39
-                         % (authheaderlist, authheaderfixlist))
40
-            authheaderlist += authheaderfixlist
41
-        for authheader in authheaderlist:
42
-            authheaderkey = authheader[0]
43
-            authheadervalue = authheader[1].strip().strip("\"")
44
-            authheaderdict[authheaderkey] = authheadervalue
45
-
46
-        _logger.debug("authDigestAuthRequest: %s" % environ["HTTP_AUTHORIZATION"])
47
-        _logger.debug("  -> %s" % authheaderdict)
48
-
49
-        if "username" in authheaderdict:
50
-            req_username = authheaderdict["username"]
51
-            req_username_org = req_username
52
-            # Hotfix for Windows XP:
53
-            #   net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
54
-            # will send the name with double backslashes ('DOMAIN\\tester')
55
-            # but send the digest for the simple name ('DOMAIN\tester').
56
-            if r"\\" in req_username:
57
-                req_username = req_username.replace("\\\\", "\\")
58
-                _logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
59
-
60
-            if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
61
-                isinvalidreq = True
62
-        else:
63
-            isinvalidreq = True
64
-
65
-            # TODO: Chun added this comments, but code was commented out
66
-            # Do not do realm checking - a hotfix for WinXP using some other realm's
67
-            # auth details for this realm - if user/password match
68
-
69
-        if 'realm' in authheaderdict:
70
-            if authheaderdict["realm"].upper() != realmname.upper():
71
-                if HOTFIX_WINXP_AcceptRootShareLogin:
72
-                    # Hotfix: also accept '/'
73
-                    if authheaderdict["realm"].upper() != "/":
74
-                        isinvalidreq = True
75
-                else:
76
-                    isinvalidreq = True
77
-
78
-        if "algorithm" in authheaderdict:
79
-            if authheaderdict["algorithm"].upper() != "MD5":
80
-                isinvalidreq = True  # only MD5 supported
81
-
82
-        if "uri" in authheaderdict:
83
-            req_uri = authheaderdict["uri"]
84
-
85
-        if "nonce" in authheaderdict:
86
-            req_nonce = authheaderdict["nonce"]
87
-        else:
88
-            isinvalidreq = True
89
-
90
-        req_hasqop = False
91
-        if "qop" in authheaderdict:
92
-            req_hasqop = True
93
-            req_qop = authheaderdict["qop"]
94
-            if req_qop.lower() != "auth":
95
-                isinvalidreq = True  # only auth supported, auth-int not supported
8
+    def computeDigestResponse(
9
+            self,
10
+            username,
11
+            realm,
12
+            password,
13
+            method,
14
+            uri,
15
+            nonce,
16
+            cnonce,
17
+            qop,
18
+            nc
19
+    ):
20
+        """
21
+        Override standard computeDigestResponse : as user password is already
22
+        hashed in database, we need to use left_digest_response_hash
23
+        to have correctly hashed digest_response.
24
+        """
25
+
26
+        # TODO - G.M - 13-03-2018 Check if environ is useful
27
+        # for get_left_digest_response. If true, find a solution
28
+        # to obtain it here without recopy-paste whole authDigestAuthRequest
29
+        # method.
30
+        left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realm, username, None)  # nopep8
31
+        if left_digest_response_hash:
32
+            return self.tracim_compute_digest_response(
33
+                left_digest_response_hash=left_digest_response_hash,
34
+                method=method,
35
+                uri=uri,
36
+                nonce=nonce,
37
+                cnonce=cnonce,
38
+                qop=qop,
39
+                nc=nc,
40
+            )
96 41
         else:
97
-            req_qop = None
98
-
99
-        if "cnonce" in authheaderdict:
100
-            req_cnonce = authheaderdict["cnonce"]
101
-        else:
102
-            req_cnonce = None
103
-            if req_hasqop:
104
-                isinvalidreq = True
105
-
106
-        if "nc" in authheaderdict:  # is read but nonce-count checking not implemented
107
-            req_nc = authheaderdict["nc"]
108
-        else:
109
-            req_nc = None
110
-            if req_hasqop:
111
-                isinvalidreq = True
112
-
113
-        if "response" in authheaderdict:
114
-            req_response = authheaderdict["response"]
115
-        else:
116
-            isinvalidreq = True
117
-
118
-        if not isinvalidreq:
119
-            left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realmname, req_username, environ)
120
-
121
-            req_method = environ["REQUEST_METHOD"]
122
-
123
-            required_digest = self.tracim_compute_digest_response(left_digest_response_hash, req_method, req_uri, req_nonce,
124
-                                                         req_cnonce, req_qop, req_nc)
125
-
126
-            if required_digest != req_response:
127
-                _logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (
128
-                realmname, req_username, required_digest, req_response))
129
-                isinvalidreq = True
130
-            else:
131
-                _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
132
-            pass
133
-
134
-        if isinvalidreq:
135
-            _logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
136
-            return self.sendDigestAuthResponse(environ, start_response)
137
-
138
-        environ["http_authenticator.realm"] = realmname
139
-        environ["http_authenticator.username"] = req_username
140
-
141
-        # Set request current user email to be able to recognise him later
142
-        cherrypy.request.current_user_email = req_username
143
-        CurrentUserGetterApi.set_thread_local_getter(CURRENT_USER_WSGIDAV)
144
-
145
-        return self._application(environ, start_response)
146
-
147
-    def tracim_compute_digest_response(self, left_digest_response_hash, method, uri, nonce, cnonce, qop, nc):
148
-        # A1 : username:realm:password
149
-        A2 = method + ":" + uri
42
+            return None
43
+
44
+    def tracim_compute_digest_response(
45
+            self,
46
+            left_digest_response_hash,
47
+            method,
48
+            uri,
49
+            nonce,
50
+            cnonce,
51
+            qop,
52
+            nc
53
+    ):
54
+        # TODO : Rename A to something more correct
55
+        A = "{method}:{uri}".format(method=method, uri=uri)
150 56
         if qop:
151
-            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + self.md5h(A2))
57
+            right_digest_response_hash = "{nonce}:{nc}:{cnonce}:{qop}:{A}".format(  # nopep8
58
+                nonce=nonce,
59
+                nc=nc,
60
+                cnonce=cnonce,
61
+                qop=qop,
62
+                method=method,
63
+                uri=uri,
64
+                A=self.md5h(A),
65
+            )
152 66
         else:
153
-            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + self.md5h(A2))
154
-            # print(A1, A2)
155
-        # print(digestresp)
67
+            right_digest_response_hash = "{nonce}:{A}".format(
68
+                nonce=nonce,
69
+                A=self.md5h(A),
70
+            )
71
+        digestresp = self.md5kd(
72
+            left_digest_response_hash,
73
+            right_digest_response_hash,
74
+        )
75
+
156 76
         return digestresp