auth

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2026 License: MIT Imports: 0 Imported by: 0

README

auth-go — shareable OAuth 2.0 client library for CLIs

Tests Lint Go Reference

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

Subpackages

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.

Security

Defense-in-depth checks layered on top of server-side validation:

  • HTTPS required. Both sts.Client and deviceflow.Client reject http:// BaseURLs unless AllowInsecureHTTP is set, and that opt-in is still limited to loopback (localhost / 127.0.0.1 / ::1) so production misconfigurations fail loudly.
  • alg:none JWTs rejected. tokens.ParseClaims decodes the JWT header and refuses the unsigned shape (any case variant of none). Even though claim use is routing-only, this keeps an obvious attack surface closed.
  • verification_uri validated. 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), embedded user:pass@host userinfo, and control characters in the URI.
  • Resource origins validated. tokenmanager.Token requires TokenRequest.Resource to be an origin URL: absolute scheme + host, HTTPS unless loopback HTTP is explicitly enabled, and no userinfo/path/query/fragment. This prevents cache fragmentation and accidental STS requests for surprising resource strings.
  • OAuth responses are bounded and strict. Success and error bodies are capped at MaxResponseBytes; oversized responses are rejected, HTML/captive-portal responses get actionable errors, and JSON success bodies with trailing data are refused.
  • STS wire shape is explicit. tokenmanager defaults subject_token_type to the RFC 8693 access-token URN and exposes Config.SubjectTokenType for STS endpoints that require the structural JWT URN.

Quick start

import (
    "github.com/entireio/auth-go/deviceflow"
    "github.com/entireio/auth-go/sts"
    "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",
    // Optional: defaults to sts.SubjectTokenTypeAccessToken. If your STS
    // requires the structural JWT URN instead, uncomment:
    // SubjectTokenType: sts.SubjectTokenTypeJWT,
})
if err != nil { /* misconfiguration */ }
Login
dfc, err := deviceflow.New(&deviceflow.Client{
    BaseURL:        issuer,
    ClientID:       clientID,
    Scope:          "cli",
    DeviceCodePath: "/oauth/device/code",
    TokenPath:      "/oauth/token",
})
if err != nil { /* required field missing */ }

dc, err := dfc.StartDeviceAuth(ctx)
// show dc.UserCode + dc.VerificationURI to user, then drive the poll loop:
ts, err := dfc.PollUntil(ctx, dc)
if err != nil { /* surface RFC 8628 §3.5 sentinel as needed */ }
if ts == nil { /* defensive: PollUntil's contract is non-nil on nil err */ }

if err := mgr.SaveCoreToken(*ts); err != nil { /* keyring failed */ }

deviceflow.New (and sts.New) validate required fields at construction time rather than at the first request — misconfiguration becomes a startup error rather than a confusing AS-side rejection mid-flow. Field-bag &deviceflow.Client{...} construction still works for callers that prefer it.

SaveCoreToken takes the full tokens.TokenSet so RefreshToken, absolute ExpiresAt, and Scope survive the round-trip through the keyring — earlier versions silently dropped these.

PollUntil is the helper most embedders want. It honours dc.Interval, applies the RFC 8628 §3.5 +5s bump on slow_down, stops at the dc.ExpiresIn ceiling, and returns terminal sentinels (ErrAccessDenied, ErrExpiredToken, ErrInvalidGrant) unwrapped so callers can errors.Is. Use PollDeviceAuth directly only when you need to render per-tick state in your own UI.

Calling a data API
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.

By default, tokenmanager sends subject_token_type=urn:ietf:params:oauth:token-type:access_token for the stored core bearer. This is the role-based RFC 8693 token type and works for STS endpoints that treat the device-flow result as an OAuth access token even when its wire format is JWT. If your STS requires the structural JWT token type, configure SubjectTokenType: sts.SubjectTokenTypeJWT.

Logout
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.

Design principles

  • 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.Client and sts.Client are 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.ParseClaims is 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 for RFC 8693 fields are explicit and overridable. tokenmanager defaults requested_token_type to the standard access-token URN and subject_token_type to access_token; callers can set RequestedTokenType, SubjectTokenType, scope, audience, resource, and package-specific Extra fields as needed.

Embedding checklist

  1. Pick a stable service name for tokenstore.NewKeyring(...). Don't change it later — renaming orphans every existing user's stored credentials.
  2. Pick a client_id that the auth server recognises.
  3. Decide your STSPath: typically the OAuth token endpoint per RFC 8693 convention, or a dedicated path if your auth server exposes one.
  4. Decide whether the STS expects subject_token_type=access_token (the tokenmanager default) or jwt (SubjectTokenType: sts.SubjectTokenTypeJWT).
  5. Construct the tokenmanager.Manager once at startup; pass it to your data-API call sites.
  6. For multi-environment users (regions, staging), key the keyring by issuer URL — Manager.Issuer() returns the configured value.

Security

The library is the client side of OAuth 2.0 device flow + RFC 8693 token exchange. It receives bearers from an authorization server and presents them to data APIs. The threat model is:

  • The auth server is trusted. This library does not verify JWT signatures — that's the data API's job. tokens.ParseClaims is documented as unverified and is used for routing decisions only.
  • The data API is trusted. The library hands it a bearer; what the API does with it is out of scope.
  • The transport is trusted only when TLS-protected. Both deviceflow.Client and sts.Client reject http:// BaseURLs unless AllowInsecureHTTP is set, and even then only loopback hosts are accepted. The Transport field is for observability and proxies, not for disabling TLS verification.
  • OS keyrings are trusted within the user's session. The tokenstore.Keyring impl keys credentials by (service, account); pick a stable, unique service name per CLI and treat anything the keyring returns as untrusted bytes (the impl already rejects JSON-shaped junk and empty access tokens).

Defenses in depth that the library applies regardless:

  • Rejects alg:none JWTs and any alg containing non-alphanumeric characters (closes whitespace / zero-width-space bypass attempts).
  • Rejects absolute Path / DeviceCodePath / TokenPath values (defeats redirect-via-config attacks against url.ResolveReference).
  • Validates TokenRequest.Resource as an origin URL and normalises it for same-host / JWT-audience shortcut comparisons and cache keys.
  • Normalises tokenmanager.Config.Issuer so cosmetic differences (trailing slash, host case, default port) can't split keyring state across two effective issuers.
  • Sanitises server-supplied error_description and non-JSON error body text (strips control chars, caps length) before wrapping into Go errors — so a hostile AS can't paint terminals or balloon logs.
  • Rejects oversized OAuth responses and JSON success responses with trailing data after the first JSON value.
  • Defaults STS subject_token_type to access_token, with Config.SubjectTokenType available when an STS expects jwt.
  • Refuses to cache exchanged tokens with non-positive expires_in (forces a fresh exchange instead of treating unknown-lifetime bearers as "valid forever").
  • Caps cache entries' lifetime at 1h when ExpiresAt is unset.

If you find a security issue, please email the maintainers privately rather than opening a public issue — coordinated disclosure gives a window to ship a fix before the report becomes searchable.

Non-goals

  • OIDC discovery / ID tokens. This library is OAuth 2.0 only. If you need OIDC /.well-known/openid-configuration + ID-token verification, layer coreos/go-oidc on 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's op package.

Status

Used in production by entireio/cli. Issues and PRs welcome.

Releasing

Every user-visible change must land with a CHANGELOG.md entry in the same PR, under the top ## Unreleased heading. Tagging a release is then a two-step move:

  1. Rename ## Unreleased to ## vX.Y.Z — YYYY-MM-DD, commit, and merge through a regular PR.
  2. Tag that merge commit with git tag -a vX.Y.Z -m "..." and git push --tags.

Each ## Unreleased bullet must describe behaviour that changed since the last tag — not since some earlier point. When a CHANGELOG entry survives a release without being moved under a version header, the next release inherits stale claims (e.g. v0.3.4's draft initially still listed v0.3.2's :access_token default as "new").

Version bumps follow 0.x SemVer with one local convention: while pre-1.0, breaking changes may ship as patch bumps when the surface is narrow and well-documented (v0.3.2 changed the default subject_token_type as a patch).

License

MIT — see LICENSE.

Documentation

Overview

Package auth is the umbrella for github.com/entireio/auth-go, a shareable OAuth 2.0 client library for CLIs.

All real code lives in the subpackages:

  • deviceflow — RFC 8628 OAuth 2.0 Device Authorization Grant client
  • sts — RFC 8693 Token Exchange client
  • refresh — RFC 6749 §6 refresh_token grant client
  • tokens — TokenSet plus unverified JWT claim parsing
  • tokenstore — pluggable persistence interface with reference impls
  • tokenmanager — orchestrates core-token storage + STS exchanges, with caching and a JWT-audience shortcut

The library is designed to talk RFC 8628 and RFC 8693 to any compliant OAuth 2.0 server. It contains no provider-specific behaviour; endpoint paths, client IDs, and token-type URIs are caller-supplied. Anything a caller learns about the server beyond what the server tells it in a public HTTP response is out of scope for this package.

Directories

Path Synopsis
Package deviceflow is an RFC 8628 OAuth 2.0 Device Authorization Grant client.
Package deviceflow is an RFC 8628 OAuth 2.0 Device Authorization Grant client.
internal
oauthhttp
Package oauthhttp holds shared HTTP-response helpers used by the auth subpackages.
Package oauthhttp holds shared HTTP-response helpers used by the auth subpackages.
proclock
Package proclock is a cross-process advisory file lock used to single-flight credential refreshes across cooperating processes.
Package proclock is a cross-process advisory file lock used to single-flight credential refreshes across cooperating processes.
Package refresh is an RFC 6749 §6 OAuth 2.0 refresh_token grant client.
Package refresh is an RFC 6749 §6 OAuth 2.0 refresh_token grant client.
Package sts is an RFC 8693 OAuth 2.0 Token Exchange client.
Package sts is an RFC 8693 OAuth 2.0 Token Exchange client.
Package tokenmanager orchestrates core-token storage and RFC 8693 token exchanges for an OAuth 2.0 device-flow client.
Package tokenmanager orchestrates core-token storage and RFC 8693 token exchanges for an OAuth 2.0 device-flow client.
Package tokens defines the post-protocol token shape and helpers for reading unverified claims out of JWT access tokens.
Package tokens defines the post-protocol token shape and helpers for reading unverified claims out of JWT access tokens.
Package tokenstore is the persistence interface for tokens, plus the Keyring reference implementation.
Package tokenstore is the persistence interface for tokens, plus the Keyring reference implementation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL