Switch to OIDC authentication
This commit is contained in:
parent
5add39a8a7
commit
9dc0fbb4fe
|
@ -1,4 +1,5 @@
|
||||||
|
authlib
|
||||||
click
|
click
|
||||||
flask
|
flask
|
||||||
flask-login
|
flask-login
|
||||||
flask-ldap3-login
|
ldap3
|
||||||
|
|
|
@ -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
|
||||||
|
|
71
web/auth.py
71
web/auth.py
|
@ -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):
|
||||||
def login():
|
settings = db.load('settings')
|
||||||
form = flask_ldap3_login.forms.LDAPLoginForm()
|
login_manager = flask_login.LoginManager(app)
|
||||||
if form.validate_on_submit():
|
oauth = authlib.integrations.flask_client.OAuth(app)
|
||||||
flask_login.login_user(form.user)
|
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():
|
||||||
|
return oauth.azure.authorize_redirect(flask.url_for('auth', _external=True))
|
||||||
|
|
||||||
|
@app.route('/auth')
|
||||||
|
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('/')
|
return flask.redirect('/')
|
||||||
return flask.render_template('auth/login.html', form=form)
|
|
||||||
|
|
||||||
@blueprint.route('/logout')
|
@app.route('/logout')
|
||||||
@flask_login.login_required
|
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
17
web/errors.py
Normal 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')
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue