active directory authentication, full mfa implementation, vpn generation

This commit is contained in:
2024-05-20 16:10:32 -07:00
parent 49962ade44
commit a64e527fac
16 changed files with 1084 additions and 19 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

8
_forms.py Normal file
View File

@@ -0,0 +1,8 @@
import wtforms
from ad import updatePassword
class ChangePasswordForm(wtforms.Form):
# oldpw = wtforms.PasswordField('OldPassword', validators=[wtforms.validators.DataRequired()])
newpw = wtforms.PasswordField('NewPassword', validators=[wtforms.validators.DataRequired(), wtforms.validators.EqualTo('conpw', message='Passwords must match')])
conpw = wtforms.PasswordField('ConPassword')
submit = wtforms.SubmitField('Submit')

View File

@@ -1,12 +1,17 @@
from flask import Flask, url_for from flask import Flask, url_for
from flask_ldap3_login import LDAP3LoginManager from flask_ldap3_login import LDAP3LoginManager
from flask_login import LoginManager, login_user, UserMixin, current_user from flask_login import LoginManager, login_user, UserMixin, current_user, logout_user
from flask import render_template_string, redirect, render_template from flask import render_template_string, redirect, render_template, request, send_file
from flask_ldap3_login.forms import LDAPLoginForm from flask_ldap3_login.forms import LDAPLoginForm
from mfa import generateOTP, generateSecret from mfa import generateOTP, generateSecret, generateProvisioningUri
# from ad import updateMfaSecret # from ad import updateMfaSecret
from db import setupMfaSecret, updateMfaSecret, getMfaSecret, removeMfa from db import setupMfaSecret, updateMfaSecret, getMfaSecret, removeMfa
import yamlcon import yamlcon
from wtforms import StringField
from wtforms.validators import DataRequired
import _forms
from ad import updatePassword
from vpn import genVPN, getVPN
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret' app.config['SECRET_KEY'] = 'secret'
@@ -65,6 +70,8 @@ class User(UserMixin):
def get_id(self): def get_id(self):
return self.dn return self.dn
class LDAPLoginForm0(LDAPLoginForm):
mfacode = StringField('MFA')
# Declare a User Loader for Flask-Login. # Declare a User Loader for Flask-Login.
# Simply returns the User if it exists in our 'database', otherwise # Simply returns the User if it exists in our 'database', otherwise
@@ -94,6 +101,11 @@ def home():
if not current_user or current_user.is_anonymous: if not current_user or current_user.is_anonymous:
return redirect(url_for('login')) return redirect(url_for('login'))
if getMfaSecret(user=current_user.data['sAMAccountName']):
mfaStatus = "MFA is active"
else:
mfaStatus = "MFA is not active"
# User is logged in, so show them a page with their cn and dn. # User is logged in, so show them a page with their cn and dn.
# template = """ # template = """
# <h1>Welcome, {{ current_user.data.givenName }}</h1> # <h1>Welcome, {{ current_user.data.givenName }}</h1>
@@ -101,12 +113,13 @@ def home():
# <h2> Email: {{ current_user.data.mail }}</h2> # <h2> Email: {{ current_user.data.mail }}</h2>
# <h2>{{ current_user.dn }}</h2> # <h2>{{ current_user.dn }}</h2>
# """ # """
print(current_user) # print(current_user)
print(current_user.data) # print(current_user.data)
print(current_user.data['objectSid']) # print(current_user.data['objectSid'])
# return render_template_string(template) # return render_template_string(template)
return render_template('home.html', current_user=current_user, mfaurl=url_for('two_factor')) return render_template('home.html', current_user=current_user, mfaurl=url_for('two_factor'), \
mfastatus=mfaStatus, logouturl=url_for('logout'), changepwurl=url_for('changepw'), vpnurl=url_for('vpn'))
@app.route('/2fa') @app.route('/2fa')
def two_factor(): def two_factor():
@@ -121,10 +134,11 @@ def two_factor():
setupMfaSecret(user=current_user.data['sAMAccountName'], secret=generateSecret()) setupMfaSecret(user=current_user.data['sAMAccountName'], secret=generateSecret())
# currSecret = current_user.data['mfaSecret'] # currSecret = current_user.data['mfaSecret']
currSecret = getMfaSecret(user=current_user.data['sAMAccountName']) currSecret = getMfaSecret(user=current_user.data['sAMAccountName'])
uri = generateProvisioningUri(currSecret=currSecret, cu=current_user.data['sAMAccountName'])
code = '' code = ''
print(currSecret) print(currSecret)
return render_template('2fa.html', currSecret=currSecret, code=code, homeurl=url_for('home'), delurl=url_for('delTwoFactor')) return render_template('2fa.html', currSecret=currSecret, uri=uri, code=code, homeurl=url_for('home'), delurl=url_for('delTwoFactor'))
@app.route('/del2fa') @app.route('/del2fa')
def delTwoFactor(): def delTwoFactor():
@@ -152,6 +166,7 @@ def login():
<form method="POST"> <form method="POST">
<label>Username{{ form.username() }}</label> <label>Username{{ form.username() }}</label>
<label>Password{{ form.password() }}</label> <label>Password{{ form.password() }}</label>
<label>MFA Code{{ form.mfacode() }}</label>
{{ form.submit() }} {{ form.submit() }}
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
</form> </form>
@@ -159,16 +174,56 @@ def login():
# Instantiate a LDAPLoginForm which has a validator to check if the user # Instantiate a LDAPLoginForm which has a validator to check if the user
# exists in LDAP. # exists in LDAP.
form = LDAPLoginForm() form = LDAPLoginForm0()
if form.validate_on_submit(): if form.validate_on_submit():
# Successfully logged in, We can now access the saved user object # Successfully logged in, We can now access the saved user object
# via form.user. # via form.user.
login_user(form.user) # Tell flask-login to log them in. login_user(form.user) # Tell flask-login to log them in.
return redirect('/') # Send them home if getMfaSecret(user=current_user.data['sAMAccountName']):
if form.mfacode.data == generateOTP(getMfaSecret(user=current_user.data['sAMAccountName'])):
return redirect('/') # Send them home
else:
logout_user()
form.mfacode.errors.append("Invalid MFA Code")
return redirect('/')
elif not getMfaSecret(user=current_user.data['sAMAccountName']):
login_user(form.user) # Tell flask-login to log them in.
return redirect('/')
return render_template_string(template, form=form) return render_template_string(template, form=form)
@app.route('/changepw', methods=['GET', 'POST'])
def changepw():
if not current_user or current_user.is_anonymous:
return redirect(url_for('login'))
form = _forms.ChangePasswordForm(request.form)
if request.method == 'POST' and form.validate():
new_password = form.newpw.data
if updatePassword(cu=current_user.data['sAMAccountName'], newpw=new_password, adinfo=adinfo):
print('Password changed successfully!')
# flash('Password changed successfully!', 'success')
return redirect(url_for('home'))
else:
print('Failed to change password. Please try again.')
# flash('Failed to change password. Please try again.', 'danger')
return render_template('changepw.html', form=form)
@app.route('/vpn', methods=['GET', 'POST'])
def vpn():
if not current_user or current_user.is_anonymous:
return redirect(url_for('login'))
if request.args.get('dev'):
return send_file(genVPN(cu=current_user.data['sAMAccountName'], dev=request.args.get('dev')), as_attachment=True)
return render_template('vpn.html')
@app.route('/logout')
def logout():
logout_user()
return redirect('/')
if __name__ == '__main__': if __name__ == '__main__':
app.run() app.run(host='0.0.0.0', port=81)

