Documentation
¶
Overview ¶
Package cognito implements the Cognito authorization-code-with-PKCE login flow a command-line tool uses to obtain a human operator's tokens.
It is built for CLIs rather than services: the flow opens a browser, receives the authorization code on a loopback listener, and hands back the token set for the caller to cache. A service verifying an incoming token wants github.com/wego/pkg/http/jwt instead.
Two deliberate constraints shape this package:
- It depends on nothing outside the standard library (bar Wego's string helpers). Hand-rolling the OAuth exchange keeps every wire parameter visible and auditable, which matters more here than the convenience an OAuth library would buy.
- It holds no package-level mutable state. Every dependency - the clock, the HTTP client, the browser opener - arrives through Config, so tests and callers never race over shared globals.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrInsecureEndpoint = errors.New("insecure cognito endpoint")
ErrInsecureEndpoint is returned for a configured URL or bind address that must not carry OAuth credentials.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// AuthorizeURL is the Cognito hosted-UI authorize endpoint.
AuthorizeURL string
// TokenURL is the Cognito token endpoint.
TokenURL string
// ClientID is the Cognito app client id.
ClientID string
// RedirectURI is the callback URL registered on the app client. Its path
// determines where the local callback server listens.
RedirectURI string
// Scopes is the space-separated scope list to request.
Scopes string
// AllowedDomain, when set, is the email suffix an operator must sign in
// with, e.g. "@wego.com".
AllowedDomain string
// CallbackAddr is the host:port the local callback server binds, e.g.
// "127.0.0.1:8110". It must agree with RedirectURI.
CallbackAddr string
// IdentityProvider, when set, is forwarded as identity_provider so Cognito
// jumps straight to that IdP instead of showing its own chooser.
IdentityProvider string
// OpenBrowser launches the authorize URL. Nil uses the OS default opener.
OpenBrowser func(string) error
// NoBrowser suppresses the browser launch. Login reports the authorize URL
// through PromptURL instead and then waits on the loopback listener exactly
// as it otherwise would, for a headless shell, a terminal on a remote host,
// or an operator who would rather open the URL themselves.
//
// The redirect still lands on CallbackAddr, so when the browser runs on a
// different machine than the CLI that port has to be reachable from it —
// usually `ssh -L <port>:localhost:<port>`. Suppressing the launch does not
// move where the code is delivered.
NoBrowser bool
// PromptURL receives the authorize URL in place of a browser launch, so the
// caller decides how to surface it: print it, render a QR code, hand it to
// another process. Required when NoBrowser or ReadRedirect is set — a
// sign-in whose URL the operator never sees cannot complete — and ignored
// otherwise.
PromptURL func(url string) error
// ReadRedirect turns the sign-in into a paste-back exchange: instead of a
// loopback listener receiving the redirect, the caller returns the URL the
// browser was redirected to and Login reads the code out of it.
//
// This is the only path that works with NO browser on this machine AND no
// way to reach its callback port. Cognito redirects to a loopback URL that
// nothing is listening on, the browser shows a connection error, and the
// address bar holds ?code=...&state=... for the operator to copy back.
//
// Cognito has no device authorization grant (RFC 8628): its discovery
// document advertises no device_authorization_endpoint, /oauth2/device_
// authorization is 404, and the token endpoint answers a device_code grant
// with unsupported_grant_type. So this is the substitute, and it needs no
// extra Cognito configuration because it is still the authorization code
// grant with PKCE, only with the redirect carried by hand.
//
// Setting it implies NoBrowser: no browser is launched here. PromptURL is
// required alongside it; CallbackAddr is not, since nothing binds a port.
// The pasted URL carries a single-use authorization code, so it should not
// travel through a shared channel; state is still checked, so a code from a
// different attempt is rejected.
//
// LIFETIME CONTRACT. Read this before wiring a reader to it.
//
// The ctx is the one passed to Login, and honouring it is the caller's
// responsibility. Login itself stops waiting when ctx is done, so a
// cancelled or timed-out Login always returns — but it CANNOT interrupt a
// read already blocked in this function. A reader that ignores ctx (a bare
// os.Stdin.Read, or bufio.Reader over it) therefore stays blocked after
// Login has returned, holding whatever it reads from.
//
// Two obligations follow:
//
// - On cancellation, tear the reader down yourself if you need the read to
// stop — close the file or pipe, or use a reader that selects on ctx.
// Nothing in this package can do it for you.
// - Do NOT start another Login while a previous invocation may still be
// blocked in here. Two live invocations reading the same stdin race for
// the operator's next line: either may win, so a retry can consume the
// paste meant for the other and the losing attempt sees a truncated or
// empty read. Wait for the previous reader to yield, or give the retry
// its own reader.
//
// The abandoned invocation is otherwise harmless: Login sends its result
// into a buffered channel and exits, so it parks rather than leaks and never
// blocks on a receiver that has gone.
ReadRedirect func(ctx context.Context) (redirectedURL string, err error)
// HTTPClient calls the token endpoint. Nil uses a client with a timeout.
HTTPClient *http.Client
// Now supplies the current time, for computing token expiry. Nil uses
// time.Now; tests inject a fixed clock.
Now func() time.Time
}
Config carries everything the login flow needs. Nothing is read from the environment or from package state, so a caller holds the whole contract.
type TokenSet ¶
type TokenSet struct {
AccessToken string `json:"access_token"` //nolint:gosec // G117: this type exists to carry tokens; stored only in the local keychain, never logged.
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"` //nolint:gosec // G117: as above - the refresh token is the credential this type is for.
ExpiresAt time.Time `json:"expires_at"`
}
TokenSet is the set of Cognito tokens a signed-in operator holds.
Holding tokens is the whole point of the type, so gosec's secret-field warning is expected here rather than a finding: these values are never logged, and the only place they are serialized to is the operator's own keychain (see github.com/wego/pkg/cognito/storage).
func Login ¶
Login runs the browser-based authorization-code-with-PKCE flow and returns the operator's tokens.
func (*TokenSet) Email ¶
Email returns the email claim from the id_token.
It reads the JWT payload WITHOUT verifying the signature, which is correct here: the token came from our own keychain, and every server that accepts it verifies the signature itself. Treat this as decoding for display and client-side UX, NOT as validation - a caller must never make an authorization decision on the strength of these claims.