Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref #2659 public HTTP Basic credentials extraction #2662

Merged
merged 8 commits into from
Sep 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Backward Incompatibilities
Features
--------

- The `_get_credentials` private method of `BasicAuthAuthenticationPolicy`
has been extracted into standalone function `extract_http_basic_credentials`
in `pyramid.authentication` module, this function extracts HTTP Basic
credentials from `request` object, and returns them as a named tuple.

Bug Fixes
---------

Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ Contributors

- Jean-Christophe Bohin, 2016/06/13

- Dariusz Gorecki, 2016/07/15

- Jon Davidson, 2016/07/18

- Keith Yang, 2016/07/22
3 changes: 3 additions & 0 deletions docs/api/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ Helper Classes
:members:


Helper Functions
~~~~~~~~~~~~~~~~

.. autofunction:: extract_http_basic_credentials
86 changes: 53 additions & 33 deletions pyramid/authentication.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import binascii
from codecs import utf_8_decode
from codecs import utf_8_encode
from collections import namedtuple
import hashlib
import base64
import re
Expand Down Expand Up @@ -1083,7 +1084,7 @@ def __init__(self, check, realm='Realm', debug=False):

def unauthenticated_userid(self, request):
""" The userid parsed from the ``Authorization`` request header."""
credentials = self._get_credentials(request)
credentials = extract_http_basic_credentials(request)
if credentials:
return credentials[0]

Expand All @@ -1100,46 +1101,65 @@ def forget(self, request):
return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)]

def callback(self, username, request):
# Username arg is ignored. Unfortunately _get_credentials winds up
# getting called twice when authenticated_userid is called. Avoiding
# that, however, winds up duplicating logic from the superclass.
credentials = self._get_credentials(request)
# Username arg is ignored. Unfortunately
# extract_http_basic_credentials winds up getting called twice when
# authenticated_userid is called. Avoiding that, however,
# winds up duplicating logic from the superclass.
credentials = extract_http_basic_credentials(request)
if credentials:
username, password = credentials
return self.check(username, password, request)

def _get_credentials(self, request):
authorization = request.headers.get('Authorization')
if not authorization:
return None
try:
authmeth, auth = authorization.split(' ', 1)
except ValueError: # not enough values to unpack
return None
if authmeth.lower() != 'basic':
return None

try:
authbytes = b64decode(auth.strip())
except (TypeError, binascii.Error): # can't decode
return None

# try utf-8 first, then latin-1; see discussion in
# https://github.com/Pylons/pyramid/issues/898
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
return None
return username, password

class _SimpleSerializer(object):
def loads(self, bstruct):
return native_(bstruct)

def dumps(self, appstruct):
return bytes_(appstruct)


http_basic_credentials = namedtuple('http_basic_credentials',
['username', 'password'])


def extract_http_basic_credentials(request):
""" A helper function for extraction of HTTP Basic credentials
from a given :term:`request`. Returned values:

- ``None`` - when credentials couldn't be extracted
- ``namedtuple`` with extracted ``username`` and ``password`` attributes

``request``
The :term:`request` object
"""
authorization = request.headers.get('Authorization')
if not authorization:
return None

try:
authmeth, auth = authorization.split(' ', 1)
except ValueError: # not enough values to unpack
return None

if authmeth.lower() != 'basic':
return None

try:
authbytes = b64decode(auth.strip())
except (TypeError, binascii.Error): # can't decode
return None

# try utf-8 first, then latin-1; see discussion in
# https://github.com/Pylons/pyramid/issues/898
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
return None

return http_basic_credentials(username, password)
73 changes: 73 additions & 0 deletions pyramid/tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,79 @@ def test_forget(self):
self.assertEqual(policy.forget(None), [
('WWW-Authenticate', 'Basic realm="SomeRealm"')])


class TestExtractHTTPBasicCredentials(unittest.TestCase):
def _get_func(self):
from pyramid.authentication import extract_http_basic_credentials
return extract_http_basic_credentials

def test_no_auth_header(self):
request = testing.DummyRequest()
fn = self._get_func()

self.assertIsNone(fn(request))

def test_invalid_payload(self):
import base64
request = testing.DummyRequest()
request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
bytes_('chrisrpassword')).decode('ascii')
fn = self._get_func()
self.assertIsNone(fn(request))

def test_not_a_basic_auth_scheme(self):
import base64
request = testing.DummyRequest()
request.headers['Authorization'] = 'OtherScheme %s' % base64.b64encode(
bytes_('chrisr:password')).decode('ascii')
fn = self._get_func()
self.assertIsNone(fn(request))

def test_no_base64_encoding(self):
request = testing.DummyRequest()
request.headers['Authorization'] = 'Basic ...'
fn = self._get_func()
self.assertIsNone(fn(request))

def test_latin1_payload(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'))
fn = self._get_func()
self.assertEqual(fn(request), (
b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'),
b'm\xc3\xb6rk\xc3\xb6password'.decode('utf-8')
))

def test_utf8_payload(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'))
fn = self._get_func()
self.assertEqual(fn(request), (
b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'),
b'm\xc3\xb6rk\xc3\xb6password'.decode('utf-8')
))

def test_namedtuple_return(self):
import base64
request = testing.DummyRequest()
request.headers['Authorization'] = 'Basic %s' % base64.b64encode(
bytes_('chrisr:pass')).decode('ascii')
fn = self._get_func()
result = fn(request)

self.assertEqual(result.username, 'chrisr')
self.assertEqual(result.password, 'pass')



class TestSimpleSerializer(unittest.TestCase):
def _makeOne(self):
from pyramid.authentication import _SimpleSerializer
Expand Down