Mock a token for flask unit tests - firebase-admin-python SDK - python

I am using the firebase-admin-python SDK to handle authentication between an iOS app and a flask backend (python). This is my backend authentication endpoint, following the firebase guide:
from flask import request
from firebase_admin import auth
def get():
"""accessed via '/api/authtoken' """
try:
fir_token = request.headers["Authorization"]
decoded_token = auth.verify_id_token(fir_token)
fir_auth_id = decoded_token["uid"]
except:
...
How do I mock the fir_token for a unit test? How do I also mock auth.verify_id_token such that I don't need to actually connect to the firebase server?

Put the logic behind an interface.
class TokenVerifier(object):
def verify(self, token):
raise NotImplementedError()
class FirebaseTokenVerifier(TokenVerifier):
def verify(self, token):
return auth.verify_id_token(token)
class MockTokenVerifier(TokenVerifier):
def verify(self, token):
# Return mock object that will help pass your tests.
# Or raise an error to simulate token validation failures.
Then make sure during unit tests your code uses the MockTokenVerifier.
It is also possible to create mock ID tokens, and stub out parts of the Admin SDK so that the auth.verify_id_token() runs normally during tests (see unit tests of the SDK). But I prefer the above solution since it's easier, cleaner and doesn't require messing with the internals of the SDK.

I achieved this using the patch decorator from unittest.mock library.
auth.py
...
from flask_restful import Resource, reqparse
from firebase_admin import auth
class Token(Resource):
def __init__(self):
self.parser = reqparse.RequestParser()
self.parser.add_argument('token', type=str, required=True)
# route for /api/auth/token
def post(self):
args = self.parser.parse_args()
try:
firebase_user = auth.verify_id_token(args['token'])
except Exception:
abort(401)
# use firebase_user
test_auth.py
from unittest import mock
mock_firebase_user = {
'user_id': 'firebasegenerateduserid',
'email': 'testuser#gmail.com',
# ... add more firebase return values
}
# client is from conftest.py
def test_auth(client):
with mock.patch('auth.auth.verify_id_token') as magic_mock:
magic_mock.return_value = mock_firebase_user
post_data = {
'token': 'firebaserusertokenid'
}
response = client.post('/api/auth/token', data=post_data)
assert response.status_code == 200

Related

struggling to test flask-dance / flask-security / flask-sqlalchemy / pytest

My application requires login to google for later use of google apis. I have flask-dance, flask-security, flask-sqlalchemy working to the point where I can do the log in and log out in my development system.
What I've been struggling with is testing of the login using pytest. I am trying force the login with the call to flask_security.login_user, but test_login fails as if nobody is logged in. I suspect this is a problem because of context setting, but I have tried a lot of different things and haven't found the magic elixir.
Unfortunately, while I have a lot of experience in software development in general and python in particular, I don't have the pytest / flask-dance / flask-security background needed to solve this.
in settings.py
class Testing():
# default database
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
TESTING = True
WTF_CSRF_ENABLED = False
# need to set SERVER_NAME to something, else get a RuntimeError about not able to create URL adapter
SERVER_NAME = 'dev.localhost'
# need a default secret key - in production replace by config file
SECRET_KEY = "<test secret key>"
# fake credentials
GOOGLE_OAUTH_CLIENT_ID = 'fake-client-id'
GOOGLE_OAUTH_CLIENT_SECRET = 'fake-client-secret'
# need to allow logins in flask-security. see https://github.com/mattupstate/flask-security/issues/259
LOGIN_DISABLED = False
in conftest.py
import pytest
from racesupportcontracts import create_app
from racesupportcontracts.dbmodel import db
from racesupportcontracts.settings import Testing
#pytest.fixture
def app():
app = create_app(Testing)
yield app
#pytest.fixture
def dbapp(app):
db.drop_all()
db.create_all()
yield app
in test_basic.py
def login_test_user(email):
from racesupportcontracts.dbmodel import db, User
from flask_security import login_user
user = User.query.filter_by(email=email).one()
login_user(user)
db.session.commit()
def test_login(dbapp):
app = dbapp
from racesupportcontracts.dbmodel import db, init_db
from racesupportcontracts import user_datastore
from flask import url_for
# init_db should create at least superadmin, admin roles
init_db(defineowner=False)
useremail = 'testuser#example.com'
with app.test_client() as client:
create_user(useremail, 'superadmin')
login_test_user(useremail)
resp = client.get('/', follow_redirects=True)
assert resp.status_code == 200
assert url_for('admin.logout') in resp.data
When you call login_user(), that modifies the flask.session object. However, when using the test client, you can only modify flask.session inside of a session transaction. It should work if you do this:
with app.test_client() as client:
with client.session_transaction() as sess:
sess["user_id"] = 1 # if you want user 1 to be logged in for this test
resp = client.get('/', follow_redirects=True)
# make whatever assertions you want
If you install the latest version of Flask-Login from GitHub, you can also use the FlaskLoginClient class to make this more readable:
# in conftest.py
from flask_login import FlaskLoginClient
#pytest.fixture
def app():
app = create_app(Testing)
app.test_client_class = FlaskLoginClient
yield app
# in test_basic.py
def test_login(app):
user = User.query.filter_by(email='testuser#example.com').one()
with app.test_client(user=user) as client:
resp = client.get('/', follow_redirects=True)
# make whatever assertions you want
Unfortunately, the author of Flask-Login refuses to publish an update of the package to PyPI, so you can't use the version of Flask-Login that is on PyPI, you have to install from GitHub. (I have no idea why he refuses to publish an update.)

