I hope that somebody can help me.
I have to write a unit test with unittest of Python in a flask api. I have a login route that works perfectly fine when accessing it through the app with a React frontend but whenever I tried to post from the test, the request.authorization is None... It drives me crazy
I looked all over the internet and tried a lot of different approach but whatever I do, request.authorization is always None when doing a test
Testing :
import unittest
import base64
from backend.peace_api import app
class TestLogin(unittest.TestCase):
# Assert login() with correct authentication
def test_login(self):
with app.app_context():
tester = app.test_client(self)
auth = 'seo#hotmail.com:password'
authheader = base64.b64encode(bytes(auth, 'UTF-8'))
headers = {"HTTP_AUTHORIZATION": "Bearer " + str(authheader), "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}
response = tester.post('/api/login/', headers=dict(headers))
print(response.json)
self.assertEqual(response.status_code, 200)
if __name__ == '__main__':
unittest.main()
Route :
import jwt
import datetime
from flask import Blueprint, request, jsonify
from backend.peace_api import database, secret_key
from backend.peace_api.flat.models.flat import Flat
login_blueprint = Blueprint("login", __name__)
#login_blueprint.route("/", methods=["POST"])
def login():
auth = request.authorization # This here is always None
print("Hello World")
print(request)
print(request.authorization)
if auth is None:
return jsonify({"success": False}, 401)
email = auth.username
password = auth.password
if email is None or email is None or password is None:
return jsonify({"success": False}, 500)
mongo_flat = database.flats.find_one({"email": email})
if mongo_flat is not None:
flat = Flat(
mongo_flat["_id"],
mongo_flat["name"],
mongo_flat["email"],
mongo_flat["password"],
mongo_flat["tasks"],
mongo_flat["members"],
)
if password == flat.password and email == flat.email:
token = jwt.encode(
{
"id": str(flat.id),
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
},
secret_key,
)
return jsonify({"token": token.decode("UTF-8")})
else:
return jsonify({"success": False}, 401)
else:
return jsonify({"success": False}, 401)
Printed message :
Testing started at 19:15 ...
Launching unittests with arguments python -m unittest test_login.TestLogin in [...]\tests
Hello World
<Request 'http://localhost/api/login/' [POST]>
None
Ran 1 test in 0.017s
OK
[{'success': False}, 401]
I have honestly no clue what I should do... Thanks for the help
So there are a few issues with your setup which are resulting in the header not being sent or being sent but being malformed.
The name of the header is "Authorization", not "HTTP_AUTHORIZATION".
The credentials value for the Authorization header needs to be base64 encoded per the spec.
The default authorization middleware for Werkzeug only supports Basic auth, so your Bearer token will not work unless you're using an extension that adds Bearer support to Werkzeug (without knowing more about your setup it's hard to know what's going on there).
Here's a very simplified Flask App that demonstrates a working test client with a functioning Authorization header:
import flask
import base64
app = flask.Flask("app")
#app.route("/")
def test():
print(flask.request.authorization)
return "Worked"
with app.test_client() as c:
c.get("/", headers={"Authorization": "Basic {}".format(base64.b64encode(b"useo#hotmail.com:pass").decode("utf8"))})
Which prints:
{'password': 'pass', 'username': 'seo#hotmail.com'}
<Response streamed [200 OK]>
A similar question was asked here:
Flask werkzeug request.authorization is none but Authorization headers present
Related
I have a simple flask app, which is intended to make a request to an api and return data. Unfortunately, I can't share the details, so you can reproduce the error. The app looks like that:
from flask import Flask
import requests
import json
from requests.auth import HTTPBasicAuth
app = Flask(__name__)
#app.route("/")
def getData():
# define variables
username = "<username>"
password = "<password>"
headers = {"Authorization": "Basic"}
reqHeaders = {"Content-Type": "application/json"}
payload = json.dumps(
{
"jobType": "<jobType>",
"jobName": "<jobName>",
"startPeriod": "<startPeriod>",
"endPeriod": "<endPeriod>",
"importMode": "<importMode>",
"exportMode": "<exportMode>"
}
)
jobId = 7044
req = requests.get("<url>", auth=HTTPBasicAuth(username, password), headers=reqHeaders, data=payload)
return req.content
if __name__ == "__main__":
app.run()
However, when executed this returns error 500: Internal Server Error
The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.
The same script but outside a flask app (just the function as it is defined here) runs with no problems at all.
What am I doing wrong?
flask app return format json. If you return req.content, it will break function. You must parse response request to json before return it.
from flask import jsonify
return jsonify(req.json())
It's better with safe load response when the request fail
req = requests.get()
if req.status_code !=200:
return {}
else:
return jsonify(req.json())
For some of my Django views I've created a decorator that performs Basic HTTP access authentication. However, while writing test cases in Django, it took me a while to work out how to authenticate to the view. Here's how I did it. I hope somebody finds this useful.
Here's how I did it:
from django.test import Client
import base64
auth_headers = {
'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('username:password'),
}
c = Client()
response = c.get('/my-protected-url/', **auth_headers)
Note: You will also need to create a user.
In your Django TestCase you can update the client defaults to contain your HTTP basic auth credentials.
import base64
from django.test import TestCase
class TestMyStuff(TestCase):
def setUp(self):
credentials = base64.b64encode('username:password')
self.client.defaults['HTTP_AUTHORIZATION'] = 'Basic ' + credentials
For python3, you can base64-encode your username:password string:
base64.b64encode(b'username:password')
This returns bytes, so you need to transfer it into an ASCII string with .decode('ascii'):
Complete example:
import base64
from django.test import TestCase
class TestClass(TestCase):
def test_authorized(self):
headers = {
'HTTP_AUTHORIZATION': 'Basic ' +
base64.b64encode(b'username:password').decode("ascii")
}
response = self.client.get('/', **headers)
self.assertEqual(response.status_code, 200)
Assuming I have a login form, I use the following technique to login through the test framework:
client = Client()
client.post('/login/', {'username': 'john.smith', 'password': 'secret'})
I then carry the client around in my other tests since it's already authenticated. What is your question to this post?
(python3) I'm using this in a test:
credentials_string = '%s:%s' % ('invalid', 'invalid')
credentials = base64.b64encode(credentials_string.encode())
self.client.defaults['HTTP_AUTHORIZATION'] = 'Basic ' + credentials.decode()
and the following in a view:
import base64
[...]
type, auth = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
auth = base64.b64decode(auth.strip()).decode()
Another way to do it is to bypass the Django Client() and use Requests instead.
class MyTest(TestCase):
def setUp(self):
AUTH = requests.auth.HTTPBasicAuth("username", "password")
def some_test(self):
resp = requests.get(BASE_URL + 'endpoint/', auth=AUTH)
self.assertEqual(resp.status_code, 200)
I am using Flask-dance in order to authenticate users to my app. The auth provider is Google.
From time to time the following exception is raised:
2019-08-09 08:07:26 default[20190809t105407] "GET / HTTP/1.1" 500
2019-08-09 08:07:26 default[20190809t105407] -- 0 --
2019-08-09 08:07:26 default[20190809t105407] -- 1 --
2019-08-09 08:07:27 default[20190809t105407] InvalidClientIdError was caught: (invalid_request) Missing required parameter: refresh_token
Looking around this problem I could find 2 directions to go with:
Use offline=True while creating the Google blueprint
Implementing error handler for TokenExpiredError
I did both and deployed my app to GAE but I still face the same error. From the stacktrace I understand that the error handler is being invoked but while the code tries to recover the 'refresh_token' is raised
My code: (The code is based on Google QuickStart and Flask-dance issue 143)
import oauthlib
from flask import Flask, redirect, url_for, flash, session, current_app
from flask_dance.contrib.google import make_google_blueprint, google
import os
import time
from oauthlib.oauth2.rfc6749.errors import InvalidClientIdError, TokenExpiredError
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", None)
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", None)
app = Flask(__name__)
app.secret_key = "TODO_TODO"
blueprint = make_google_blueprint(
client_id=GOOGLE_CLIENT_ID,
client_secret=GOOGLE_CLIENT_SECRET,
offline=True
)
app.register_blueprint(blueprint, url_prefix="/login")
#app.route('/logout', methods=['GET'])
def logout():
_revoke_token_and_empty_session()
return redirect(url_for('app.index'))
def _revoke_token_and_empty_session():
print('inside _revoke_token_and_empty_session')
if google.authorized:
try:
google.get(
'https://accounts.google.com/o/oauth2/revoke',
params={
'token':
current_app.blueprints['google'].token['access_token']},
)
except TokenExpiredError:
pass
except InvalidClientIdError:
# Our OAuth session apparently expired. We could renew the token
# and logout again but that seems a bit silly, so for now fake
# it.
pass
session.clear()
#app.errorhandler(oauthlib.oauth2.rfc6749.errors.TokenExpiredError)
def token_expired(_):
print('In TokenExpiredError')
del blueprint.token
_revoke_token_and_empty_session()
flash('Your session had expired. Please submit the request again',
'error')
return redirect(url_for('app.index'))
#app.route("/")
def index():
print('-- 0 --')
if not google.authorized:
return redirect(url_for("google.login"))
print('-- 1 --')
user_info_url = 'https://openidconnect.googleapis.com/v1/userinfo'
try:
resp = google.get(user_info_url)
except InvalidClientIdError as e:
#
# Here is the problem
#
print('InvalidClientIdError was caught: {}'.format(str(e)))
return 'Having an InvalidClientIdError issue: {}'.format(str(e)), 500
else:
print('-- 2 --')
user_info = resp.json()
return "You are {user_name} on Google. Time: {t}".format(user_name=user_info['name'], t=time.time())
if __name__ == "__main__":
app.run()
My current understanding is that the TokenExpiredError was caught and the function index was called. When the function tries to call resp = google.get(user_info_url) the InvalidClientIdError: (invalid_request) Missing required parameter: refresh_token is raised.
Any idea how to solve it?
From your code, and flask code, looks like you are trying to delete the token before revoking it, even though it's already invalid. If you want to avoid passing "offline=True" then try to simplify the code since there is no need to flash anything (the user will be redirected), and the token is already invalid, so it doesn't make much sense to revoke it either. I'm also setting only one error handler.
Here is the code that worked for me:
import oauthlib
from flask import Flask, redirect, url_for, flash, session, current_app
from flask_dance.contrib.google import make_google_blueprint, google
import os
import time
from oauthlib.oauth2.rfc6749.errors import InvalidClientIdError, TokenExpiredError
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", None)
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", None)
app = Flask(__name__)
app.secret_key = "TODO_TODO"
blueprint = make_google_blueprint(
client_id=GOOGLE_CLIENT_ID,
client_secret=GOOGLE_CLIENT_SECRET,
offline=True
)
app.register_blueprint(blueprint, url_prefix="/login")
#app.route('/logout', methods=['GET'])
def logout():
"""
This endpoint tries to revoke the token
and then it clears the session
"""
if google.authorized:
try:
google.get(
'https://accounts.google.com/o/oauth2/revoke',
params={
'token':
current_app.blueprints['google'].token['access_token']},
)
except TokenExpiredError:
pass
except InvalidClientIdError:
# Our OAuth session apparently expired. We could renew the token
# and logout again but that seems a bit silly, so for now fake
# it.
pass
_empty_session()
return redirect(url_for('app.index'))
def _empty_session():
"""
Deletes the google token and clears the session
"""
if 'google' in current_app.blueprints and hasattr(current_app.blueprints['google'], 'token'):
del current_app.blueprints['google'].token
session.clear()
#app.errorhandler(oauthlib.oauth2.rfc6749.errors.TokenExpiredError)
#app.errorhandler(oauthlib.oauth2.rfc6749.errors.InvalidClientIdError)
def token_expired(_):
_empty_session()
return redirect(url_for('app.index'))
#app.route("/")
def index():
print('-- 0 --')
if not google.authorized:
return redirect(url_for("google.login"))
print('-- 1 --')
user_info_url = 'https://openidconnect.googleapis.com/v1/userinfo'
resp = google.get(user_info_url)
user_info = resp.json()
return "You are {user_name} on Google. Time: {t}".format(user_name=user_info['name'], t=time.time())
if __name__ == "__main__":
app.run()
I'm new to Python and I try to implement REST API service on Flask. I faced with issue related to testing of my code. My Flask app looks something like that:
from flask import Flask, jsonify, make_response, request
from flask_httpauth import HTTPBasicAuth
import os
auth = HTTPBasicAuth()
#auth.get_password
def get_password(username):
if username == os.environ['SERVICE_KEY']:
return os.environ['SERVICE_PASS']
return None
#auth.error_handler
def unauthorized():
return make_response(jsonify({'error': 'Unauthorized access'}), 403)
app = Flask(__name__)
tweets = [
{
'id': 1,
'profileId': '1',
'message': 'My test tweet'
},
{
'id': 2,
'profileId': '1',
'message': 'Second tweet!'
}
]
#app.route('/api/v1/tweets', methods=['GET'])
#auth.login_required
def get_tweets():
return jsonify({'tweets': tweets}), 200
#app.errorhandler(404)
#auth.login_required
def not_found(error):
return make_response(jsonify({'error': 'Not found'}), 404)
if __name__ == '__main__':
app.run(debug=True)
And here is my test (currently it is only for not_found method):
import unittest
from app import app
class TestApp(unittest.TestCase):
def setUp(self):
self.app = app.test_client()
def test_404(self):
rv = self.app.get('/i-am-not-found')
self.assertEqual(rv.status_code, 404)
if __name__ == '__main__':
unittest.main()
But when I try to run test, it fails due to I get 'Unauthorized access' response:
>python test.py
F
======================================================================
FAIL: test_404 (__main__.TestApp)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 25, in test_404
self.assertEqual(rv.status_code, 404)
AssertionError: 403 != 404
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
Which approach for testing route-methods are more correct to handle authorization? And how can I fix that failed test?
You need to create a custom header that includes your auth details and send it along with your request. Something like this:
from base64 import b64encode
...
headers = {'Authorization': 'Basic ' + b64encode("{0}:{1}".format(username, password))}
rv = self.app.get('/i-am-not-found', headers=headers)
...
import unittest
from app import app
class TestApp(unittest.TestCase):
def setUp(self):
self.app = app.test_client()
def test_404(self):
headers = {
'Authorization': 'Basic ' + b64encode("username:password")
}
rv = self.app.get('/i-am-not-found', headers=headers)
self.assertEqual(rv.status_code, 404)
if __name__ == '__main__':
unittest.main()
Your username and password is sent in the form username:password but is base64 encoded. If expanding this there are ways to make this simpler such as extracting into a function to always pass the header and externalising username/password for testing.
EDIT: Additionally I think you should be returning a 401 code here. 401 is usually used when credentials are incorrect, 403 is usually used when you have successfully authenticated yourself but do not have access to a resource. A very simplified example being logged into Facebook but being restricted from accessing another person's photo that is marked as private.
I tried to use flask_oauthlib to access my twitter api, but all I get is the error : Failed to generate request token. Here is the code.
from flask_oauthlib.client import OAuth
from flask import Flask, url_for, request, jsonify
app = Flask(__name__)
oauth = OAuth()
twitter = oauth.remote_app(
'twitter',
base_url='https://api.twitter.com/1/',
request_token_url='https://api.twitter.com/oauth/request_token',
access_token_url='https://api.twitter.com/oauth/access_token',
authorize_url='https://api.twitter.com/oauth/authorize',
consumer_key='dOJjyxB6gxXWTjdtfPUZcZPjl',
consumer_secret='im not telling you',
)
#app.route('/login')
def login():
return twitter.authorize(callback=url_for('authorized',
next=request.args.get('next') or request.referrer or None))
#app.route('/authorized')
#twitter.authorized_handler
def authorized(resp):
if resp is None:
return 'Access denied: error=%s' % (
request.args['error']
)
if 'oauth_token' in resp:
# session['example_oauth'] = resp
print(resp)
return jsonify(resp)
return str(resp)
if __name__ == '__main__':
app.run(port=8000, debug=True)
This didn't work while using http://term.ie/oauth/example/client.php, I managed to get a request token.
I inspired myself with https://github.com/lepture/example-oauth1-server/blob/master/client.py and http://flask-oauthlib.readthedocs.io/en/latest/client.html
EDIT
Weird fact : I tried the code here : https://github.com/lepture/flask-oauthlib/blob/master/example/twitter.py
I didn't changed the key and secret and it worked.
So I tried to change them for my own credentials, and it stopped working. I really can't understand...
Ok I found the problem. It appears that the callback URL is mandatory when using flask-oauthlib. So I added a fake one since i'm still on localhost, and it solved this problem.
In case anyone found this issue. I'm the author of Flask-OAuthlib. I suggest that you use Authlib instead, browser the source code at https://github.com/lepture/authlib. There are many built-in social connections in https://github.com/authlib/loginpass.