Authentication and authorization

Our monolithic application is almost ready.

One last thing that we need to add is a way for users to authenticate. Runnerly needs to know who's connected since the dashboard will display user-specific data. Forms also need to be secured. For instance, we don't want users to be able to edit other users' information.

For our monolithic solution, we'll implement a very simple basic authentication (https://en.wikipedia.org/wiki/Basic_access_authentication) scheme where the user sends its credentials in the Authorization header. From a security point of view, using basic authentication is fine as long as the server uses SSL. When websites are called through HTTPS, the entire request is encrypted (including the query part of the URL), so the transport is secured.

As far as passwords are concerned, the simplest form of protection is to make sure you don't store them in the clear in the database, but instead store them in a hashed form that can't be converted back to the original password. That will minimize the risk of leaking passwords if your server is compromised. For the authentication process, it just means that when the user logs in, you need to hash the incoming password to compare it to the stored hash.

The transport layer is usually not the weak spot for an application security. What happens to the service once the request is received is what matters the most. When the authentication process happens, there's a window during which an attacker can intercept the password (in clear or hashed form). In Chapter 7, Securing Your Services, we'll talk about ways to reduce this attack surface.

Werkzeug provides a few helpers to deal with password hashes, generate_password_hash() and check_password_hash(), which can be integrated into our User class.

By default, ;Werkzeug uses PBKDF2 (https://en.wikipedia.org/wiki/PBKDF2) with SHA-1, which is a secure way to hash a value with salt.

Let's extend our User class with methods to set and verify a password:

    from werkzeug.security import generate_password_hash, check_password_hash 
class User(db.Model):
__tablename__ = 'user'
# ... all the Columns ...

def __init__(self, *args, **kw):
super(User, self).__init__(*args, **kw)
self._authenticated = False

def set_password(self, password):
self.password = generate_password_hash(password)

@property
def is_authenticated(self):
return self._authenticated

def authenticate(self, password):
checked = check_password_hash(self.password, password)
self._authenticated = checked
return self._authenticated

When creating new users in the database, the set_password() method can be used to hash and store a password in the User model. Any attempt to verify the password can be made with authenticate(), which will compare hashes.

Once we have that mechanism in place, the Flask-Login (https://flask-login.readthedocs.io/) extension provides everything needed to log in and log out users, and to keep track of who's connected so you can change how your app works.

Flask-Login provides two functions to set a user in the current Flask session: login_user() and logout_user(). When the login_user() method is called, the user ID is stored in the Flask session, and a cookie is set on the client side. The user will be remembered for the next requests until they log out.

To have this mechanism in place, a LoginManager instance needs to be created on your application at startup.

Here's the implementation of the login and logout views, along with the LoginManager creation:

    from flask_login import LoginManager, login_user, logout_user 

@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
email, password = form.data['email'], form.data['password']
q = db.session.query(User).filter(User.email == email)
user = q.first()
if user is not None and user.authenticate(password):
login_user(user)
return redirect('/')
return render_template('login.html', form=form)

@app.route("/logout")
def logout():
logout_user()
return redirect('/')

login_manager = LoginManager()
login_manager.init_app(app)

@login_manager.user_loader
def load_user(user_id):
user = User.query.get(user_id)
if user is not None:
user._authenticated = True
return user

The @login_manager.user_loader decorated function is used every time Flask-Login needs to convert a stored user ID to an actual user instance.

The authentication part is done in the login view by calling user.authenticate(), and then set in the session with login_user(user).

The last thing to do is to protect some of our views from unauthorized access. For instance, the user edition form should not be accessible if you are not logged in. The @login_required decorator will reject any attempt to access a view if you are not logged in with a 401 Unauthorized error.

It needs to be placed after the @app.route() call:

    @app.route('/create_user', methods=['GET', 'POST']) 
@login_required
def create_user():
# ... code

In the code, @login_required will ensure that you are a valid user and that you've authenticated.

However, this decorator does not deal with permissions. Permissions handling is out of scope for the Flask-Login project, and an extension such as Flask-Principal (https://pythonhosted.org/Flask-Principal/) can be used to handle this on the top of Flask-Login.

However, for our very simple use case, it might be overkill. One specific role Runnerly users have is admin. Admins have super powers across the app, while simple users can only change their info.

If we add an is_admin Boolean flag in the User model, we can create a similar decorator such as @login_required, which will also check this flag:

    def admin_required(func): 
@functools.wraps(func)
def _admin_required(*args, **kw):
admin = current_user.is_authenticated and current_user.is_admin
if not admin:
return app.login_manager.unauthorized()
return func(*args, **kw)
return _admin_required

In the same vein, more granular permission verifications can be done by looking at the current_user variable Flask-Login sets in the application context. For example, you could use this to allow a user to change their data, but prevent the user from changing other users' data.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset