auth

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package auth mints the bearer tokens fft sends to fulfillmenttools.

fulfillmenttools authenticates nobody. Its swagger declares one security scheme — a bearer JWT — and contains no login endpoint at all. Sign-in happens out of band against Google Identity Platform (Firebase), and only the id token that comes back is ever shown to a tenant.

The API key is not a credential

The Firebase *Web* API key identifies the Firebase project and grants nothing by itself. It belongs on Google's two identity endpoints as ?key=, and nowhere else. Client therefore owns its own *http.Client whose transport refuses every host but those two, so the key is structurally incapable of reaching a fulfillmenttools tenant — a leak no amount of care at the call site could reliably prevent.

Two responses, two shapes

Sign-in answers in camelCase (idToken, refreshToken, expiresIn); refresh answers in snake_case (id_token, refresh_token, expires_in). Both are decoded by their own struct, because one struct for both unmarshals *cleanly* into an empty token — the failure would surface as a 401 on the next request rather than as a decode error here. In both shapes the expiry is a JSON string, not a number.

Index

Constants

View Source
const Leeway = 5 * time.Minute

Leeway is how much of an id token's life fft refuses to rely on. A token with less than this left is refreshed before it is used, so that a request cannot expire in flight.

Variables

View Source
var ErrReauthRequired = errors.New("re-authentication required")

ErrReauthRequired means fft has run out of ways to authenticate: the refresh token is dead and no stored password could replace it. The only cure is the user typing their password again.

Functions

func Refused

func Refused(err error) bool

Refused reports whether Google rejected the credential itself, as opposed to the network failing on the way there or Google failing behind it.

Only a refusal justifies burning the password on a fresh sign-in: a timeout must surface as a timeout, not as "your refresh token is dead".

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to Google Identity Platform on behalf of one Firebase project.

It owns its HTTP client rather than accepting one, and that client's transport refuses any host but Google's two identity endpoints. The point is not to distrust the caller: it is that the API key rides on every request as a query parameter, so the only way to guarantee it never reaches fulfillmenttools is to make a request there impossible to send.

func NewClient

func NewClient(apiKey string, opts ...Option) (*Client, error)

NewClient returns a Client for the Firebase project identified by apiKey.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context, refreshToken string) (Token, error)

Refresh renews an id token from a refresh token, without the password.

func (*Client) SignIn

func (c *Client) SignIn(ctx context.Context, email, password string) (Token, error)

SignIn exchanges an email and password for a token.

type Error

type Error struct {
	Status  int
	Code    string
	Message string
}

Error is Google Identity Platform refusing a credential.

Code is the machine-readable reason (TOKEN_EXPIRED, INVALID_LOGIN_CREDENTIALS, …) that the caller branches on; the message is the sentence the user reads.

func (*Error) Error

func (e *Error) Error() string

func (*Error) ExitCode

func (e *Error) ExitCode() int

ExitCode implements the interface exitcode.FromError looks for. A refusal is an authentication failure; a 5xx is Google having a bad day, which is a different thing for a script to react to.

type FirebaseTokenSource

type FirebaseTokenSource struct {
	// contains filtered or unexported fields
}

FirebaseTokenSource authenticates one project against Google Identity Platform, caching the id token in memory and in the credential store.

It refreshes with the stored refresh token when it can, and falls back to a full password sign-in when that token is dead — which it eventually always is.

func NewFirebaseTokenSource

func NewFirebaseTokenSource(client *Client, p config.Project, store secrets.Store, now func() time.Time) *FirebaseTokenSource

NewFirebaseTokenSource returns the TokenSource for project p. The password is read from store at the moment it is needed, rather than held in this struct for the lifetime of the process.

func (FirebaseTokenSource) Renew

func (c FirebaseTokenSource) Renew(ctx context.Context) (Token, error)

Renew implements Renewer: it mints a new token even though the cached one is still good. `fft auth refresh` is how a user proves the refresh path works against their tenant, so it must not be short-circuited by the cache.

