Help us shape the future of Go! Take the 2026 Go Developer Survey

auth

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: GPL-3.0 Imports: 27 Imported by: 0

Documentation

Overview

Package auth implements core.Auth: it maps the X-Plex-Token a Plex app presents to an Ether user, and keeps the users table with per-user settings (stream limit, content rating cap, languages, sections).

A token is recognised, in order, as

  1. the server's own token (the owner),
  2. a server access token synced from plex.tv for the owner or a friend,
  3. any other plex.tv token, validated against plex.tv (GET /api/v2/user) and checked for access to this server (owner id, synced friend, or the server in that token's resources list).

Results of (3) are cached (positive and negative) so plex.tv is asked at most once per token per TTL; during a plex.tv outage a previously valid token keeps working for StaleTTL.

Index

Constants

View Source
const (

	// OwnerSessionTTL is how long an unused session stays valid. Use
	// extends it, at most once a day, up to OwnerSessionMaxAge.
	OwnerSessionTTL = 30 * 24 * time.Hour
	// OwnerSessionMaxAge bounds a session regardless of use.
	OwnerSessionMaxAge = 90 * 24 * time.Hour
)

Owner console sessions. Signing in to the admin console used to hand the browser the owner's plex.tv account token, which then sat in localStorage where any script on the page could read it and take over the Plex account. The console now exchanges that credential for a server-local session: it only opens the admin API, is never accepted as a Plex or playback credential, and can be revoked. Only hashes persist, and every check reads the shared database, so a session issued on one cluster node works on all of them and dies everywhere the moment it is revoked or ownership changes.

Variables

View Source
var (
	ErrViewerIsOwner     = errors.New("auth: viewer is the owner")
	ErrViewerPlexManaged = errors.New("auth: viewer is managed on plex.tv")
	ErrMergeSameViewer   = errors.New("auth: cannot merge a viewer into itself")
)

Viewer lifecycle refusals. Plex viewers are managed on plex.tv, full stop: Ether never pauses, removes or merges them. The owner can never be paused, removed or merged either.

Functions

func HashToken

func HashToken(token string) string

HashToken is how tokens are stored: never in plain text.

func IsOwnerSession added in v0.2.48

func IsOwnerSession(token string) bool

IsOwnerSession reports whether token has the shape of an owner console session. It says nothing about validity.

func StoredIdentity

func StoredIdentity(ctx context.Context, db *sql.DB, id int64) (*core.User, error)

StoredIdentity looks up an import destination without granting access or creating an authentication service. Inactive viewers retain their history.

Types

type Acceptance added in v0.3.0

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

Acceptance is minted only by Service.Accept. The zero value is invalid. Formatting, JSON and logs show a fixed label so a receipt cannot leak a credential digest.

func (Acceptance) Format added in v0.3.0

func (a Acceptance) Format(f fmt.State, _ rune)

func (Acceptance) GoString added in v0.3.0

func (a Acceptance) GoString() string

func (Acceptance) MarshalJSON added in v0.3.0

func (a Acceptance) MarshalJSON() ([]byte, error)

func (Acceptance) String added in v0.3.0

func (a Acceptance) String() string

func (Acceptance) Valid added in v0.3.0

func (a Acceptance) Valid() bool

Valid reports whether Accept minted this value. It does not recheck revocation.

func (Acceptance) ViewerID added in v0.3.0

func (a Acceptance) ViewerID() int64

ViewerID is the viewer named by the acceptance. ReceiptCurrent still rechecks that identity against the live credential.

type Options

type Options struct {
	// Validator checks unknown tokens; nil means only server and synced
	// tokens are accepted.
	Validator Validator
	// RefreshUnknownToken, when set, runs a rate-limited friend/access-token
	// sync when an unrecognised token arrives, so a newly granted server
	// access token is accepted without waiting for the periodic sync.
	RefreshUnknownToken func(context.Context) error
	// CacheTTL is how long a plex.tv-validated token is trusted (15m).
	CacheTTL time.Duration
	// NegativeTTL is how long a rejected token is remembered (2m).
	NegativeTTL time.Duration
	// StaleTTL lets a validated token keep working this long past CacheTTL
	// while plex.tv is unreachable (24h).
	StaleTTL time.Duration
	// MaxCacheEntries bounds the token caches (10000).
	MaxCacheEntries int
	// MaxConcurrentValidations bounds distinct unknown tokens being checked
	// with plex.tv (16). Excess requests fail fast; cached tokens still work.
	MaxConcurrentValidations int
	Logger                   *slog.Logger
	Now                      func() time.Time
}

Options configures the Service.

type PlexTVValidator

type PlexTVValidator struct {
	Client    *plextv.Client
	MachineID string
}

PlexTVValidator is the production Validator.

func (PlexTVValidator) Account

func (v PlexTVValidator) Account(ctx context.Context, token string) (*plextv.Account, error)

Account implements Validator.

func (PlexTVValidator) HasServer

func (v PlexTVValidator) HasServer(ctx context.Context, token string) (bool, error)

HasServer implements Validator.

type PlexViewerDefaults added in v0.3.0

type PlexViewerDefaults struct {
	AudioLanguage    string `json:"audioLanguage,omitempty"`
	AutoSelectAudio  bool   `json:"autoSelectAudio,omitempty"`
	SubtitleLanguage string `json:"subtitleLanguage,omitempty"`
	SubtitleMode     int    `json:"subtitleMode,omitempty"`
	ForcedOnly       bool   `json:"forcedOnly,omitempty"`
	HasProfile       bool   `json:"hasProfile,omitempty"`
	FilterMovies     string `json:"filterMovies,omitempty"`
	FilterTelevision string `json:"filterTelevision,omitempty"`
	AllowSync        *bool  `json:"allowSync,omitempty"`
	HasSettings      bool   `json:"hasSettings,omitempty"`
}

PlexViewerDefaults is the cached plex.tv grant for one Ether viewer. It is a default layer only: owner Ether policy and the viewer's own picks always win.

func (PlexViewerDefaults) EtherMode added in v0.3.0

func (d PlexViewerDefaults) EtherMode() string

EtherMode is the Ether subtitle mode for this cached plex.tv profile.

type Service

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

Service implements core.Auth and plextv.UserSync.

func New

func New(ctx context.Context, db *sql.DB, o Options) (*Service, error)

New loads users and known tokens from db (migrated with store.Migrate).

func (*Service) Accept added in v0.3.0

func (s *Service) Accept(ctx context.Context, token string) (*core.User, Acceptance, error)

Accept authenticates token and returns the receipt minted by that acceptance. Callers must keep the receipt from this call. Reconstructing one from the request does not establish which branch accepted it.

func (*Service) ActiveUser

func (s *Service) ActiveUser(ctx context.Context, id int64) (*core.User, error)

ActiveUser returns a current access snapshot, including for stream grants.

func (*Service) AllUsers

func (s *Service) AllUsers(ctx context.Context) ([]UserInfo, error)

AllUsers lists every known user including those who lost access.

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, token string) (*core.User, error)

