From 1c5123a2b47006e59739959ab67b51129d39a761 Mon Sep 17 00:00:00 2001 From: "B. Stack" Date: Mon, 21 Jun 2021 16:52:56 -0400 Subject: add ldap support --- INTERACT.md | 4 +- session_app.py.publish | 130 ++++++++++++++++++++++++++++------------------ session_ldap.py | 30 +++++++++++ templates/login_form.html | 7 ++- 4 files changed, 116 insertions(+), 55 deletions(-) create mode 100644 session_ldap.py diff --git a/INTERACT.md b/INTERACT.md index b3c3914..c36b238 100644 --- a/INTERACT.md +++ b/INTERACT.md @@ -46,4 +46,6 @@ Visit protected page now that we have a session. Logged in through: kerberos -2021-06-20 ldap basic auth, and a login form are still pending. +For submitting to the form, pass in form data using fields `username`, `password`, and optionally `logintype` which can be defined within the application. An included option is `ldap`. Kerberos auth through the form is not supported. + + curl -L -X POST http://d2-03a:5000/login/ --data 'username=bgstack15&password=qwerty' -b ~/cookiejar.txt -c ~/cookiejar.txt diff --git a/session_app.py.publish b/session_app.py.publish index 520f676..4a806ed 100755 --- a/session_app.py.publish +++ b/session_app.py.publish @@ -10,25 +10,23 @@ # future: https://code.tutsplus.com/tutorials/flask-authentication-with-ldap--cms-23101 # better timeout session: https://stackoverflow.com/a/49891626/3569534 # Improve: -# purge sessions after 15 minutes? +# move all configs to config file +# move all references to references section +# accept a /login/basic endpoint with Authorization: header, use ldap +# accept a bind credential so we can perform lookups of users who match "uid=%s" under a basedn. # Run: # FLASK_APP=session_app.py FLASK_DEBUG=1 flask run --host 0.0.0.0 # Dependencies: # apt-get install python3-flask # pip3 install Flask-kerberos kerberos -from flask import Flask, Response, redirect, url_for, render_template, request - +from flask import Flask, Response, redirect, url_for, render_template, request, _request_ctx_stack as stack, make_response, session from flask_kerberos import init_kerberos, requires_authentication, _unauthorized, _forbidden, _gssapi_authenticate -from flask import _request_ctx_stack as stack, make_response, session -#from flask.ext.login import LoginManager import kerberos from functools import wraps -from socket import gethostname import binascii, datetime - -from functools import wraps import os +import session_ldap DEBUG=True app = Flask(__name__) @@ -37,10 +35,12 @@ app.debug=True secret_key_value = os.urandom(24) secret_key_value_hex_encoded = binascii.hexlify(secret_key_value) app.config['SECRET_KEY'] = secret_key_value_hex_encoded -#app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=7) -#session.permanent = True -minutes = 2 -app.permanent_session_lifetime=datetime.timedelta(minutes=minutes) +app.config['LDAP_URI'] = "ldaps://dns1.ipa.internal.com:636" +app.config['LDAP_USER_BASEDN'] = "cn=users,cn=accounts,dc=ipa,dc=internal,dc=com" +app.config['LDAP_GROUP_BASEDN'] = "cn=groups,cn=accounts,dc=ipa,dc=internal,dc=com" +app.config['LDAP_USER_FORMAT'] = "uid=%s,cn=users,cn=accounts,dc=ipa,dc=internal,dc=com" +app.config['minutes'] = 2 +app.permanent_session_lifetime=datetime.timedelta(minutes=app.config['minutes']) def requires_session(function): ''' @@ -97,6 +97,27 @@ def requires_authn_kerberos(function): return _unauthorized_kerberos() return decorated +def requires_authn_ldap(function): + ''' + Require that the wrapped view function only be called by users + authenticated with ldap. The view function will have the authenticated + users principal passed to it as its first argument. + :param function: flask view function + :type function: function + :returns: decorated function + :rtype: function + ''' + @wraps(function) + def decorated(*args, **kwargs): + username = request.form['username'] + pw = request.form['password'] + ll = ldap_login(username,pw) + if ll: + return function(ll.user,*args, **kwargs) + else: + return _unauthorized_ldap() + return decorated + def _unauthorized_kerberos(): ''' Indicate that authentication is required @@ -104,20 +125,13 @@ def _unauthorized_kerberos(): # from https://billstclair.com/html-redirect2.html return Response(f'Unauthorized! No kerberos auth provided. Trying ldap automatically in a moment.', 401, {'WWW-Authenticate': 'Negotiate'}) +def _unauthorized_ldap(): + return Response(f'Unauthorized! Invalid ldap credentials... returning to login form', 401) + @app.route("/") def index(): return render_template('index.html') -@app.route("/open/") -def open(): - header = request.headers.get("Authorization") - if header: - print("Header!") - token = ''.join(header.split()[1:]) - print("token",token) - print("something") - return "here", 200 - @app.route("/protected/") @requires_session def protected_page(): @@ -131,11 +145,11 @@ def protected_page_real(): return render_template('view.html', c_user = c_user, s_user=s_user, cookie=cookie) @app.route("/login/new") +@app.route("/login/new/") def login_new(): return redirect(url_for("login", new="")) @app.route("/login/", methods=['POST','GET']) -#@requires_authn_kerberos def login(user="None"): if request.method == "GET": if 'user' in session and request.cookies.get('user') == session['user'] and (not 'new' in request.args): @@ -148,31 +162,44 @@ def login(user="None"): # default, show login form return redirect(url_for("login_form")) elif request.method == "POST": - # so far only the login form sends a POST to this endpoint. - username=request.form['username'] - pw=request.form['password'] - #pw="******" - args="" - for i in request.args: - args += str(i) - #resp = Response(f"Login functionality still in progress.
Args: {args}
data: {request.data}
query_string: {request.query_string}
values: {request.values}",200) - ldap_result = ldap_login(username,pw) - resp = Response(f"Login functionality still in progress.
username: {username}
password: {pw}
form: {request.form}
ldap result:{ldap_result}",200) - return resp - + # redirect to whichever option was chosen in the drop-down + if 'logintype' in request.form: + logintype = request.form['logintype'] + else: + # choose default logintype for user + logintype = "ldap" + if "ldap" == logintype: + # preserve POST with code 307 https://stackoverflow.com/a/15480983/3569534 + return redirect(url_for("login_ldap"), code=307) + else: + return f"Authentication method {logintype} not supported yet.",400 + def ldap_login(username,password): - response = f"Trying user {username} with pw '{password}'" - print(response) - return response - + print(f"Trying user {username} with pw '{password}'") + user = session_ldap.authenticated_user( + app.config['LDAP_URI'], + app.config['LDAP_USER_FORMAT'], + username, + password + ) + if user: + return user + else: + return False + return False @app.route("/login/kerberos") +@app.route("/login/kerberos/") @requires_authn_kerberos def login_kerberos(user): resp = Response(f'success with kerberos') #resp.headers['login'] = "from-kerberos" - resp.set_cookie('user',user) resp.set_cookie('type',"kerberos") + resp = login_generic(session,resp,user,None) + return resp + +def login_generic(session,resp,user,groups=[]): + resp.set_cookie('user',user) end_time = datetime.datetime.now(datetime.timezone.utc) + app.permanent_session_lifetime end_time_str = datetime.datetime.strftime(end_time,"%FT%TZ") resp.set_cookie('timestamp',end_time_str) @@ -181,27 +208,30 @@ def login_kerberos(user): session['end_time'] = end_time_str return resp -# WORKHERE: ldap auth -# WIP 2021-06-18 17:42; make this unauthenticated GET send to a form. @app.route("/login/ldap", methods=['POST','GET']) -#@app.route("/login/ldap/") -def login_ldap(user = "none"): - resp = Response(f"success, from user {user}") - resp.headers['login'] = "from-ldap" - resp.set_cookie('user',user) +@app.route("/login/ldap/", methods=['POST','GET']) +@requires_authn_ldap +def login_ldap(user,groups=[]): + resp = Response(f'success with ldap') resp.set_cookie('type',"ldap") - session['user']=user - resp.set_cookie('timestamp',app.permanent_session_lifetime) + resp = login_generic(session,resp,user,groups) return resp +@app.route("/login/form", methods=['GET']) @app.route("/login/form/", methods=['GET']) def login_form(): options = { "ldap": "ldap", + "other": "other" } - return render_template("login_form.html",login_url=url_for("login"),options=options) + return render_template("login_form.html", + login_url = url_for("login"), + options=options, + kerberos_url = url_for("login_kerberos") + ) @app.route("/logout") +@app.route("/logout/") def logout(): resp = Response(f"logged out") # Doing anything with session here leaves a cookie. diff --git a/session_ldap.py b/session_ldap.py new file mode 100644 index 0000000..b478ef5 --- /dev/null +++ b/session_ldap.py @@ -0,0 +1,30 @@ +# python3 library +# Startdate: 2021-06-21 +# Dependencies: +# req-devuan: python3-ldap3 + +# reference: https://github.com/ArtemAngelchev/flask-basicauth-ldap/blob/master/flask_basicauth_ldap.py + +import ldap3 +from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError + +def authenticated_user(server_uri, user_format, username, password): + user = user_format.replace("%s",username) + print(f"server_uri: {server_uri}") + print(f"username: {username}") + print(f"user_format: {user_format}") + print(f"user: {user}") + try: + server = ldap3.Server(server_uri) + conn = ldap3.Connection(server, auto_bind=True, user=user, password=password) + return conn + except LDAPBindError as e: + if 'invalidCredentials' in str(e): + print("Invalid credentials.") + return False + else: + raise e + #except (LDAPPasswordIsMandatoryError, LDAPBindError): + # print("Either an ldap password is required, or we had another bind error.") + # return False + return False diff --git a/templates/login_form.html b/templates/login_form.html index 1b42e60..254d45c 100644 --- a/templates/login_form.html +++ b/templates/login_form.html @@ -5,15 +5,14 @@

Login Form

-
+Use kerberos + {% if options %}Login type {% endif %}

Username

Password

-

+

-- cgit