oauth2 is a small, self-contained OAuth 2.0 client for Tcl. It implements the OAuth 2.0 Authorization Code grant (RFC 6749) with optional PKCE (RFC 7636), in pure Tcl, depending only on http and tls. It opens the browser, catches the redirect on a transient local socket, exchanges the code for tokens, persists them to a private (0600) JSON file, refreshes expired access tokens transparently, and gives you a one-liner for making authenticated API calls.
The source, a manpage and runnable examples live at johnbuckman/tcl_oauth2_library (Tcl/Tk license). It is in real production use against Intuit / QuickBooks Online and Basecamp.
Every useful web API now speaks OAuth 2.0 — Google, Microsoft, QuickBooks, GitHub, Basecamp, Twitter/X. Tcl had snippets, but no clean, reusable library, and copy-pasting the bearer-token / refresh / redirect dance into every script is a bug farm.
This one grew out of a real production problem. We had been driving OAuth2 from a native (C++) library that tended to core dump, and whose local callback listener would occasionally crash and block the redirect port. Worst of all, our QuickBooks Online tokens kept silently going invalid despite an hourly automatic refresh, and we could never work out why. The pure-Tcl rewrite simply does not have these problems: tokens stay alive and refresh cleanly days later.
It is deliberately provider-agnostic. Differences between providers are expressed as configuration, not code — so the same code path drives Basecamp, QuickBooks Online and Twitter/X even though each bends the spec in its own way.
The library drives the classic three-legged dance for you:
Thereafter oauth2::token hands back a valid access token, refreshing it behind the scenes when it is within 60 seconds of expiry, and oauth2::request attaches it as a Bearer header.
One handle per provider connection. The first oauth2::login opens the browser and saves the tokens; every run after that just reuses (and silently refreshes) them.
package require oauth2
set c [oauth2::new \
-auth_url https://provider.example/authorize \
-token_url https://provider.example/token \
-client_id $env(CLIENT_ID) \
-client_secret $env(CLIENT_SECRET) \
-redirect_uri http://localhost:9876/callback \
-scope "read write" \
-pkce S256 \
-auth_extra {response_type code} \
-token_exchange_extra {grant_type authorization_code} \
-token_refresh_extra {grant_type refresh_token} \
-token_file ~/.config/myapp/tokens.json]
oauth2::login $c ;# first run: browser opens, tokens saved
set body [oauth2::get $c https://api.example/v1/things]oauth2::get / oauth2::post / oauth2::request add the bearer token automatically, and on an HTTP 401 they refresh the token once and retry — so an expired key heals itself transparently.
A phone, desktop or CLI app can't hide a client secret — ship it and anyone can extract it. PKCE replaces the secret with a one-time proof: the app invents a random verifier, sends only its SHA-256 hash (the challenge) to start login, then presents the verifier to redeem the code. A stolen authorization code is then useless — only the app that started the login can finish it.
In this library it is a single option, -pkce S256 (or plain). The SHA-256 it needs is implemented in the package itself, so the dependency footprint stays at just http + tls — no Tcllib, no C extension. Twitter/X requires PKCE; it is recommended anywhere the provider supports it.
The interactive oauth2::login binds a loopback port to catch the redirect, so it suits desktop and command-line use. On a headless server, drive the two pieces yourself: oauth2::authorize_url (builds the URL and stores a fresh state/CSRF token and PKCE verifier) and oauth2::exchange_code.
Here it is inside NaviServer — no loopback listener, no inbound port; the provider just redirects to a normal page.
The login page builds the URL and stashes state in a cookie:
# login.adp — send the user off to log in lassign [oauth2::authorize_url $c] url state ns_setcookie -samesite lax -path / oauth_state $state ns_adp_puts "<a href=$url>Log in</a>"
The callback page validates state, exchanges the code, and reports success:
# oauth_redir.adp — the callback
set code [ns_queryget code]
set state [ns_queryget state]
set ck [ns_getcookie oauth_state ""]
if {$state ne $ck} { ;# CSRF guard
ns_return 400 text/plain "CSRF"; return
}
oauth2::exchange_code $c $code ;# swaps code for tokens, saves them
ns_return 200 text/plain "Login successful!"The reason one code path serves everyone is that each provider's deviations from the spec are just options passed to oauth2::new:
Runnable programs for all three, each in a plain-Tcl and a small Tk variant, are in the examples/ directory.
On Windows, tls does not read the system certificate store, so set the OAUTH2_CAFILE environment variable to a downloaded bundle (e.g. cacert.pem).
See the manpage in the source for the full option and command reference.