openziti / ziti-console

https://openziti.io
Apache License 2.0
23 stars 16 forks source link

cert auth #388

Closed qrkourier closed 4 months ago

qrkourier commented 4 months ago

If a client cert is available in the web browser visiting the SPA in the controller's web API, then it will already have negotiated mTLS by the time the SPA is loaded.

The SPA could send POST /edge/management/v1/authenticate?method=cert with an empty body. If the response is not an error then .data.token is zt-session for subsequent requests.

There's no harm in attempting this and failing silently, since there's no way to know if a client cert was available during TLS negotiation.

If it succeeds, then skip the password form and go straight to the dashboard.

qrkourier commented 4 months ago

I verified this works as intended with the following userscript after importing the admin identity's client cert in my web browser and visiting the console binding /zac on the mgmt API URL.

// ==UserScript==
// @name         mTLS POST Request Test
// @namespace    openziti.io
// @version      2024-06-30
// @description  fetch .data.token and use as zt-session header in subsequent requests
// @author       @qrkourier
// @match        https://ziti.ken.demo.openziti.org:1280/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=openziti.org
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Create a POST request
    const url = 'https://ziti.ken.demo.openziti.org:1280/edge/management/v1';

    fetch(url+'/authenticate?method=cert', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
    })
    .then(response => {
        // Print the full HTTP response
        console.log('HTTP Response:', response);

        // Check if the response is JSON and print it
        return response.json().then(jsonData => {
            if (jsonData && jsonData.data && jsonData.data.token) {
                // If .data.token is defined and not empty, use it in a subsequent request
                const token = jsonData.data.token;
                return fetch(url+'/current-identity', {
                    method: 'GET',
                    headers: {
                        'Content-Type': 'application/json',
                        'zt-session': token
                    },
                })
                .then(response => {
                    // Print the full HTTP response
                    console.log('Subsequent HTTP Response:', response);
                    return response.json();
                })
                .then(jsonData => {
                    console.log('Subsequent Response JSON:', jsonData);
                    return jsonData;
                })
                .catch(error => {
                    console.error('Subsequent Response Error:', error);
                });
            } else {
                console.error('No token found in response');
            }
        }).catch(error => {
            // If not JSON, print the text response
            return response.text().then(textData => {
                console.log('Response Text:', textData);
                return textData;
            });
        });
    })
    .catch((error) => {
        console.error('Error:', error);
    });
})();

Test procedure:

Get an admin identity token

ziti edge create identity kpop4 -A -o /tmp/kpop4.jwt

Enroll to obtain client cert

ziti edge enroll -o /tmp/kpop4.json <(<<< eyJhbGciOiJSUzI1NiIsImtpZCI6IjBlNDNmYWNhNGI1ODNlMTc4N2NjNjYwZDdjNzRkNDk2NmI0OWVkY2QiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3ppdGkua2VuLmRlbW8ub3BlbnppdGkub3JnOjEyODAiLCJzdWIiOiI1dUdyYm5wby0iLCJhdWQiOlsiIl0sImV4cCI6MTcxOTc2MjU1NiwianRpIjoiYjFmZjU4YzktNzU2Zi00YzZhLWI1ZTktZTZhNjlmNmRlN2I3IiwiZW0iOiJvdHQiLCJjdHJscyI6bnVsbH0.i9PeNhjGrHA0WxFJ5_uREXYibOUV5ydLlv3fn5GMHbAs0Svw8--uWrVvVBGSPs0ia8hekE-TXEk6IXywxnD1plwQWkfBBs4probJzRoPmD7S3XkMt5Zp6QUML83X7mkSbCDr97QHR-SdqYZfKQy3RLQiLeyamOF-10WtVsas_xgH0WLJB43FBtBDvcWKw5GTeVRY5KMI0l0AA0IEd9aiv_42YQnPjvKMFcxfqHhNU_DFO9DSqed_6R9yxPOUGXaqWmJVqLYLHZ1wBYhzZuZRzvzxBITl9dxsTJ8Rzd6qoKgX_SWgxnxaodjpEML-h0aLPlQhlUD6s1VymwS7mwqnsM4UH8IQYdBIKucyBvrYTeeKY9uIQ2Loov78rD61m2KpRsBb3cdir9m9rUi44Z9SkZt2O-zEKKjb-0EAoQDVKK9p1LOJvyQVr3Pwxw0Vl3PL7rSI8d6rbsRIDbkzQDM5C9Y_ipOSx2Uy4eFFrZS5QTPbW3O1xLs6SVOv3qduJbDR7suMFX_0hW3YNewK50sfx-SZ5KEUNpmxTDxOojQ1ga3BgGfBuDdtCDR5MOMdJs1vazqYTHubC_H48b-Zy300yjGwMnEtWN_XCD0rPGqX_lPZvOQ4cpfbmM64bhz65smzEK30A5xOldldRuMAzTWsJQ_zYfZWlu1UYRePHeukYKQ)

Unwrap the JSON identity file into separate cert, key, etc.

ziti ops unwrap /tmp/kpop4.json

Fix filemodes because unwrap did not obey the umask

chmod u+rw /tmp/kpop4.*

Compose a keystore for import

openssl pkcs12 -export -in /tmp/kpop4.cert -inkey /tmp/kpop4.key -out /tmp/kpop4.p12 -name "kpop4"

In Chrome security settings > certs > my certs > import

Then visit ZAC with userscript @match of the mgmt API base URL