Switch to OIDC authentication

This commit is contained in:
Timotej Lazar 2023-09-06 14:28:06 +02:00
parent 5add39a8a7
commit 9dc0fbb4fe
5 changed files with 88 additions and 81 deletions

View file

@ -1,4 +1,5 @@
authlib
click click
flask flask
flask-login flask-login
flask-ldap3-login ldap3

View file

@ -3,7 +3,6 @@ import syslog
import secrets import secrets
import flask import flask
import flask_ldap3_login
import flask_login import flask_login
def create_app(test_config=None): def create_app(test_config=None):
@ -14,13 +13,13 @@ def create_app(test_config=None):
settings = { settings = {
'secret_key': secrets.token_hex(), 'secret_key': secrets.token_hex(),
'ldap_host': '', 'ldap_host': '',
'ldap_port': '636',
'ldap_user': '', 'ldap_user': '',
'ldap_pass': '', 'ldap_pass': '',
'ldap_admin': '',
'ldap_base_dn': '', 'ldap_base_dn': '',
'ldap_user_dn': '', 'oidc_tenant': '',
'ldap_login_attr': 'userPrincipalName', 'oidc_client_id': '',
'oidc_client_secret': '',
'admin_group': '',
'admin_mail': '', 'admin_mail': '',
'wg_endpoint': '', 'wg_endpoint': '',
'wg_port': '51820', 'wg_port': '51820',
@ -35,18 +34,15 @@ def create_app(test_config=None):
db.write('settings', settings) db.write('settings', settings)
app.config['SECRET_KEY'] = settings.get('secret_key', '') app.config['SECRET_KEY'] = settings.get('secret_key', '')
app.config['LDAP_USE_SSL'] = True
app.config['LDAP_HOST'] = settings.get('ldap_host', '')
app.config['LDAP_PORT'] = int(settings.get('ldap_port', '636'))
app.config['LDAP_BASE_DN'] = settings.get('ldap_base_dn', '')
app.config['LDAP_USER_DN'] = settings.get('ldap_user_dn', '')
app.config['LDAP_BIND_USER_DN'] = settings.get('ldap_user', '')
app.config['LDAP_BIND_USER_PASSWORD'] = settings.get('ldap_pass', '')
app.config['LDAP_USER_LOGIN_ATTR'] = settings.get('ldap_login_attr', 'userPrincipalName')
app.config['LDAP_USER_SEARCH_SCOPE'] = 'SUBTREE'
from . import auth from . import auth
app.register_blueprint(auth.blueprint) auth.init_app(app)
from . import errors
errors.init_app(app)
from . import system
system.init_app(app)
from . import config from . import config
app.register_blueprint(config.blueprint) app.register_blueprint(config.blueprint)
@ -63,36 +59,6 @@ def create_app(test_config=None):
from . import vpn from . import vpn
app.register_blueprint(vpn.blueprint) app.register_blueprint(vpn.blueprint)
from . import system
system.init_app(app)
login_manager = flask_login.LoginManager(app)
ldap_manager = flask_ldap3_login.LDAP3LoginManager(app)
users = {}
@login_manager.user_loader
def load_user(id):
return users.get(id)
@ldap_manager.save_user
def save_user(dn, username, data, memberships):
user = auth.User(dn, username, data)
users[dn] = user
return user
@login_manager.unauthorized_handler
def unauth_handler():
return flask.redirect(flask.url_for('auth.login', next=flask.request.endpoint))
@app.errorhandler(TimeoutError)
def timeout_error(e):
return flask.render_template('busy.html')
@app.errorhandler(Exception)
def internal_server_error(e):
return flask.Response(f'something went catastrophically wrong: {e}',
status=500, mimetype='text/plain')
@app.route('/') @app.route('/')
@flask_login.login_required @flask_login.login_required
def home(): def home():
@ -101,17 +67,11 @@ def create_app(test_config=None):
@app.route('/nodes') @app.route('/nodes')
@flask_login.login_required @flask_login.login_required
def nodes(): def nodes():
try:
if not flask_login.current_user.is_admin: if not flask_login.current_user.is_admin:
return flask.Response('forbidden', status=403, mimetype='text/plain') return flask.Response('forbidden', status=403, mimetype='text/plain')
with db.locked('nodes'): with db.locked('nodes'):
version = db.load('settings').get('version') version = db.load('settings').get('version')
nodes = db.read('nodes') nodes = db.read('nodes')
return flask.render_template('nodes.html', version=version, nodes=nodes) return flask.render_template('nodes.html', version=version, nodes=nodes)
except TimeoutError:
return flask.render_template('busy.html')
except Exception as e:
return flask.Response(f'something went catastrophically wrong: {e}',
status=400, mimetype='text/plain')
return app return app

View file

@ -1,39 +1,68 @@
import authlib.integrations.flask_client
import flask import flask
import flask_login import flask_login
import flask_ldap3_login.forms import urllib.parse
from . import db from . import db
blueprint = flask.Blueprint('auth', __name__, url_prefix='/auth') login_manager = None
auth = None
users = {}
class User(flask_login.UserMixin): class User(flask_login.UserMixin):
def __init__(self, dn, username, data): def __init__(self, userinfo):
self.dn = dn self.username = userinfo['preferred_username']
self.username = username self.groups = set(userinfo.get('groups', ()))
self.data = data self.data = userinfo
self.groups = set(data.get('memberOf', ()))
try: try:
self.is_admin = db.load('settings').get('ldap_admin') in self.groups self.is_admin = db.load('settings').get('admin_group') in self.groups
except: except:
self.is_admin = False self.is_admin = False
def __repr__(self): def __repr__(self):
return self.dn return f'{self.username} {self.groups}'
def get_id(self): def get_id(self):
return self.dn return self.username
@blueprint.route('/login', methods=['GET', 'POST']) def init_app(app):
settings = db.load('settings')
login_manager = flask_login.LoginManager(app)
oauth = authlib.integrations.flask_client.OAuth(app)
oauth.register(
name='azure',
server_metadata_url=f'https://login.microsoftonline.com/{settings.get("oidc_tenant")}/v2.0/.well-known/openid-configuration',
client_id=settings.get('oidc_client_id'),
client_secret=settings.get('oidc_client_secret'),
client_kwargs={'scope': 'openid profile email'})
@login_manager.user_loader
def load_user(username):
return users.get(username)
@login_manager.unauthorized_handler
def unauth_handler():
return flask.redirect(flask.url_for('login', next=flask.request.endpoint))
@app.route('/login')
def login(): def login():
form = flask_ldap3_login.forms.LDAPLoginForm() return oauth.azure.authorize_redirect(flask.url_for('auth', _external=True))
if form.validate_on_submit():
flask_login.login_user(form.user)
return flask.redirect('/')
return flask.render_template('auth/login.html', form=form)
@blueprint.route('/logout') @app.route('/auth')
@flask_login.login_required def auth():
token = oauth.azure.authorize_access_token()
user = users[user.username] = User(oauth.azure.parse_id_token(token))
flask_login.login_user(user)
return flask.redirect('/')
@app.route('/logout')
def logout(): def logout():
flask_login.logout_user() flask_login.logout_user()
return flask.redirect('/') return flask.redirect(
f'https://login.microsoftonline.com/common/oauth2/v2.0/logout?'
+ urllib.parse.urlencode(
{
'returnTo': flask.url_for('home', _external=True),
'client_id': settings.get('oidc_client_id')
},
quote_via=urllib.parse.quote_plus))

17
web/errors.py Normal file
View file

@ -0,0 +1,17 @@
import flask
import werkzeug.exceptions
def init_app(app):
@app.errorhandler(werkzeug.exceptions.HTTPException)
def http_error(e):
return e
@app.errorhandler(TimeoutError)
def timeout_error(e):
return flask.render_template('busy.html')
@app.errorhandler(Exception)
def internal_server_error(e):
return flask.Response(f'something went catastrophically wrong: {e}',
status=500, mimetype='text/plain')

View file

@ -53,7 +53,7 @@ ul.keys a {
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<div style="float:right;"> <div style="float:right;">
{{ current_user['username'] }} | {{ current_user['username'] }} |
<a href="{{ url_for('auth.logout') }}">Odjava</a> <a href="{{ url_for('logout') }}">Odjava</a>
</div> </div>
{% endif %} {% endif %}