Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion to remove server.go and listen for POST SAML response #9

Open
JohnPolansky opened this issue Apr 24, 2021 · 0 comments
Open

Comments

@JohnPolansky
Copy link

Hey so you may have already read my other tickets, as you can guess I spent a lot of time and had lots of frustrating fun learning the ins and outs of AWS VPN with Okta SAML provider. My company ONLY uses Okta SAML so these changes will likely only work for OKTA but I'm guessing the "logic" should work for others with some tweaking.

I got to wondering why do we need to open a browser window and login and run a server.go to listen for any SAML responses, while a really ingenious idea I was curious if there might be another option. Now I won't lie I spent many many hours fighting with this stuff enough so that were I do it again I probably would have stopped with your solution, which is great and has the advantage of using the browser cache for tokens so you don't have to login constantly.

Using Firefox's Network Tools I was able to track the transactions going through to see how the process worked, and I could see how when the browser had an okta token cached it never had to login but just automatically sent the POST and got the SAML reponse. I was curious could I use normal REST calls to perform the same transactions and to perform an OKTA Login and get a SID and SessionToken which I could then submit to the AWS Client VPN endpoint and connect openvpn.

Well the short answer I was able to do so, now of course the main concern here is.. if we have to do a login everytime that would be a big pain for the user, which is one of the advantages of your solution that using the browser you usually don't have to login but 1x per day. However with lots of trial and error I learned that as long as you preserve the SessionToken and Okta SID in a file, you could use those to create a new SAML reponse multiple times and connect to AWS Client VPN without every having to enter your user/pass/mfa info. Great!

Now please bear in mind this code is rough and I'm no hard-core developer, so please feel free to improve if you like. But I created a NodeJS script that performs all the HTTP calls. The logic is pretty straight-forward.

  • when you run aws-connect-okta.sh it no longer needs the server.go instead it starts the nodejs script: get-saml-response.js
  • this script first checks to see if it already has a OKTA SID (saml-sid.txt) and Okta Session Token.
    ** If they exists it then tries to immediately get a new SAML response,
    ** if successful you are done we return back to aws-connect-okta.sh and opevpn connects and everyone is happy.
  • If the SID/Token do not exist or have expired the SAML response request will fail and
    ** this will trigger new HTTP requests to OKTA to "login w/ user/password" and
    ** perform an MFA challenge which in turn gets us a sessionToken.
    ** We then send the sessionToken to AWS VPN Client saml app and this returns us both an OKTA SID (useful to allow future SAML response requests without login) and the SAML Response, which the script then saves to files and exits returning back to aws-connect-okta.sh for the VPN connect.

Now as a reminder I have only tested this on OKTA, I'm sure other providers will require some changes to the HTTP URLs and if you don't use MFA the same, but the logic is pretty cool I think. I don't know if it's worth trying to explore this further for other scenarios, but my objective of OKTA Provider + AWS Client VPN seems to work fairly nicely. I'm sure there are a few bugs in there to work out, but I wanted to share and say:

Thank you for your hard work which made this all possible

Attached here are the two files: You will need to set the following to make this work:
company, provider id, username, password

aws-connect.sh
Add the following just before the wait_file "saml_response.txt"

node get-saml-response.js

get-saml-response.js

#!/usr/bin/env node

const axios = require("axios").default;
const fs = require("fs").promises;
const he = require("he");
const prompt = require("prompt");

const username = "USERNAME/EMAIL";
const password = "PASSWORD";

const properties = [
    {
        name: 'mfaToken',
        hidden: true
    }
];

