Overview
This is the second part of a tutorial that provides instructions for how to create an authentication mechanism for a web application utilizing Flask as the Python web framework and Elasticsearch (ES) as the NoSQL data store.
The first part of the tutorial covered the prerequisites, the Main API, the User model, and the Users API end point. In this second part of the tutorial, I will be covering the Flask-Login and session management modifications required for the main API, the User model, and the Auth API.
Once again, feel free to ask any questions below and I’ll be happy to respond!
Flask-Login
Flask-Login provides user session management for basic authentication tasks; logging a user in and logging out a user, in your application. You can restrict specific views for non-authenticated users by adding a decorator to your view routes. For this tutorial example, I have followed the basic configuration and created a custom user_loader for ES.
Main API
In the Main API, we define the ‘login_manager’ and the ‘load_user’ function for the Flask-Login ‘user_loader’ decorator which sets the callback for reloading a user from the session. The ‘load_user’ funcation creates a User object, checks if the user exists in ES, then returns the User object:
1 | @login_manager.user_loader |
Then we define the APP_SECRET_KEY as a global variable, then assign it to the main app and instantiate the ‘login_manager’:
1 | app.secret_key = APP_SECRET_KEY login_manager.init_app(app) |
That’s all the changes required for the ‘main.py’. We need to modify the User model but those changes are minor too.
User model
For the User model, we need to add a few functions that are required for Flask-Login. The function doc strings should be self explanatory.
1 | def is_authenticated(self): |
Auth API
Now for the Auth API, we create a ‘login’ route for authenticating a user and a ‘logout’ for unauthenticating a user. For the ‘login’ route, first, we verify the user submitting the request is valid by checking if the user key exists in ES. Next, we check if the request payload includes the correct password by comparing the password value with the hashed password from the database. Finally, we add the valid user into session via ‘login_user’. The ‘login’ route is almost identical to the ‘new’ user route from the User API, but we add the password check and add the authenticated user via ‘login_user’:
1 | ... |
Import the new auth and test Blueprints and register it with the URL route to the app in main.py:
1 | from example.v1.api.auth.views import auth |
Start the application again with the ‘main.py’ and run curl -X GET -D - http://127.0.0.1:8000/v1/test
. You should recieve an 401 unauthorized response:
1 | $ curl -X GET -D - http://127.0.0.1:8000/v1/test HTTP/1.0 401 UNAUTHORIZED Content-Type: application/json Content-Length: 294 Set-Cookie: session=eyJfaWQiOnsiIGIiOiJOalk0TldVMU1XWXdaamsyT0Roa1pqVmxOamN3TnpRNU5tSmpNamsxTVRJPSJ9fQ.B6pYAg.q2HbuYgeleBAGU1kKfDCCnGEugg; HttpOnly; Path=/ Server: Werkzeug/0.9.6 Python/2.6.6 Date: Tue, 20 Jan 2015 01:18:19 GMT { "error": "401: Unauthorized", "message": "The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password), or your browser doesn't understand how to supply the credentials required.", "success": false } |
We need to first authenticate our test user, store the cookie, then send the request again. Let’s authenticate the user we created in Part I, ‘test@abc.com’ and store the cookies into a file, ‘cookies.txt’
1 | $ curl -X POST -s -D - -c ~/cookies.txt -H 'Content-Type: application/json' -d '{"email_address": "test@abc.com", "password": "test"}' http://127.0.0.1:8000/v1/auth/login HTTP/1.0 200 OK Content-Type: application/json Content-Length: 360 Set-Cookie: session=eyJfZnJlc2giOnRydWUsIl9pZCI6eyIgYiI6Ik5qWTROV1UxTVdZd1pqazJPRGhrWmpWbE5qY3dOelE1Tm1Kak1qazFNVEk9In0sInVzZXJfaWQiOiJ0ZXN0QGFiYy5jb20ifQ.B58_Qg.Ez4andKJ01l51Ltd5nDg9EyXzTQ; HttpOnly; Path=/ Server: Werkzeug/0.9.6 Python/2.6.6 Date: Tue, 20 Jan 2015 01:22:10 GMT { "data": { "_id": "test@abc.com", "_index": "example", "_source": { "_type": "user", "created_at": 1417912435.2168, "email_address": "test@abc.com", "is_active": true }, "_type": "user", "_version": 1, "found": true }, "message": "'test@abc.com' successfully logged in!", "success": true } |
Boom! We’ve successfully authenitcated our test user! You can view the ‘cookies.txt’ to see the current session cookie. Now we can use that session variable to send a request to ‘test’ again: curl -X GET -s -D - -b ~/cookies.txt http://127.0.0.1:8000/v1/test
1 | $ curl -X GET -s -D - -b ~/cookies.txt http://127.0.0.1:8000/v1/test HTTP/1.0 200 OK Content-Type: application/json Content-Length: 273 Set-Cookie: session=eyJfZnJlc2giOnRydWUsIl9pZCI6eyIgYiI6Ik5qWTROV1UxTVdZd1pqazJPRGhrWmpWbE5qY3dOelE1Tm1Kak1qazFNVEk9In0sInVzZXJfaWQiOiJ0ZXN0QGFiYy5jb20ifQ.B58_6Q.JoOanNrX80o0hiBnrwGllvUg1G8; HttpOnly; Path=/ Server: Werkzeug/0.9.6 Python/2.6.6 Date: Tue, 20 Jan 2015 01:24:57 GMT { "data": { "cookies": { "session": "eyJfZnJlc2giOnRydWUsIl9pZCI6eyIgYiI6Ik5qWTROV1UxTVdZd1pqazJPRGhrWmpWbE5qY3dOelE1Tm1Kak1qazFNVEk9In0sInVzZXJfaWQiOiJ0ZXN0QGFiYy5jb20ifQ.B58_Qg.Ez4andKJ01l51Ltd5nDg9EyXzTQ" } }, "message": "Test", "success": true } |
That’s it! There’s not alot too it. You can use the ‘login_required’ decorator on any view that requires authentication. There are some session expiration configuration options and custom authentication params that are confgiurable in Flask-Login.
I hope you have found this tutorial helpful and maybe even learned a thing or two about Python, Flask, authentication, etc. Let me know if you have any questions.
Best,
Chris