Flask Web-Interface to manage FreeRADIUS users via SAML
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

196 lines
6.2 KiB

# Inspired by https://github.com/jpf/okta-pysaml2-example/ which is under copyright by Okta, Inc. and licensed under Apache 2 Licence.
# Copyright 2020 Leo "em0lar" Maroni
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import fileinput
import logging
import os
import re
import subprocess
import uuid
import requests
from flask import (
Flask,
redirect,
render_template,
request,
session,
url_for,
)
from saml2 import (
BINDING_HTTP_POST,
BINDING_HTTP_REDIRECT,
entity,
)
from saml2.client import Saml2Client
from saml2.config import Config as Saml2Config
from xkcdpass import xkcd_password as xkcdpass
metadata_url_for = {
'keycloak': 'https://auth.labcode.de/auth/realms/labcode/protocol/saml/descriptor'
}
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', str(uuid.uuid4())) # Replace with your secret key
logging.basicConfig(level=logging.WARN)
def saml_client_for(idp_name=None):
if idp_name not in metadata_url_for:
raise Exception("Settings for IDP '{}' not found".format(idp_name))
acs_url = url_for(
"idp_initiated",
idp_name=idp_name,
_external=True)
https_acs_url = url_for(
"idp_initiated",
idp_name=idp_name,
_external=True,
_scheme='https')
if bool(os.environ.get("HTTPS_ONLY", default=False)):
acs_url = https_acs_url
rv = requests.get(metadata_url_for[idp_name])
settings = {
'entityid': 'wifi_radius_manager',
'metadata': {
'inline': [rv.text],
},
'service': {
'sp': {
'endpoints': {
'assertion_consumer_service': [
(acs_url, BINDING_HTTP_REDIRECT),
(acs_url, BINDING_HTTP_POST),
(https_acs_url, BINDING_HTTP_REDIRECT),
(https_acs_url, BINDING_HTTP_POST)
],
},
'allow_unsolicited': True,
'authn_requests_signed': False,
'logout_requests_signed': True,
'want_assertions_signed': True,
'want_response_signed': True,
},
},
}
spConfig = Saml2Config()
spConfig.load(settings)
spConfig.allow_unknown_attributes = True
saml_client = Saml2Client(config=spConfig)
return saml_client
@app.route("/saml/sso/<idp_name>", methods=['POST'])
def idp_initiated(idp_name):
saml_client = saml_client_for(idp_name)
authn_response = saml_client.parse_authn_request_response(
request.form['SAMLResponse'],
entity.BINDING_HTTP_POST)
authn_response.get_identity()
user_info = authn_response.get_subject()
username = user_info.text
session['username'] = username
# session['saml_attributes'] = authn_response.ava
session['logged_in'] = True
return redirect("/")
@app.route("/saml/login/<idp_name>")
def sp_initiated(idp_name):
saml_client = saml_client_for(idp_name)
reqid, info = saml_client.prepare_for_authenticate()
redirect_url = None
for key, value in info['headers']:
if key == 'Location':
redirect_url = value
response = redirect(redirect_url, code=302)
response.headers['Cache-Control'] = 'no-cache, no-store'
response.headers['Pragma'] = 'no-cache'
return response
@app.route("/")
def main_page():
try:
if session['logged_in']:
return render_template(
'main_page.html',
username=session['username'],
current_password=get_password(session['username']))
else:
return redirect("/saml/login/keycloak")
except KeyError:
return redirect("/saml/login/keycloak")
@app.route("/generate_new_password")
def generate_new_password_page():
try:
if session['logged_in']:
wordfile = xkcdpass.locate_wordfile()
wordlist = xkcdpass.generate_wordlist(wordfile=wordfile, min_length=4, max_length=6)
new_password = xkcdpass.generate_xkcdpassword(
wordlist=wordlist,
numwords=5,
delimiter="_"
)
set_password(session['username'], capitalize_first_letter(new_password))
return redirect("/")
else:
return redirect("/saml/login/keycloak")
except KeyError:
return redirect("/saml/login/keycloak")
def capitalize_first_letter(s):
new_str = []
s = s.split("_")
for i, c in enumerate(s):
if i+1 == len(s):
new_str.append(c.capitalize())
else:
new_str.append(c.capitalize() + "_")
return "".join(new_str)
def set_password(username: str, password: str):
found = False
for line in fileinput.input(os.environ.get("USER_FILE_PATH"), inplace=True):
if not found and line.startswith(username):
print(username + ' Cleartext-Password := "' + password + '"', end='\n')
found = True
elif line != "\n" and not line.startswith(username):
print(line, end='')
user_file = open(os.environ.get("USER_FILE_PATH"), "a")
if not found:
user_file.write(username + ' Cleartext-Password := "' + password + '"\n')
if os.environ.get("HUP_PID_PATH"):
pid = subprocess.getoutput("cat " + os.environ.get("HUP_PID_PATH"))
subprocess.run(["kill", "-s", "HUP", pid])
def get_password(username: str):
user_file = open(os.environ.get("USER_FILE_PATH"), "r")
for line in user_file.readlines():
if line.startswith(username):
return re.search('"\w*"', line).group().replace('"', '')
if __name__ == "__main__":
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port)