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

identityservice

package
v0.2.5 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package identityservice owns remote account and credential policies.

Index

Constants

View Source
const (
	InviteLifetime     = 48 * time.Hour
	GoogleFlowLifetime = 10 * time.Minute
)
View Source
const (
	PurposeCollector      Purpose = "collector"
	PurposeAPI            Purpose = "api"
	SessionLifetime               = 7 * 24 * time.Hour
	DefaultTokenLifetime          = 90 * 24 * time.Hour
	MaximumTokenLifetime          = 365 * 24 * time.Hour
	SetupLifetime                 = 24 * time.Hour
	PasswordResetLifetime         = 24 * time.Hour
)

Variables

View Source
var (
	// ErrGoogleDisabled reports Google sign-in is not configured.
	ErrGoogleDisabled = errors.New("google sign-in is not configured")
	// ErrNoAccount reports a Google account without a diffx account or invite
	// while sign-up is closed.
	ErrNoAccount = errors.New("no account for this Google account")
	// ErrInviteMismatch reports a verified Google email that differs from the invite.
	ErrInviteMismatch = errors.New("google account does not match the invite")
)
View Source
var (
	ErrAuthentication = errors.New("invalid credentials")
	ErrInvalidInput   = errors.New("invalid identity input")
	ErrInvalidEmail   = fmt.Errorf("%w: invalid email address", ErrInvalidInput)
	ErrConflict       = errors.New("email already has an account")
	ErrNotFound       = errors.New("identity not found")
	// ErrForbidden refuses an administrative action an admin takes on itself.
	ErrForbidden = errors.New("identity operation not permitted")
	// ErrLastAdmin refuses an action that would leave no enabled admin.
	ErrLastAdmin = errors.New("the last admin cannot be removed")
	// ErrDisabled reports a sign-up or sign-in method this deployment turned off.
	ErrDisabled = errors.New("identity method is disabled")
)

Functions

This section is empty.

Types

type Account

type Account struct {
	ID string
	// Email identifies the account: the canonical password sign-in email, or
	// the latest one Google reported for a Google account.
	Email  string
	Admin  bool
	Method Method
}

Account is an authenticated owner. Admin marks accounts that may also sign in to the console and manage other accounts.

type AccountSummary added in v0.2.2

type AccountSummary struct {
	Account
	Disabled  bool
	CreatedAt time.Time
}

AccountSummary is the administrative view of an account.

type Audience added in v0.2.4

type Audience string

Audience is the surface a credential is presented to. Every account is a member of the web app and its APIs; admins also use the console.

const (
	AudienceMembers Audience = "members"
	AudienceConsole Audience = "console"
)

type CreatedToken

type CreatedToken struct {
	Token
	Secret string
}

type GoogleClient added in v0.2.2

type GoogleClient interface {
	AuthorizeURL(state, nonce, verifier string) string
	Exchange(ctx context.Context, code, verifier, nonce string) (google.Identity, error)
}

GoogleClient is the Google wire adapter this service drives.

type GoogleFlow added in v0.2.2

type GoogleFlow struct {
	BindingHash string
	Nonce       string
	Verifier    string
	InviteID    string
	SetupHash   string
	ExpiresAt   time.Time
}

GoogleFlow is one started authorization, bound to the starting browser. It may accept an invite or complete first-admin setup.

type GoogleStore added in v0.2.2

