Juju CLI login flow

This is documenting what happens when a user does juju login -u admin at a point when no user is logged in. It has been done by observing what messages are exchanged between client and controller in the websocket connection and what http requests the client makes to the controller.

In the following the controller host is CONTROLLER_HOST and the admin password is PASSWORD.

Read this document for an easy intro to macaroons: https://github.com/rescrv/libmacaroons/blob/master/README. For a more theoretical approach you can read this: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/41892.pdf.

Overview

Login with just username and no creds:

  • triggers macaroon discharge workflow to ultimately, all going well, get a macaroon with a TTL of 24 hours and username caveat, stored in client cookie jar

  • first step is for client to prompt for password and redirect to trusted 3rd party identity provider to validate identity

  • results in short lived “login macaroon” which proves the user has logged in

  • controller then mints the “real” macaroon to return to the client with declared username caveat and op set to login

  • client logs in again with this newly minted macaroon and auth passes and login succeeds

1. Get websocket connection

Open a websocket connection to wss://CONTROLLER_HOST:17070/api

2. In websocket: first Login request (fails)

The first thing that happens in the websocket is an attempt to call the Admin.Login endpoint.

Request:

{
    "request-id": 1,
    "type": "Admin",
    "version": 3,
    "request": "Login",
    "params": {
        "auth-tag": "user-admin",
        "credentials": "",
        "nonce": "",
        "macaroons": null,
        "bakery-version": 3,
        "cli-args": "[...]",
        "user-data": "",
        "client-version": "2.9.26"
    }
}

Because there are no credentials or macaroons in the request, the response contains a macaroon with a third party caveat that needs to be discharged to authenticate the user. This macaroon is made in apiserver/authentication/user.go, UserAuthenticator.authenticateMacaroons().

From looking at that code, The condition of the 3rd party caveat is need-declared username admin

Q: I guess it’s encoded in the “v64” value below?

Response:

{
    "request-id": 1,
    "response": {
        "discharge-required": {
            "c": [
                {
                    "i": "time-before 2022-03-10T09:26:13.554951585Z"
                },
                {
                    "i64": "AwA",
                    "v64": "84lQ1w2X1y5_pzw6J43_UgGHkExE_T97jn95tTHEZAgaJW3IgRuNI7Yk0qpLM11_kPQ497PxW9yiv6sShpvgUYg7vgPRHi0r",
                    "l": "https://CONTROLLER_HOST:17070/auth"
                }
            ],
            "l": "juju model 10c91043-b22b-4e62-8f44-30132106b057",
            "i64": "AwoQOJOvmtzn4H9e3RXl7OzfUhIgOTQzM2Q1MmFlNWY3ZjdmN2U2NzdhZjU0YzllMTcwYTkaDgoFbG9naW4SBWxvZ2lu",
            "s64": "jAUuaROGZqMCjIoD-BSow9GCMlxELWNjYGArgqeLK0Q"
        },
        "bakery-discharge-required": {
            "m": {
                "c": [
                    {
                        "i": "time-before 2022-03-10T09:26:13.554951585Z"
                    },
                    {
                        "i64": "AwA",
                        "v64": "84lQ1w2X1y5_pzw6J43_UgGHkExE_T97jn95tTHEZAgaJW3IgRuNI7Yk0qpLM11_kPQ497PxW9yiv6sShpvgUYg7vgPRHi0r",
                        "l": "https://CONTROLLER_HOST:17070/auth"
                    }
                ],
                "l": "juju model 10c91043-b22b-4e62-8f44-30132106b057",
                "i64": "AwoQOJOvmtzn4H9e3RXl7OzfUhIgOTQzM2Q1MmFlNWY3ZjdmN2U2NzdhZjU0YzllMTcwYTkaDgoFbG9naW4SBWxvZ2lu",
                "s64": "jAUuaROGZqMCjIoD-BSow9GCMlxELWNjYGArgqeLK0Q"
            },
            "v": 3,
            "cdata": {
                "AwA": "A6Ve5aR-tWB-Q0M7ziC2TW_oC9RxW0PwIjQI_eVUhslCKaXcdTmTr5rc5-B_Xt0V5ezs31IttCIUNp99pEQKvgrQDDBRGbJ1SXRGp9WOt8DJpr9dZRfIp_nIG4S0LSzrLnF0TiEy4jZvFcXucApDF7GgPULqnwVX7X_l630n-ZCsTcdZP1W2vnJKsUc_po0jYemzyUBDVckiJz1hDgo"
            },
            "ns": "std:"
        },
        "discharge-required-error": "invalid login macaroon"
    }
}

3. Discharging the third party caveat

The client must now try to discharge the caveat, which will prove that the user is who they claim to be. This is done by contacting an auth server (in this case implemented on the juju controller) as follows.

This flow is implemented in github.com/go-macaroon-bakery/macaroon-bakery/httpbakery, in the Client.AcquireDischarge method.

Q: Is this the macaroon/Candid flow?

3.1. HTTP: First attempt to discharge macaroon (fails)

POST https://CONTROLLER_HOST:17070/auth/discharge

Query params:

caveat64=A6Ve5aR-tWB-Q0M7ziC2TW_oC9RxW0PwIjQI_eVUhslCKaXcdTmTr5rc5-B_Xt0V5ezs31IttCIUNp99pEQKvgrQDDBRGbJ1SXRGp9WOt8DJpr9dZRfIp_nIG4S0LSzrLnF0TiEy4jZvFcXucApDF7GgPULqnwVX7X_l630n-ZCsTcdZP1W2vnJKsUc_po0jYemzyUBDVckiJz1hDgo

