From 2be101fdfcc7d51aa17ee154c74a7221b1df7de0 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Tue, 1 Oct 2019 21:06:29 -0400 Subject: [PATCH 01/17] new: [authentication] Flask-login authentication via MISP instance. --- config/config.cfg.default | 4 + requirements.txt | 2 + server.py | 156 +++++++++++++++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 1 deletion(-) diff --git a/config/config.cfg.default b/config/config.cfg.default index 22eb734..615f0d6 100644 --- a/config/config.cfg.default +++ b/config/config.cfg.default @@ -3,6 +3,10 @@ host = localhost port = 8001 debug = False +[Auth] +misp_fqdn = "https://misp.local" + + [Dashboard] #hours graph_log_refresh_rate = 1 diff --git a/requirements.txt b/requirements.txt index 3cb0733..b53aed9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ argparse flask +flask-login +wtforms geoip2 redis phonenumbers diff --git a/server.py b/server.py index e240de6..f5286ab 100755 --- a/server.py +++ b/server.py @@ -6,6 +6,7 @@ import json import logging import math import os +import re import random from time import gmtime as now from time import sleep, strftime @@ -14,10 +15,14 @@ import redis import util from flask import (Flask, Response, jsonify, render_template, request, - send_from_directory, stream_with_context) + send_from_directory, stream_with_context, url_for, redirect) +from flask_login import (UserMixin, LoginManager, current_user, login_user, logout_user, login_required) from helpers import (contributor_helper, geo_helper, live_helper, trendings_helper, users_helper) +import requests +from wtforms import Form, SubmitField, StringField, PasswordField, validators + configfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config/config.cfg') cfg = configparser.ConfigParser() cfg.read(configfile) @@ -28,6 +33,7 @@ logger.setLevel(logging.ERROR) server_host = cfg.get("Server", "host") server_port = cfg.getint("Server", "port") server_debug = cfg.get("Server", "debug") +auth_host = cfg.get("Auth", "misp_fqdn") app = Flask(__name__) @@ -56,6 +62,113 @@ contributor_helper = contributor_helper.Contributor_helper(serv_redis_db, cfg) users_helper = users_helper.Users_helper(serv_redis_db, cfg) trendings_helper = trendings_helper.Trendings_helper(serv_redis_db, cfg) +login_manager = LoginManager(app) +login_manager.init_app(app) + +########## +## Auth ## +########## + +class User(UserMixin): + def __init__(self, id, password): + self.id = id + self.password = password + + def misp_login(self): + """ + Use login form data to authenticate a user to MISP. + + This function uses requests to log a user into the MISP web UI. When authentication is successful MISP redirects the client to the '/users/routeafterlogin' endpoint. The requests session history is parsed for a redirect to this endpoint. + :param misp_url: The FQDN of a MISP instance to authenticate against. + :param user: The user account to authenticate. + :param password: The user account password. + :return: + """ + post_data = { + "data[_Token][key]": "", + "data[_Token][fields]": "", + "data[_Token][unlocked]": "", + "data[User][email]": self.id, + "data[User][password]": self.password, + } + + misp_login_page = auth_host + "/users/login" + session = requests.Session() + + # The login page contains hidden form values required for authenticaiton. + login_page = session.get(misp_login_page, ssl=True) + + # This regex matches the "data[_Token][fields]" value needed to make a POST request on the MISP login page. + token_fields_exp = re.compile(r'name="data\[_Token]\[fields]" value="([^\s]+)"') + token_fields = token_fields_exp.search(login_page.text) + + # This regex matches the "data[_Token][fields]" value needed to make a POST request on the MISP login page. + token_key_exp = re.compile(r'name="data\[_Token]\[key]" value="([^\s]+)"') + token_key = token_key_exp.search(login_page.text) + + post_data["data[_Token][fields]"] = token_fields.group(1) + post_data["data[_Token][key]"] = token_key.group(1) + + # POST request with user credentials + hidden form values. + post_to_login_page = session.post(misp_login_page, data=post_data) + + # Authentication is successful if MISP returns a redirect to '/users/routeafterlogin'. + for resp in post_to_login_page.history: + if resp.url == auth_host + '/users/routeafterlogin': + return True + return None + + +@login_manager.user_loader +def load_user(user_id): + """ + Return a User object required by flask-login to keep state of a user session. + + Typically load_user is used to perform a user lookup on a db; it should return a User object or None if the user is not found. Authentication is defered to MISP via User.misp_login() and so this function always returns a User object . + :param user_id: A MISP username. + :return: + """ + return User(user_id, "") + + +@app.route('/logout') +@login_required +def logout(): + """ + Logout the user and redirect to the login form. + :return: + """ + logout_user() + return redirect(url_for('login')) + + +@app.route('/login', methods=['GET','POST']) +def login(): + """ + Login form. + :return: + """ + if current_user.is_authenticated: + return redirect(url_for('index')) + + form = LoginForm(request.form) + if request.method == 'POST' and form.validate(): + user = User(form.username.data, form.password.data) + + if user.misp_login(): + login_user(user) + return redirect(url_for('index')) + + return redirect(url_for('login')) + return render_template('login.html', title='Login', form=form) + + + +class LoginForm(Form): + username = StringField('Username', [validators.Length(min=4, max=50)]) + password = PasswordField('Password', [validators.Length(min=4, max=50)]) + submit = SubmitField('Sign In') + ########## ## UTIL ## @@ -159,6 +272,7 @@ class EventMessage(): ''' MAIN ROUTE ''' @app.route("/") +@login_required def index(): ratioCorrection = 88 pannelSize = [ @@ -180,11 +294,13 @@ def index(): ) @app.route('/favicon.ico') +@login_required def favicon(): return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') @app.route("/geo") +@login_required def geo(): return render_template('geo.html', zoomlevel=cfg.getint('GEO' ,'zoomlevel'), @@ -192,6 +308,7 @@ def geo(): ) @app.route("/contrib") +@login_required def contrib(): categ_list = contributor_helper.categories_in_datatable categ_list_str = [ s[0].upper() + s[1:].replace('_', ' ') for s in categ_list] @@ -243,12 +360,14 @@ def contrib(): ) @app.route("/users") +@login_required def users(): return render_template('users.html', ) @app.route("/trendings") +@login_required def trendings(): maxNum = request.args.get('maxNum') try: @@ -265,6 +384,7 @@ def trendings(): ''' INDEX ''' @app.route("/_logs") +@login_required def logs(): if request.accept_mimetypes.accept_json or request.method == 'POST': key = 'Attribute' @@ -283,6 +403,7 @@ def logs(): return Response(stream_with_context(event_stream_log()), mimetype="text/event-stream") @app.route("/_maps") +@login_required def maps(): if request.accept_mimetypes.accept_json or request.method == 'POST': key = 'Map' @@ -292,6 +413,7 @@ def maps(): return Response(event_stream_maps(), mimetype="text/event-stream") @app.route("/_get_log_head") +@login_required def getLogHead(): return json.dumps(LogItem('').get_head_row()) @@ -325,6 +447,7 @@ def event_stream_maps(): ''' GEO ''' @app.route("/_getTopCoord") +@login_required def getTopCoord(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -334,6 +457,7 @@ def getTopCoord(): return jsonify(data) @app.route("/_getHitMap") +@login_required def getHitMap(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -343,6 +467,7 @@ def getHitMap(): return jsonify(data) @app.route("/_getCoordsByRadius") +@login_required def getCoordsByRadius(): try: dateStart = datetime.datetime.fromtimestamp(float(request.args.get('dateStart'))) @@ -359,14 +484,17 @@ def getCoordsByRadius(): ''' CONTRIB ''' @app.route("/_getLastContributors") +@login_required def getLastContributors(): return jsonify(contributor_helper.getLastContributorsFromRedis()) @app.route("/_eventStreamLastContributor") +@login_required def getLastContributor(): return Response(eventStreamLastContributor(), mimetype="text/event-stream") @app.route("/_eventStreamAwards") +@login_required def getLastStreamAwards(): return Response(eventStreamAwards(), mimetype="text/event-stream") @@ -404,6 +532,7 @@ def eventStreamAwards(): subscriber_lastAwards.unsubscribe() @app.route("/_getTopContributor") +@login_required def getTopContributor(suppliedDate=None, maxNum=100): if suppliedDate is None: try: @@ -417,6 +546,7 @@ def getTopContributor(suppliedDate=None, maxNum=100): return jsonify(data) @app.route("/_getFameContributor") +@login_required def getFameContributor(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -427,6 +557,7 @@ def getFameContributor(): return getTopContributor(suppliedDate=date, maxNum=10) @app.route("/_getFameQualContributor") +@login_required def getFameQualContributor(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -437,10 +568,12 @@ def getFameQualContributor(): return getTopContributor(suppliedDate=date, maxNum=10) @app.route("/_getTop5Overtime") +@login_required def getTop5Overtime(): return jsonify(contributor_helper.getTop5OvertimeFromRedis()) @app.route("/_getOrgOvertime") +@login_required def getOrgOvertime(): try: org = request.args.get('org') @@ -449,6 +582,7 @@ def getOrgOvertime(): return jsonify(contributor_helper.getOrgOvertime(org)) @app.route("/_getCategPerContrib") +@login_required def getCategPerContrib(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -458,6 +592,7 @@ def getCategPerContrib(): return jsonify(contributor_helper.getCategPerContribFromRedis(date)) @app.route("/_getLatestAwards") +@login_required def getLatestAwards(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -467,10 +602,12 @@ def getLatestAwards(): return jsonify(contributor_helper.getLastAwardsFromRedis()) @app.route("/_getAllOrg") +@login_required def getAllOrg(): return jsonify(contributor_helper.getAllOrgFromRedis()) @app.route("/_getOrgRank") +@login_required def getOrgRank(): try: org = request.args.get('org') @@ -479,6 +616,7 @@ def getOrgRank(): return jsonify(contributor_helper.getCurrentOrgRankFromRedis(org)) @app.route("/_getContributionOrgStatus") +@login_required def getContributionOrgStatus(): try: org = request.args.get('org') @@ -487,6 +625,7 @@ def getContributionOrgStatus(): return jsonify(contributor_helper.getCurrentContributionStatus(org)) @app.route("/_getHonorBadges") +@login_required def getHonorBadges(): try: org = request.args.get('org') @@ -495,6 +634,7 @@ def getHonorBadges(): return jsonify(contributor_helper.getOrgHonorBadges(org)) @app.route("/_getTrophies") +@login_required def getTrophies(): try: org = request.args.get('org') @@ -503,7 +643,9 @@ def getTrophies(): return jsonify(contributor_helper.getOrgTrophies(org)) @app.route("/_getAllOrgsTrophyRanking") +@login_required @app.route("/_getAllOrgsTrophyRanking/") +@login_required def getAllOrgsTrophyRanking(categ=None): return jsonify(contributor_helper.getAllOrgsTrophyRanking(categ)) @@ -511,6 +653,7 @@ def getAllOrgsTrophyRanking(categ=None): ''' USERS ''' @app.route("/_getUserLogins") +@login_required def getUserLogins(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -522,10 +665,12 @@ def getUserLogins(): return jsonify(data) @app.route("/_getAllLoggedOrg") +@login_required def getAllLoggedOrg(): return jsonify(users_helper.getAllOrg()) @app.route("/_getTopOrglogin") +@login_required def getTopOrglogin(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -536,6 +681,7 @@ def getTopOrglogin(): return jsonify(data) @app.route("/_getLoginVSCOntribution") +@login_required def getLoginVSCOntribution(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -546,6 +692,7 @@ def getLoginVSCOntribution(): return jsonify(data) @app.route("/_getUserLoginsAndContribOvertime") +@login_required def getUserLoginsAndContribOvertime(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -558,6 +705,7 @@ def getUserLoginsAndContribOvertime(): ''' TRENDINGS ''' @app.route("/_getTrendingEvents") +@login_required def getTrendingEvents(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -571,6 +719,7 @@ def getTrendingEvents(): return jsonify(data) @app.route("/_getTrendingCategs") +@login_required def getTrendingCategs(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -584,6 +733,7 @@ def getTrendingCategs(): return jsonify(data) @app.route("/_getTrendingTags") +@login_required def getTrendingTags(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -597,6 +747,7 @@ def getTrendingTags(): return jsonify(data) @app.route("/_getTrendingSightings") +@login_required def getTrendingSightings(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -609,6 +760,7 @@ def getTrendingSightings(): return jsonify(data) @app.route("/_getTrendingDisc") +@login_required def getTrendingDisc(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -622,6 +774,7 @@ def getTrendingDisc(): return jsonify(data) @app.route("/_getTypeaheadData") +@login_required def getTypeaheadData(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -634,6 +787,7 @@ def getTypeaheadData(): return jsonify(data) @app.route("/_getGenericTrendingOvertime") +@login_required def getGenericTrendingOvertime(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) From 1356e0003e8d4117d898f25940e0f04f90cd2dbf Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 11:17:08 -0400 Subject: [PATCH 02/17] chg: [authentication] removed auth required on endpoints --- server.py | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/server.py b/server.py index f5286ab..d0eaf78 100755 --- a/server.py +++ b/server.py @@ -132,7 +132,6 @@ def load_user(user_id): @app.route('/logout') -@login_required def logout(): """ Logout the user and redirect to the login form. @@ -272,7 +271,6 @@ class EventMessage(): ''' MAIN ROUTE ''' @app.route("/") -@login_required def index(): ratioCorrection = 88 pannelSize = [ @@ -294,13 +292,11 @@ def index(): ) @app.route('/favicon.ico') -@login_required def favicon(): return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') @app.route("/geo") -@login_required def geo(): return render_template('geo.html', zoomlevel=cfg.getint('GEO' ,'zoomlevel'), @@ -308,7 +304,6 @@ def geo(): ) @app.route("/contrib") -@login_required def contrib(): categ_list = contributor_helper.categories_in_datatable categ_list_str = [ s[0].upper() + s[1:].replace('_', ' ') for s in categ_list] @@ -360,14 +355,12 @@ def contrib(): ) @app.route("/users") -@login_required def users(): return render_template('users.html', ) @app.route("/trendings") -@login_required def trendings(): maxNum = request.args.get('maxNum') try: @@ -384,7 +377,6 @@ def trendings(): ''' INDEX ''' @app.route("/_logs") -@login_required def logs(): if request.accept_mimetypes.accept_json or request.method == 'POST': key = 'Attribute' @@ -403,7 +395,6 @@ def logs(): return Response(stream_with_context(event_stream_log()), mimetype="text/event-stream") @app.route("/_maps") -@login_required def maps(): if request.accept_mimetypes.accept_json or request.method == 'POST': key = 'Map' @@ -413,7 +404,6 @@ def maps(): return Response(event_stream_maps(), mimetype="text/event-stream") @app.route("/_get_log_head") -@login_required def getLogHead(): return json.dumps(LogItem('').get_head_row()) @@ -447,7 +437,6 @@ def event_stream_maps(): ''' GEO ''' @app.route("/_getTopCoord") -@login_required def getTopCoord(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -457,7 +446,6 @@ def getTopCoord(): return jsonify(data) @app.route("/_getHitMap") -@login_required def getHitMap(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -467,7 +455,6 @@ def getHitMap(): return jsonify(data) @app.route("/_getCoordsByRadius") -@login_required def getCoordsByRadius(): try: dateStart = datetime.datetime.fromtimestamp(float(request.args.get('dateStart'))) @@ -484,17 +471,14 @@ def getCoordsByRadius(): ''' CONTRIB ''' @app.route("/_getLastContributors") -@login_required def getLastContributors(): return jsonify(contributor_helper.getLastContributorsFromRedis()) @app.route("/_eventStreamLastContributor") -@login_required def getLastContributor(): return Response(eventStreamLastContributor(), mimetype="text/event-stream") @app.route("/_eventStreamAwards") -@login_required def getLastStreamAwards(): return Response(eventStreamAwards(), mimetype="text/event-stream") @@ -532,7 +516,6 @@ def eventStreamAwards(): subscriber_lastAwards.unsubscribe() @app.route("/_getTopContributor") -@login_required def getTopContributor(suppliedDate=None, maxNum=100): if suppliedDate is None: try: @@ -546,7 +529,6 @@ def getTopContributor(suppliedDate=None, maxNum=100): return jsonify(data) @app.route("/_getFameContributor") -@login_required def getFameContributor(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -557,7 +539,6 @@ def getFameContributor(): return getTopContributor(suppliedDate=date, maxNum=10) @app.route("/_getFameQualContributor") -@login_required def getFameQualContributor(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -568,12 +549,10 @@ def getFameQualContributor(): return getTopContributor(suppliedDate=date, maxNum=10) @app.route("/_getTop5Overtime") -@login_required def getTop5Overtime(): return jsonify(contributor_helper.getTop5OvertimeFromRedis()) @app.route("/_getOrgOvertime") -@login_required def getOrgOvertime(): try: org = request.args.get('org') @@ -582,7 +561,6 @@ def getOrgOvertime(): return jsonify(contributor_helper.getOrgOvertime(org)) @app.route("/_getCategPerContrib") -@login_required def getCategPerContrib(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -592,7 +570,6 @@ def getCategPerContrib(): return jsonify(contributor_helper.getCategPerContribFromRedis(date)) @app.route("/_getLatestAwards") -@login_required def getLatestAwards(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -602,12 +579,10 @@ def getLatestAwards(): return jsonify(contributor_helper.getLastAwardsFromRedis()) @app.route("/_getAllOrg") -@login_required def getAllOrg(): return jsonify(contributor_helper.getAllOrgFromRedis()) @app.route("/_getOrgRank") -@login_required def getOrgRank(): try: org = request.args.get('org') @@ -616,7 +591,6 @@ def getOrgRank(): return jsonify(contributor_helper.getCurrentOrgRankFromRedis(org)) @app.route("/_getContributionOrgStatus") -@login_required def getContributionOrgStatus(): try: org = request.args.get('org') @@ -625,7 +599,6 @@ def getContributionOrgStatus(): return jsonify(contributor_helper.getCurrentContributionStatus(org)) @app.route("/_getHonorBadges") -@login_required def getHonorBadges(): try: org = request.args.get('org') @@ -634,7 +607,6 @@ def getHonorBadges(): return jsonify(contributor_helper.getOrgHonorBadges(org)) @app.route("/_getTrophies") -@login_required def getTrophies(): try: org = request.args.get('org') @@ -643,9 +615,7 @@ def getTrophies(): return jsonify(contributor_helper.getOrgTrophies(org)) @app.route("/_getAllOrgsTrophyRanking") -@login_required @app.route("/_getAllOrgsTrophyRanking/") -@login_required def getAllOrgsTrophyRanking(categ=None): return jsonify(contributor_helper.getAllOrgsTrophyRanking(categ)) @@ -653,7 +623,6 @@ def getAllOrgsTrophyRanking(categ=None): ''' USERS ''' @app.route("/_getUserLogins") -@login_required def getUserLogins(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -665,12 +634,10 @@ def getUserLogins(): return jsonify(data) @app.route("/_getAllLoggedOrg") -@login_required def getAllLoggedOrg(): return jsonify(users_helper.getAllOrg()) @app.route("/_getTopOrglogin") -@login_required def getTopOrglogin(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -681,7 +648,6 @@ def getTopOrglogin(): return jsonify(data) @app.route("/_getLoginVSCOntribution") -@login_required def getLoginVSCOntribution(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -692,7 +658,6 @@ def getLoginVSCOntribution(): return jsonify(data) @app.route("/_getUserLoginsAndContribOvertime") -@login_required def getUserLoginsAndContribOvertime(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -705,7 +670,6 @@ def getUserLoginsAndContribOvertime(): ''' TRENDINGS ''' @app.route("/_getTrendingEvents") -@login_required def getTrendingEvents(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -719,7 +683,6 @@ def getTrendingEvents(): return jsonify(data) @app.route("/_getTrendingCategs") -@login_required def getTrendingCategs(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -733,7 +696,6 @@ def getTrendingCategs(): return jsonify(data) @app.route("/_getTrendingTags") -@login_required def getTrendingTags(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -747,7 +709,6 @@ def getTrendingTags(): return jsonify(data) @app.route("/_getTrendingSightings") -@login_required def getTrendingSightings(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -760,7 +721,6 @@ def getTrendingSightings(): return jsonify(data) @app.route("/_getTrendingDisc") -@login_required def getTrendingDisc(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -774,7 +734,6 @@ def getTrendingDisc(): return jsonify(data) @app.route("/_getTypeaheadData") -@login_required def getTypeaheadData(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -787,7 +746,6 @@ def getTypeaheadData(): return jsonify(data) @app.route("/_getGenericTrendingOvertime") -@login_required def getGenericTrendingOvertime(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) From 2b99e131106b91ab4b325faf52e35892538d6860 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 12:34:08 -0400 Subject: [PATCH 03/17] chg: [authentication] added login page --- templates/login.html | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 templates/login.html diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..b02670a --- /dev/null +++ b/templates/login.html @@ -0,0 +1,85 @@ + + + + + + + Users - MISP + + + + + + + + + + +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ +
+
+ + + + + + +
+ + +
+ +
+ +
+ Welcome to MISP-Dashboard +
+ +
+ Login
+ +
+ {{ form.password.label }}
+ {{ form.password(size=32) }} +
+ +
+

{{ form.submit() }}

+{# #} +
+ +
+
+
+
+ + + + From e44f7e2c4b342dbd2136807ac2d3fbb52504b1b2 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 12:35:21 -0400 Subject: [PATCH 04/17] chg: [authentication] added misp logo --- static/pics/misp-logo.png | Bin 0 -> 10376 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/pics/misp-logo.png diff --git a/static/pics/misp-logo.png b/static/pics/misp-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5f2d4dd577987fbaf4b5a0028cea64e69ae58eb0 GIT binary patch literal 10376 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 literal 0 HcmV?d00001 From b7c8f6b5779fe7c59f1edf6e02a73af6b9280c62 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 12:46:37 -0400 Subject: [PATCH 05/17] chg: [authentication] enforce session ssl --- server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index d0eaf78..bcc7b79 100755 --- a/server.py +++ b/server.py @@ -94,9 +94,10 @@ class User(UserMixin): misp_login_page = auth_host + "/users/login" session = requests.Session() + session.verify = True # The login page contains hidden form values required for authenticaiton. - login_page = session.get(misp_login_page, ssl=True) + login_page = session.get(misp_login_page) # This regex matches the "data[_Token][fields]" value needed to make a POST request on the MISP login page. token_fields_exp = re.compile(r'name="data\[_Token]\[fields]" value="([^\s]+)"') From e18728e8b1b3d682ea712bfba592cc9745ac9fb9 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 13:35:12 -0400 Subject: [PATCH 06/17] chg: [authentication] enforce session ssl --- config/config.cfg.default | 2 +- server.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/config.cfg.default b/config/config.cfg.default index 615f0d6..ec9d131 100644 --- a/config/config.cfg.default +++ b/config/config.cfg.default @@ -5,7 +5,7 @@ debug = False [Auth] misp_fqdn = "https://misp.local" - +ssl_verify = True [Dashboard] #hours diff --git a/server.py b/server.py index bcc7b79..60de476 100755 --- a/server.py +++ b/server.py @@ -34,6 +34,7 @@ server_host = cfg.get("Server", "host") server_port = cfg.getint("Server", "port") server_debug = cfg.get("Server", "debug") auth_host = cfg.get("Auth", "misp_fqdn") +auth_ssl_verify = cfg.get("Auth", "ssl_verify") app = Flask(__name__) @@ -94,7 +95,7 @@ class User(UserMixin): misp_login_page = auth_host + "/users/login" session = requests.Session() - session.verify = True + session.verify = auth_ssl_verify # The login page contains hidden form values required for authenticaiton. login_page = session.get(misp_login_page) From a0ccff71efb85f13fbdfaa2079dfa31983bf05fd Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 16:34:27 -0400 Subject: [PATCH 07/17] chg: [authentication] added flask session secret --- config/config.cfg.default | 1 + server.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/config.cfg.default b/config/config.cfg.default index ec9d131..50fa496 100644 --- a/config/config.cfg.default +++ b/config/config.cfg.default @@ -6,6 +6,7 @@ debug = False [Auth] misp_fqdn = "https://misp.local" ssl_verify = True +session_secret = **Change_Me** [Dashboard] #hours diff --git a/server.py b/server.py index 60de476..6e30692 100755 --- a/server.py +++ b/server.py @@ -34,9 +34,11 @@ server_host = cfg.get("Server", "host") server_port = cfg.getint("Server", "port") server_debug = cfg.get("Server", "debug") auth_host = cfg.get("Auth", "misp_fqdn") -auth_ssl_verify = cfg.get("Auth", "ssl_verify") +auth_ssl_verify = cfg.getboolean("Auth", "ssl_verify") +auth_session_secret = cfg.get("Auth", "session_secret") app = Flask(__name__) +app.secret_key = auth_session_secret redis_server_log = redis.StrictRedis( host=cfg.get('RedisGlobal', 'host'), From 83df3e4f742426afd24805305f50ae7ef79b0459 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 17:14:22 -0400 Subject: [PATCH 08/17] chg: [authentication] increased password field length to 255. minor changes to login page. --- server.py | 4 ++-- templates/login.html | 42 +++++++----------------------------------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/server.py b/server.py index 6e30692..78a2823 100755 --- a/server.py +++ b/server.py @@ -168,8 +168,8 @@ def login(): class LoginForm(Form): - username = StringField('Username', [validators.Length(min=4, max=50)]) - password = PasswordField('Password', [validators.Length(min=4, max=50)]) + username = StringField('Username', [validators.Length(max=255)]) + password = PasswordField('Password', [validators.Length(max=255)]) submit = SubmitField('Sign In') diff --git a/templates/login.html b/templates/login.html index b02670a..0d6cd1b 100644 --- a/templates/login.html +++ b/templates/login.html @@ -12,74 +12,46 @@ - -
-
-
-
-
-
-
-
-
- - - -
-
+
- + - +
-
-
- Welcome to MISP-Dashboard -
- Login
+
Welcome to MISP-Dashboard
{{ form.password.label }}
- {{ form.password(size=32) }} + {{ form.password(size=32, maxlength=255) }}

{{ form.submit() }}

-{# #} -
+
-
- From 71780003d079430b8998b4196eccd711c0bda0ca Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 17:25:00 -0400 Subject: [PATCH 09/17] chg: [authentication] turn off password autocomplete --- templates/login.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/login.html b/templates/login.html index 0d6cd1b..cfd60fc 100644 --- a/templates/login.html +++ b/templates/login.html @@ -35,11 +35,11 @@
Welcome to MISP-Dashboard
{{ form.password.label }}
- {{ form.password(size=32, maxlength=255) }} + {{ form.password(size=32, maxlength=255, autocomplete="off") }}
From 708addaa34432ae743b2696a807dfae7af851dcb Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 18:01:27 -0400 Subject: [PATCH 10/17] chg: [authentication] add required login to dashboard views --- server.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index 78a2823..17f38df 100755 --- a/server.py +++ b/server.py @@ -136,6 +136,7 @@ def load_user(user_id): @app.route('/logout') +@login_required def logout(): """ Logout the user and redirect to the login form. @@ -148,7 +149,7 @@ def logout(): @app.route('/login', methods=['GET','POST']) def login(): """ - Login form. + Login form route. :return: """ if current_user.is_authenticated: @@ -168,6 +169,9 @@ def login(): class LoginForm(Form): + """ + WTForm form object. This object defines form fields in the login endpoint. + """ username = StringField('Username', [validators.Length(max=255)]) password = PasswordField('Password', [validators.Length(max=255)]) submit = SubmitField('Sign In') @@ -275,6 +279,7 @@ class EventMessage(): ''' MAIN ROUTE ''' @app.route("/") +@login_required def index(): ratioCorrection = 88 pannelSize = [ @@ -296,11 +301,13 @@ def index(): ) @app.route('/favicon.ico') +@login_required def favicon(): return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') @app.route("/geo") +@login_required def geo(): return render_template('geo.html', zoomlevel=cfg.getint('GEO' ,'zoomlevel'), @@ -308,6 +315,7 @@ def geo(): ) @app.route("/contrib") +@login_required def contrib(): categ_list = contributor_helper.categories_in_datatable categ_list_str = [ s[0].upper() + s[1:].replace('_', ' ') for s in categ_list] @@ -359,12 +367,14 @@ def contrib(): ) @app.route("/users") +@login_required def users(): return render_template('users.html', ) @app.route("/trendings") +@login_required def trendings(): maxNum = request.args.get('maxNum') try: From 88cc920bd3b07a03a2b270e0cb84f578c3184541 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 18:12:52 -0400 Subject: [PATCH 11/17] chg: [authentication] add unauthorized_handler to redirect unauthorized user to login page. --- server.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server.py b/server.py index 17f38df..836a020 100755 --- a/server.py +++ b/server.py @@ -135,6 +135,15 @@ def load_user(user_id): return User(user_id, "") +@login_manager.unauthorized_handler +def unauthorized(): + """ + Redirect unauthorized user to login page. + :return: + """ + return redirect(url_for('login')) + + @app.route('/logout') @login_required def logout(): From 9c028e697f7716298cc89e113c6959ba04cecd77 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 18:20:11 -0400 Subject: [PATCH 12/17] chg: [authentication] require authorization on hidden endpoints. --- server.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/server.py b/server.py index 836a020..573e8d3 100755 --- a/server.py +++ b/server.py @@ -400,6 +400,7 @@ def trendings(): ''' INDEX ''' @app.route("/_logs") +@login_required def logs(): if request.accept_mimetypes.accept_json or request.method == 'POST': key = 'Attribute' @@ -418,6 +419,7 @@ def logs(): return Response(stream_with_context(event_stream_log()), mimetype="text/event-stream") @app.route("/_maps") +@login_required def maps(): if request.accept_mimetypes.accept_json or request.method == 'POST': key = 'Map' @@ -427,6 +429,7 @@ def maps(): return Response(event_stream_maps(), mimetype="text/event-stream") @app.route("/_get_log_head") +@login_required def getLogHead(): return json.dumps(LogItem('').get_head_row()) @@ -460,6 +463,7 @@ def event_stream_maps(): ''' GEO ''' @app.route("/_getTopCoord") +@login_required def getTopCoord(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -469,6 +473,7 @@ def getTopCoord(): return jsonify(data) @app.route("/_getHitMap") +@login_required def getHitMap(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -478,6 +483,7 @@ def getHitMap(): return jsonify(data) @app.route("/_getCoordsByRadius") +@login_required def getCoordsByRadius(): try: dateStart = datetime.datetime.fromtimestamp(float(request.args.get('dateStart'))) @@ -494,14 +500,17 @@ def getCoordsByRadius(): ''' CONTRIB ''' @app.route("/_getLastContributors") +@login_required def getLastContributors(): return jsonify(contributor_helper.getLastContributorsFromRedis()) @app.route("/_eventStreamLastContributor") +@login_required def getLastContributor(): return Response(eventStreamLastContributor(), mimetype="text/event-stream") @app.route("/_eventStreamAwards") +@login_required def getLastStreamAwards(): return Response(eventStreamAwards(), mimetype="text/event-stream") @@ -539,6 +548,7 @@ def eventStreamAwards(): subscriber_lastAwards.unsubscribe() @app.route("/_getTopContributor") +@login_required def getTopContributor(suppliedDate=None, maxNum=100): if suppliedDate is None: try: @@ -552,6 +562,7 @@ def getTopContributor(suppliedDate=None, maxNum=100): return jsonify(data) @app.route("/_getFameContributor") +@login_required def getFameContributor(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -562,6 +573,7 @@ def getFameContributor(): return getTopContributor(suppliedDate=date, maxNum=10) @app.route("/_getFameQualContributor") +@login_required def getFameQualContributor(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -572,10 +584,12 @@ def getFameQualContributor(): return getTopContributor(suppliedDate=date, maxNum=10) @app.route("/_getTop5Overtime") +@login_required def getTop5Overtime(): return jsonify(contributor_helper.getTop5OvertimeFromRedis()) @app.route("/_getOrgOvertime") +@login_required def getOrgOvertime(): try: org = request.args.get('org') @@ -584,6 +598,7 @@ def getOrgOvertime(): return jsonify(contributor_helper.getOrgOvertime(org)) @app.route("/_getCategPerContrib") +@login_required def getCategPerContrib(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -593,6 +608,7 @@ def getCategPerContrib(): return jsonify(contributor_helper.getCategPerContribFromRedis(date)) @app.route("/_getLatestAwards") +@login_required def getLatestAwards(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -602,10 +618,12 @@ def getLatestAwards(): return jsonify(contributor_helper.getLastAwardsFromRedis()) @app.route("/_getAllOrg") +@login_required def getAllOrg(): return jsonify(contributor_helper.getAllOrgFromRedis()) @app.route("/_getOrgRank") +@login_required def getOrgRank(): try: org = request.args.get('org') @@ -614,6 +632,7 @@ def getOrgRank(): return jsonify(contributor_helper.getCurrentOrgRankFromRedis(org)) @app.route("/_getContributionOrgStatus") +@login_required def getContributionOrgStatus(): try: org = request.args.get('org') @@ -622,6 +641,7 @@ def getContributionOrgStatus(): return jsonify(contributor_helper.getCurrentContributionStatus(org)) @app.route("/_getHonorBadges") +@login_required def getHonorBadges(): try: org = request.args.get('org') @@ -630,6 +650,7 @@ def getHonorBadges(): return jsonify(contributor_helper.getOrgHonorBadges(org)) @app.route("/_getTrophies") +@login_required def getTrophies(): try: org = request.args.get('org') @@ -639,6 +660,7 @@ def getTrophies(): @app.route("/_getAllOrgsTrophyRanking") @app.route("/_getAllOrgsTrophyRanking/") +@login_required def getAllOrgsTrophyRanking(categ=None): return jsonify(contributor_helper.getAllOrgsTrophyRanking(categ)) @@ -646,6 +668,7 @@ def getAllOrgsTrophyRanking(categ=None): ''' USERS ''' @app.route("/_getUserLogins") +@login_required def getUserLogins(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -657,10 +680,12 @@ def getUserLogins(): return jsonify(data) @app.route("/_getAllLoggedOrg") +@login_required def getAllLoggedOrg(): return jsonify(users_helper.getAllOrg()) @app.route("/_getTopOrglogin") +@login_required def getTopOrglogin(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -671,6 +696,7 @@ def getTopOrglogin(): return jsonify(data) @app.route("/_getLoginVSCOntribution") +@login_required def getLoginVSCOntribution(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -681,6 +707,7 @@ def getLoginVSCOntribution(): return jsonify(data) @app.route("/_getUserLoginsAndContribOvertime") +@login_required def getUserLoginsAndContribOvertime(): try: date = datetime.datetime.fromtimestamp(float(request.args.get('date'))) @@ -693,6 +720,7 @@ def getUserLoginsAndContribOvertime(): ''' TRENDINGS ''' @app.route("/_getTrendingEvents") +@login_required def getTrendingEvents(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -706,6 +734,7 @@ def getTrendingEvents(): return jsonify(data) @app.route("/_getTrendingCategs") +@login_required def getTrendingCategs(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -719,6 +748,7 @@ def getTrendingCategs(): return jsonify(data) @app.route("/_getTrendingTags") +@login_required def getTrendingTags(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -732,6 +762,7 @@ def getTrendingTags(): return jsonify(data) @app.route("/_getTrendingSightings") +@login_required def getTrendingSightings(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -744,6 +775,7 @@ def getTrendingSightings(): return jsonify(data) @app.route("/_getTrendingDisc") +@login_required def getTrendingDisc(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -757,6 +789,7 @@ def getTrendingDisc(): return jsonify(data) @app.route("/_getTypeaheadData") +@login_required def getTypeaheadData(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) @@ -769,6 +802,7 @@ def getTypeaheadData(): return jsonify(data) @app.route("/_getGenericTrendingOvertime") +@login_required def getGenericTrendingOvertime(): try: dateS = datetime.datetime.fromtimestamp(float(request.args.get('dateS'))) From bd5984faad97015ac1ec7e6715d2c64ad6b72406 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 19:10:35 -0400 Subject: [PATCH 13/17] chg: [authentication] set session protection to kill session when session identifier does not match --- server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server.py b/server.py index 573e8d3..5877aca 100755 --- a/server.py +++ b/server.py @@ -66,6 +66,7 @@ users_helper = users_helper.Users_helper(serv_redis_db, cfg) trendings_helper = trendings_helper.Trendings_helper(serv_redis_db, cfg) login_manager = LoginManager(app) +login_manager.session_protection = "strong" login_manager.init_app(app) ########## From 07f68cb33fed2c1b8b23877446d7fd543540db7b Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 19:32:39 -0400 Subject: [PATCH 14/17] chg: [authentication] configure misp-dashboard cookie policy --- config/config.cfg.default | 6 ++++++ server.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/config/config.cfg.default b/config/config.cfg.default index 50fa496..480991e 100644 --- a/config/config.cfg.default +++ b/config/config.cfg.default @@ -7,6 +7,12 @@ debug = False misp_fqdn = "https://misp.local" ssl_verify = True session_secret = **Change_Me** +# Only send cookies with requests over HTTPS if the cookie is marked secure. +session_cookie_secure = True +# Prevent sending cookies in all external requests including regular links. +session_cookie_samesite = Strict +# Expire session cookie after n days. +permanent_session_lifetime = 1 [Dashboard] #hours diff --git a/server.py b/server.py index 5877aca..d6fbdbb 100755 --- a/server.py +++ b/server.py @@ -7,6 +7,7 @@ import logging import math import os import re +from datetime import timedelta import random from time import gmtime as now from time import sleep, strftime @@ -36,9 +37,18 @@ server_debug = cfg.get("Server", "debug") auth_host = cfg.get("Auth", "misp_fqdn") auth_ssl_verify = cfg.getboolean("Auth", "ssl_verify") auth_session_secret = cfg.get("Auth", "session_secret") +auth_session_cookie_secure = cfg.getboolean("Auth", "session_cookie_secure") +auth_session_cookie_samesite = cfg.getboolean("Auth", "session_cookie_samesite") +auth_permanent_session_lifetime = cfg.getint("Auth", "permanent_session_lifetime") app = Flask(__name__) -app.secret_key = auth_session_secret +#app.secret_key = auth_session_secret +app.config.update( + SECRET_KEY=auth_session_secret, + SESSION_COOKIE_SECURE=auth_session_cookie_secure, + SESSION_COOKIE_SAMESITE=auth_session_cookie_samesite, + PERMANENT_SESSION_LIFETIME=timedelta(days=auth_permanent_session_lifetime) +) redis_server_log = redis.StrictRedis( host=cfg.get('RedisGlobal', 'host'), From 3b0ebe8c72e882d6500ad016e519e547671728f5 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 19:35:58 -0400 Subject: [PATCH 15/17] chg: [authentication] session_cookie_sametime is str --- server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.py b/server.py index d6fbdbb..fc2f80e 100755 --- a/server.py +++ b/server.py @@ -38,7 +38,7 @@ auth_host = cfg.get("Auth", "misp_fqdn") auth_ssl_verify = cfg.getboolean("Auth", "ssl_verify") auth_session_secret = cfg.get("Auth", "session_secret") auth_session_cookie_secure = cfg.getboolean("Auth", "session_cookie_secure") -auth_session_cookie_samesite = cfg.getboolean("Auth", "session_cookie_samesite") +auth_session_cookie_samesite = cfg.get("Auth", "session_cookie_samesite") auth_permanent_session_lifetime = cfg.getint("Auth", "permanent_session_lifetime") app = Flask(__name__) From b313b7cc74550274a7372428c86aeec269dde791 Mon Sep 17 00:00:00 2001 From: VVX7 Date: Wed, 2 Oct 2019 20:15:34 -0400 Subject: [PATCH 16/17] chg: [authentication] add logout endpoint to dashboard dropdown --- templates/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/index.html b/templates/index.html index c90d5ea..903a3b5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -221,6 +221,7 @@ div.leaflet-bottom {
  • MISP Contributors
  • MISP Users
  • MISP Trendings
  • +
  • Logout
  • From 4d5ee49357002152706b8598398ead09480e01dd Mon Sep 17 00:00:00 2001 From: VVX7 Date: Thu, 3 Oct 2019 17:26:58 -0400 Subject: [PATCH 17/17] chg: [Authentication] User authentication can be disabled in config. If disabled, users are automatically logged in with a randomly generated account name and redirected to /index. --- config/config.cfg.default | 3 ++- server.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/config/config.cfg.default b/config/config.cfg.default index 480991e..a9eb19e 100644 --- a/config/config.cfg.default +++ b/config/config.cfg.default @@ -4,7 +4,8 @@ port = 8001 debug = False [Auth] -misp_fqdn = "https://misp.local" +auth_enabled = False +misp_fqdn = https://misp.local ssl_verify = True session_secret = **Change_Me** # Only send cookies with requests over HTTPS if the cookie is marked secure. diff --git a/server.py b/server.py index fc2f80e..0e23678 100755 --- a/server.py +++ b/server.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import configparser import datetime +import uuid import errno import json import logging @@ -35,6 +36,7 @@ server_host = cfg.get("Server", "host") server_port = cfg.getint("Server", "port") server_debug = cfg.get("Server", "debug") auth_host = cfg.get("Auth", "misp_fqdn") +auth_enabled = cfg.getboolean("Auth", "auth_enabled") auth_ssl_verify = cfg.getboolean("Auth", "ssl_verify") auth_session_secret = cfg.get("Auth", "session_secret") auth_session_cookie_secure = cfg.getboolean("Auth", "session_cookie_secure") @@ -172,6 +174,12 @@ def login(): Login form route. :return: """ + if not auth_enabled: + # Generate a random user name and redirect the automatically authenticated user to index. + user = User(str(uuid.uuid4()).replace('-',''), '') + login_user(user) + return redirect(url_for('index')) + if current_user.is_authenticated: return redirect(url_for('index'))