├── MANIFEST.in ├── README.org ├── demo ├── tlsauth_wsgi.py └── webapp.py ├── flask_tlsauth ├── __init__.py └── templates │ ├── certify.html │ ├── csrs.html │ └── register.html └── setup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include templates/*.html 2 | include README.org 3 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Flask-TLSAuth 2 | 3 | Flask-TLSAuth integrates a minimal certificate authority (CA) and 4 | implements TLS client certificate authentication. It depends on nginx 5 | for handling the TLS authentication part. 6 | 7 | ** Installation 8 | #+BEGIN_SRC sh 9 | pip install flask_tlsauth 10 | #+END_SRC 11 | Flask-TLSAuth depends on tlsauth which provides minimal tools to 12 | act as a CA. Please follow the "CA and https service install" steps 13 | from https://github.com/stef/tlsauth to set up your webserver and CA. 14 | 15 | ** tlsauth decorator 16 | Flask-TLSAuth provides a simple decorator to guard your entry points: 17 | #+BEGIN_SRC python 18 | from flask import Flask, Response, redirect 19 | import os 20 | app = Flask(__name__) 21 | app.secret_key = 'some secret randomness' 22 | 23 | # we need a CA 24 | from tlsauth import CertAuthority 25 | import flask_tlsauth as tlsauth 26 | 27 | # previously we setup up the CA according to the tlsauth doc 28 | ca=CertAuthority('') 29 | 30 | adminOs=['CA admins'] 31 | # grants admin access to anyone with a 32 | # valid cert asserting membership in "CA admins" 33 | tlsauth.tlsauth_init(app, ca, groups=adminOs) 34 | 35 | def unauth(): 36 | return redirect("/") 37 | 38 | @app.route('/hello') 39 | 40 | # lets protect this valuable function, 41 | # redirecting unauthorized visitors to / 42 | @tlsauth.tlsauth(unauth=unauth, groups=adminOs) 43 | def hello(): 44 | return Response("hello world") 45 | #+END_SRC 46 | 47 | ** Managing certs 48 | Flask-TLSAuth provides a few default routes to manage the certs and 49 | the CA. 50 | 51 | *** /tlsauth/register/ 52 | Visitors can register like on a normal site, but when done, they get a 53 | PKCS12 certificate ready to be saved and imported in all 54 | browsers. This is totally automatic and there's no check if the 55 | specified organization is not a privileged one (like "CA admins" in 56 | the above example). This really provides no security, for bots and 57 | scripts it's even easier to use these certs than for normal humans. 58 | Other mechanisms must be deployed to provide meaningful authentication. 59 | 60 | *** /tlsauth/certify/ 61 | Visitors can submit their Certificate Signing Request (can be easily 62 | generated using gencert.sh from tlsauth), which depending on 63 | configuration either returns automatically a signed certificate (no 64 | meaningful authentication this way, avoid this!), or it gets stored 65 | for later approval by the "CA admins". 66 | 67 | *** /tlsauth/cert/ 68 | Returns the CA root certificate in PEM format, for import into your browser. 69 | 70 | *** /tlsauth/csrs/ 71 | Displays a list of incoming CSRs to any certified member of the "CA 72 | admin" group. The certs can be either rejected or signed, in the later 73 | case the resulting certificate is sent to the email address of the 74 | subject. 75 | 76 | *** /tlsauth/test/ 77 | Displays whether you are TLS authenticated and what your distinguished name is. 78 | -------------------------------------------------------------------------------- /demo/tlsauth_wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | sys.path.insert(0, "/home/stef/tasks/tlsauth/env/") 4 | sys.path.insert(0, "/home/stef/tasks/tlsauth/flask_tlsauth/demo") 5 | 6 | activate_this = '/home/stef/tasks/tlsauth/env/bin/activate_this.py' 7 | execfile(activate_this, dict(__file__=activate_this)) 8 | 9 | from webapp import app as application 10 | #application.run(debug=True) 11 | -------------------------------------------------------------------------------- /demo/webapp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # run with 4 | # env/bin/uwsgi --socket 127.0.0.1:8080 --chdir $PWD/demo -pp $PWD -w tlsauth_wsgi -p 1 --virtualenv $PWD/env --py-autoreload 1 5 | # also create a ca in ../../x509-ca - for more info see tlsauth README 6 | 7 | from flask import Flask, Response 8 | import os 9 | app = Flask(__name__) 10 | app.secret_key = 'zxcvzxcvz' 11 | #app.debug = True 12 | 13 | from tlsauth import CertAuthority 14 | import flask_tlsauth as tlsauth 15 | 16 | ca=CertAuthority('../../x509-ca') 17 | 18 | app.debug = True 19 | adminOs=['CA admins'] 20 | tlsauth.tlsauth_init(app, ca, groups=adminOs) 21 | 22 | @app.route('/hello') 23 | @tlsauth.tlsauth(groups=adminOs) 24 | def hello(): 25 | return Response("hello world") 26 | -------------------------------------------------------------------------------- /flask_tlsauth/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from flask import request, Response, render_template, redirect 4 | from flask.ext.wtf import Form 5 | from wtforms import TextField, TextAreaField 6 | from tlsauth import todn, mailsigned, load 7 | import os, datetime, jinja2 8 | BASEPATH = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | def tlsauth(unauth=None, groups=None): 11 | """ (FLASK) decorator letting execution pass if TLS authenticated 12 | and if given certs Organization is in groups. 13 | 14 | if TLS authentication fails execution is diverted to unauth, 15 | which per default returns 403 16 | """ 17 | if not unauth: 18 | def unauth(): 19 | return Response("Forbidden",403) 20 | def decor(func): 21 | def wrapped(*args,**kwargs): 22 | if request.environ['verified']=="SUCCESS" and (not groups or todn(request.environ['dn']).get('O') in groups): 23 | return func(*args,**kwargs) 24 | return unauth(*args,**kwargs) 25 | return wrapped 26 | return decor 27 | 28 | class UserForm(Form): 29 | """ (FLASK) simple registration WTForm 30 | """ 31 | name = TextField('Name') 32 | email = TextField('Email') 33 | org = TextField('Organization') 34 | 35 | class CSRForm(Form): 36 | """ (FLASK) even simpler CSR submission WTForm 37 | """ 38 | csr = TextAreaField('Certificate Signing Request') 39 | 40 | def renderUserForm(ca): 41 | """ (FLASK) form handles user registration requests. 42 | This is the web-based way to do this irresponsibly. 43 | """ 44 | def wrapped(): 45 | form = UserForm() 46 | if form.validate_on_submit(): 47 | return Response( 48 | ca.gencert(str(form.name.data), 49 | str(form.email.data), 50 | str(form.org.data)), 51 | mimetype="application/x-pkcs12") 52 | return render_template('register.html', form=form) 53 | return wrapped 54 | 55 | def renderCSRForm(ca, blindsign=False, scrutinizer=None): 56 | """ (FLASK) form handles CSR submissions. 57 | blindsign disables reviewing by authorized personel, and enables automatic signing. 58 | scrutinizer is a function returning true if allowed to be signed. 59 | """ 60 | def wrapped(): 61 | form = CSRForm() 62 | if form.validate_on_submit(): 63 | if blindsign: 64 | if not scrutinizer or scrutinizer(str(form.csr.data)): 65 | return Response(ca.signcsr(str(form.csr.data)), 66 | mimetype="text/plain") 67 | else: 68 | ca.submit(str(form.csr.data)) 69 | return Response("Thank you. If all goes well you should soon " 70 | "receive your signed certificate.") 71 | return render_template('certify.html', form=form, blindsign=blindsign) 72 | return wrapped 73 | 74 | def renderCert(ca): 75 | """ provides the CA root cert 76 | """ 77 | def wrapped(): 78 | return Response(ca._pub, mimetype="text/plain") 79 | return wrapped 80 | 81 | def certify(ca, groups=None): 82 | """ provides facility for users belonging to `groups` to sign incoming CSRs 83 | """ 84 | def wrapped(id): 85 | err=authenticated(groups) 86 | if err: return err 87 | path=ca._incoming+'/'+request.path.split('/')[3] 88 | print "certifying", path 89 | cert=ca.signcsr(load(path)) 90 | mailsigned([cert]) 91 | os.unlink(path) 92 | return redirect('/tlsauth/csrs/') 93 | return wrapped 94 | 95 | def reject(ca, groups=None): 96 | """ provides facility for users belonging to `groups` to reject incoming CSRs 97 | """ 98 | def wrapped(id): 99 | err=authenticated(groups) 100 | if err: return err 101 | path=ca._incoming+'/'+request.path.split('/')[3] 102 | os.unlink(path) 103 | return redirect('/tlsauth/csrs/') 104 | return wrapped 105 | 106 | def authenticated(groups): 107 | """ (FLASK) helper function to check if user is authenticated and in a given group. 108 | """ 109 | if not request.environ['verified']=="SUCCESS" or (groups and todn(request.environ['dn']).get('O') not in groups): 110 | return Response("Forbidden",403) 111 | 112 | def showcsrs(ca,groups=None): 113 | """ (FLASK) authenticated view list of submitted CSRs 114 | """ 115 | def wrapped(): 116 | try: 117 | err=authenticated(groups) 118 | if err: return err 119 | return render_template('csrs.html', 120 | certs=[(todn(cert.get_subject()), 121 | datetime.datetime.fromtimestamp(os.stat(path).st_mtime), 122 | os.path.basename(path)) 123 | for cert, path 124 | in ca.incoming()]) 125 | except: 126 | import traceback 127 | print traceback.format_exc() 128 | return wrapped 129 | 130 | def testAuth(): 131 | """ test if you are TLS authenticated 132 | mountpoint: /tlsauth/test/ 133 | """ 134 | return Response(request.environ['verified'] + "
" + request.environ['dn']) 135 | 136 | def tlsauth_init(app, ca, groups=None): 137 | """ automatically register routes for flask 138 | """ 139 | app.add_url_rule('/tlsauth/register/', 'register', renderUserForm(ca), methods=("GET", "POST")) 140 | app.add_url_rule('/tlsauth/certify/', 'certify', renderCSRForm(ca, blindsign=False), methods=("GET", "POST")) 141 | app.add_url_rule('/tlsauth/cert/', 'cert', renderCert(ca)) 142 | app.add_url_rule('/tlsauth/csrs/', 'csrs', showcsrs(ca, groups=groups)) 143 | app.add_url_rule('/tlsauth/sign/', 'sign', certify(ca, groups=groups)) 144 | app.add_url_rule('/tlsauth/reject/', 'reject', reject(ca, groups=groups)) 145 | app.add_url_rule('/tlsauth/test/', 'test', testAuth) 146 | 147 | app.jinja_loader = jinja2.ChoiceLoader([ 148 | app.jinja_loader, 149 | jinja2.FileSystemLoader(BASEPATH+'/templates'), 150 | ]) 151 | -------------------------------------------------------------------------------- /flask_tlsauth/templates/certify.html: -------------------------------------------------------------------------------- 1 | You might want to download and install the Root CA cert of this site.
' 2 |
3 | {{ form.hidden_tag() }} 4 | {{ form.csr.label }} {{ form.csr }} 5 | 6 |
7 | You can always try if it works at /tlsauth/test/. 8 | -------------------------------------------------------------------------------- /flask_tlsauth/templates/csrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for item, date, id in certs %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% endfor %} 23 | 24 |
UserEmailOrgDateSignReject
{{item.CN}}{{item.emailAddress}}{{item.O}}{{date}}signreject
25 | -------------------------------------------------------------------------------- /flask_tlsauth/templates/register.html: -------------------------------------------------------------------------------- 1 | You might want to download and install the Root CA cert of this site.
2 |
3 | {{ form.hidden_tag() }} 4 | {{ form.name.label }} {{ form.name }} 5 | {{ form.email.label }} {{ form.email }} 6 | {{ form.org.label }} {{ form.org }} 7 | 8 |
9 | Please import the file offered into your browsers certificate store. 10 |

You can always try if it works at /tlsauth/test/.

11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup,find_packages 3 | 4 | def read(fname): 5 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 6 | 7 | setup( 8 | name = "flask_tlsauth", 9 | version = "0.1.3", 10 | author = "Stefan Marsiske", 11 | author_email = "s@ctrlc.hu", 12 | license = "BSD", 13 | keywords = "flask crypto authentication TLS certificate x509 CA", 14 | packages = find_packages(), 15 | package_data = { '': ['templates/*.html'], }, 16 | include_package_data=True, 17 | install_requires = ['tlsauth', 'Flask'], 18 | url = "http://packages.python.org/flask_tlsauth", 19 | description = "Flask extension implementing TLS Authentication - simple client certificate CA inclusive", 20 | long_description=read('README.org'), 21 | classifiers = ["Development Status :: 4 - Beta", 22 | "License :: OSI Approved :: BSD License", 23 | "Topic :: Security :: Cryptography", 24 | "Environment :: Web Environment", 25 | ], 26 | ) 27 | --------------------------------------------------------------------------------