id64=AwA

Q: even though it is a POST request, query params are used instead of form data in the body. Why is that?

Response:

{
    "Code": "interaction required",
    "Message": "cannot discharge: interaction required",
    "Info": {
        "InteractionMethods": {
            "juju_userpass": {
                "url": "/auth/form"
            }
        },
        "VisitURL": "/auth/login?waitid=fef72f1d163d4f6d2eb01a49",
        "WaitURL": "/auth/wait?waitid=fef72f1d163d4f6d2eb01a49"
    }
}

3.2. HTTP: Obtain a token to discharge macaroon

The client chooses the “juju_userpass” interaction method, asks the user for their password on CLI and then verifies the credentials.

POST https://CONTROLLER_HOST:17070/auth/form

Body:

{
    "form": {
        "password": "PASSWORD",
        "user": "admin"
    }
}

Response:

{
    "token": {
        "kind": "juju_userpass",
        "value": "MDVhZjU1NDM3NjYxZGE5YTJkMGMxMDky"
    }
}

3.3. HTTP: Second attempt to discharge macaroon (succeeds)

Now that the username/password has been verified, we have everything we need to get the discharge macaroon.

POST https://CONTROLLER_HOST:17070/auth/discharge

Query params:

caveat64=A6Ve5aR-tWB-Q0M7ziC2TW_oC9RxW0PwIjQI_eVUhslCKaXcdTmTr5rc5-B_Xt0V5ezs31IttCIUNp99pEQKvgrQDDBRGbJ1SXRGp9WOt8DJpr9dZRfIp_nIG4S0LSzrLnF0TiEy4jZvFcXucApDF7GgPULqnwVX7X_l630n-ZCsTcdZP1W2vnJKsUc_po0jYemzyUBDVckiJz1hDgo

id64=AwA

token=05af55437661da9a2d0c1092

token-kind=juju_userpass

Note that the value of token in the query params is the base64 decoded value of the value of the “value” field in the previous step’s json response.

Response:

{
    "Macaroon": {
        "m": {
            "c": [
                {
                    "i": "declared username admin"
                },
                {
                    "i": "time-before 2022-03-10T09:26:18.497625359Z"
                }
            ],
            "i64": "AwA",
            "s64": "Jcdpeh0Nmx8f85wWyVF3w7gB1RzYNuWoNyLiCGRe5Q0"
        },
        "v": 3,
        "ns": "std:"
    }
}

Now we have a macaroon that “discharges” the third party caveat. It just needs to be combined with the original macaroon to make a macaroon slice that allows authentication.

4. Websocket: Second call to Login (succeeds)

Request:

{
    "request-id": 2,
    "type": "Admin",
    "version": 3,
    "request": "Login",
    "params": {
        "auth-tag": "user-admin",
        "credentials": "",
        "nonce": "",
        "macaroons": [
            [
                {
                    "c": [
                        {
                            "i": "time-before 2022-03-10T09:26:13.554951585Z"
                        },
                        {
                            "i64": "AwA",
                            "v64": "84lQ1w2X1y5_pzw6J43_UgGHkExE_T97jn95tTHEZAgaJW3IgRuNI7Yk0qpLM11_kPQ497PxW9yiv6sShpvgUYg7vgPRHi0r",
                            "l": "https://CONTROLLER_HOST:17070/auth"
                        }
                    ],
                    "l": "juju model 10c91043-b22b-4e62-8f44-30132106b057",
                    "i64": "AwoQOJOvmtzn4H9e3RXl7OzfUhIgOTQzM2Q1MmFlNWY3ZjdmN2U2NzdhZjU0YzllMTcwYTkaDgoFbG9naW4SBWxvZ2lu",
                    "s64": "jAUuaROGZqMCjIoD-BSow9GCMlxELWNjYGArgqeLK0Q"
                },
                {
                    "c": [
                        {
                            "i": "declared username admin"
                        },
                        {
                            "i": "time-before 2022-03-10T09:26:18.497625359Z"
                        }
                    ],
                    "i64": "AwA",
                    "s64": "AZiBA_Qhxevs54KGBgGQz59jDzMIqtCtg31Rz-v0Vcs"
                }
            ]
        ],
        "bakery-version": 3,
        "cli-args": "[...]",
        "user-data": "",
        "client-version": "2.9.26"
    }
}

Success!

Response:

{
    "request-id": 2,
    "response": {
        "servers": [
            [
                {
                    "value": "CONTROLLER_HOST",
                    "type": "ipv4",
                    "scope": "local-cloud",
                    "port": 17070
                },
                {
                    "value": "127.0.0.1",
                    "type": "ipv4",
                    "scope": "local-machine",
                    "port": 17070
                },
                {
                    "value": "::1",
                    "type": "ipv6",
                    "scope": "local-machine",
                    "port": 17070
                }
            ]
        ],
        "controller-tag": "controller-6d7d0374-6ef2-422c-8216-1354eacc11d4",
        "user-info": {
            "display-name": "",
            "identity": "user-admin",
            "controller-access": "superuser",
            "model-access": ""
        },
        "facades": [...],
        "server-version": "2.9.25.1"
    }
}

Done!