In a way this goes with the general ethos of micro frameworks - it isn't being forced down your throat unless you want it.
However in this case I think that at least some structured 'recipes' are required to provide peer guidance, best practice or whatever you want to call it. Too often the advice is "roll your own", but I'm not sure I'd be comfortable recommending that to the inexperienced.
So where are we?
Flask-Principal and Flask-Login
At the moment there is only one approved auth extension called Flask-Principal which takes care of authorizing different roles of user. It does not dictate what those roles should be and it does not force you to use any particular method to authenticate those users.
A second extension called Flask-Login is currently awaiting approval, which I suspect it will soon get. This extension also takes care of authorizing users, but only understands one type of user role, so in other words you are either a user or not. Like Flask-Principal it does not force you to use any particular method to authenticate those users.
The code below is sort of a basic outline that we will fill in using proper code in a moment.
The User model is very silly. If you create a User by passing an id of '1' you get a User with a username of 'user1' and a password of 'user1_secret'. To help with the examples we then create users one to twenty (user1 to user20). Obviously you would normally have them stored in a database or something, but we're keeping it simple.
At this stage there is nothing protecting access to the 'home' resource and 'login' and 'logout' don't work yet either.
from flask import Flask, Response, redirect, \ url_for, request, session, abort app = Flask(__name__) # config app.config.update( DEBUG = True, SECRET_KEY = 'secret_xxx' ) # middlewares etc go here # silly user model class User(object): def __init__(self, id): self.id = id self.name = "user" + str(id) self.password = self.name + "_secret" def __repr__(self): return "%d/%s/%s" % (self.id, self.name, self.password) # create some users with ids 1 to 20 users = [User(id) for id in range(1, 21)] # we'd like to protect this resource @app.route('/') def home(): return Response("Hello World!") # somewhere to login @app.route('/login', methods=['GET', 'POST']) def login(): pass # this does not work yet # somewhere to logout @app.route("/logout") def logout(): pass # this does not work yet # handle login failed @app.errorhandler(401) def page_not_found(e): return Response('Login failed') @app.errorhandler(403) def page_not_found(e): return Response('Unauthorized') if __name__ == "__main__": app.run()Download the code!
Now that we know how that works, or doesn't, let's look first at how flask principal might set about protecting the home resource.
As I said earlier, Flask-Principal doesn't tell you what roles you need for your app. In this example I'm only using one called 'normal' which we need to tell the extension about before we tell Flask to initialize the extension:
# flask-principal principals = Principal() normal_role = RoleNeed('normal') normal_permission = Permission(normal_role) principals._init_app(app)
After that we can protect the 'home' resource thus:
If your request's session doesn't have all the stuff it's asked for (ie. has not logged in) then Flask-Principal gives it a HTTP 403 (Forbidden) response which redirects it to:
@app.errorhandler(403) def page_not_found(e): session['redirected_from'] = request.url return redirect(url_for('login'))
Which, as you can see stores the URL that you wanted in the first place ('home' in this case) and then redirects you to the login page. Since by default this request is a GET, you are shown the login form and not the login process which is used for POSTs.
Enter a valid username/password combination, e.g. 'user7' and 'user7_secret' and you get logged in and sent back to the 'home' page. The login sends a signal to the extension to tell it who has logged in and congratulations you get to see the 'Hello World' message!
If you enter an invalid username/password combination you get served an HTTP 401 (Unauthorized) and thus you end up in the 401 errorhandler seeing 'Login Failed'
If you go to 'logout' you the code will destroy any of the relevent session information and tell you you are 'Logged Out':
@app.route("/logout") def logout(): for key in ['identity.name', 'identity.auth_type', 'redirected_from']: try: del session[key] except: pass return Response('Logged out ')
Try out the code!
With Flask-Login you need to tell Flask to use the extension and this time tell the extension where the login page is (in this case "login"):
# flask-login login_manager = LoginManager() login_manager.setup_app(app) login_manager.login_view = "login"
Secondly you have to use a mixin to create your User class. The mixin just makes some standard functions available for the User.
Thirdly, you need to set up a user_loader that will load your user based on the user id:
# callback to relaad the user object @login_manager.user_loader def load_user(userid): return User(userid)
This time when you go to 'home' and you haven't yet logged in, you don't get a HTTP 403 (Forbidden), but instead get sent to the resource you defined earlier; 'login'.
Login successfully and this time the login retrieves the actual User object and tells the extension using the login_user function. Again, congratulations!
Try out the code!
You can work out for yourself how to store the User object in a database or datastore and how to check the passwords. There are a good ways to do both, but this is not an article about that. Should we have one in a future article?