auth

package
v0.0.0-...-c848cff Latest Latest
Warning

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

Go to latest
Published: Nov 5, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package auth turns MQTT CONNECT credentials into a tenant-scoped identity.

Index

Constants

View Source
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"
)
View Source
const PrincipalKindAPIKey = "api_key"

PrincipalKindAPIKey is the Identity.PrincipalKind of a keysmith key.

Variables

View Source
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")

	// ErrUnavailable means authentication could not be decided, and the
	// 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

func (j *JWT) Authenticate(ctx context.Context, c Credentials) (*Identity, error)

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.

func (JWTConfig) IsZero

func (c JWTConfig) IsZero() bool

IsZero reports whether no field is set, which 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

func (k *Keysmith) Authenticate(ctx context.Context, c Credentials) (*Identity, error)

Authenticate implements Authenticator.

func (*Keysmith) Evict

func (k *Keysmith) Evict(keyID string)

Evict forgets every cached validation for the key ID. Call it when a key is revoked, rotated or suspended, or its device changes. A validation already running when Evict is called does not cache its result.

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.

type TenantResolver

type TenantResolver interface {
	ResolveTenant(ctx context.Context, appID, externalID string) (*tenant.Tenant, error)
}

TenantResolver maps the host application's (app, external) pair to a tenant. *service.Tenants satisfies it.

Jump to

Keyboard shortcuts

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