-
-
Notifications
You must be signed in to change notification settings - Fork 228
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -195,6 +195,49 @@ def ensure_sync(self, f): | |
return f | ||
|
||
|
||
class HTTPCookieAuth(HTTPAuth): | ||
def __init__(self, scheme=None, realm=None, cookie_name=None): | ||
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' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a cookie named |
||
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) | ||
|
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"'} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
@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"') |
There was a problem hiding this comment.
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.