func (FirebaseTokenSource) Token

func (c FirebaseTokenSource) Token(ctx context.Context) (string, error)

Token implements TokenSource. It refreshes proactively: a token with less than Leeway left is replaced now rather than expiring mid-request.

type Option

type Option func(*Client)

Option configures a Client.

func WithClock

func WithClock(now func() time.Time) Option

WithClock replaces the clock a token's expiry is computed against. Specs use it to make "this token has four minutes left" a fact rather than a race.

func WithDebug

func WithDebug(w io.Writer) Option

WithDebug logs the identity traffic to w, which is what --debug does. The request URL carries the API key and the request body carries the password, so the dump is redacted — see httplog.Redact.

type ReauthError

type ReauthError struct {
	Project string
	Err     error
}

ReauthError carries ErrReauthRequired together with the project it happened to, so the hint can name the exact command that fixes it.

func (*ReauthError) Error

func (e *ReauthError) Error() string

func (*ReauthError) ExitCode

func (e *ReauthError) ExitCode() int

ExitCode implements the interface exitcode.FromError looks for.

func (*ReauthError) Hint

func (e *ReauthError) Hint() string

Hint tells the user the one thing they can do about it.

func (*ReauthError) Unwrap

func (e *ReauthError) Unwrap() []error

Unwrap returns both the sentinel and the cause, so that errors.Is finds ErrReauthRequired and errors.As still finds the Error underneath it.

type Renewer

type Renewer interface {
	Renew(ctx context.Context) (Token, error)
}

Renewer is a TokenSource that can be forced to mint a fresh token, which is what `fft auth refresh` does. A StaticTokenSource is not one: FFT_ID_TOKEN is a fixed string with nothing behind it to renew from.

type Token

type Token struct {
	// ID is the JWT sent as `Authorization: Bearer …`.
	ID string
	// Refresh renews the id token without the password. It is long-lived, so it
	// is as sensitive as the password itself.
	Refresh string
	// ExpiresAt is when ID stops being accepted.
	ExpiresAt time.Time
	// Email is the address that authenticated. Google reports it on sign-in and
	// not on refresh, so it is carried forward rather than re-derived.
	Email string
}

Token is a minted id token, what it takes to renew it, and when it dies.

func (Token) Fresh

func (t Token) Fresh(now time.Time, leeway time.Duration) bool

Fresh reports whether the token can still be used at now, with leeway to spare.

type TokenSource

type TokenSource interface {
	// Token returns an id token that is valid now, minting or refreshing one if
	// it has to.
	Token(ctx context.Context) (string, error)
}

TokenSource yields the bearer token for the fulfillmenttools API.

It is the seam a future machine-to-machine or OIDC mode plugs into: nothing outside this package knows that today's token comes from a password sign-in.

func StaticTokenSource

func StaticTokenSource(token string) TokenSource

StaticTokenSource returns a TokenSource that always yields token.

It backs FFT_ID_TOKEN: a CI job that has already signed in elsewhere hands fft the token and nothing else. There is no password and no refresh token behind it, so when it expires the only honest thing to do is fail.

type Transport

type Transport struct {
	// Source mints the token. It is required.
	Source TokenSource

	// Base is the transport underneath. nil means http.DefaultTransport.
	Base http.RoundTripper
}

Transport adds `Authorization: Bearer …` to every request it carries.

It carries nothing else. In particular the Firebase API key never appears here: the key is Google's, Client is the only thing that holds it, and a fulfillmenttools request that contained it would be handing a third party's credential to a tenant.

Where the 401 retry is not

A reactive "on 401, refresh and retry" belongs in the client wrapper, not here. A RoundTripper is handed a request whose body is a one-shot io.ReadCloser: by the time the 401 comes back the body has been consumed, and replaying it is impossible in the general case. Retrying at this layer would work in the specs, where the body is a bytes.Reader, and silently send an empty POST in production.

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper.

Jump to

Keyboard shortcuts

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