Authenticate implements core.Auth. It keeps the historical user and error result; Accept is the same decision plus the receipt minted inside it.

func (*Service) BootstrapOwner

func (s *Service) BootstrapOwner(ctx context.Context, p config.Plex) error

BootstrapOwner creates the owner from config without network access (on startup, before plex.tv is reachable) and registers the server token.

func (*Service) Close

func (s *Service) Close()

Close disconnects the cluster bus.

func (*Service) FlushSeen

func (s *Service) FlushSeen(ctx context.Context) error

FlushSeen persists last-seen times (call periodically and at shutdown). Every node flushes its own observations; the update only ever moves last_seen_at forward, so nodes flushing in any order keep the latest.

func (*Service) Generation added in v0.3.0

func (s *Service) Generation() uint64

Generation changes when a household member loses access.

func (*Service) Invalidate

func (s *Service) Invalidate(token string)

Invalidate forgets cached validations of token.

func (*Service) IssueDelegation added in v0.2.7

func (s *Service) IssueDelegation(ctx context.Context, userID int64) (string, error)

IssueDelegation creates a server-local credential, never a plex.tv account token. Only hashes persist; expiry and membership are checked on every use, including when another cluster node receives the credential.

func (*Service) IssueOwnerSession added in v0.2.48

func (s *Service) IssueOwnerSession(ctx context.Context, credential string) (string, time.Time, error)

