Documentation
¶
Overview ¶
Package publicclient lets a public client, such as a command-line tool, sign a person in to SystemAuth and call SystemForge-powered APIs with DPoP-bound access tokens (RFC 9449).
A public client cannot keep a secret, so it signs in with the authorization code flow and PKCE on a loopback redirect (RFC 8252) and proves possession of a key instead. Nothing is registered ahead of time: Login generates a P-256 key, SystemAuth binds the tokens it issues to the key's thumbprint (cnf.jkt), and every API request a Session sends carries a fresh proof by the key, bound to the access token. The key matters only while tokens bound to it live; logging in again makes a new one.
The client must be registered at SystemAuth as a public client with the redirect URI http://127.0.0.1/callback, which SystemAuth matches on any port as RFC 8252 requires, and the authorization_code and refresh_token grants. An application marked first_party skips the consent screen, which suits a tool shipped to the deployment's own users; any other client asks the person to consent. Never grant a distributed client SystemAuth's allow_bearer_tokens exception: the binding is what makes a leaked token useless.
Index ¶
- Constants
- Variables
- type Config
- type Credentials
- type FileStore
- type Identity
- type Locker
- type Session
- func (s *Session) AccessToken(ctx context.Context) (string, error)
- func (s *Session) DPoPThumbprint() string
- func (s *Session) HTTPClient() *http.Client
- func (s *Session) Identity() Identity
- func (s *Session) Logout(ctx context.Context) (err error)
- func (s *Session) Transport(base http.RoundTripper) http.RoundTripper
- type Store
Constants ¶
const CallbackPath = "/callback"
CallbackPath is the path of the loopback redirect URI.
const RegisteredRedirectURI = "http://127.0.0.1" + CallbackPath
RegisteredRedirectURI is the redirect URI to register for the client at SystemAuth; the login listens on a free port, which SystemAuth accepts for a loopback URI.
Variables ¶
var ErrNotLoggedIn = errors.New("publicclient: not logged in")
ErrNotLoggedIn means the store holds no credentials: the person has not logged in, or has logged out.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Issuer is the SystemAuth issuer URL. Required.
Issuer string
// ClientID is the client's OAuth client ID at SystemAuth. Required.
ClientID string
// Scopes requested at login. Default: relyingparty.DefaultScopes,
// which include offline_access for a refresh token.
Scopes []string
// Store keeps the key and the tokens between runs. Required.
Store Store
// HTTPClient makes the requests to SystemAuth, and is the base of
// [Session.HTTPClient]. Default: http.DefaultClient.
HTTPClient *http.Client
// Metadata is SystemAuth's provider metadata, when already known; nil
// discovers it from Issuer.
Metadata *relyingparty.Metadata
// CallbackPort is the loopback port Login listens on. Default: a free
// port.
CallbackPort int
}
Config configures a public client.
type Credentials ¶
type Credentials struct {
// Issuer and ClientID name the SystemAuth deployment and the client
// the tokens were issued by and to.
Issuer string `json:"issuer"`
ClientID string `json:"client_id"`
// DPoPKey is the serialized key pair (dpop.KeyPair.SerializeJSON).
DPoPKey json.RawMessage `json:"dpop_key"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
Expiry time.Time `json:"expiry,omitzero"`
// Subject and Email identify the person, from the verified ID token.
Subject string `json:"subject"`
Email string `json:"email,omitempty"`
}
Credentials are what a public client keeps between runs: the DPoP key its tokens are bound to, and the tokens. The key is as sensitive as the refresh token: together they act for the person until the refresh token is revoked.
type FileStore ¶
type FileStore struct {
// Path is the credentials file, e.g. a file under
// os.UserConfigDir(). Required.
Path string
}
FileStore keeps credentials in one JSON file readable by its owner only (0600, in a 0700 directory), written atomically and durably. On Unix-like systems it refuses a file that others can read, as ssh does for private keys. Its lock is an operating-system lock on a file next to it (flock, LockFileEx), which the system releases when the holder exits.
func (FileStore) Load ¶
func (f FileStore) Load(_ context.Context) (*Credentials, error)
Load implements Store.
func (FileStore) Lock ¶
Lock implements Locker with an operating-system lock on a file next to Path. The lock file is never removed, so two processes always lock the same file, and the system releases the lock when its holder exits, so a process that dies holding it does not block the others.
func (FileStore) Save ¶
func (f FileStore) Save(_ context.Context, c *Credentials) (err error)
Save implements Store: it writes a temporary file next to Path, flushes it, renames it over Path and flushes the directory, so a reader never sees a partial file and a crash cannot bring back the previous one. That matters: the previous refresh token has already been redeemed, and presenting it again revokes the login.
type Locker ¶
type Locker interface {
// Lock waits for the lock and returns the function that releases it.
Lock(ctx context.Context) (unlock func() error, err error)
}
Locker is implemented by a Store that several processes share, such as a command-line tool and an MCP server it started. A Session holds the lock while it refreshes: SystemAuth rotates refresh tokens and treats a redeemed one presented again as stolen, revoking the whole family, so two processes must never redeem the same refresh token.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session holds a signed-in person's DPoP key and tokens. It is safe for concurrent use.
func Login ¶
func Login(ctx context.Context, cfg Config, open func(authURL string) error) (s *Session, err error)
Login signs a person in: it listens on a loopback port, calls open with the authorization URL (to open a browser, or print it for the person), waits for SystemAuth's redirect, exchanges the code for tokens bound to a new DPoP key, verifies the ID token, and saves the key and tokens in cfg.Store, replacing what was there.
func Open ¶
Open returns the session stored in cfg.Store, or ErrNotLoggedIn. It refuses credentials of another issuer or client.
func (*Session) AccessToken ¶
AccessToken returns an access token valid for at least a minute, refreshing it first when needed. Present it only with a DPoP proof by the session's key, as the Transport does.
func (*Session) DPoPThumbprint ¶
DPoPThumbprint returns the thumbprint of the key the session's tokens are bound to.
func (*Session) HTTPClient ¶
HTTPClient returns an http.Client whose requests the session authenticates (see Transport), based on cfg.HTTPClient.
func (*Session) Logout ¶
Logout revokes the refresh token at SystemAuth, which revokes its token family, and deletes the stored credentials. It holds the store's lock, so a refresh in another process cannot save credentials after they are deleted. The credentials are deleted even when the revocation fails; its error is returned.
func (*Session) Transport ¶
func (s *Session) Transport(base http.RoundTripper) http.RoundTripper
Transport returns an http.RoundTripper that authenticates each request as the session's person: Authorization: DPoP with a valid access token, and a DPoP proof by the session's key for the request's method and URL, bound to the token. It refreshes the token when it is about to expire. A request refused with 401 is retried once, when its body can be sent again (GetBody): with the server's nonce for use_dpop_nonce, or with a refreshed token for invalid_token. base nil is http.DefaultTransport.
Send these requests only to the APIs the person means to use: a server that receives them can present the token for as long as it lives, but cannot make new proofs, so it cannot use the token elsewhere.
type Store ¶
type Store interface {
// Load returns the stored credentials, or ErrNotLoggedIn.
Load(ctx context.Context) (*Credentials, error)
// Save replaces the stored credentials.
Save(ctx context.Context, c *Credentials) error
// Delete removes the stored credentials. Deleting none succeeds.
Delete(ctx context.Context) error
}
Store keeps a public client's Credentials between runs.