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

add HTTPCookieAuth for token auth in req cookies #166

Closed
Closed
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
43 changes: 43 additions & 0 deletions src/flask_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,49 @@ def ensure_sync(self, f):
return f


class HTTPCookieAuth(HTTPAuth):
def __init__(self, scheme=None, realm=None, cookie_name=None):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scheme and Realm apply to HTTP Authentication. I don't see how they help when using cookies.

super(HTTPCookieAuth, self).__init__(
scheme or 'Bearer',
realm,
cookie_name
)

self.verify_cookie_callback = None
self.cookie_name = cookie_name

def verify_cookie(self, f):
self.verify_cookie_callback = f
return f

def authenticate(self, auth, _):
cookie = getattr(auth, 'token', '')
if self.verify_cookie_callback:
return self.ensure_sync(self.verify_cookie_callback)(cookie)

def get_auth(self):
expected_cookie_name = self.cookie_name or 'Authorization'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a cookie named Authorization is confusing. Does anybody do this? I have never seen it.

cookie_val = request.cookies.get(expected_cookie_name, '')
token = ''
if self.scheme != 'ApiKey':
# if scheme is Bearer or anything else besides ApiKey,
# split on scheme name
if isinstance(cookie_val, str) and len(cookie_val) > 0:
try:
scheme, token = cookie_val.split(' ')
except ValueError:
# not enough values to unpack
return None
# ensure scheme names match (case insensitive)
if scheme.lower() != (self.scheme or "Bearer").lower():
return None
else:
# for ApiKey scheme, use whole cookie value
token = cookie_val
auth = Authorization(self.scheme, token=token)
return auth


class HTTPBasicAuth(HTTPAuth):
def __init__(self, scheme=None, realm=None):
super(HTTPBasicAuth, self).__init__(scheme or 'Basic', realm)
Expand Down
193 changes: 193 additions & 0 deletions tests/test_cookie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import base64
import unittest
from flask import Flask
from flask_httpauth import HTTPCookieAuth


class HTTPAuthTestCase(unittest.TestCase):
def setUp(self):
app = Flask(__name__)
app.config['SECRET_KEY'] = 'my secret'

cookie_auth = HTTPCookieAuth('MyToken')
cookie_auth2 = HTTPCookieAuth('Token', realm='foo')
cookie_auth3 = HTTPCookieAuth(scheme='ApiKey', cookie_name='X-API-Key')
cookie_default = HTTPCookieAuth()

@cookie_auth.verify_cookie
def verify_cookie(token):
if token == 'this-is-the-token!':
return 'user'

@cookie_auth3.verify_cookie
def verify_cookie3(token):
if token == 'this-is-the-token!':
return 'user'

@cookie_default.verify_cookie
def verify_cookie_default(token):
if token == 'this-is-the-token!':
return 'user'

@cookie_auth.error_handler
def error_handler():
return 'error', 401, {'WWW-Authenticate': 'MyToken realm="Foo"'}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really not something I can agree with. Here you are mixing your own custom authentication implementation based on cookies with parts of the HTTP Authentication standard, which uses the Authorization header as vehicle for the client to send credentials. There is no good reason to return the WWW-Authenticate header that I can see and I have never seen any implementation that does it.


@app.route('/')
def index():
return 'index'

@app.route('/protected')
@cookie_auth.login_required
def cookie_auth_route():
return 'cookie_auth:' + cookie_auth.current_user()

@app.route('/protected-optional')
@cookie_auth.login_required(optional=True)
def cookie_auth_optional_route():
return 'cookie_auth:' + str(cookie_auth.current_user())

@app.route('/protected2')
@cookie_auth2.login_required
def cookie_auth_route2():
return 'cookie_auth2'

@app.route('/protected3')
@cookie_auth3.login_required
def cookie_auth_route3():
return 'cookie_auth3:' + cookie_auth3.current_user()

@app.route('/protected-default')
@cookie_default.login_required
def cookie_default_auth_route():
return 'cookie_default:' + cookie_default.current_user()