(async function () {
    var myArgs = process.argv.slice(2);

    checkExistingCredentials = async function () {
        try {
            console.log("Reading saml-sid.txt")
            oktaSid = await fs.readFile("saml-sid.txt");
            console.log("Reading saml-sessionToken.txt")
            sessionToken = await fs.readFile("saml-sessionToken.txt");
            console.log(`Existing Creds Found: SessionToken: ${sessionToken} OktaSID: ${oktaSid}`)
            await getSamlResponse(sessionToken, oktaSid);
            return true
        } catch (err) {
            console.log("Error while checking for existing Credentials, proceeding with new session request.");
            console.log(err);
            return false;
        }
    };

    getSamlResponse = async function (sessionToken, oktaSid) {
        options = {};
        if (oktaSid) {
            options = {
                headers: {
                    Cookie: "sid=" + oktaSid,
                }
            };
        }
        try {
            response = await axios.get(
                `https://<company>.okta.com/app/aws_clientvpn/<provider id>/sso/saml?sessionToken=${sessionToken}`,
                options
            );
            oktaSid = null;
            response.headers["set-cookie"].forEach((cookie) => {
                match = /sid=(.*); Path=.*/.exec(cookie);
                if (match) {
                    oktaSid = match[1];
                }
            });
            if (!oktaSid) {
                console.log("Error parsing Sid cookie, we have to abort");
                process.exit(4);
            }
            console.log(`Okta SID: ${oktaSid}`);
            await fs.writeFile("saml-sid.txt", oktaSid);

            // Third.3 - We need to parse SAMLResponse out of XML response
            xmlOneLine = response.data.toString().replace(/[\n\r]+/g, "");
            match = /.*SAMLResponse" type="hidden" value="(.*)"\/>\s+ <input name.*/g.exec(xmlOneLine);
            samlResponseBase64 = he.decode(match[1]);
            samlResponseUriEncoded = encodeURIComponent(samlResponseBase64);
            await fs.writeFile("saml-response.txt", samlResponseUriEncoded);
            console.log('SamlResponse written')
            return true;
        } catch (err) {
            if (err.response && err.response.status === 302) {
                console.log("Warning: Redirect detects on SAML response, this usually means your token has expired");
                return false;
            } else {
                console.log("Error Unknown, stopping");
                console.log(err);
                process.exit(3);
            }
        }
    };

    getOktaLogin = async function (username, password) {
        try {
            response = await axios.post("https://<company>.okta.com/api/v1/authn", {
                username: username,
                password: password,
                options: {
                    multiOptionalFactorEnroll: true,
                    warnBeforePasswordExpired: true,
                },
            });
        } catch (err) {
            console.log("Error authenticating with user/pass");
            console.log(err.response.data);
            process.exit(1);
        }
        let stateToken = response.data.stateToken;
        let mfaVerifyUrl = response.data._embedded.factors[0]._links.verify.href;
        console.log(`StateToken received: ${stateToken}`);
        console.log(`MFA Verify URL: ${mfaVerifyUrl}`);
        return { stateToken, mfaVerifyUrl }
    };

    getMfaVerify = async function (stateToken, mfaToken, mfaVerifyUrl) {
        // Second we need to perform MFA Verify
        try {
            response = await axios.post(mfaVerifyUrl, {
                stateToken: stateToken,
                passCode: mfaToken,
            });
        } catch (err) {
            console.log("Error verifying MFA, did you reuse a token?");
            console.log(response.data);
            process.exit(2);
        }
        sessionToken = response.data.sessionToken;
        console.log(`SessionToken: ${sessionToken}`);
        await fs.writeFile("saml-sessionToken.txt", sessionToken);
        return sessionToken;
    };

    //////////////////////////////////////////////////////////////////////
    //////////////////         M  A  I  N         ////////////////////////
    //////////////////////////////////////////////////////////////////////
    credsCheck = await checkExistingCredentials();
    if (credsCheck) {
        console.log("Existing Credentials are still valid, SAML Response written to file.");
        process.exit(0);
    }
    
    // We need to prompt the user for the MFA Token
    prompt.start()
    const { mfaToken } = await prompt.get(properties)

    const { stateToken, mfaVerifyUrl } = await getOktaLogin(username, password);
    sessionToken = await getMfaVerify(stateToken, mfaToken, mfaVerifyUrl);
    credCheck = await getSamlResponse(sessionToken, false);
    if (credCheck) {
        console.log("Credentials appear valid, SAML Response written to file.");
        process.exit(0);
    }
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant