]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
add juniper-sso-auth test: add unit test for Azure MFA SSO
authorJoachim Kuebart <joachim.kuebart@gmail.com>
Fri, 2 Apr 2021 21:16:10 +0000 (23:16 +0200)
committerDaniel Lenski <dlenski@gmail.com>
Mon, 5 Apr 2021 16:20:45 +0000 (09:20 -0700)
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 <joachim.kuebart@gmail.com>
tests/Makefile.am
tests/fake-juniper-sso-server.py [new file with mode: 0755]
tests/fake-tncc.py [new file with mode: 0755]
tests/juniper-sso-auth [new file with mode: 0755]

index 805285c00ae5da7e868a9519733806e95d495a7b..5295f863b29a9a3000881f788612f37dbab284bc 100644 (file)
@@ -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 (executable)
index 0000000..515f083
--- /dev/null
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+#
+# Copyright © 2021 Joachim Kuebart <joachim.kuebart@gmail.com>
+#
+# 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/>
+
+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("""<!doctype html>
+                <form action="{{ url_for("ls") }}"
+                      id="loginForm"
+                      method="post">
+                    <input id="userNameInput" name="UserName" type="email">
+                    <input id="passwordInput" name="Password" type="password">
+                    <input id="KmsiInput" name="Kmsi" type="checkbox">
+                    <input id="optionForms"
+                           name="AuthMethod"
+                           type="hidden"
+                           value="FormsAuthentication">
+                </form>
+                <form action="{{ url_for("ls") }}" id="options" method="post">
+                    <input id="optionSelection"
+                           name="AuthMethod"
+                           type="hidden">
+                </form>""")
+
+        # 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("""<!doctype html>
+                <form id="loginForm" method="post">
+                    <input id="autheMethod"
+                           name="AuthMethod"
+                           type="hidden"
+                           value="AzureMfaAuthentication">
+                    <input id="context" name="Context" type="hidden">
+                    <input id="__EVENTTARGET"
+                           name="__EVENTTARGET"
+                           type="hidden">
+                    {% if request.method == "POST" %}
+                        <input id="verificationCodeInput"
+                               name="VerificationCode"
+                               type="text">
+                        <input id="signInButton"
+                               name="SignIn"
+                               type="submit"
+                               value="Sign in">
+                    {% endif %}
+                </form>
+                <form action="{{ url_for("ls") }}" id="options" method="post">
+                    <input id="optionSelection"
+                           name="AuthMethod"
+                           type="hidden">
+                </form>""")
+
+        # Step 7: TOTP success.
+        resp = redirect(url_for("ls"))
+        resp.set_cookie("MSISAuth1", "success")
+        return resp
+
+    # Step 8.
+    return render_template_string("""<!doctype html>
+        <form action="{{ url_for("saml_consumer0") }}"
+              method="post"
+              name="hiddenform">
+            <input name="SAMLResponse" type="hidden">
+            <input name="RelayState" type="hidden">
+            <input type="submit" value="Submit">
+        </form>""")
+
+@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("""<!doctype html>
+        <form action="{{ url_for("saml_consumer") }}"
+              id="formSAMLSSO"
+              method="post">
+            <input id="RelayState" name="RelayState" type="hidden">
+            <input id="SAMLResponse" name="SAMLResponse" type="hidden">
+            <input id="input_saml-response-post_1"
+                   type="submit"
+                   value="Continue">
+        </form>""")
+
+
+@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 "<form></form>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 (executable)
index 0000000..3beda9f
--- /dev/null
@@ -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 (executable)
index 0000000..9ed72ee
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/sh -efu
+#
+# Copyright © 2021 Joachim Kuebart <joachim.kuebart@gmail.com>
+#
+# 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/>
+
+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