type GoogleStore interface {
	// CreateInvite revokes the email's pending invites in the same transaction;
	// an email linked to an account is ErrConflict.
	CreateInvite(ctx context.Context, invite Invite, tokenHash string) error
	Invites(context.Context) ([]Invite, error)
	Invite(ctx context.Context, id string) (Invite, error)
	RevokeInvite(ctx context.Context, id string, at time.Time) error
	// PendingInvite resolves an unexpired, unused, unrevoked invite by its secret.
	PendingInvite(ctx context.Context, tokenHash string, now time.Time) (Invite, error)
	// AcceptInvite creates the account and its Google identity and uses the
	// pending invite in one transaction. A taken email is ErrConflict; an
	// unavailable invite or an already linked subject is ErrNotFound.
	AcceptInvite(ctx context.Context, inviteID string, account Account, identity google.Identity, now time.Time) error
	// AcceptPasswordInvite creates a password account and uses the pending
	// invite in one transaction, refusing with ErrDisabled unless every setting
	// required is on. A taken email is ErrConflict; an unavailable invite is ErrNotFound.
	AcceptPasswordInvite(ctx context.Context, inviteID string, account Account, passwordHash string, now time.Time, required Settings) error
	// CreateGoogleAccount creates an account and its Google identity without an
	// invite, refusing with ErrDisabled unless every setting required is on. A
	// taken email is ErrConflict; an already linked subject is ErrNotFound.
	CreateGoogleAccount(ctx context.Context, account Account, identity google.Identity, now time.Time, required Settings) error
	// CreateGoogleAdmin consumes the setup secret and creates the first admin
	// with its Google identity in one transaction. A taken email is
	// ErrConflict; a missing, expired or used secret or a linked subject is ErrNotFound.
	CreateGoogleAdmin(ctx context.Context, account Account, identity google.Identity, setupHash string, now time.Time) error
	// PromoteSetupAdmin consumes the setup secret and makes an existing,
	// enabled account the first admin; otherwise it is ErrNotFound.
	PromoteSetupAdmin(ctx context.Context, accountID, setupHash string, now time.Time) error
	// GoogleAccount resolves a linked subject, recording the latest email.
	GoogleAccount(ctx context.Context, identity google.Identity, now time.Time) (account Account, disabled bool, err error)
	PutGoogleFlow(ctx context.Context, stateHash string, flow GoogleFlow) error
	// TakeGoogleFlow consumes an unexpired flow exactly once.
	TakeGoogleFlow(ctx context.Context, stateHash string, now time.Time) (GoogleFlow, error)
}

GoogleStore persists Google identities, invites and authorization flows.

type Invite added in v0.2.2

type Invite struct {
	ID        string
	Email     string
	CreatedAt time.Time
	ExpiresAt time.Time
	UsedAt    *time.Time
	// UsedBy is the email of the account that accepted the invite while it exists.
	UsedBy    string
	RevokedAt *time.Time
}

Invite admits one email to the web app as a member.

func (Invite) Status added in v0.2.2

func (invite Invite) Status(now time.Time) InviteStatus

type InviteStatus added in v0.2.2

type InviteStatus string
const (
	InvitePending InviteStatus = "pending"
	InviteUsed    InviteStatus = "used"
	InviteExpired InviteStatus = "expired"
	InviteRevoked InviteStatus = "revoked"
)

type Method added in v0.2.4

type Method string

Method is how an account signs in. It is fixed when the account is created: an account with a Google identity uses Google, every other one a password.

const (
	MethodPassword Method = "password"
	MethodGoogle   Method = "google"
)

type Option added in v0.2.2

type Option func(*Service)

func WithGoogle added in v0.2.2

func WithGoogle(client GoogleClient) Option

WithGoogle enables Google sign-in.

type PasswordCredential

type PasswordCredential struct {
	Account Account
	Hash    string
}

type Purpose

type Purpose string

type Service

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

func New

func New(store Store, options ...Option) *Service

func (*Service) AcceptPasswordInvite added in v0.2.4

func (service *Service) AcceptPasswordInvite(ctx context.Context, secret, email, password string) (Session, error)

AcceptPasswordInvite creates a password account from an invite link and signs it in while password signup is on. Holding the link is the proof; the invited email is not verified.

func (*Service) AccountSummaries added in v0.2.2

func (service *Service) AccountSummaries(ctx context.Context) ([]AccountSummary, error)

func (*Service) Accounts

func (service *Service) Accounts(ctx context.Context) ([]Account, error)

Accounts lists enabled members, the owners the ingestion worker may serve.

func (*Service) AuthenticateSession

func (service *Service) AuthenticateSession(ctx context.Context, audience Audience, secret string) (Session, error)

AuthenticateSession resolves a browser session for an audience; sessions of accounts the audience does not admit fail like unknown ones.

func (*Service) AuthenticateToken

func (service *Service) AuthenticateToken(ctx context.Context, secret string) (TokenIdentity, error)

AuthenticateToken resolves an API token, a member credential.

func (*Service) ChangePassword

func (service *Service) ChangePassword(ctx context.Context, accountID, current, password string) error

func (*Service) CompleteGoogle added in v0.2.2

func (service *Service) CompleteGoogle(ctx context.Context, audience Audience, state, binding, code string) (Session, error)

CompleteGoogle finishes authorization in the browser that started it. A linked Google subject signs in to an audience that admits it, keeping its account when the email changes. For the web app, an invite flow creates the account when Google verified the invited email, and open signup creates one for any verified email. The console creates only the first admin, from a setup flow.

func (*Service) CompletePasswordReset added in v0.2.4

func (service *Service) CompletePasswordReset(ctx context.Context, audience Audience, secret, password string) error

CompletePasswordReset sets a new password from a reset link and revokes the account's browser sessions; its API tokens are unaffected.

func (*Service) CompleteSetup added in v0.2.2