12
ad.py
View File

@@ -1,8 +1,18 @@
from ms_active_directory import ADDomain from ms_active_directory import ADDomain
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
def updateMfaSecret(user, secret): def updateMfaSecret(user, secret):
domain = ADDomain('corp.bbrunson.com') domain = ADDomain('corp.bbrunson.com')
session = domain.create_session_as_user('administrator@bbrunson.com', 'Mariposa2502$$$$') session = domain.create_session_as_user('administrator@bbrunson.com', 'Mariposa2502$$$$')
success = session.overwrite_attribute_for_user(user, 'mfaSecret', success = session.overwrite_attribute_for_user(user, 'mfaSecret',
secret) secret)
def updatePassword(cu, newpw, adinfo):
domain = ADDomain('corp.bbrunson.com')
session = domain.create_session_as_user(user=adinfo.get('adbind_user', ''), password=adinfo.get('adbind_pass', ''))
return session.reset_password_for_account(account=(session.find_user_by_sam_name(cu)), new_password=newpw)
# updatePassword(current_user='brandon', newpw='Mariposa2502$$', oldpw='Mariposa2502$')

3
mfa.py
View File

@@ -12,3 +12,6 @@ def generateOTP(secret):
def generateSecret(): def generateSecret():
return pyotp.random_base32() return pyotp.random_base32()
def generateProvisioningUri(currSecret, cu):
return pyotp.totp.TOTP(currSecret).provisioning_uri(name=cu, issuer_name='Bbrunson Services')

