Skip to content

add HTTPCookieAuth for token auth in req cookies #166

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

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
44 changes: 44 additions & 0 deletions src/flask_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,50 @@ 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)
Copy link
Owner

Choose a reason for hiding this comment

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

What is the purpose of scheme or realm when using cookies?

Copy link
Author

@mattproetsch mattproetsch Aug 3, 2024

Choose a reason for hiding this comment

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

Scheme mimics the behavior from HTTPTokenAuth with a user-specified header so that when scheme='ApiKey' the entire cookie value is treated as the token. When scheme is any other, the value of the cookie is expected to be a space-delimited value like '<Scheme> <Token>'. Realm isn't used for authentication, but will be passed along in WWW-Authenticate if auth fails.


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

def get_auth_password(self, auth):
try:
return getattr(auth, 'token', '')
except KeyError:
return ""


class HTTPBasicAuth(HTTPAuth):
def __init__(self, scheme=None, realm=None):
super(HTTPBasicAuth, self).__init__(scheme or 'Basic', realm)
Expand Down
192 changes: 192 additions & 0 deletions tests/test_cookie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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