At my current job, I happened to be a part of a backend team, which is creating an API. The API should be then served to JavaScript application and needs to be quite fast (100 ms or so). However, it is not.
After some profiling, we figured out that it is the token authentication in Flask-security
, which is holding us back (please see the MWE).
MWE
import flask
from flask_security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin, auth_required
from flask_sqlalchemy import SQLAlchemy
app = flask.Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/database.sqlite3'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['WTF_CSRF_ENABLED'] = False
app.config['SECURITY_TOKEN_AUTHENTICATION_HEADER'] = 'Authorization'
app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512'
app.config['SECURITY_PASSWORD_SALT'] = b'secret'
app.config['SECRET_KEY'] = "super_secret"
db = SQLAlchemy(app)
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
# Setup Flask-Security
security = Security(app, user_datastore)
db.drop_all()
db.create_all()
admin_role = Role(**{'name': 'admin', 'description': 'Admin role'})
db.session.add(admin_role)
db.session.commit()
user_datastore.create_user(email='test@example.com', password='test', active=True, roles=[Role.query.first()])
db.session.commit()
@app.route('/')
@auth_required('basic', 'token')
def hello():
return flask.jsonify({'hello': 'world'})
if __name__ == '__main__':
app.run(debug=True)
Basic authentication timing
The timings are perfect (bellow 100 ms) but that is not the way we should do it.
time curl http://127.0.0.1:5000/ -u "test@example.com:test"
{
"hello": "world"
}
real 0m0.076s
user 0m0.008s
sys 0m0.006s
Token authentication timing
Getting the token is OK.
curl -H "Content-Type: application/json" -X POST -d '{"email":"test@example.com","password":"test"}' http://127.0.0.1:5000/login
{
"meta": {
"code": 200
},
"response": {
"user": {
"authentication_token": "WyIxIiwiJDUkcm91bmRzPTUzNTAwMCRFRUpLRFNONlB2L1hzL2lRJDhMWFZvZlpLMmVoa1BVdWtpRlhUR1lvNEJ3T3FjS3dKMVhVWGlOczRwZDMiXQ.DOLjcQ.oBrT4gr1m49rISyxhaj9Lxu1VNk",
"id": "1"
}
}
}
But than the request is terribly slow. The timings are 20 times slower.
time curl "http://127.0.0.1:5000/?auth_token=WyIxIiwiJDUkcm91bmRzPTUzNTAwMCRFRUpLRFNONlB2L1hzL2lRJDhMWFZvZlpLMmVoa1BVdWtpRlhUR1lvNEJ3T3FjS3dKMVhVWGlOczRwZDMiXQ.DOLjcQ.oBrT4gr1m49rISyxhaj9Lxu1VNk"
{
"hello": "world"
}
real 0m2.371s
user 0m0.005s
sys 0m0.006s
What with that???
I know that Flask-security
wraps together several other flask security packages (Flask-login
, Flask-WTF
, ...).
- Do you know what could be the cause? (is it
Flask-security
orFlask-login
or something deeper?) - It seems that the hashing algorithm, which is slow, is running for every request.
However, it might not be necessary to do it every time.
It should be enough, to only store the token and check if the incoming token is the same as the stored one.
Is there a way to do it like that (either with
Flask-security
or not)? - Can I set the app (
app.config
) a different way to make it faster (still using the token auth.)? - Is there a workaround (still using
Flask-security
)? - Shall I write it myself? Is it the
Flask-security
holding us back? - Anyone have a clue about this ?
I have cross-posted this as an issue on GitHub.