self.app = app
self.cookie_auth = cookie_auth
self.client = app.test_client()

def tearDown(self) -> None:
self.client._cookies.clear()

def test_cookie_auth_prompt(self):
response = self.client.get('/protected')
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'MyToken realm="Foo"')

def test_cookie_auth_ignore_options(self):
response = self.client.options('/protected')
self.assertEqual(response.status_code, 200)
self.assertTrue('WWW-Authenticate' not in response.headers)

def test_cookie_auth_login_valid(self):
self.client.set_cookie("Authorization", "MyToken this-is-the-token!")
response = self.client.get('/protected')
self.assertEqual(response.data.decode('utf-8'), 'cookie_auth:user')

def test_cookie_auth_login_valid_different_case(self):
self.client.set_cookie("Authorization", "mytoken this-is-the-token!")
response = self.client.get('/protected')
self.assertEqual(response.data.decode('utf-8'), 'cookie_auth:user')

def test_cookie_auth_login_optional(self):
response = self.client.get('/protected-optional')
self.assertEqual(response.data.decode('utf-8'), 'cookie_auth:None')

def test_cookie_auth_login_invalid_token(self):
self.client.set_cookie("Authorization",
"MyToken this-is-not-the-token!")
response = self.client.get('/protected')
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'MyToken realm="Foo"')

def test_cookie_auth_login_invalid_scheme(self):
self.client.set_cookie("Authorization", "Foo this-is-the-token!")
response = self.client.get('/protected')
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'MyToken realm="Foo"')

def test_cookie_auth_login_invalid_header(self):
self.client.set_cookie("Authorization", "this-is-a-bad-cookie")
response = self.client.get('/protected')
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'MyToken realm="Foo"')

def test_cookie_auth_login_invalid_no_callback(self):
self.client.set_cookie("Authorization", "Token this-is-the-token!")
response = self.client.get('/protected2')
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'Token realm="foo"')

def test_cookie_auth_custom_header_valid_token(self):
self.client.set_cookie("X-API-Key", "this-is-the-token!")
response = self.client.get('/protected3')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.decode('utf-8'), 'cookie_auth3:user')

def test_cookie_auth_custom_header_invalid_token(self):
self.client.set_cookie("X-API-Key", "invalid-token-should-fail")
response = self.client.get('/protected3')
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)

def test_cookie_auth_custom_header_invalid_header(self):
self.client.set_cookie("API-Key", "this-is-the-token!")
response = self.client.get('/protected3')
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'ApiKey realm="Authentication Required"')

def test_cookie_auth_header_precedence(self):
self.client.set_cookie("X-API-Key", "this-is-the-token!")
basic_creds = base64.b64encode(b'susan:bye').decode('utf-8')
response = self.client.get(
'/protected3', headers={'Authorization': 'Basic ' + basic_creds})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.decode('utf-8'), 'cookie_auth3:user')

def test_cookie_auth_default_bearer(self):
self.client.set_cookie("Authorization", "Bearer this-is-the-token!")
response = self.client.get("/protected-default")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.decode('utf-8'), 'cookie_default:user')

def test_cookie_auth_default_bearer_valid_token(self):
self.client.set_cookie("Authorization", "Bearer this-is-the-token!")
response = self.client.get("/protected-default")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.decode('utf-8'), 'cookie_default:user')

def test_cookie_auth_default_bearer_invalid_token(self):
self.client.set_cookie("Authorization", "Bearer Invalid-token!")
response = self.client.get("/protected-default")
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'Bearer realm="Authentication Required"')

def test_cookie_auth_default_bearer_malformed_value(self):
self.client.set_cookie("Authorization", "this-shouldn't-parse")
response = self.client.get("/protected-default")
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'Bearer realm="Authentication Required"')

def test_cookie_auth_default_bearer_missing_cookie(self):
self.client.set_cookie("Otterization", "Bearer this-is-the-token!")
response = self.client.get("/protected-default")
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'Bearer realm="Authentication Required"')
Loading
Loading