Adding resources with jwt_required?

I've created an API using flask, where the authentication is all working fine using flask_jwt_extended.
However if I add a resource that has a jwt_required decorator I get this error.
File "/Library/Python/2.7/site-packages/flask_jwt/__init__.py", line 176, in decorator
_jwt_required(realm or current_app.config['JWT_DEFAULT_REALM'])
KeyError: 'JWT_DEFAULT_REALM'
Example resource:
class Endpoint(Resource):
#jwt_required()
def get(self):
return {"State": "Success"}
Initialising the app:
app = Flask(__name__)
api = Api(app)
Adding the resource:
api.add_resource(resource_class, "/myEndpoint")
The only way I can get it to work is to define the Endpoint class in the same file as the API.
I think I need someway to pass the Realm into the endpoint class and have use the optional parameter on jwt_required to set the Realm.
Discovered the issue, in the resource I was importing the jwt_required:
from flask_jwt_extended import jwt_required
However I needed to import jwt_required from the class that where JWT was initalized.
I think you forgot to initialize JWT instance. You can do it in 2 ways. First:
from flask import Flask
from flask_jwt import jwt_required, JWT
from flask_restful import Resource, Api
class Endpoint(Resource):
#jwt_required()
def get(self):
return {"State": "Success"}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'super-secret'
def authenticate(username, password):
# you should find user in db here
# you can see example in docs
user = None
if user:
# do something
return user
def identity(payload):
# custom processing. the same as authenticate. see example in docs
user_id = payload['identity']
return None
# here what you need
jwt = JWT(app, authenticate, identity)
api = Api(app)
api.add_resource(Endpoint, '/myEndpoint')
if __name__ == '__main__':
app.run(debug=True)
app.run(host='0.0.0.0')
The second way is update our configuration of application. Just change:
jwt = JWT(app, authenticate, identity)
To:
app.config.update(
JWT=JWT(app, authenticate, identity)
)
Let's open our route. You will see:
{
"description": "Request does not contain an access token",
"error": "Authorization Required",
"status_code": 401
}
Hope it helps.

How to implement login required decorator in Flask