BIN
templates/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -8,6 +8,9 @@
<body> <body>
<h1>{{ currSecret }}</h1> <h1>{{ currSecret }}</h1>
<h1>{{ code }}</h1> <h1>{{ code }}</h1>
<a href="{{ uri }}">
<button>Add to Authenticator App</button>
</a>
<a href="{{ homeurl }}"> <a href="{{ homeurl }}">
<button>Back home</button> <button>Back home</button>
</a> </a>

18
templates/changepw.html Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accounts - Change PW</title>
</head>
<body>
{{ get_flashed_messages() }}
{{ form.errors }}
<form method="POST">
<label>New Password{{ form.newpw() }}</label>
<label>Confirm Password{{ form.conpw() }}</label>
{{ form.submit() }}
</form>
</body>
</html>

View File

@@ -9,11 +9,40 @@
<h1>Welcome, {{ current_user.data.givenName }}</h1> <h1>Welcome, {{ current_user.data.givenName }}</h1>
<h2>Email: {{ current_user.data.mail }}</h2> <h2>Email: {{ current_user.data.mail }}</h2>
<h2>{{ current_user.dn }}</h2> <h2>{{ current_user.dn }}</h2>
MFA status:<br>
<a href="{{ mfaurl }}"> <a href="{{ mfaurl }}">
<button>MFA</button> {{ mfastatus }}
</a> </a>
<a href="http://accounts.bbrunson.com"> <style>
.styled-box {
width: 300px;
height: 150px;
background-color: #3498db;
color: #fff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
padding: 20px;
}
.styled-box:hover {
background-color: #2980b9;
}
</style>
<a href="{{ mfaurl }}">
<div class="styled-box">
<h2>MFA Status:</h2>
<p>{{ mfastatus }}</p>
</div>
</a>
<a href="{{ changepwurl }}">
<button>Change Password</button> <button>Change Password</button>
</a> </a>
<a href="{{ vpnurl }}">
<button>VPN</button>
</a>
<a href="{{ logouturl }}">
<button>Logout</button>
</a>
</body> </body>
</html> </html>

910
templates/people.html Normal file

File diff suppressed because one or more lines are too long

17
templates/vpn.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accounts - VPN</title>
</head>
<body>
<form action="/vpn?dev=ios" method="post">
<button type="submit">Download VPN for iOS</button>
</form>
<form action="/vpn?dev=mac" method="post">
<button type="submit">Download VPN for Mac</button>
</form>
</body>
</html>

12
vpn.py Normal file
View File

@@ -0,0 +1,12 @@
import sys
import subprocess
def genVPN(cu, dev):
result = subprocess.call(['sh', 'ikev2.sh', '--addclient', cu+'-'+dev])
if result == "Error: Invalid client name. Client " + cu+'-'+dev + " already exists.":
return getVPN(cu, dev)
else:
return "profiles/"+cu+'-'+dev+".mobileconfig"
def getVPN(cu, dev):
return "profiles/"+cu+'-'+dev+".mobileconfig"