Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a cert-based authentication implementation with gunicorn #169

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ The authorization is enabled using the python package
Authorization could be enhanced by changing the method "decorated" using
@auth.get_password in medallion/__init__.py

Scripts are also provided to run medallion through gunicorn with certificate-
based authentication:

```
MEDALLION_CONFFILE="<config_file>" gunicorn -w 4 -b 0.0.0.0:5000 --log-level 'debug' --worker-class 'medallion.scripts.cert_auth_worker.CertAuthWorker' --ca-certs '<ca_cert_file>' --certfile '<certificate_file>' --keyfile '<key_file>' --cert-reqs 2 --do-handshake-on-connect "medallion.scripts.cert_auth_app:getApp()"
```

Configs may also contain a "taxii" section as well, as shown below:

.. code-block:: json
Expand Down
75 changes: 75 additions & 0 deletions medallion/scripts/cert_auth_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from functools import wraps
import logging
import os

import flask
from werkzeug.security import check_password_hash

import medallion
from medallion import connect_to_backend, register_blueprints, set_config
from medallion.common import (
APPLICATION_INSTANCE, get_application_instance_config_values
)
import medallion.config
from medallion.scripts.cert_auth_gunicorn import ClientAuthApplication

log = logging.getLogger("medallion")


class CertAuth(object):

def login_required(self, f):
@wraps(f)
def decorated_function(*args, **kwargs):
username = flask.request.headers.get('X-USER')
stored_password = medallion.get_pwd(username)
client_password = flask.request.headers.get('X-PASS')
if not check_password_hash(stored_password, client_password):
raise medallion.exceptions.BackendError("Unauthorized", 401)

return f(*args, **kwargs)
return decorated_function


medallion.auth = CertAuth()


def getApp():
# Gunicorn messes up argparse, so only use env vars
# use one or the other, not both
configuration = medallion.config.load_config(
os.environ.get(
"MEDALLION_CONFFILE", medallion.config.DEFAULT_CONFFILE
),
os.environ.get(
"MEDALLION_CONFDIR", medallion.config.DEFAULT_CONFDIR
),
)

ca_path = configuration.get('ca_path')
cert_path = configuration.get('cert_path')
key_path = configuration.get('key_path')
host = configuration.get('host')
port = configuration.get('port')

set_config(APPLICATION_INSTANCE, "users", configuration)
set_config(APPLICATION_INSTANCE, "taxii", configuration)
set_config(APPLICATION_INSTANCE, "backend", configuration)

APPLICATION_INSTANCE.medallion_backend = connect_to_backend(get_application_instance_config_values(APPLICATION_INSTANCE, "backend"))
if (not APPLICATION_INSTANCE.blueprints):
register_blueprints(APPLICATION_INSTANCE)

app = ClientAuthApplication(
APPLICATION_INSTANCE,
ca_path,
cert_path,
key_path,
host,
port,
)
return app.application


if __name__ == "__main__":
getApp().run()
39 changes: 39 additions & 0 deletions medallion/scripts/cert_auth_gunicorn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# From https://eugene.kovalev.systems/posts/flask-client-side-tls-authentication/

from multiprocessing import cpu_count
from pathlib import Path

import gunicorn.app.base

NUMBER_OF_WORKERS = (cpu_count() * 2) + 1


class ClientAuthApplication(gunicorn.app.base.BaseApplication):
def __init__(self, app, ca_path: Path, cert_path: Path, key_path: Path,
hostname='localhost', port=80, num_workers=NUMBER_OF_WORKERS, timeout=30):
self.options = {
'loglevel': 'debug',
'bind': '{}:{}'.format(hostname, port),
'workers': num_workers,
'worker_class': 'medallion.scripts.cert_auth_worker.CertAuthWorker',
'timeout': timeout,
'ca_certs': str(ca_path),
'certfile': str(cert_path),
'keyfile': str(key_path),
'cert_reqs': 2,
'do_handshake_on_connect': True
}
self.application = app
super().__init__()

def init(self, parser, opts, args):
return super().init(parser, opts, args)

def load_config(self):
config = dict([(key, value) for key, value in self.options.items()
if key in self.cfg.settings and value is not None])
for key, value in config.items():
self.cfg.set(key.lower(), value)

def load(self):
return self.application
31 changes: 31 additions & 0 deletions medallion/scripts/cert_auth_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# From https://eugene.kovalev.systems/posts/flask-client-side-tls-authentication/

"""A worker for Gunicorn that sets the client's Common Name as the X-USER header
variable for every request after the client has been authenticated so that
a Flask application can contain the authorization logic.
Based on: https://gist.github.com/kgriffs/289206f07e23b9a30d29a2b23e28c41c"""

import ssl

from gunicorn.workers.sync import SyncWorker


class CertAuthWorker(SyncWorker):
"""A custom worker for putting authentication information into the X-USER
header variable of each request."""
def handle_request(self, listener, req, client, addr):
"""Handles each incoming request after a client has been authenticated."""
subject = dict([i for subtuple in client.getpeercert().get('subject') for i in subtuple])
issuer = dict([i for subtuple in client.getpeercert().get('issuer') for i in subtuple])
headers = dict(req.headers)
headers['X-USER'] = subject.get('commonName')
serial = client.getpeercert().get('serialNumber')
headers['X-PASS'] = serial
not_before = client.getpeercert().get('notBefore')
not_after = client.getpeercert().get('notAfter')
headers['X-NOT_BEFORE'] = ssl.cert_time_to_seconds(not_before)
headers['X-NOT_AFTER'] = ssl.cert_time_to_seconds(not_after)
headers['X-ISSUER'] = issuer['commonName']

req.headers = list(headers.items())
super(CertAuthWorker, self).handle_request(listener, req, client, addr)
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def get_long_description():
"mongo": [
"pymongo",
],
"certauth": [
"gunicorn",
],
},
project_urls={
'Documentation': 'https://medallion.readthedocs.io/',
Expand Down