]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
add auth-f5 tests
authorDaniel Lenski <dlenski@gmail.com>
Sun, 21 Feb 2021 23:02:44 +0000 (15:02 -0800)
committerDaniel Lenski <dlenski@gmail.com>
Mon, 29 Mar 2021 03:57:25 +0000 (20:57 -0700)
This tests OpenConnect's ability to authenticate with a (fake) F5
server, using username+password (the only option that OpenConnect
currently supports).

The fake F5 authentication server requires python3 and Flask.

Signed-off-by: Daniel Lenski <dlenski@gmail.com>
tests/Makefile.am
tests/auth-f5 [new file with mode: 0755]
tests/fake-f5-server.py [new file with mode: 0755]

index 50d49a724a35165570334485d3858b53438c81ce..6b16a7a2859b2551fe8b7e5b6fe1e2a1f720daf2 100644 (file)
@@ -46,7 +46,7 @@ EXTRA_DIST = certs/ca.pem certs/ca-key.pem certs/user-cert.pem $(USER_KEYS) $(US
        configs/user-cert.prm configs/server-cert.prm \
        softhsm2.conf.in softhsm ns.sh configs/test-dtls-psk.config \
        scripts/vpnc-script scripts/vpnc-script-detect-disconnect \
-       suppressions.lsan fake-fortinet-server.py
+       suppressions.lsan fake-fortinet-server.py fake-f5-server.py
 
 dist_check_SCRIPTS = autocompletion
 
@@ -63,6 +63,7 @@ dist_check_SCRIPTS += auth-username-pass auth-certificate auth-nonascii cert-fin
 
 if HAVE_PYTHON36_FLASK
 dist_check_SCRIPTS += auth-fortinet
+dist_check_SCRIPTS += auth-f5
 endif
 
 if TEST_PKCS11
diff --git a/tests/auth-f5 b/tests/auth-f5
new file mode 100755 (executable)
index 0000000..8f76b79
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/sh
+#
+# Copyright © 2021 Daniel Lenski
+#
+# This file is part of openconnect.
+#
+# This is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation; either version 2.1 of
+# the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>
+
+# This test uses LD_PRELOAD
+PRELOAD=1
+srcdir=${srcdir:-.}
+top_builddir=${top_builddir:-..}
+
+. `dirname $0`/common.sh
+
+FINGERPRINT="--servercert=d66b507ae074d03b02eafca40d35f87dd81049d3"
+CERT=$certdir/server-cert.pem
+KEY=$certdir/server-key.pem
+
+echo "Testing F5 auth against fake server ... "
+
+OCSERV=./fake-f5-server.py
+launch_simple_sr_server $ADDRESS 443 $CERT $KEY > /dev/null 2>&1
+PID=$!
+wait_server $PID 1
+
+echo -n "Authenticating with username/password... "
+( echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=f5 -q $ADDRESS:443 -u test $FINGERPRINT --cookieonly >/dev/null 2>&1) ||
+    fail $PID "Could not receive cookie from fake F5 server"
+
+echo ok
+
+cleanup
+
+exit 0
diff --git a/tests/fake-f5-server.py b/tests/fake-f5-server.py
new file mode 100755 (executable)
index 0000000..abb5c02
--- /dev/null
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+#
+# Copyright © 2021 Daniel Lenski
+#
+# This file is part of openconnect.
+#
+# This is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation; either version 2.1 of
+# the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>
+
+########################################
+# This program emulates the authentication-phase behavior of a F5
+# server enough to test OpenConnect's authentication behavior against it.
+# Specifically, it emulates the following requests:
+#
+#    GET /
+#    GET /my.policy
+#    POST /my.policy
+#
+# It does not actually validate the credentials in any way, but attempts to
+# verify their consistency from one request to the next, by saving their
+# values via a (cookie-based) session.
+########################################
+
+import sys
+import ssl
+import random
+import base64
+import time
+from json import dumps
+from functools import wraps
+from flask import Flask, request, abort, redirect, url_for, make_response, session
+
+host, port, *cert_and_maybe_keyfile = sys.argv[1:]
+
+context = ssl.SSLContext()
+context.load_cert_chain(*cert_and_maybe_keyfile)
+
+app = Flask(__name__)
+app.config.update(SECRET_KEY=b'fake', DEBUG=True, HOST=host, PORT=int(port), SESSION_COOKIE_NAME='fake')
+
+########################################
+
+def cookify(jsonable):
+    return base64.urlsafe_b64encode(dumps(jsonable).encode())
+
+def require_MRHSession(fn):
+    @wraps(fn)
+    def wrapped(*args, **kwargs):
+        if not request.cookies.get('MRHSession'):
+            session.clear()
+            return redirect(url_for('get_policy'))
+        return fn(*args, **kwargs)
+    return wrapped
+
+def check_form_against_session(*fields, use_query=False):
+    def inner(fn):
+        @wraps(fn)
+        def wrapped(*args, **kwargs):
+            source = request.args if use_query else request.form
+            source_name = 'args' if use_query else 'form'
+            for f in fields:
+                assert session.get(f) == source.get(f), \
+                    f'at step {session.get("step")}: {source_name} {f!r} {source.get(f)!r} != session {f!r} {session.get(f)!r}'
+            return fn(*args, **kwargs)
+        return wrapped
+    return inner
+
+########################################
+
+# Respond to initial 'GET /' with a redirect to '/my.policy'
+@app.route('/')
+def root():
+    session.update(step='initial-GET')
+    # print(session)
+    return redirect(url_for('get_policy'))
+
+
+# Respond to 'GET /my.policy with a placeholder stub (since OpenConnect doesn't even try to parse the form)
+@app.route('/my.policy')
+def get_policy():
+    session.update(step='GET-login-form')
+    return 'login page'
+
+
+# Respond to 'POST /my.policy with an empty response containing MRHSession and F5_ST
+# cookies (OpenConnect uses the combination of the two to detect successful authentication)
+@app.route('/my.policy', methods=['POST'])
+def post_policy():
+    session.update(step='POST-login', username=request.form.get('username'), credential=request.form.get('password'))
+    # print(session)
+
+    resp = make_response('')
+    resp.set_cookie('MRHSession', cookify(dict(session)))
+    resp.set_cookie('F5_ST', '1z1z1z%dz%d' % (time.time(), 3600))
+    return resp
+
+
+# Respond to 'GET /remote/logout' by clearing session and MRHSession
+@app.route('/remote/logout')
+@require_MRHSession
+def logout():
+    assert request.args == {'hangup_error': '1'}
+    session.clear()
+    resp = make_response('successful logout')
+    resp.set_cookie('MRHSession', '')
+    return resp
+
+
+app.run(host=app.config['HOST'], port=app.config['PORT'], debug=app.config['DEBUG'],
+        ssl_context=context, use_debugger=False)