auth

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: AGPL-3.0 Imports: 33 Imported by: 0

Documentation

Overview

Package auth resolves who is calling: API tokens for scripts and AI agents, one-time codes for linking chat accounts, and web accounts and sessions (ADR 0006).

Index

Constants

View Source
const BackupCodeCount = 10

BackupCodeCount is how many backup codes an enrolment gives.

View Source
const DefaultOIDCName = "Single sign-on"

DefaultOIDCName is what the sign-in button says when no name is set.

View Source
const DesktopTokenFile = "desktop-token"

DesktopTokenFile is the file, in the data directory, that holds it.

View Source
const DesktopTokenName = "Exhale menu bar" //nolint:gosec // a name, not a secret

DesktopTokenName names the token of the menu bar icon (ADR 0032).

View Source
const MaxDestinations = 5

MaxDestinations is how many destinations of one channel a member can have.

View Source
const MinPasswordLength = 10

MinPasswordLength follows NIST SP 800-63B: length over composition rules.

View Source
const TokenPrefix = "exhale_pat_"

TokenPrefix marks Exhale personal access tokens (secret-scanning friendly).

Variables

View Source
var (
	ErrSSOUnverified = errors.New("the provider has not verified this email")
	ErrSSOEmailTaken = errors.New("this email belongs to an account linked to another identity")
	ErrSSONoAccount  = errors.New("no account for this identity; ask the owner for an invite")
	ErrSSOInvite     = errors.New("the invite has expired or was already used")
	ErrSSOSetupCode  = errors.New("the setup code is wrong or expired")
)

Why a provider sign-in is refused. The web shows a sentence for each.

View Source
var ErrBadCode = errors.New("the code is wrong or was already used")

ErrBadCode is returned for a wrong, expired or already used code.

View Source
var ErrBadCredentials = errors.New("wrong email or password")

ErrBadCredentials is returned for a wrong email or password. It does not say which one was wrong.

View Source
var ErrUnauthorized = errors.New("unauthorized")

ErrUnauthorized is returned for missing or invalid credentials.

Functions

func BearerToken

func BearerToken(h http.Header) string

BearerToken extracts the token of an "Authorization: Bearer …" header.

func ChangePassword

func ChangePassword(ctx context.Context, st *store.Store, memberID, current, next string) error

ChangePassword replaces a member's password after checking the current one. Every other session of the member is signed out.

func CheckSecondFactor

func CheckSecondFactor(ctx context.Context, st *store.Store, memberID, code string) error

CheckSecondFactor accepts a code from the app (6 digits) or a backup code and uses it up: an app code's time step and a backup code never work again.

func CheckSetupCode

func CheckSetupCode(ctx context.Context, st *store.Store, code string) bool

CheckSetupCode reports whether code is the current setup code.

func CreateInvite

func CreateInvite(ctx context.Context, st *store.Store, actor app.Actor, role domain.Role) (string, time.Time, error)

CreateInvite returns a one-time invite code (valid 7 days) to join the actor's household. Only owners can invite.

func CreateLinkCode

func CreateLinkCode(ctx context.Context, st *store.Store, memberID, channel string) (string, error)

CreateLinkCode returns a one-time code (valid 10 minutes) that links a chat account to a member.

func CreateToken

func CreateToken(ctx context.Context, st *store.Store, memberID, name string, scope app.Scope) (string, error)

CreateToken issues a token for a member and returns its plaintext once.

func DisableTwoFactor

func DisableTwoFactor(ctx context.Context, st *store.Store, actor app.Actor, password, code string) error

DisableTwoFactor turns two-factor sign-in off for the actor, with the password and a code (or a backup code).

func EnableTwoFactor

func EnableTwoFactor(ctx context.Context, st *store.Store, actor app.Actor, code string) ([]string, error)

EnableTwoFactor confirms an enrolment with a first code from the app and returns the backup codes, shown once. Every session of the member is signed out; the caller renews its own (RefreshSession).

func EndSession

func EndSession(ctx context.Context, sm *scs.SessionManager) error

EndSession signs out.

func EnsureDesktopToken added in v0.4.0

func EnsureDesktopToken(ctx context.Context, st *store.Store, memberID, path string) error

EnsureDesktopToken makes sure path holds a working token of memberID for the menu bar icon, which runs as the same person on the same computer: whoever can read the file can read the database next to it anyway. A missing or revoked token is replaced, and the old ones are revoked.

func HasSetupCode

func HasSetupCode(ctx context.Context, st *store.Store) bool

