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
- Variables
- func BearerToken(h http.Header) string
- func ChangePassword(ctx context.Context, st *store.Store, memberID, current, next string) error
- func CheckSecondFactor(ctx context.Context, st *store.Store, memberID, code string) error
- func CheckSetupCode(ctx context.Context, st *store.Store, code string) bool
- func CreateInvite(ctx context.Context, st *store.Store, actor app.Actor, role domain.Role) (string, time.Time, error)
- func CreateLinkCode(ctx context.Context, st *store.Store, memberID, channel string) (string, error)
- func CreateToken(ctx context.Context, st *store.Store, memberID, name string, scope app.Scope) (string, error)
- func DisableTwoFactor(ctx context.Context, st *store.Store, actor app.Actor, password, code string) error
- func EnableTwoFactor(ctx context.Context, st *store.Store, actor app.Actor, code string) ([]string, error)
- func EndSession(ctx context.Context, sm *scs.SessionManager) error
- func HasSetupCode(ctx context.Context, st *store.Store) bool
- func Hash(secret string) string
- func HashPassword(p string) (string, error)
- func IdentityHost(m db.Member) string
- func Join(ctx context.Context, st *store.Store, in JoinInput) (app.Actor, error)
- func JoinFromChat(ctx context.Context, st *store.Store, code, channel, externalID, name string) (app.Actor, string, error)
- func Login(ctx context.Context, st *store.Store, email, password string) (db.Member, error)
- func MemberForChannel(ctx context.Context, st *store.Store, channel, externalID string) (app.Actor, error)
- func NewSessions(st *store.Store, secure bool) *scs.SessionManager
- func NewSetupCode(ctx context.Context, st *store.Store) (string, error)
- func NormalizeEmail(email string) (string, error)
- func Random(n int) string
- func RedeemLinkCode(ctx context.Context, st *store.Store, channel, code, externalID string) (string, error)
- func RefreshSession(ctx context.Context, sm *scs.SessionManager, st *store.Store, memberID string) error
- func RegenerateBackupCodes(ctx context.Context, st *store.Store, actor app.Actor, password string) ([]string, error)
- func RemoveMember(ctx context.Context, st *store.Store, actor app.Actor, id, payerTo string) error
- func RenameMember(ctx context.Context, st *store.Store, actor app.Actor, id, name string) error
- func ResetTwoFactor(ctx context.Context, st *store.Store, actor app.Actor, memberID string) error
- func ResetTwoFactorFromServer(ctx context.Context, st *store.Store, memberID string) (bool, error)
- func ResolveToken(ctx context.Context, st *store.Store, plain string) (app.Actor, error)
- func RevokeToken(ctx context.Context, st *store.Store, memberID, id string) error
- func SessionActor(ctx context.Context, sm *scs.SessionManager, st *store.Store) (app.Actor, bool)
- func SetLanguage(ctx context.Context, st *store.Store, memberID, code string) error
- func SetMemberRole(ctx context.Context, st *store.Store, actor app.Actor, id string, ...) error
- func SetPassword(ctx context.Context, st *store.Store, memberID, password string) error
- func Setup(ctx context.Context, svc *app.Service, in SetupInput, local bool) (app.Actor, error)
- func SetupRequired(ctx context.Context, st *store.Store) (bool, error)
- func SignInWithIdentity(ctx context.Context, svc *app.Service, id Identity, in SSOSignIn) (app.Actor, error)
- func StartSession(ctx context.Context, sm *scs.SessionManager, st *store.Store, memberID string) error
- func TwoFactorEnabled(ctx context.Context, st *store.Store, memberID string) (bool, error)
- func TwoFactorMembers(ctx context.Context, st *store.Store, actor app.Actor) ([]string, error)
- func Unlink(ctx context.Context, st *store.Store, memberID, id string) error
- func ValidatePassword(p string) error
- type Identity
- type InviteInfo
- type JoinInput
- type LinkInfo
- type OIDC
- func (o *OIDC) AuthURL(ctx context.Context, redirectURI, state, nonce, verifier string) (string, error)
- func (o *OIDC) Discover(ctx context.Context) error
- func (o *OIDC) Exchange(ctx context.Context, redirectURI, code, verifier, nonce string) (Identity, error)
- func (o *OIDC) InsecureIssuer() bool
- func (o *OIDC) Name() string
- type OIDCConfig
- type SSOSignIn
- type SessionStore
- func (s SessionStore) Commit(token string, b []byte, expiry time.Time) error
- func (s SessionStore) CommitCtx(ctx context.Context, token string, b []byte, expiry time.Time) error
- func (s SessionStore) Delete(token string) error
- func (s SessionStore) DeleteCtx(ctx context.Context, token string) error
- func (s SessionStore) Find(token string) ([]byte, bool, error)
- func (s SessionStore) FindCtx(ctx context.Context, token string) ([]byte, bool, error)
- type SetupInput
- type TokenInfo
- type TwoFactorSetup
- type TwoFactorStatus
Constants ¶
const BackupCodeCount = 10
BackupCodeCount is how many backup codes an enrolment gives.
const DefaultOIDCName = "Single sign-on"
DefaultOIDCName is what the sign-in button says when no name is set.
const MaxDestinations = 5
MaxDestinations is how many destinations of one channel a member can have.
const MinPasswordLength = 10
MinPasswordLength follows NIST SP 800-63B: length over composition rules.
const TokenPrefix = "exhale_pat_"
TokenPrefix marks Exhale personal access tokens (secret-scanning friendly).
Variables ¶
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.
var ErrBadCode = errors.New("the code is wrong or was already used")
ErrBadCode is returned for a wrong, expired or already used code.
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.
ErrUnauthorized is returned for missing or invalid credentials.
Functions ¶
func BearerToken ¶
BearerToken extracts the token of an "Authorization: Bearer …" header.
func ChangePassword ¶
ChangePassword replaces a member's password after checking the current one. Every other session of the member is signed out.
func CheckSecondFactor ¶
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 ¶
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 ¶
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 HasSetupCode ¶
HasSetupCode reports whether an unexpired setup code exists, so a restart does not invalidate a code someone already has.
func Hash ¶
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 ¶
HashPassword returns the stored form of a password.
func IdentityHost ¶
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 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 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 ¶
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 ¶
NormalizeEmail validates an email address and lowercases it.
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 ¶
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 ¶
RenameMember changes a member's display name. Members rename themselves; owners can rename anyone.
func ResetTwoFactor ¶
ResetTwoFactor turns another member's two-factor sign-in off, for someone who lost their phone and their backup codes. Owners only.
func ResetTwoFactorFromServer ¶
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 ¶
ResolveToken returns the actor of a plaintext token.
func RevokeToken ¶
RevokeToken revokes one of a member's tokens.
func SessionActor ¶
SessionActor returns the member signed in on this request, if any.
func SetLanguage ¶
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 ¶
SetPassword sets a password without checking the old one (CLI recovery) and signs the member out everywhere.
func Setup ¶
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 ¶
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):
- An identity already linked to a member is that member, whatever its email is today.
- On a fresh instance (nobody can sign in yet) the identity becomes the owner, with the same setup-code rule as the password wizard.
- 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.
- 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 ¶
TwoFactorEnabled reports whether a member must give a code after the password.
func TwoFactorMembers ¶
TwoFactorMembers lists the members of the actor's household with two-factor sign-in on. Owners only.
func ValidatePassword ¶
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 ¶
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.
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) 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 ¶
InsecureIssuer reports whether the issuer is reached over plain HTTP, which lets anyone on the network path forge sign-ins.
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) Delete ¶
func (s SessionStore) Delete(token string) 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.
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.