From 64a0ba69e53d065f4d2ba4e89e6ff10926d6c895 Mon Sep 17 00:00:00 2001 From: Daniel Lenski Date: Thu, 12 May 2022 14:58:22 -0700 Subject: [PATCH] Add a fake SAML handler/form to fake-gp-server.py This allows authenticating to the fake server with https://github.com/dlenski/gp-saml-gui # Start fake server $ ./fake-gp-server localhost 8080 certs/server-{cert,key}.pem 2>&1 >/dev/null & # Configure fake server for SAML on the portal interface $ curl -sk https://localhost:8080/CONFIGURE -d portal_saml=portal-userauthcookie -d portal_cookie=portal-userauthcookie # Use gp-saml-gui to authenticate to it $ gp-saml-gui --no-verify localhost:8080 ... ... pops up window ... fills out login form ... HOST=https://localhost:8080/global-protect/getconfig.esp:portal-userauthcookie USER=nobody COOKIE=FAKE_username_nobody_password_whatever OS=linux-64 The goal of this is to have a SAML-supporting GP server to test against while modifying openconnect to directly call the GP SAML webview handler itself (see https://github.com/dlenski/gp-saml-gui/issues/45). Signed-off-by: Daniel Lenski --- tests/fake-gp-server.py | 56 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/tests/fake-gp-server.py b/tests/fake-gp-server.py index 9800c97d..bcbe1dd3 100755 --- a/tests/fake-gp-server.py +++ b/tests/fake-gp-server.py @@ -83,6 +83,7 @@ class TestConfiguration: portal_saml: str = None gateway_saml: str = None C = TestConfiguration() +OUTSTANDING_SAML_TOKENS = set() @app.route('/CONFIGURE', methods=('POST', 'GET')) @@ -107,8 +108,13 @@ def prelogin(interface): ifname = if_path2name[interface] demand_saml = getattr(C, ifname + '_saml') if demand_saml: + # The (cookie-based) session isn't shared between OpenConnect and the external browser + # that does the SAML auth, so we need another way to track that the SAML form gets + # returned. Use a global variable for now. + token = '%08x' % randint(0x10000000, 0xffffffff) + OUTSTANDING_SAML_TOKENS.add((ifname, token)) saml = 'REDIRECT{}'.format( - base64.standard_b64encode(url_for('saml_form', interface=interface, _external=True).encode()).decode()) + base64.standard_b64encode(url_for('saml_handler', ifname=ifname, token=token, _external=True).encode()).decode()) else: saml = '' session.update(step='%s-prelogin' % ifname) @@ -127,10 +133,50 @@ def prelogin(interface): '''.format(ifname=ifname, saml=saml) -# Simple SAML form (not actually hooked up, for now) -@app.route('//SAML_FORM') -def saml_form(interface): - abort(503) +# In a "real" GP VPN with SAML, this lives on a completely different server like subdomain.okta.com +# or login.microsoft.com. +# It will be opened by an external browser or SAML-wrangling script, *not* by OpenConnect. +@app.route('/ANOTHER-HOST/SAML-ENDPOINT') +def saml_handler(): + ifname, token = request.args.get('ifname'), request.args.get('token') + + # Submit to saml_complete endpoint + # In a "real" GP setup, this would be on a different server which is why we use _external=True + saml_complete = url_for('saml_complete', _external=True) + + return '''

Please login to this fake GP VPN {ifname} interface via SAML

+
+
+
+ + + +
'''.format(ifname=ifname, saml_complete=saml_complete, token=token) + + +# This is the "return path" where SAML authentication ends up on real GP servers after +# successfully completing. +# It will be opened by an external browser or SAML-wrangling script, *not* by OpenConnect. +@app.route('/SAML20/SP/ACS', methods=('POST',)) +def saml_complete(): + ifname, token = request.form.get('ifname'), request.form.get('token') + assert ifname in ('portal', 'gateway') + + try: + OUTSTANDING_SAML_TOKENS.remove((ifname, token)) + except KeyError: + # Token and/or endpoint were bogus + abort(401) + + # Build a response containing the magical headers that indicate SAML completion + saml_headers = { + 'saml-auth-status': 1, + 'saml-username': request.form.get('username'), + getattr(C, ifname + '_saml'): 'FAKE_username_{username}_password_{password}'.format(**request.form), + } + + body = 'Login Successful!'.format(''.join('<{0}>{1}'.format(*kv) for kv in saml_headers.items())) + return body, saml_headers def challenge_2fa(where): -- 2.49.0