HasSetupCode reports whether an unexpired setup code exists, so a restart does not invalidate a code someone already has.

func Hash

func Hash(secret string) string

Hash returns the stored form of a secret. Tokens are high-entropy random values, so a fast hash is the right tool (passwords use argon2id).

func HashPassword

func HashPassword(p string) (string, error)

HashPassword returns the stored form of a password.

func IdentityHost

func IdentityHost(m db.Member) string

IdentityHost returns the host of the provider a member is linked to, for display ("auth.example.com"), or "" if the member has no identity.

func Join

func Join(ctx context.Context, st *store.Store, in JoinInput) (app.Actor, error)

Join creates a member from an invite.

func JoinFromChat

func JoinFromChat(ctx context.Context, st *store.Store, code, channel, externalID, name string) (app.Actor, string, error)

JoinFromChat accepts an invite from a chat app: it creates the member (named after the chat profile, no web login yet) and links the chat. It returns the member and the household name.

func Login

func Login(ctx context.Context, st *store.Store, email, password string) (db.Member, error)

Login checks an email and password and returns the member.

func MemberForChannel

func MemberForChannel(ctx context.Context, st *store.Store, channel, externalID string) (app.Actor, error)

MemberForChannel returns the actor linked to a chat account.

func NewSessions

func NewSessions(st *store.Store, secure bool) *scs.SessionManager

NewSessions returns the session manager. secure marks the cookie Secure; set it when Exhale is served over HTTPS.

func NewSetupCode

func NewSetupCode(ctx context.Context, st *store.Store) (string, error)

NewSetupCode issues the one-time code the setup wizard asks for when it is opened from another machine. It replaces any earlier code and is valid for 24 hours.

func NormalizeEmail

func NormalizeEmail(email string) (string, error)

NormalizeEmail validates an email address and lowercases it.

func Random

func Random(n int) string

Random returns n random bytes encoded in lowercase base32.

func RedeemLinkCode

func RedeemLinkCode(ctx context.Context, st *store.Store, channel, code, externalID string) (string, error)

RedeemLinkCode links externalID on channel to the code's member.

func RefreshSession

func RefreshSession(ctx context.Context, sm *scs.SessionManager, st *store.Store, memberID string) error

RefreshSession keeps the current session valid after the member's own password change (which bumps the version and signs out other sessions).

func RegenerateBackupCodes

func RegenerateBackupCodes(ctx context.Context, st *store.Store, actor app.Actor, password string) ([]string, error)

RegenerateBackupCodes replaces every backup code after checking the password. The old ones stop working.

func RemoveMember

func RemoveMember(ctx context.Context, st *store.Store, actor app.Actor, id, payerTo string) error

RemoveMember removes a member with their chats, tokens and sessions. What they paid for moves to another member (the caller by default) so no subscription is left without a payer. Owners remove others; anyone can leave, except the last owner.

func RenameMember

func RenameMember(ctx context.Context, st *store.Store, actor app.Actor, id, name string) error

RenameMember changes a member's display name. Members rename themselves; owners can rename anyone.

func ResetTwoFactor

func ResetTwoFactor(ctx context.Context, st *store.Store, actor app.Actor, memberID string) error

ResetTwoFactor turns another member's two-factor sign-in off, for someone who lost their phone and their backup codes. Owners only.

func ResetTwoFactorFromServer

func ResetTwoFactorFromServer(ctx context.Context, st *store.Store, memberID string) (bool, error)

ResetTwoFactorFromServer turns a member's two-factor sign-in off from the command line, for lock-out recovery. It reports whether it was on.

func ResolveToken

func ResolveToken(ctx context.Context, st *store.Store, plain string) (app.Actor, error)

ResolveToken returns the actor of a plaintext token.

func RevokeToken

func RevokeToken(ctx context.Context, st *store.Store, memberID, id string) error

RevokeToken revokes one of a member's tokens.

func SessionActor

func SessionActor(ctx context.Context, sm *scs.SessionManager, st *store.Store) (app.Actor, bool)

SessionActor returns the member signed in on this request, if any.

func SetLanguage

func SetLanguage(ctx context.Context, st *store.Store, memberID, code string) error

SetLanguage saves the language a member chose; empty follows the household's.

func SetMemberRole

func SetMemberRole(ctx context.Context, st *store.Store, actor app.Actor, id string, role domain.Role) error

SetMemberRole makes a member an owner or a plain member. Owners only; the last owner cannot be demoted.

func SetPassword

func SetPassword(ctx context.Context, st *store.Store, memberID, password string) error