I have 2 Flask apps (different projects) that work together . One implements some API which uses tokens for auth. The second one consumes the API and makes a web interface for it. Now I have a login function that sends the username and password to the API, and if correct, gets the auth token in return. Once I have the token, I save it to the session of the user and the user should now be considered as logged in/ autheticated. How can I implement the login_required decorator for such a case.
Here is my login function -
def login(self):
response = make_request(BASE_URL + 'login/', clean_data(self.data))
if response.status_code == 200:
session['auth_token'] = response.json().get('auth_token')
return True
return False
How can I make the login_required decorator?
Also I am using Redis to store sessions if that matters.
Have a look at the official flask docs regarding decorators:
https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/ or the python docs https://www.python.org/dev/peps/pep-0318/ as well.
Your decorator should look something like:
from functools import wraps
from flask import abort
import jwt
def authorize(f):
#wraps(f)
def decorated_function(*args, **kws):
if not 'Authorization' in request.headers:
abort(401)
user = None
data = request.headers['Authorization'].encode('ascii','ignore')
token = str.replace(str(data), 'Bearer ','')
try:
user = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])['sub']
except:
abort(401)
return f(user, *args, **kws)
return decorated_function
... and then in your app.py you may have:
#app.route('/api/game', methods=['POST'])
#authorize
def create(user):
data = json.loads(request.data)
....
In this particular case I have used JWT as token and your token can be different respectively the decoding of the token can be your custom implementation, but the basic mechanisms are pretty much as on the example above.
I would place the following decorator function in somewhere common
def validate_api_token(validation_func):
def decorator(f):
#wraps(f)
def decorated_function(*args, **kws):
api_token = request.headers.get('Authorization')
is_valid_api_token = validation_func(api_token)
if is_valid_api_token:
return f(*args, **kws)
return 'Invalid API Token', 401
return decorated_function
return decorator
For small POC flask apps, if you're ok with storing the tokens in a non-versioned file, the following can work:
# tokens are read from a non-versioned `.tokens` file and loaded into a set
api_tokens = load_api_tokens()
def simple_api_token_validation(api_token):
return api_token in api_tokens
#app.route("/v1/my/secret/function", methods=['POST'])
#validate_api_token(simple_api_token_validation)
def my_secret_function():
body = request.get_json()
# ...
Another simple option is to query against a database (e.g. redis):
redis_session = Redis(host=REDIS_HOST, password=REDIS_PASSWORD)
def redis_api_token_validation(api_token):
if not api_token:
return False
api_token_hash = hashlib.sha256(api_token.encode()).hexdigest()
return redis_session.exists(f'api:tokens:{api_token_hash}')
#app.route("/v1/my/secret/function", methods=['POST'])
#validate_api_token(redis_api_token_validation)
def my_secret_function():
body = request.get_json()
# ...
Best IMO as #Velin answered is to use jwt to validate the token
Given that each subsequent request will contain the API token, the decorator should do the following
Accept a generic request. You can use *args and **kargs for that
Extract the token from the header and compare it with the token stored in db (not Redis, but wherever the token generated is stored in the backend)
If authenticated, the *args and **kargs should be passed on to the decorated function
The output of the decorated function should then be returned as is
If the authentication failed, an error message should be returned.
For explanation on decorators, check out this link:
http://thecodeship.com/patterns/guide-to-python-function-decorators/

I am restructuring my Flask-restful app, but having trouble placing the HTTP-auth in order to get app running

Essentially, I have a directory as such:
/app
runserver.py
/myapp
__init__.py
api.py
auth.py
/resources
__init.py
users.py
login.py
/models
__init.py
models.py
/common
/assets
In my auth.py I have a standard HTTP-basic username/password authentication. I will use these for areas where login is a must, and I want to verify each user. Login.py is where I need to add my decorator, but the whole app does not run due to this error: AttributeError: 'module' object has no attribute 'login_required'
from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()
#auth.verify_password
def verify_password(username, password):
user = User.query.filter_by(username = username).first()
if not user or not user.verify_password(password):
return False
g.user = user
return True
#auth.error_handler
def unauthorized():
return make_response(jsonify({'message': 'Unauthorized'}), 403)
My code for the login.py, which calls the decorator and then asks for the auth.
from flask_restful import Resource, reqparse
from myapp.models.users import User
from myapp import auth
class login(Resource):
decorators = [auth.login_required]
def __init__(self):
self.reqparse = reqparse.RequestParser()
self.reqparse.add_argument('userid', type = str , default="")
self.reqparse.add_argument('username', type = str, default="")
self.reqparse.add_argument('password', type = str, default="")
super(login, self).__init__()
def post(self):
args = self.reqparse.parse_args()
username = args['username']
password = args['password']
message = {'status': 'Authorized'}
return message
So to wrap it up, my question is: How and where do I add the flask-httpauth class so I can use the decorators. My option right now may be to paste that auth code in every resource class that needs it, but there seems there must be a better way to organize that. Help?
You are importing your auth module when really you want to be importing the HTTPBasicAuth object in that module. It is also possible you're running in to problems due to the fact that your module has the same name as the HTTPBasicAuth object.
I recommend renaming your auth.py to something else, such as authentication.py, and change your import to:
from ..authentication import auth
This gets a bit confusing because you have an auth.py module that defines an auth variable inside.
The line:
from myapp import auth
is importing the module, not the variable defined in it. Change it to:
from myapp.auth import auth
And I think that will work.
Sorry this is a bit old, but for the sake of others with this question, I would suggest not using flask.ext.httpauth. I found it isn't very useful. Here is how I do my HTTP basic auth with flask-restful.
This is in the myapp/init.py:
from flask import Flask, request
from flask.ext.restful import abort
def requires_auth(f):
#wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth:
abort(401)
user = User.query.filter(User.username == auth.username).first()
auth_ok = False
if user != None:
auth_ok = verify_password(auth.password) == user.password
if not auth_ok:
return abort(401)
return f(*args, **kwargs)
return decorated
Resource script that has a resource that requires authorization to access the resource.
from myapp import requires_auth
#requires_auth
def get(self):
# do something

