Repository: Pylons/pyramid

Issue:
BasicAuthAuthenticationPolicy is unable to handle intl characters
Decoding only ASCII characters is supported. Using "pörpör" as password to basic auth yields the following kind of Exception:

2013-03-09 16:22:12,479 ERROR [waitress][Dummy-3] Exception when serving /_debug_toolbar/static/img/source.png
Traceback (most recent call last):
  File "/usr/lib/python2.6/site-packages/waitress-0.8.2-py2.6.egg/waitress/channel.py", line 329, in service
    task.service()
  File "/usr/lib/python2.6/site-packages/waitress-0.8.2-py2.6.egg/waitress/task.py", line 173, in service
    self.execute()
  File "/usr/lib/python2.6/site-packages/waitress-0.8.2-py2.6.egg/waitress/task.py", line 380, in execute
    app_iter = self.channel.server.application(env, start_response)
  File "/usr/lib/python2.6/site-packages/pyramid-1.4-py2.6.egg/pyramid/router.py", line 251, in __call__
    response = self.invoke_subrequest(request, use_tweens=True)
  File "/usr/lib/python2.6/site-packages/pyramid-1.4-py2.6.egg/pyramid/router.py", line 227, in invoke_subrequest
    response = handle_request(request)
  File "/usr/lib/python2.6/site-packages/pyramid_debugtoolbar-1.0.4-py2.6.egg/pyramid_debugtoolbar/toolbar.py", line 111, in toolbar_tween
    return handler(request)
  File "/usr/lib/python2.6/site-packages/pyramid-1.4-py2.6.egg/pyramid/tweens.py", line 21, in excview_tween
    response = handler(request)
  File "/usr/lib/python2.6/site-packages/pyramid-1.4-py2.6.egg/pyramid/router.py", line 161, in handle_request
    response = view_callable(context, request)
  File "/usr/lib/python2.6/site-packages/pyramid-1.4-py2.6.egg/pyramid/config/views.py", line 264, in _authdebug_view
    request)
  File "/usr/lib/python2.6/site-packages/pyramid-1.4-py2.6.egg/pyramid/authentication.py", line 128, in effective_principals
    userid = self.unauthenticated_userid(request)
  File "/usr/lib/python2.6/site-packages/pyramid-1.4-py2.6.egg/pyramid/authentication.py", line 1096, in unauthenticated_userid
    credentials = self._get_credentials(request)
  File "/usr/lib/python2.6/site-packages/pyramid-1.4-py2.6.egg/pyramid/authentication.py", line 1132, in _get_credentials
    auth = b64decode(auth.strip()).decode('ascii')
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 7: ordinal not in range(128)

Test basic authentication with unicode username/password
For https://github.com/Pylons/pyramid/issues/898



Discussion:
I can confirm this issue.  Could you come up with a pull request which adds tests for this issue?



Resolving patch:
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -1176,10 +1176,17 @@ def _get_credentials(self, request):
             return None
         if authmeth.lower() != 'basic':
             return None
+
         try:
-            auth = b64decode(auth.strip()).decode('ascii')
+            authbytes = b64decode(auth.strip())
         except (TypeError, binascii.Error): # can't decode
             return None
+
+        try:
+            auth = authbytes.decode('utf-8')
+        except UnicodeDecodeError:
+            auth = authbytes.decode('latin-1')
+
         try:
             username, password = auth.split(':', 1)
         except ValueError: # not enough values to unpack


Test patch:
diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py
--- a/pyramid/tests/test_authentication.py
+++ b/pyramid/tests/test_authentication.py
@@ -1374,6 +1374,32 @@ def check(username, password, request):
         policy = self._makeOne(check)
         self.assertEqual(policy.authenticated_userid(request), 'chrisr')
 
+    def test_authenticated_userid_utf8(self):
+        import base64
+        request = testing.DummyRequest()
+        inputs = (b'm\xc3\xb6rk\xc3\xb6:'
+                  b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8')
+        request.headers['Authorization'] = 'Basic %s' % (
+            base64.b64encode(inputs.encode('utf-8')).decode('latin-1'))
+        def check(username, password, request):
+            return []
+        policy = self._makeOne(check)
+        self.assertEqual(policy.authenticated_userid(request),
+                         b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'))
+
+    def test_authenticated_userid_latin1(self):
+        import base64
+        request = testing.DummyRequest()
+        inputs = (b'm\xc3\xb6rk\xc3\xb6:'
+                  b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8')
+        request.headers['Authorization'] = 'Basic %s' % (
+            base64.b64encode(inputs.encode('latin-1')).decode('latin-1'))
+        def check(username, password, request):
+            return []
+        policy = self._makeOne(check)
+        self.assertEqual(policy.authenticated_userid(request),
+                         b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'))
+
     def test_unauthenticated_userid_invalid_payload(self):
         import base64
         request = testing.DummyRequest()