Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Flask-SocketIO with Flask-Login and HTTP Basic Auth

I'm trying to implement a really simple webpage for internal monitoring. It is supposed to display some data, which is updated in real-time via socketio. The server runs a thread in the background, which fetches the data and relays it to the client.

I'd like to protect the page with a login form. To keep things simple, I chose HTTP Basic Auth, mostly because I didn't want to design a login form.

I have done the following:

  • Under @login_manager.request_handler, I check for request.authorization. If it is valid, I return an authenticated User object.
  • Under @login_manager.unauthorized_handler, I trigger the authentication dialog.
  • The '/' page is protected with @login_required.
  • I also intercept the socketio.on('connect') event and I check for current_user there. If it is not authenticated, I drop the connection.

Here's the entire working example:

## Standard imports, disregard them
import functools
import gevent

## Otherwise I'm getting KeyError on shutdown
import gevent.monkey
gevent.monkey.patch_all()

from flask import Flask, request, Response
from flask.ext.login import LoginManager, UserMixin, login_required, current_user
from flask.ext.socketio import SocketIO

## To see the logging.debug call in socketio.on('connect')
import logging
logging.getLogger().setLevel(logging.DEBUG)

## App configuration
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a long and random string'

login_manager = LoginManager()
login_manager.init_app(app)
socketio = SocketIO(app)

## This thing sends updates to the client
class BackgroundThread(gevent.Greenlet):
    def run(self):
        while True:
            socketio.emit(
                'my event',
                {'my field': 'my data'},
                namespace='/my-namespace'
            )
            gevent.sleep(2)

## Not bothering with a database
class User(UserMixin):
    users = {
        u'1': (u'myname', u'mypass')
    }

    def __init__(self, username, password):
        self.username = username
        self.password = password

    def get_id(self):
        return u'1'

    @classmethod
    def get_by_username(cls, requested_username):
        for username, password in cls.users.itervalues():
            if username == requested_username:
                return User(username, password)
        return None

## From https://flask-socketio.readthedocs.org/en/latest/
def authenticated_only(f):
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        if not current_user.is_authenticated():
            request.namespace.disconnect()
        else:
            return f(*args, **kwargs)
    return wrapped

## The password is checked here
@login_manager.request_loader
def load_request(request):
    auth = request.authorization

    if auth is not None:
        username, password = auth['username'], auth['password']
        user = User.get_by_username(username)
        if user is not None and user.password == password:
            return user
    return None

## From http://flask.pocoo.org/snippets/8/
@login_manager.unauthorized_handler
def http_basic_auth():
    return Response(
    'Could not verify your access level for that URL.\n'
    'You have to login with proper credentials', 401,
    {'WWW-Authenticate': 'Basic realm="Login Required"'})


@app.route('/')
@login_required
def index():
    return "My page"  # in real code this is actually a render_template call


@socketio.on('connect', namespace='/my-namespace')
@authenticated_only
def test_connect():
    logging.debug('Client connected: {.username}.'.format(current_user))


if __name__ == '__main__':
    thread = BackgroundThread()
    thread.start()

    socketio.run(app)
  • Is this setup secure, provided that I use HTTPS with a self-signed certificate?
  • The Flask-Login docs stress that to actually login the user, I have to explicitly call login_user. I don't do that and yet I can log in. How is that possible?

UPD: In the foreseeable future I am going to be the only user, so mostly I am concerned whether it is possible to intercept and decrypt the traffic, or send data through the Websocket connection without being authenticated.

like image 921
Pastafarianist Avatar asked Oct 20 '22 03:10

Pastafarianist


1 Answers

Is this setup secure, provided that I use HTTPS with a self-signed certificate?

You have the user passwords stored in plain text in your database (I know, you don't have a database yet, but I assume you'll have one eventually?). If your database ever gets hacked, then your users will hate you, specially those who use the same password for their online banking. You should store hashed passwords in your database to protect them from hackers. Look at Flask-Bcrypt or the password hashing functions in Werkzeug.

Using HTTPS is good, but since you are also using WebSocket, you need to evaluate if the data that goes over the socket connection also needs encryption.

A self-signed certificate is not a good idea, since browsers are unable to verify their authenticity so they'll (rightly) advice your users to stay away from your site.

The Flask-Login docs stress that to actually login the user, I have to explicitly call login_user. I don't do that and yet I can log in. How is that possible?

The idea of logging users in is that you don't have to re-authenticate them with every request they send. The login_user just records that the user is logged in to the session. In subsequent requests Flask-Login will find the user in the session, so it will not need to invoke your callback to do the authentication again.

In your case you are using HTTP basic authentication. The browser will send the Authorization header with every request, and since Flask-Login never finds anything in the session, it always calls your callback, which authenticates the user every time. I don't see any problem with this, but if you want to avoid the effort of constantly authenticating the user (specially after you add password hashing, which is CPU intensive), you may want to consider calling the login_user function to make things a bit more efficient.

Update: so you claim you plan to leave the user list written in plain text in the code. This is a really really bad idea. You want to make the effort to make the data going between client and server secure, so you should also take good security practices in how you store your passwords.

The biggest risk I see with having the passwords in the code for a small site of which you are the only user is you exposing the code by mistake. For example, if you want to put your code under version control, you will have a copy of your password there in addition to the copy that runs on the server (one more place from where it can be hacked). If you also make backups of your scripts, it'll be there too.

So do yourself a favor and do not write your password in the code. At the very least, read it from an environment variable at start up.

like image 165
Miguel Avatar answered Oct 22 '22 11:10

Miguel