Skip to content

woodglue.cli_login

wgl login and wgl logout.

wgl login and wgl logout: Personal Access Tokens for remote servers.

login gets a token the way gh auth login does:

  • It listens on a random port on 127.0.0.1 and opens the browser at the server's /auth/cli page (signing in first if needed), passing that loopback address and a PKCE challenge.
  • After the person approves, the server redirects the browser to the loopback address with a one-time code.
  • The CLI exchanges the code and its PKCE verifier for the token at /auth/cli/token, so the token never travels in a URL.

Tokens are stored per server URL in credentials.json under $WOODGLUE_CONFIG_DIR (default ~/.config/woodglue), readable only by you. woodglue.client.WoodglueClient uses a stored token when given no other. logout revokes the token on the server and forgets it.

normalize_server(server)

>>> normalize_server("https://wg.example.com/")
'https://wg.example.com'
Source code in src/woodglue/cli_login.py
def normalize_server(server: str) -> str:
    """
    >>> normalize_server("https://wg.example.com/")
    'https://wg.example.com'
    """
    return server.rstrip("/")

login(server, name='wgl', open_browser=_open_in_browser, timeout=LOGIN_TIMEOUT_SECONDS)

Get a Personal Access Token for server through the browser and store it. Raises RuntimeError if the browser does not come back in time or the server refuses.

Source code in src/woodglue/cli_login.py
def login(
    server: str,
    name: str = "wgl",
    open_browser: Callable[[str], None] = _open_in_browser,
    timeout: float = LOGIN_TIMEOUT_SECONDS,
) -> StoredToken:
    """
    Get a Personal Access Token for `server` through the browser and store it.
    Raises `RuntimeError` if the browser does not come back in time or the
    server refuses.
    """
    server = normalize_server(server)
    verifier = secrets.token_urlsafe(48)
    state = secrets.token_urlsafe(16)
    result = _Callback()
    listener = _listener(result)
    port = listener.server_address[1]
    thread = threading.Thread(target=listener.serve_forever, daemon=True)
    thread.start()
    try:
        query = urlencode(
            {
                "redirect_uri": f"http://127.0.0.1:{port}/callback",
                "state": state,
                "code_challenge": pkce_challenge(verifier),
                "name": name,
            }
        )
        open_browser(f"{server}/auth/cli?{query}")
        if not result.done.wait(timeout):
            raise RuntimeError("Timed out waiting for the browser to approve")
    finally:
        listener.shutdown()
        listener.server_close()
    params = result.params or {}
    if params.get("state") != state or "code" not in params:
        raise RuntimeError("The browser came back without a valid approval")
    request = urllib.request.Request(
        f"{server}/auth/cli/token",
        data=json.dumps({"code": params["code"], "code_verifier": verifier}).encode(),
        headers={"Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(request, timeout=30) as resp:
            issued = json.load(resp)
    except urllib.error.HTTPError as exc:
        raise RuntimeError(f"{server} refused the code: HTTP {exc.code}") from exc
    stored = StoredToken(token=issued["token"], token_id=issued["id"])
    credentials = load_credentials()
    credentials[server] = stored
    _save_credentials(credentials)
    return stored

logout(server)

Revoke the stored token for server and forget it. Returns whether there was one. The token is forgotten even if the server cannot be reached.

Source code in src/woodglue/cli_login.py
def logout(server: str) -> bool:
    """
    Revoke the stored token for `server` and forget it. Returns whether there
    was one. The token is forgotten even if the server cannot be reached.
    """
    server = normalize_server(server)
    credentials = load_credentials()
    stored = credentials.pop(server, None)
    if stored is None:
        return False
    request = urllib.request.Request(
        f"{server}/rpc",
        data=json.dumps(
            {
                "jsonrpc": "2.0",
                "method": "system.revoke_token",
                "params": {"token_id": stored.token_id},
                "id": 1,
            }
        ).encode(),
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {stored.token}"},
    )
    try:
        with urllib.request.urlopen(request, timeout=30):
            pass
    except OSError:
        pass
    _save_credentials(credentials)
    return True