What's the proper way to test token-based auth using APIRequestFactory?

The query to my endpoint works fine (as long as I pass it a valid token), it returns the json representation of my response data.
The code in the service api that calls my endpoint, passing an auth token in the header:
headers = {'content-type': 'application/json',
'Authorization': 'Token {}'.format(myToken)}
url = 'http://localhost:8000/my_endpoint/'
r = session.get(url=url, params=params, headers=headers)
In views.py, I have a method decorator that wraps the dispatch method on the view (viewsets.ReadOnlyModelViewSet):
def login_required(f):
def check_login_and_call(request, *args, **kwargs):
authentication = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(authentication, str):
authentication = authentication.encode(HTTP_HEADER_ENCODING)
key = authentication.split()
if not key or len(key) != 2:
raise PermissionDenied('Authentication failed.')
user, token = authenticate_credentials(key[1])
return f(request, *args, **kwargs)
return check_login_and_call
I'm trying to write a test to authenticate the request using a token:
from rest_framework.authtoken.models import Token
from rest_framework.test import APIRequestFactory
from rest_framework.test import APITestCase
from rest_framework.test import force_authenticate
class EndpointViewTest(APITestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = User.objects.create_user(
username='user#foo.com', email='user#foo.com', password='top_secret')
self.token = Token.objects.create(user=self.user)
self.token.save()
def test_token_auth(self):
request = self.factory.get('/my_endpoint')
force_authenticate(request, token=self.token.key)
view = views.EndpointViewSet.as_view({'get': 'list'})
response = view(request)
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.render().content)['results']
For some reason, I cannot get the request to properly pass the token for this test. Using force_authenticate doesn't seem to change the header that I'm using for validating the token. The current output is raising "PermissionDenied: Authentication failed." because the token isn't being set on the request.
Is there a proper way to set this in the request header in my test or to refactor the way I'm using it in the first place?
I found a way to get the test to pass, but please post if you have a better idea of how to handle any of this.
request = self.factory.get('/my_endpoint', HTTP_AUTHORIZATION='Token {}'.format(self.token))
force_authenticate(request, user=self.user)
After changing the above two lines of the test, it seems to authenticate based on the token properly.
I wanted to test the authentication function itself, so forcing authentication wans't an option.
One way to properly pass the token is to use APIClient, which you already have imported.
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
response = client.get('/api/vehicles/')
That sets your given token into the request header and lets the back end decide if it's valid or not.
Sorry for digging this old thread up, but if someone is using APIClient() to do their tests you can do the following:
from rest_framework.test import APITestCase
from rest_framework.test import APIClient
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
class VehicleCreationTests(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_superuser('admin', 'admin#admin.com', 'admin123')
self.token = Token.objects.create(user=self.user)
def testcase(self):
self.client.force_login(user=self.user)
response = self.client.post('/api/vehicles/', data=vehicle_data, format='json', HTTP_AUTHORIZATION=self.token)
self.assertEqual(response.status_code, 201)
Really good resource that I've used to come up with this is django-rest-framework-jwt tests
The simpler way to force_authentication using a built-in method from APITestCase is:
class Test(APITestCase):
def setUp(self):
user1 = User.objects.create_user(username='foo')
self.client.force_authenticate(user=user1) # self.client is from APITestCase
... the rest of your tests ...

Categories

Resources