IssueOwnerSession exchanges an owner credential (a plex.tv token Ether accepts as the owner's) for a console session. Sessions and delegated credentials cannot be exchanged, so a stolen session cannot extend itself past its absolute lifetime.

func (*Service) LinkPlex added in v0.3.0

func (s *Service) LinkPlex(ctx context.Context, viewerID int64, token string) error

LinkPlex verifies a Plex account and its access to this server before attaching it to an existing active viewer. Existing viewers are never merged.

func (*Service) MergeViewers added in v0.3.2

func (s *Service) MergeViewers(ctx context.Context, fromID, intoID int64) error

MergeViewers moves everything from the source viewer into the target (store.MergeViewer: the target wins conflicts, watch history and plays union), then removes the source, in one transaction. Neither side may be the owner or a Plex viewer.

func (*Service) Owner

func (s *Service) Owner(ctx context.Context) (*core.User, error)

Owner implements core.Auth.

func (*Service) OwnerSession added in v0.2.48

func (s *Service) OwnerSession(ctx context.Context, token string) (*core.User, error)

OwnerSession returns the owner a console session belongs to. It fails with core.ErrUnauthorized once the session expired, was revoked, or its user is no longer the active owner.

func (*Service) PauseViewer added in v0.3.2

func (s *Service) PauseViewer(ctx context.Context, viewerID int64) error

PauseViewer pauses a viewer's access: everything of theirs is kept, but they can no longer sign in or watch. The caller signs them out everywhere as part of pausing (admin does, via identity grants).

func (*Service) PlexDefaults added in v0.3.0

func (s *Service) PlexDefaults(userID int64) (PlexViewerDefaults, bool)

PlexDefaults returns the cached plex.tv layer for a local viewer id.

func (*Service) PlexDefaultsHandler added in v0.3.0

func (s *Service) PlexDefaultsHandler() http.Handler

PlexDefaultsHandler serves GET /{id} as the read-only plex.tv layer shown in the owner console.

func (*Service) PolicyInTx added in v0.3.0

func (s *Service) PolicyInTx(ctx context.Context, tx *sql.Tx, id int64) (*core.User, error)

PolicyInTx reads durable membership on an open transaction so a caller can fence it with the rest of an authorization snapshot.

func (*Service) PolicyUser added in v0.3.0

func (s *Service) PolicyUser(ctx context.Context, id int64) (*core.User, error)

PolicyUser reads durable membership and restrictions without the process cache.

func (*Service) ReceiptCurrent added in v0.3.0

func (s *Service) ReceiptCurrent(ctx context.Context, a Acceptance) error

ReceiptCurrent reports whether a still names the credential Accept minted and that credential's current local revocation state. It does not ask plex.tv.

func (*Service) RefreshViewers added in v0.3.0

func (s *Service) RefreshViewers(ctx context.Context) error

RefreshViewers reloads viewer state after an identity service has made an atomic membership change. Cluster peers receive the same invalidation.

func (*Service) RemoveViewer added in v0.3.2

func (s *Service) RemoveViewer(ctx context.Context, viewerID int64) error

RemoveViewer deletes a viewer and all of their data in one transaction: every registered per-viewer purge (store.PurgeViewer), then the auth_users row itself, whose ON DELETE CASCADE children are a backstop.

func (*Service) ResumeViewer added in v0.3.2

func (s *Service) ResumeViewer(ctx context.Context, viewerID int64) error

ResumeViewer ends a pause.

func (*Service) RevokeOwnerSession added in v0.2.48

func (s *Service) RevokeOwnerSession(ctx context.Context, token string) error

RevokeOwnerSession ends a console session. Unknown tokens are not an error: signing out twice is harmless.

func (*Service) RevokeViewerSessions added in v0.3.2

func (s *Service) RevokeViewerSessions(ctx context.Context, viewerID int64) (int, error)

RevokeViewerSessions deletes the viewer's console sessions and delegated credentials, returning how many were revoked. Device grants and app passwords live in identity and are revoked there; together they make "sign out everywhere".

func (*Service) SetBus

func (s *Service) SetBus(bus clusterbus.Bus)

SetBus connects the cluster bus (nil disconnects): after every write this service tells the other nodes to reload users and tokens, and it reloads when they write. A standalone server never calls it.

func (*Service) SetOwner

func (s *Service) SetOwner(ctx context.Context, a plextv.Account, serverToken string) error

SetOwner implements plextv.UserSync: it makes a the owner and registers serverToken as the owner's token. A unique index admits one owner row, so when another node changes the owner concurrently the losing transaction is retried against the winner's committed state.

func (*Service) SetOwnerEnabled added in v0.3.0

func (s *Service) SetOwnerEnabled(ctx context.Context, viewerID int64, enabled bool) error

SetOwnerEnabled changes the owner's membership decision. Plex sharing remains independently authoritative and can keep a viewer inactive.

func (*Service) SetUnknownTokenRefresher added in v0.3.0

func (s *Service) SetUnknownTokenRefresher(fn func(context.Context) error)

SetUnknownTokenRefresher installs or replaces the callback invoked when an unrecognised token arrives before falling back to plex.tv validation.

func (*Service) SetViewerMaxRating added in v0.3.0

func (s *Service) SetViewerMaxRating(ctx context.Context, id int64, rating string) error

SetViewerMaxRating changes only the content ceiling without overwriting other viewer settings.

func (*Service) SyncFriends

func (s *Service) SyncFriends(ctx context.Context, friends []plextv.Friend) error

SyncFriends implements plextv.UserSync: it replaces the synced access tokens, upserts friends' profiles (never their Ether settings) and deactivates every non-owner absent from the authoritative membership list, including viewers first discovered through personal-token validation.

func (*Service) UpdateUser

func (s *Service) UpdateUser(ctx context.Context, u core.User) error

func (*Service) User

func (s *Service) User(ctx context.Context, id int64) (*core.User, error)

User returns one user by local id (active or not).

func (*Service) Users

func (s *Service) Users(ctx context.Context) ([]core.User, error)

Users implements core.Auth: active users, owner first, then by name.

type UserInfo

type UserInfo struct {
	core.User
	Active       bool      `json:"active"`
	Origin       string    `json:"origin"`
	Home         bool      `json:"home"`
	Restricted   bool      `json:"restricted"`
	PlexSections []int     `json:"plexSections"`
	LastSeen     time.Time `json:"lastSeen,omitempty"`
	// OwnerEnabled is false when the owner paused this viewer;
	// PlexShared is whether plex.tv still shares the server with them;
	// HasPlex is whether the viewer carries a Plex account id.
	OwnerEnabled bool `json:"ownerEnabled"`
	PlexShared   bool `json:"plexShared"`
	HasPlex      bool `json:"hasPlex"`
}

UserInfo is a user plus bookkeeping for the admin UI.

type Validator

type Validator interface {
	// Account returns the account behind token. An error matching
	// core.ErrUnauthorized means the token is invalid; other errors mean
	// plex.tv could not answer.
	Account(ctx context.Context, token string) (*plextv.Account, error)
	// HasServer reports whether token's resources list includes this
	// server (i.e. the account was granted access).
	HasServer(ctx context.Context, token string) (bool, error)
}

Validator checks tokens Ether does not already know against plex.tv.

Jump to

Keyboard shortcuts

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