SetPassword sets a password without checking the old one (CLI recovery) and signs the member out everywhere.

func Setup

func Setup(ctx context.Context, svc *app.Service, in SetupInput, local bool) (app.Actor, error)

Setup creates the owner account. On a fresh instance it creates the household too; on one made with `exhale init` it gives the existing owner a login. local skips the setup code for requests from the server's own machine.

func SetupRequired

func SetupRequired(ctx context.Context, st *store.Store) (bool, error)

SetupRequired reports whether nobody can sign in yet (no member has a password or a provider identity), so the setup wizard must run.

func SignInWithIdentity

func SignInWithIdentity(ctx context.Context, svc *app.Service, id Identity, in SSOSignIn) (app.Actor, error)

SignInWithIdentity returns the member of a provider identity. These rules are the security core of single sign-on (ADR 0006):

  1. An identity already linked to a member is that member, whatever its email is today.
  2. On a fresh instance (nobody can sign in yet) the identity becomes the owner, with the same setup-code rule as the password wizard.
  3. An identity whose email is a member's email is linked to that member only if the provider says the email is verified, and only if the member is not linked to another identity of the same provider.
  4. Otherwise an account is created only with a valid invite.

Authenticating at the provider never creates an account by itself.

func StartSession

func StartSession(ctx context.Context, sm *scs.SessionManager, st *store.Store, memberID string) error

StartSession signs a member in on this request's session, renewing the token to prevent session fixation.

func TwoFactorEnabled

func TwoFactorEnabled(ctx context.Context, st *store.Store, memberID string) (bool, error)

TwoFactorEnabled reports whether a member must give a code after the password.

func TwoFactorMembers

func TwoFactorMembers(ctx context.Context, st *store.Store, actor app.Actor) ([]string, error)

TwoFactorMembers lists the members of the actor's household with two-factor sign-in on. Owners only.

func Unlink(ctx context.Context, st *store.Store, memberID, id string) error

Unlink removes one of a member's chat links.

func ValidatePassword

func ValidatePassword(p string) error

ValidatePassword checks a new password.

Types

type Identity

type Identity struct {
	Issuer        string // as published by the provider's discovery document
	Subject       string // stable id of the person at that provider
	Email         string
	EmailVerified bool
	Name          string
}

Identity is who the provider says signed in.

type InviteInfo

type InviteInfo struct {
	HouseholdName string `json:"householdName"`
	InvitedBy     string `json:"invitedBy"`
	Role          string `json:"role"`
}

InviteInfo describes a pending invite to the person opening it.

func LookupInvite

func LookupInvite(ctx context.Context, st *store.Store, code string) (InviteInfo, error)

LookupInvite returns what an invite is for.

type JoinInput

type JoinInput struct {
	Code     string `json:"code"`
	Name     string `json:"name"`
	Email    string `json:"email"`
	Password string `json:"password"`
	// Language is the one the person used to join: it becomes theirs when
	// it is not the household's.
	Language string `json:"language,omitempty" doc:"code of a registered language; kept as the person's own when it differs from the household's"`
}

JoinInput accepts an invite.

type LinkInfo

type LinkInfo struct {
	ID        string `json:"id"`
	Channel   string `json:"channel"`
	CreatedAt string `json:"createdAt"`
	// Destination and HasTemplate are filled by the API for notify-only
	// channels (email, ntfy, webhook); ExternalID never leaves the server.
	Destination string `json:"destination,omitempty" doc:"email address, ntfy topic or webhook host; empty for chat accounts"`
	HasTemplate bool   `json:"hasTemplate,omitempty" doc:"the webhook has its own body template"`
	ExternalID  string `json:"-"`
}

LinkInfo describes a linked chat account or notify-only destination.

func AddDestination

func AddDestination(ctx context.Context, st *store.Store, memberID, channel, externalID string) (LinkInfo, error)

AddDestination links a destination to a member. The caller validates externalID for its channel first. A destination belongs to one member: adding someone else's is refused, never moved.

func Link(ctx context.Context, st *store.Store, memberID, id string) (LinkInfo, error)

Link returns one of a member's links.

func ListLinks(ctx context.Context, st *store.Store, memberID string) ([]LinkInfo, error)

ListLinks returns a member's linked chat accounts.

type OIDC

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

OIDC talks to the configured provider. Discovery happens on first use and is retried until it works, so Exhale starts while the provider is down.

func NewOIDC

func NewOIDC(cfg OIDCConfig) (*OIDC, error)

NewOIDC validates the configuration. It does not touch the network.