func (service *Service) CompleteSetup(ctx context.Context, secret, email, password string) (Session, error)

CompleteSetup creates the admin from a valid setup secret and signs it in.

func (*Service) CreateInvite added in v0.2.2

func (service *Service) CreateInvite(ctx context.Context, email string) (Invite, string, error)

CreateInvite issues a single-use 48 hour member invite for one email, replacing any pending invite for it. The secret is returned once and stored hashed. Invites work whether or not signup is open; the settings decide whether a password can accept it.

func (*Service) CreateToken

func (service *Service) CreateToken(ctx context.Context, accountID, name string, purpose Purpose, expires time.Time) (CreatedToken, error)

func (*Service) DeleteAccount added in v0.2.2

func (service *Service) DeleteAccount(ctx context.Context, actorID, accountID string) error

DeleteAccount removes the account and every review, token and integration it owns.

func (*Service) DisableAccount added in v0.2.2

func (service *Service) DisableAccount(ctx context.Context, actorID, accountID string) error

DisableAccount, EnableAccount, DeleteAccount and SetAdmin are an admin's actions on another account; an admin never acts on itself, so it cannot lock itself out.

func (*Service) EnableAccount added in v0.2.2

func (service *Service) EnableAccount(ctx context.Context, actorID, accountID string) error

func (*Service) GoogleEnabled added in v0.2.2

func (service *Service) GoogleEnabled() bool

func (*Service) Invites added in v0.2.2

func (service *Service) Invites(ctx context.Context) ([]Invite, error)

func (*Service) LoadSettings added in v0.2.4

func (service *Service) LoadSettings(ctx context.Context) (Settings, error)

LoadSettings reads the settings and keeps them as this process's snapshot.

func (*Service) Login

func (service *Service) Login(ctx context.Context, audience Audience, email, password string) (Session, error)

Login signs a password account in to an audience it is admitted to. Every failure is the same ErrAuthentication, so it never reveals which accounts exist, use Google or belong to another audience.

func (*Service) Logout

func (service *Service) Logout(ctx context.Context, secret string) error

func (*Service) PendingInvite added in v0.2.2

func (service *Service) PendingInvite(ctx context.Context, secret string) (Invite, error)

PendingInvite resolves an invite link for the invite page.

func (*Service) PendingPasswordReset added in v0.2.4

func (service *Service) PendingPasswordReset(ctx context.Context, audience Audience, secret string) (Account, error)

PendingPasswordReset resolves a reset link for an audience's reset page.

func (*Service) PrepareSetup added in v0.2.2

func (service *Service) PrepareSetup(ctx context.Context) (string, error)

PrepareSetup issues a one-time admin setup secret while no admin exists, replacing any earlier one. It returns "" once an admin exists. The secret is stored hashed, so each start issues a new one.

func (*Service) PutSettings added in v0.2.4

func (service *Service) PutSettings(ctx context.Context, actorID string, settings Settings) error

PutSettings is an admin's change to the settings.

func (*Service) ResetPassword

func (service *Service) ResetPassword(ctx context.Context, accountID string) (Account, string, error)

ResetPassword issues a one-time reset secret for a password account, replacing its pending one. It returns the account so the caller links the secret to the surface the account signs in to. Google accounts are ErrNotFound.

func (*Service) ResetPasswordByEmail added in v0.2.5

func (service *Service) ResetPasswordByEmail(ctx context.Context, email string) (Account, string, error)

ResetPasswordByEmail is ResetPassword for offline recovery by email.

func (*Service) RevokeInvite added in v0.2.2

func (service *Service) RevokeInvite(ctx context.Context, id string) error

func (*Service) RevokeToken

func (service *Service) RevokeToken(ctx context.Context, accountID, tokenID string) error

func (*Service) SetAdmin added in v0.2.4

func (service *Service) SetAdmin(ctx context.Context, actorID, accountID string, admin bool) error

SetAdmin grants or removes the admin role. Removing it ends the account's console sessions at their next request.

func (*Service) Settings added in v0.2.4

func (service *Service) Settings() Settings

Settings is the latest loaded snapshot; before the first load every setting is off. Account creation re-checks the stored settings.

func (*Service) SetupRequired added in v0.2.2

func (service *Service) SetupRequired(ctx context.Context) (bool, error)

func (*Service) SignUp added in v0.2.4

func (service *Service) SignUp(ctx context.Context, email, password string) (Session, error)

SignUp creates a password member and signs it in, while both signup and password signup are on.

func (*Service) StartGoogle added in v0.2.2

func (service *Service) StartGoogle(ctx context.Context, inviteSecret string) (string, string, error)

