linnarsson-lab / loom-viewer

Tool for sharing, browsing and visualizing single-cell data stored in the Loom file format
BSD 2-Clause "Simplified" License
35 stars 6 forks source link

Authentication #32

Open slinnarsson opened 8 years ago

slinnarsson commented 8 years ago

We will eventually want to have some simple system of authentication and authorization, so that we can make the Loom web site public and still keep private datasets there. Some options:

slinnarsson commented 8 years ago

Implemented the third option above. The server serves .loom files from a datasets directory (given as an argument at startup). Under this directory, there are one or more projects directories, each of which contains the .loom files. The server now accepts Basic Auth username and password headers. It checks each project directory and:

  1. If the directory does not contain a auth.txt file, then the project is public to all, with or without Basic Auth Headers in the request
  2. If the directory contains a auth.txt file, then the project is only visible to requests that (a) provide a basic Auth header with username and password, and (b) the auth.txt file contains a line username,password that matches the provided credentials. Otherwise, the request returns 404 Not Found.

Thus, a project is public and visible if it does not contain an auth.txt file. If it does contain such a file, the project is visible and accessible only to users who authenticate with credentials listed in the file.

Example of a datasets directory:

datasets/
   public_project/
      somefile.loom
      anotherfile.loom
   private_project/
      auth.txt
      visible_only_to_some.loom

Example of a auth.txt file:

sten.linnarsson@ki.se,secretpassword01
timSnowman,timsPassword

The file is comma-delimited, with exactly two fields per line, no extra whitespace. If the file violates these rules, then the project will be inaccessible to all users.

JobLeonard commented 6 years ago

It's probably time to look into this again...

WARNING FOR ANYONE READING THIS IN THE FUTURE: THIS "SECURITY" MEASURE IS DESIGNED TO CREATE A SIMPLE LAYER OF PROTECTION WITH MINIMAL OVERHEAD FOR SMALL RESEARCH GROUPS HOSTING THEIR OWN LOOM-VIEWER SERVER

This is not intended to scale for big, complex authorisation situations with many users on a big system. If this viewer would ever be adapted for that purpose, this security needs to be changed.

For the current and likely-future use-cases, a dumb, simple, but effective solution would using HTTPS + Basic Auth.

It seems like we need to implement the following steps for this:

Server

Forcing HTTPS from the Flask side

https://github.com/kennethreitz/flask-sslify

This is a simple Flask extension that configures your Flask application to redirect all incoming requests to HTTPS.

Combining with basic auth is probably enough for our use-case:

Security consideration using basic auth

When using basic auth, it is important that the redirect occurs before the user is prompted for credentials. Flask-SSLify registers a before_request handler, to make sure this handler gets executed before credentials are entered it is advisable to not prompt for any authentication inside a before_request handler.

The example found at http://flask.pocoo.org/snippets/8/ works nicely, as the view function's decorator will never have an effect before the before_request hooks are executed.

Basic Auth snippet

As suggested to combine with Flask-SSLify

For very simple applications HTTP Basic Auth is probably good enough. Flask makes this very easy. The following decorator applied around a function that is only available for certain users does exactly that:

from functools import wraps
from flask import request, Response

def check_auth(username, password):
    """This function is called to check if a username /
    password combination is valid.
    """
    return username == 'admin' and password == 'secret'

def authenticate():
    """Sends a 401 response that enables 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"'})

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            return authenticate()
        return f(*args, **kwargs)
    return decorated

To use this decorator, just wrap a view function:

@app.route('/secret-page')
@requires_auth
def secret_page():
    return render_template('secret_page.html')

If you are using basic auth with mod_wsgi you will have to enable auth forwarding, otherwise apache consumes the required headers and does not send it to your application: WSGIPassAuthorization.

http://flask.pocoo.org/snippets/8/

Client

Add "login" UI for user

Depending on whether the serve requires basic authentication or not, the first page should be a login page or directly show the dataset list.

The login page is really just storing the user/password in a javascript object (and yes, I know that is insecure! But I doubt we have to worry about cross-site scripting attacks targeting people known to have access to the data any time soon...)

This user:password string is then used in fetch calls to let the server play nice with our authorisation.

Update fetch calls to include authorisation

Example fetch with authorization header:

fetch('URL_GOES_HERE', { 
   method: 'post', 
   headers: {
     'Authorization': 'Basic '+btoa('username:password'), 
     'Content-Type': 'application/x-www-form-urlencoded'
   }, 
   body: 'A=1&B=2'
 });

https://stackoverflow.com/a/35780539/3211791

JobLeonard commented 6 years ago

If we do have to use session-based security (which I hope to avoid because we really don't need a database...), the following set-up is taken from this blog, and probably the simplest way of using a session-based authorisation in a Single Page App:

POST /api/session This API endpoint is responsible for creating a session. It accepts a username/password combination, and returns either a session object with an access token or an error that the credentials are incorrect.
Schema image

We should then look into Flask-Security.