Documentation
¶
Overview ¶
Package auth turns MQTT CONNECT credentials into a tenant-scoped identity.
Index ¶
Constants ¶
const ( // PrincipalKindUser is the Identity.PrincipalKind of a person's JWT. PrincipalKindUser = "user" // PrincipalKindServiceAccount is the Identity.PrincipalKind of a service // account's JWT. PrincipalKindServiceAccount = "service_account" )
const PrincipalKindAPIKey = "api_key"
PrincipalKindAPIKey is the Identity.PrincipalKind of a keysmith key.
Variables ¶
var ( // ErrBadCredentials means the credentials are not recognised: an unknown, // inactive or expired key, or a username that is not the key's ID. // CONNACK 0x86. ErrBadCredentials = errors.New("tether/auth: bad credentials") // ErrNotAuthorized means the credentials are valid but may not connect: // the tenant is unknown or disabled, the key has no device, or the device // is pinned to another client ID. CONNACK 0x87. ErrNotAuthorized = errors.New("tether/auth: not authorized") // failure is not one this package can tell is about the credentials. It // is returned when the device or tenant lookup fails with anything but // "not found", when the key validator returns an error that is not a // recognised keysmith credential error, and when it returns no key at all. // A keysmith key store that cannot answer is one of those: since keysmith // v1.9.0 its lookup error is not keysmith.ErrInvalidKey. The broker // answers it with CONNACK 0x88 on MQTT 5 and 0x03 on 3.1.1 and counts it // as reason "unavailable", and POST /v1/publish answers 503 with no // WWW-Authenticate header. ErrUnavailable = errors.New("tether/auth: unavailable") // ErrRateLimited means the key has been rate limited by its policy. // CONNACK 0x9F. ErrRateLimited = errors.New("tether/auth: rate limited") )
Errors an Authenticator returns. The broker matches them with errors.Is and maps each to a CONNACK reason code. Every failure an Authenticator reports wraps exactly one of them.
Functions ¶
This section is empty.
Types ¶
type Authenticator ¶
type Authenticator interface {
// Authenticate returns the identity for c, or an error wrapping
// ErrBadCredentials, ErrNotAuthorized, ErrUnavailable or ErrRateLimited.
Authenticate(ctx context.Context, c Credentials) (*Identity, error)
}
Authenticator decides whether CONNECT credentials may connect.
func Chain ¶
func Chain(jwt, keysmith Authenticator) Authenticator
Chain returns an Authenticator that sends a password that looks like a JWT to jwt and every other password to keysmith. A nil jwt returns keysmith itself.
A JWT is three dot-separated base64url segments, the first one starting "ey" (the encoding of the header object's opening brace and quote). A keysmith key has no such shape. The choice is made from the password alone, and a JWT-shaped password that jwt refuses is never retried against keysmith. Credentials, SkipClientIDPin included, go to the chosen authenticator unchanged.
func NewStatic ¶
func NewStatic(identities map[string]*Identity) Authenticator
NewStatic returns an Authenticator that looks identities up by username and ignores the password. It is for tests and examples, never for production. Callers get a copy, so they cannot change the map's identities.
type Credentials ¶
type Credentials struct {
// ClientID is the MQTT client identifier.
ClientID string
// Username is the MQTT username. For keysmith it is the key ID.
Username string
// Password is the MQTT password. For keysmith it is the raw key.
Password []byte
// Remote is the peer's network address, for logs.
Remote string
// SkipClientIDPin makes the authenticator ignore the device's client ID
// pin. Set it only in a non-MQTT caller that has no client ID to offer
// (the HTTP publish endpoint), and read the pin from Identity.ClientID.
// The broker never sets it, so a CONNECT is always checked against the
// pin.
SkipClientIDPin bool
}
Credentials are the values from an MQTT CONNECT packet.
type DeviceLookup ¶
type DeviceLookup interface {
GetDeviceByKey(ctx context.Context, keyID string) (*device.Device, error)
}
DeviceLookup finds the device that owns a keysmith key. Every store.Store satisfies it.
type Identity ¶
type Identity struct {
// Tenant is the tenant the principal connects as.
Tenant *tenant.Tenant
// PrincipalKind says what PrincipalID names, "api_key" for keysmith.
PrincipalKind string
// PrincipalID identifies the principal. For keysmith it is the key ID.
PrincipalID string
// Username is the MQTT username the principal authenticated with.
Username string
// DeviceID is the Tether device the principal maps to, when there is one.
DeviceID string
// ClientID is the client ID the device is pinned to, empty when it is
// not pinned. It is the device's setting, not the ID a client sent.
ClientID string
// ExpiresAt is when the credentials stop being valid. The zero time means
// they do not expire. The broker disconnects the client at that time.
ExpiresAt time.Time
}
Identity is an authenticated principal and the tenant it belongs to.
type JWT ¶
type JWT struct {
// contains filtered or unexported fields
}
JWT authenticates MQTT clients with a JWT that authsome issued. The JWT is the MQTT password. The username must be the token's subject.
Authenticate checks the signature against the issuer's JWKS, then "exp" (required), "nbf", "iss" (equal to the configured issuer) and "aud" (any of the configured audiences). authsome's own validator checks neither "iss" nor "aud", so a token meant for another service verifies there; here it does not.
The tenant comes from the token's "app_id" and "org_id" claims, resolved the way a keysmith key's (AppID, TenantID) is. A token without either is refused as not authorized. The identity is a copy the caller owns.
Tokens that need more than a bearer check are refused: ones bound to a DPoP key ("cnf", which MQTT cannot prove), ones issued to a third-party OAuth client ("client_id", whose scope Tether cannot enforce) and delegated ones ("act", someone acting for the subject, whom the subject's grants say nothing about). So are principals other than users and service accounts.
func NewJWT ¶
func NewJWT(cfg JWTConfig, t TenantResolver) (*JWT, error)
NewJWT returns the JWT authenticator for cfg. It fetches nothing until the first token arrives. The error names the field when the config is not usable.
func (*JWT) Authenticate ¶
Authenticate implements Authenticator. SkipClientIDPin has no meaning for a JWT and is ignored.
type JWTConfig ¶
type JWTConfig struct {
// JWKSURL is where the identity provider publishes its signing keys,
// authsome's /.well-known/jwks.json. Plain http is allowed, so local
// development works. Required.
JWKSURL string `json:"jwks_url" yaml:"jwks_url"`
// Issuer is the exact "iss" a token must carry. Required.
Issuer string `json:"issuer" yaml:"issuer"`
// Audience lists the "aud" values Tether answers to. A token must carry at
// least one of them. Required.
Audience []string `json:"audience" yaml:"audience"`
// Algorithms lists the accepted signature algorithms. Default RS256,
// EdDSA, ES256. HMAC algorithms and "none" are refused.
Algorithms []string `json:"algorithms" yaml:"algorithms"`
// RefreshInterval is how long fetched keys are used before the JWKS is
// fetched again. Default 10 minutes. Independently of this, a token that
// names an unknown key ID makes the JWKS fetch again, at most once a
// minute.
RefreshInterval time.Duration `json:"refresh_interval" yaml:"refresh_interval"`
// Leeway is the clock skew tolerated on "nbf": a token a little ahead of
// this clock is accepted. Default 30 seconds; a negative value means
// none. It does not stretch "exp": the broker refuses credentials whose
// expiry has passed and disconnects a client when its token expires,
// whatever the leeway.
Leeway time.Duration `json:"leeway" yaml:"leeway"`
}
JWTConfig configures the authsome JWT authenticator. The zero value means JWT authentication is off.
type KeyValidator ¶
type KeyValidator interface {
ValidateKey(ctx context.Context, rawKey string) (*keysmith.ValidationResult, error)
}
KeyValidator validates a raw API key. *keysmith.Engine satisfies it.
type Keysmith ¶
type Keysmith struct {
// contains filtered or unexported fields
}
Keysmith authenticates devices with keysmith API keys. The MQTT username is the key ID and the password is the raw key.
Authenticate validates the key, requires the username to equal the key's ID (the raw key does not carry its ID), reads the tenant from the key's (AppID, TenantID) pair, and requires an enabled tenant, a Tether device for the key in that tenant, and a matching client ID when the device is pinned to one.
A successful validation is cached for the TTL, keyed by the SHA-256 of the raw password. A cache hit still checks the username and the client ID pin, which come from the request, and resolves the tenant again, so disabling a tenant takes effect at once. It does not call keysmith again, which means a revoked key keeps working until its entry expires or Evict(keyID) is called, and keysmith's per-key rate limit and usage counting see one call per TTL instead of one per CONNECT.
func NewKeysmith ¶
func NewKeysmith(v KeyValidator, t TenantResolver, d DeviceLookup, opts ...KeysmithOption) *Keysmith
NewKeysmith returns a keysmith-backed Authenticator.
func (*Keysmith) Authenticate ¶
Authenticate implements Authenticator.
type KeysmithOption ¶
type KeysmithOption func(*Keysmith)
KeysmithOption configures a Keysmith authenticator.
func WithCacheTTL ¶
func WithCacheTTL(d time.Duration) KeysmithOption
WithCacheTTL sets how long a successful key validation is cached. The default is 60 s. Zero or less turns the cache off. An entry never outlives the key's own expiry.