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
|