Provider-agnostic Go library for CLIs that authenticate end-users via OAuth 2.0 device flow (RFC 8628), present resource-scoped bearer tokens to data APIs, and (when the auth host and data API live on different origins) exchange tokens via RFC 8693 STS.
No global state, no env-var reads, no implicit URLs. Every endpoint, identifier, and default value is supplied by the embedding CLI through a Config struct.
go get github.com/entireio/auth-go@latest
| Package | What it does |
|---|---|
deviceflow |
RFC 8628 OAuth 2.0 Device Authorization Grant client. Polls the token endpoint, surfaces RFC 8628 §3.5 error codes (authorization_pending, slow_down, access_denied, expired_token, invalid_grant) as Go sentinels with optional error_description. |
sts |
RFC 8693 OAuth 2.0 Token Exchange client. Provider-agnostic — caller supplies endpoint path, subject_token_type, requested_token_type, optional audience / resource / scope, and any provider-specific Extra form fields (e.g. client_id). |
tokens |
TokenSet value type plus unverified JWT claim parsing. Rejects alg:none (RFC 7515 / RFC 7518 §3.6 known attack vector). The package never validates signatures — that's the issuing server's responsibility. Callers use Claims for routing decisions (which issuer, which audience) and UX (display the principal handle), not as a security boundary. |
tokenstore |
Store interface for token persistence + Keyring reference impl backed by github.com/zalando/go-keyring. Each CLI passes its own service name so credentials are isolated across CLIs sharing this library. Returns ErrNotFound for unknown profiles and ErrMalformed (wrapped) when a stored entry exists but can't be decoded — used by upgrade fallbacks. |
tokenmanager |
Orchestration: stores the device-flow core token, runs RFC 8693 exchanges when needed to obtain resource-scoped bearers, caches the results until expiry, and short-circuits when no exchange is needed (same-host or core-token's aud already covers the resource). Most CLIs only need to interact with this package directly. |
The internal/oauthhttp package holds shared HTTP body-reading + JSON-decoding helpers (detects HTML responses from captive portals / proxy intercepts and surfaces them as actionable errors instead of unmarshal failures). It is unexported in the Go sense — not importable by other modules — and not part of the public API surface.
Defense-in-depth checks layered on top of server-side validation:
- HTTPS required. Both
sts.Clientanddeviceflow.Clientrejecthttp://BaseURLs unlessAllowInsecureHTTPis set. Callers typically opt that in only for loopback (localhost/127.0.0.1/::1) so production misconfigurations fail loudly. alg:noneJWTs rejected.tokens.ParseClaimsdecodes the JWT header and refuses the unsigned shape (any case variant ofnone). Even though claim use is routing-only, this keeps an obvious attack surface closed.verification_urivalidated. The device-code response field is what your CLI echoes and opens in the user's browser — a malicious AS pointing it at a phishing page would be a credential-harvesting vector. The library rejects non-https (loopback http excepted), embeddeduser:pass@hostuserinfo, and control characters in the URI.
import (
"github.com/entireio/auth-go/deviceflow"
"github.com/entireio/auth-go/tokenmanager"
"github.com/entireio/auth-go/tokenstore"
)
const (
issuer = "https://auth.example.com" // auth host base URL
clientID = "my-cli" // public OAuth client_id
)
store := tokenstore.NewKeyring("my-cli") // service name = your CLI's name
// One Manager per CLI process. Construct from your CLI's identity.
mgr, err := tokenmanager.New(tokenmanager.Config{
Issuer: issuer,
ClientID: clientID,
STSPath: "/oauth/token", // RFC 8693 endpoint; usually the OAuth token endpoint
Store: store,
Scope: "cli",
})
if err != nil { /* misconfiguration */ }dfc := &deviceflow.Client{
BaseURL: issuer,
ClientID: clientID,
Scope: "cli",
DeviceCodePath: "/oauth/device/code",
TokenPath: "/oauth/token",
}
dc, err := dfc.StartDeviceAuth(ctx)
// ... show dc.UserCode + dc.VerificationURI to user, then poll ...
ts, err := dfc.PollDeviceAuth(ctx, dc.DeviceCode)
if err != nil { /* surface RFC 8628 §3.5 sentinel as needed */ }
if err := mgr.SaveCoreToken(ts.AccessToken); err != nil { /* keyring failed */ }bearer, err := mgr.TokenForResource(ctx, "https://api.example.com")
if errors.Is(err, tokenmanager.ErrNotLoggedIn) {
// prompt user to run `mycli login`
}
// bearer is valid for https://api.example.com
req.Header.Set("Authorization", "Bearer "+bearer)The manager picks the right strategy automatically:
- Same-host (
Issuer == resource): hands back the core token verbatim. - JWT-
aud-includes shortcut: same, when the core token's audience already covers the resource (e.g. multi-audience tokens). - Otherwise: runs an RFC 8693 exchange against
Issuer + STSPath, caches the exchanged token by(core, resource, audience, requested_token_type, scope)until expiry.
if err := mgr.DeleteCoreToken(); err != nil { /* keyring failed */ }Deletes the keyring entry first; only clears the in-memory exchange cache on success, so a failed delete doesn't leave the CLI thinking it's logged out while the keyring still holds the token.
- No globals, no env-var reads, no implicit URLs. Everything ships through
Config. The library should compile and run identically inside any CLI. - Provider-agnostic.
deviceflow.Clientandsts.Clientare field-bag structs; neither knows about your provider's endpoint paths or token-type URIs. Pass them in. - Bearer-presenter, not bearer-validator. This library is for CLIs that receive tokens from an auth server and present them to a resource server. JWT signature verification is intentionally not done — the resource server validates.
tokens.ParseClaimsis documented as unverified and used only for routing decisions. - Per-CLI keyring isolation. Each CLI passes a unique service name to
tokenstore.NewKeyring. OS keyrings key by(service, account), so consumers naturally get separate credential stores. - Caller controls the wire shape. Default values (RFC 8693
requested_token_type,scope, audience-empty) live in the embedding CLI's wiring, not in this library.
- Pick a stable service name for
tokenstore.NewKeyring(...). Don't change it later — renaming orphans every existing user's stored credentials. - Pick a
client_idthat the auth server recognises. - Decide your
STSPath: typically the OAuth token endpoint per RFC 8693 convention, or a dedicated path if your auth server exposes one. - Construct the
tokenmanager.Manageronce at startup; pass it to your data-API call sites. - For multi-environment users (regions, staging), key the keyring by issuer URL —
Manager.Issuer()returns the configured value.
- OIDC discovery / ID tokens. This library is OAuth 2.0 only. If you need OIDC
/.well-known/openid-configuration+ ID-token verification, layercoreos/go-oidcon top. - PKCE / authorization code flow. Device flow only; CLIs almost never need code flow.
- Server-side OIDC. If you're building an issuer, look at
zitadel/oidc'soppackage.
Used in production by entireio/cli. Issues and PRs welcome.
MIT — see LICENSE.