StartGoogle begins Google authorization, optionally for an invite. It returns the authorization URL and a browser binding the callback must present.

func (*Service) StartGoogleSetup added in v0.2.4

func (service *Service) StartGoogleSetup(ctx context.Context, setupSecret string) (string, string, error)

StartGoogleSetup begins Google authorization that completes first-admin setup with a pending setup secret; others are ErrNotFound.

func (*Service) Tokens

func (service *Service) Tokens(ctx context.Context, accountID string) ([]Token, error)

type Session

type Session struct {
	Account   Account
	Secret    string
	CSRFToken string
	ExpiresAt time.Time
}

type Settings added in v0.2.4

type Settings struct {
	// Signup lets people create accounts without an invite.
	Signup bool
	// PasswordSignup lets new accounts choose a password, by signup or invite.
	// Existing password accounts always sign in, whatever its value.
	PasswordSignup bool
}

Settings decide how people join the web app; admins change them in the console. They never affect existing accounts or the console.

type Store

type Store interface {
	// PutAdminSetup replaces the pending setup secret while no admin exists and
	// reports whether it was stored. Setup creates the first admin only.
	PutAdminSetup(context.Context, string, time.Time) (bool, error)
	AdminExists(context.Context) (bool, error)
	// CreateAdmin consumes the unexpired setup secret and creates the admin in
	// one transaction; a missing, expired or used secret is ErrNotFound.
	CreateAdmin(ctx context.Context, account Account, passwordHash, setupHash string, now time.Time) error
	// PendingAdminSetup reports whether the setup secret is unexpired while no admin exists.
	PendingAdminSetup(ctx context.Context, setupHash string, now time.Time) (bool, error)
	// CreateAccount commits a member and its password hash; a taken email
	// is ErrConflict. It reads the settings in its transaction and refuses with
	// ErrDisabled unless every setting required is on.
	CreateAccount(ctx context.Context, account Account, passwordHash string, created time.Time, required Settings) error
	AppSettings(context.Context) (Settings, error)
	// PutAppSettings replaces the settings and notifies every replica at commit.
	PutAppSettings(ctx context.Context, settings Settings, actorID string, at time.Time) error
	AccountSummaries(context.Context) ([]AccountSummary, error)
	// SetAccountDisabled revokes sessions and tokens when disabling. It,
	// DeleteAccount and SetAdmin refuse with ErrLastAdmin when the change would
	// leave no enabled admin, deciding atomically with concurrent changes.
	SetAccountDisabled(ctx context.Context, accountID string, disabled bool, at time.Time) error
	DeleteAccount(context.Context, string) error
	SetAdmin(ctx context.Context, accountID string, admin bool) error
	// AccountPassword resolves an enabled account by its canonical email.
	AccountPassword(context.Context, string) (PasswordCredential, error)
	AccountPasswordByID(context.Context, string) (PasswordCredential, error)
	ReplacePassword(context.Context, string, string, string) error
	// PutPasswordReset replaces the account's pending reset secret.
	PutPasswordReset(ctx context.Context, accountID, tokenHash string, expires time.Time) error
	// PasswordReset resolves an unexpired reset of an enabled account to its credential.
	PasswordReset(ctx context.Context, tokenHash string, now time.Time) (PasswordCredential, error)
	// CompletePasswordReset consumes the unexpired reset, replaces the password
	// fenced on the expected hash and revokes browser sessions in one
	// transaction; API tokens are independent. A missing reset is ErrNotFound.
	CompletePasswordReset(ctx context.Context, tokenHash, expected, replacement string, now time.Time) error
	CreateSession(context.Context, string, string, string, string, time.Time) error
	ResolveSession(context.Context, string, time.Time) (Session, error)
	DeleteSession(context.Context, string) error
	CreateIdentityToken(context.Context, string, Token, string) error
	IdentityTokens(context.Context, string) ([]Token, error)
	DeleteIdentityToken(context.Context, string, string) error
	ResolveIdentityToken(context.Context, string, time.Time) (TokenIdentity, error)
	IdentityAccounts(context.Context) ([]Account, error)
	GoogleStore
}

Store operations preserve credential races atomically. Password replacement revokes sessions in its transaction; session creation compares the verified hash and refuses disabled accounts.

type Token

type Token struct {
	ID         string
	Name       string
	Purpose    Purpose
	CreatedAt  time.Time
	LastUsedAt *time.Time
	ExpiresAt  time.Time
}

type TokenIdentity

type TokenIdentity struct {
	Account   Account
	Purpose   Purpose
	ExpiresAt time.Time
}

Jump to

Keyboard shortcuts

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