func (*OIDC) AuthURL

func (o *OIDC) AuthURL(ctx context.Context, redirectURI, state, nonce, verifier string) (string, error)

AuthURL returns where to send the browser to sign in. state protects the callback against forgery, nonce binds the ID token to this attempt and verifier is the PKCE secret (only its S256 hash leaves the server here).

func (*OIDC) Discover

func (o *OIDC) Discover(ctx context.Context) error

Discover reads the provider's configuration if it is not known yet.

func (*OIDC) Exchange

func (o *OIDC) Exchange(ctx context.Context, redirectURI, code, verifier, nonce string) (Identity, error)

Exchange trades the authorization code for the person's identity. It verifies the ID token (signature, issuer, audience, expiry) and its nonce. Errors never contain tokens or the client secret.

func (*OIDC) InsecureIssuer

func (o *OIDC) InsecureIssuer() bool

InsecureIssuer reports whether the issuer is reached over plain HTTP, which lets anyone on the network path forge sign-ins.

func (*OIDC) Name

func (o *OIDC) Name() string

Name is the provider's display name for the sign-in button.

type OIDCConfig

type OIDCConfig struct {
	Issuer       string
	ClientID     string
	ClientSecret string // empty for a public client (PKCE only)
	Name         string
	Scopes       []string
}

OIDCConfig is the provider registration, set with flags or environment.

type SSOSignIn

type SSOSignIn struct {
	Invite string // invite code, when joining a household

	// First run only.
	SetupCode string
	Local     bool // request from the server's own machine: no setup code
	Currency  string
	Timezone  string
	Locale    string
}

SSOSignIn is what the browser carried through the provider's pages.

type SessionStore

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

SessionStore keeps scs sessions in SQLite.

func (SessionStore) Commit

func (s SessionStore) Commit(token string, b []byte, expiry time.Time) error

func (SessionStore) CommitCtx

func (s SessionStore) CommitCtx(ctx context.Context, token string, b []byte, expiry time.Time) error

func (SessionStore) Delete

func (s SessionStore) Delete(token string) error

func (SessionStore) DeleteCtx

func (s SessionStore) DeleteCtx(ctx context.Context, token string) error

func (SessionStore) Find

func (s SessionStore) Find(token string) ([]byte, bool, error)

func (SessionStore) FindCtx

func (s SessionStore) FindCtx(ctx context.Context, token string) ([]byte, bool, error)

type SetupInput

type SetupInput struct {
	Code          string `json:"code,omitempty" doc:"setup code printed in the server log; not needed from the server's own machine"`
	Name          string `json:"name"`
	Email         string `json:"email"`
	Password      string `json:"password"`
	HouseholdName string `json:"householdName,omitempty"`
	Currency      string `json:"currency,omitempty"`
	Timezone      string `json:"timezone,omitempty"`
	Locale        string `json:"locale,omitempty"`
}

SetupInput creates the first account and, if needed, the household.

type TokenInfo

type TokenInfo struct {
	ID         string `json:"id"`
	Name       string `json:"name"`
	Scope      string `json:"scope"`
	CreatedAt  string `json:"createdAt"`
	LastUsedAt string `json:"lastUsedAt,omitempty"`
}

TokenInfo describes an API token without its secret.

func ListTokens

func ListTokens(ctx context.Context, st *store.Store, memberID string) ([]TokenInfo, error)

ListTokens returns a member's active tokens.

type TwoFactorSetup

type TwoFactorSetup struct {
	Secret string `json:"secret" doc:"base32 secret to type into the app"`
	URI    string `json:"uri" doc:"otpauth:// link, shown as a QR code"`
}

TwoFactorSetup is what an authenticator app needs. It is shown during enrolment only; the secret is never returned again.

func BeginTwoFactor

func BeginTwoFactor(ctx context.Context, st *store.Store, memberID, password string) (TwoFactorSetup, error)

BeginTwoFactor checks the password and starts an enrolment with a new secret. Nothing changes at sign-in until EnableTwoFactor confirms a code.

type TwoFactorStatus

type TwoFactorStatus struct {
	Enabled         bool `json:"enabled"`
	BackupCodesLeft int  `json:"backupCodesLeft" doc:"unused backup codes"`
}

TwoFactorStatus is what the account settings show.

func GetTwoFactorStatus

func GetTwoFactorStatus(ctx context.Context, st *store.Store, memberID string) (TwoFactorStatus, error)

GetTwoFactorStatus returns a member's two-factor state.

Jump to

Keyboard shortcuts

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