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 ¶
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 ¶
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 ¶
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.
type Error ¶
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.
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.
type Option ¶
type Option func(*Client)
Option configures a Client.
type ReauthError ¶
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 ¶
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.
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.