From: Joachim Kuebart Date: Fri, 2 Apr 2021 21:16:10 +0000 (+0200) Subject: add juniper-sso-auth test: add unit test for Azure MFA SSO X-Git-Tag: v8.20~311^2~4 X-Git-Url: https://www.infradead.org/git/?a=commitdiff_plain;h=b3b271bfc5eefbd1c3c8ba893f54fa507fa258c5;p=users%2Fdwmw2%2Fopenconnect.git add juniper-sso-auth test: add unit test for Azure MFA SSO Adds a new fake-juniper-sso-server.py, alongside fake-juniper-server.py (clearer this way, since they share very little in the way of auth handling). Signed-off-by: Joachim Kuebart --- diff --git a/tests/Makefile.am b/tests/Makefile.am index 805285c0..5295f863 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -46,7 +46,8 @@ 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 fake-f5-server.py fake-juniper-server.py + suppressions.lsan fake-fortinet-server.py fake-f5-server.py fake-juniper-server.py \ + fake-juniper-sso-server.py fake-tncc.py dist_check_SCRIPTS = autocompletion @@ -65,6 +66,7 @@ if HAVE_PYTHON36_FLASK dist_check_SCRIPTS += fortinet-auth-and-config dist_check_SCRIPTS += f5-auth-and-config dist_check_SCRIPTS += juniper-auth +dist_check_SCRIPTS += juniper-sso-auth endif if TEST_PKCS11 diff --git a/tests/fake-juniper-sso-server.py b/tests/fake-juniper-sso-server.py new file mode 100755 index 00000000..515f0831 --- /dev/null +++ b/tests/fake-juniper-sso-server.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# +# Copyright © 2021 Joachim Kuebart +# +# 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 + +from flask import ( + Flask, make_response, redirect, render_template_string, request, + url_for +) +import re +import ssl +import sys + +host, port, *cert_and_maybe_keyfile = sys.argv[1:] + +context = ssl.SSLContext() +context.load_cert_chain(*cert_and_maybe_keyfile) + +app = Flask(__name__) + +@app.route("/") +def root(): + # Step 0. Step 11. + return redirect(url_for("welcome")) + +@app.route("/dana-na/auth/url_default/welcome.cgi") +def welcome(): + if request.args.get("p") != "preauth": + # Step 1. Step 12. + return redirect(url_for("login")) + + if request.cookies.get("DSPREAUTH") != "success": + # Step 14: set DSPREAUTH. + resp = make_response("Waiting for host checker…") + resp.set_cookie("DSPREAUTH", "hostchecker") + return resp + + # Step 15. + return redirect(url_for("login", loginmode="mode_postAuth")) + +@app.route("/dana-na/auth/url_default/login.cgi") +def login(): + if request.cookies.get("DSASSERTREF") != "assert_ref": + # Step 2. + return redirect(url_for("ls")) + + if request.args.get("loginmode") != "mode_postAuth": + # Step 13. + return redirect(url_for("welcome", p="preauth")) + + # Step 16: set DSID. + resp = redirect(url_for("starter0")) + resp.set_cookie("DSID", "dsid") + return resp + +@app.route("/adfs/ls/", methods=["GET", "POST"]) +def ls(): + if request.cookies.get("MSISAuth") != "success": + if ( + request.method == "GET" or + request.form.get("UserName") != "test@example.com" or + request.form.get("Password") != "test" + ): + # Step 3: user/password form. + return render_template_string(""" +
+ + + + +
+
+ +
""") + + # Step 4: username/password success. + resp = redirect(url_for("ls")) + resp.set_cookie("MSISAuth", "success") + return resp + + if request.cookies.get("MSISAuth1") != "success": + if ( + "VerificationCode" not in request.form or + not re.match("^\\d{6}$", request.form["VerificationCode"]) + ): + # Step 5. Step 6: TOTP form. + return render_template_string(""" +
+ + + + {% if request.method == "POST" %} + + + {% endif %} +
+
+ +
""") + + # Step 7: TOTP success. + resp = redirect(url_for("ls")) + resp.set_cookie("MSISAuth1", "success") + return resp + + # Step 8. + return render_template_string(""" +
+ + + +
""") + +@app.route("/data-na/auth/saml-consumer0.cgi", methods=["POST"]) +def saml_consumer0(): + # Step 9: in reality, this is hosted on a different domain than the + # next step. + return render_template_string(""" +
+ + + +
""") + + +@app.route("/data-na/auth/saml-consumer.cgi", methods=["POST"]) +def saml_consumer(): + # Step 10. + resp = redirect(url_for("root")) + resp.set_cookie("DSASSERTREF", "assert_ref") + return resp + +@app.route("/dana/home/starter0.cgi") +def starter0(): + # Need to provide a form to make the parser happy. + return "
The DSID is in your cookie jar." + +app.run(host=host, port=int(port), ssl_context=context) diff --git a/tests/fake-tncc.py b/tests/fake-tncc.py new file mode 100755 index 00000000..3beda9f5 --- /dev/null +++ b/tests/fake-tncc.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import os + +def main(): + """ + Reply "success" when we receive "hostchecker". + """ + io = os.fdopen(0, "r+b", buffering=0) + started = False + for line in io: + line = line.decode("ascii").rstrip() + if line == "start": + started = True + if started and line == "Cookie=hostchecker": + io.write(b"200\n3\nsuccess\n\n\n") + started = False + +if __name__ == "__main__": + main() diff --git a/tests/juniper-sso-auth b/tests/juniper-sso-auth new file mode 100755 index 00000000..9ed72eeb --- /dev/null +++ b/tests/juniper-sso-auth @@ -0,0 +1,55 @@ +#!/bin/sh -efu +# +# Copyright © 2021 Joachim Kuebart +# +# 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 + +PRELOAD=0 +srcdir=${srcdir:-.} +top_builddir=${top_builddir:-..} + +. `dirname $0`/common.sh + +FINGERPRINT="--servercert=d66b507ae074d03b02eafca40d35f87dd81049d3" +CERT=$certdir/server-cert.pem +KEY=$certdir/server-key.pem +FAKE_TOKEN="--token-mode=totp --token-secret=ABCD" + +echo "Testing Juniper SSO auth against fake server ... " + +./fake-juniper-sso-server.py $ADDRESS 1443 $CERT $KEY >/dev/null 2>&1 & +PID=$! +wait_server $PID 1 + +echo -n "Azure SSO with MFA. " +( + echo test | + $OPENCONNECT \ + $FAKE_TOKEN \ + $FINGERPRINT \ + --cookieonly \ + --csd-wrapper ./fake-tncc.py \ + --protocol nc \ + --quiet \ + --user "test@example.com" \ + $ADDRESS:1443 \ + >/dev/null 2>&1 +) || + fail $PID "Could not receive cookie from fake Juniper server" + +echo ok + +cleanup