Documentation
¶
Overview ¶
Package auth owns identity: users, their credentials, and their sessions. It is the only place in the system that handles a password, and the only place that can turn a session token into a user. Other services hold a user reference and nothing else.
Index ¶
- Constants
- Variables
- func NormalizeEmail(email string) string
- func NormalizeUsername(username string) string
- func UsernameFromEmail(email string) string
- func ValidateEmail(email string) error
- func ValidateUsername(username string) error
- type ChangePasswordRequest
- type FirstUserHook
- type FirstUserHookFunc
- type Identity
- type IdentityRepository
- type Links
- type Mailer
- type Options
- type ProviderClaims
- type RegisterRequest
- type Service
- func (svc *Service) Authenticate(ctx context.Context, login, password string) (*User, error)
- func (svc *Service) ChangePassword(ctx context.Context, req ChangePasswordRequest) error
- func (svc *Service) CompleteSetup(ctx context.Context, req RegisterRequest) (*User, error)
- func (svc *Service) ConfirmEmailChange(ctx context.Context, secret string) (*User, error)
- func (svc *Service) ConsumeEmailLogin(ctx context.Context, secret string, allowSignUp bool) (*User, error)
- func (svc *Service) CreateSession(ctx context.Context, userID string, meta SessionMeta) (string, *Session, error)
- func (svc *Service) DeleteExpiredSessions(ctx context.Context) (int64, error)
- func (svc *Service) DeleteExpiredTokens(ctx context.Context) (int64, error)
- func (svc *Service) DeleteUser(ctx context.Context, id string) error
- func (svc *Service) GetUser(ctx context.Context, id string) (*User, error)
- func (svc *Service) GetUserByEmail(ctx context.Context, email string) (*User, error)
- func (svc *Service) GetUserByRef(ctx context.Context, userRef string) (*User, error)
- func (svc *Service) GetUserByUsername(ctx context.Context, username string) (*User, error)
- func (svc *Service) LinkProvider(ctx context.Context, userID, provider string, claims ProviderClaims) error
- func (svc *Service) ListIdentities(ctx context.Context, userID string) ([]Identity, error)
- func (svc *Service) ListUsers(ctx context.Context, limit int) ([]User, error)
- func (svc *Service) Register(ctx context.Context, req RegisterRequest) (*User, error)
- func (svc *Service) RequestEmailChange(ctx context.Context, userID, newEmail string) error
- func (svc *Service) RequestEmailLogin(ctx context.Context, email, next string, allowSignUp bool) error
- func (svc *Service) RequestPasswordReset(ctx context.Context, email string) error
- func (svc *Service) ResetPassword(ctx context.Context, secret, newPassword string) (*User, error)
- func (svc *Service) ResolveSession(ctx context.Context, token string) (*Session, *User, error)
- func (svc *Service) RevokeSession(ctx context.Context, token string) error
- func (svc *Service) SendVerification(ctx context.Context, userID string) error
- func (svc *Service) SetupOpen(ctx context.Context) (bool, error)
- func (svc *Service) SignInWithProvider(ctx context.Context, provider string, claims ProviderClaims, allowSignUp bool) (*User, error)
- func (svc *Service) UnlinkProvider(ctx context.Context, userID, provider, subject string) error
- func (svc *Service) UpdateProfile(ctx context.Context, req UpdateProfileRequest) (*User, error)
- func (svc *Service) VerifyEmail(ctx context.Context, secret string) (*User, error)
- type Session
- type SessionMeta
- type SessionRepository
- type Token
- type TokenPurpose
- type TokenRepository
- type UpdateProfileRequest
- type User
- type UserRepository
Constants ¶
const ( // ServiceName is the service segment of every reference this package owns. ServiceName = "auth" // UserResourceType is the type segment of a user reference. UserResourceType = "user" // GroupAuthenticated holds everyone with a live session. authz knows it only as a subject that // a caller names along with the user. GroupAuthenticated = "urn:" + ServiceName + ":group:authenticated" )
const ( MinUsernameLength = 3 MaxUsernameLength = 32 )
const MaxEmailLength = 254
Variables ¶
var ( // ErrInvalidCredentials covers an unknown account, a wrong password and an account with no // password alike. Telling them apart would turn the login form into an account oracle. ErrInvalidCredentials = errors.New("invalid username, email or password") ErrUserNotFound = errors.New("user not found") // ErrInvalidUsername wraps every username rule failure, so callers can show the reason without // matching on message text. ErrInvalidUsername = errors.New("invalid username") ErrUsernameTaken = errors.New("username is already taken") ErrInvalidEmail = errors.New("invalid email address") ErrEmailTaken = errors.New("email address is already in use") ErrSessionNotFound = errors.New("session not found") // ErrTokenNotFound covers a token that never existed, was used, has expired, or is for another // purpose. A link that does not work gets one answer whatever the reason. ErrTokenNotFound = errors.New("link is invalid or has expired") ErrIdentityNotFound = errors.New("identity not found") ErrIdentityTaken = errors.New("identity is linked to another account") // ErrAccountExists stops a provider sign-in from taking over an account whose email address // nobody has proved. The person signs in another way and links the provider from settings. ErrAccountExists = errors.New("an account with this email address already exists") // ErrLastSignInMethod keeps an account from losing its only way in. ErrLastSignInMethod = errors.New("this is the account's only way to sign in") ErrSignUpClosed = errors.New("sign-up is closed") // ErrSetupClosed is returned once the first user exists. Setup never reopens. ErrSetupClosed = errors.New("setup is already complete") )
Functions ¶
func NormalizeEmail ¶
NormalizeEmail lowercases the whole address. The local part is case-sensitive by the letter of RFC 5321, but no mainstream provider treats it so, and two accounts differing only in case would be a support problem rather than a feature.
func NormalizeUsername ¶
NormalizeUsername is applied before every lookup and every insert, so "Ada" and "ada" are the same account rather than two.
func UsernameFromEmail ¶
UsernameFromEmail proposes a valid username for an account whose owner never picked one: the email's local part with every other character turned into a hyphen.
func ValidateEmail ¶
ValidateEmail accepts a normalized bare address: no display name, no angle brackets.
func ValidateUsername ¶
ValidateUsername accepts a normalized username.
TODO: usernames are not NFKC-normalized or checked for confusables (UTS #39), so two visually identical names can coexist. That matters once usernames are shown as an identity claim.
Types ¶
type ChangePasswordRequest ¶
type FirstUserHook ¶
FirstUserHook runs once, when setup creates the very first user, so the composition root can make that account an administrator without auth knowing an authorization service exists.
type FirstUserHookFunc ¶
func (FirstUserHookFunc) OnFirstUser ¶
func (f FirstUserHookFunc) OnFirstUser(ctx context.Context, userRef string) error
type Identity ¶
type Identity struct {
Provider string
// Subject is the provider's stable id for the account. An email address can change; this
// cannot.
Subject string
UserID string
Email string
CreatedAt time.Time
}
Identity links an account at an external provider, such as Google, to a user.
type IdentityRepository ¶
type IdentityRepository interface {
Insert(ctx context.Context, identity *Identity) error
Get(ctx context.Context, provider, subject string) (*Identity, error)
// ListByUser orders by provider, then subject.
ListByUser(ctx context.Context, userID string) ([]Identity, error)
Delete(ctx context.Context, userID, provider, subject string) error
}
IdentityRepository returns ErrIdentityNotFound for an absent identity and ErrIdentityTaken for a collision.
type Links ¶
type Links interface {
VerifyEmail(token string) string
ResetPassword(token string) string
EmailLogin(token, next string) string
ConfirmEmailChange(token string) string
}
Links turns a token into the absolute URL mailed to the person. The pages own their paths, so they build the links.
type Options ¶
type Options struct {
Passwords *hash.Registry
FirstUserHook FirstUserHook
SessionTTL time.Duration
Mailer Mailer
Links Links
Logger *slog.Logger
}
Options are the dependencies a Service needs beyond its storage.
type ProviderClaims ¶
ProviderClaims is what a provider vouches for about the person signing in.
type RegisterRequest ¶
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
func NewService ¶
func NewService( userRepo UserRepository, sessionRepo SessionRepository, tokenRepo TokenRepository, identityRepo IdentityRepository, options Options, ) (*Service, error)
func (*Service) Authenticate ¶
Authenticate takes a username or an email address as login.
func (*Service) ChangePassword ¶
func (svc *Service) ChangePassword(ctx context.Context, req ChangePasswordRequest) error
ChangePassword checks the current password only when the account has one. Setting a first password on an account without one relies on the caller having just re-authenticated the person.
func (*Service) CompleteSetup ¶
CompleteSetup creates the first user and hands its reference to the first-user hook. A failing hook removes the user, so setup stays open rather than leaving an account nobody can use.
func (*Service) ConfirmEmailChange ¶
func (*Service) ConsumeEmailLogin ¶
func (svc *Service) ConsumeEmailLogin( ctx context.Context, secret string, allowSignUp bool, ) (*User, error)
ConsumeEmailLogin returns the account the link signs in to, creating it for a link that was sent to an address with no account.
func (*Service) CreateSession ¶
func (svc *Service) CreateSession( ctx context.Context, userID string, meta SessionMeta, ) (string, *Session, error)
CreateSession issues a new token. It is called on every login, so there is no existing session identifier an attacker could have fixed beforehand.
func (*Service) DeleteExpiredSessions ¶
func (*Service) DeleteExpiredTokens ¶
func (*Service) DeleteUser ¶
Sessions go with the account, so anyone signed in as it is signed out at once.
TODO: resources this user created in other services still carry their reference. References are opaque and never dereferenced, so nothing breaks, but an operator may want them reassigned.
func (*Service) GetUserByEmail ¶
GetUserByEmail takes the address in any case.
func (*Service) GetUserByRef ¶
func (*Service) GetUserByUsername ¶
GetUserByUsername takes the username in any case.
func (*Service) LinkProvider ¶
func (svc *Service) LinkProvider( ctx context.Context, userID, provider string, claims ProviderClaims, ) error
LinkProvider adds a provider identity to a signed-in account. An identity already on this account is no error; one on another account is.
func (*Service) ListIdentities ¶
func (*Service) Register ¶
FIXME: no rate limiting. Registration, login, and password change are all open to unlimited attempts (OWASP ASVS V2.2.1). A limiter keyed by username and client address is the next step.
A verification link goes to the new address. Failing to send it does not fail the registration: the person can ask for another from their settings.
func (*Service) RequestEmailChange ¶
RequestEmailChange mails a confirmation link to the new address. The account keeps its current address until the link is followed, so a typo cannot lock anyone out.
func (*Service) RequestEmailLogin ¶
func (svc *Service) RequestEmailLogin( ctx context.Context, email, next string, allowSignUp bool, ) error
RequestEmailLogin mails a sign-in link. An address with no account gets a link that creates one when allowSignUp is set, and nothing otherwise. The answer is the same either way.
FIXME: no rate limiting, as for RequestPasswordReset.
func (*Service) RequestPasswordReset ¶
RequestPasswordReset answers the same whether or not the address has an account, so the form cannot be used to find out which addresses are registered.
FIXME: no rate limiting, so anyone can make this send mail to any address repeatedly. It shares the limiter Register is waiting for.
func (*Service) ResetPassword ¶
ResetPassword also verifies the address, since following the link proved it, and signs out every session: whoever knew the old password is out.
func (*Service) ResolveSession ¶
ResolveSession turns a token into its session and user. An expired session is deleted and reported as absent so a stale cookie heals itself on the next request.
func (*Service) RevokeSession ¶
func (*Service) SendVerification ¶
SendVerification mails a link that proves the account's address. An address already verified is left alone.
func (*Service) SignInWithProvider ¶
func (svc *Service) SignInWithProvider( ctx context.Context, provider string, claims ProviderClaims, allowSignUp bool, ) (*User, error)
SignInWithProvider returns the account the provider identity belongs to. An identity seen for the first time links to the account with the same address when both sides have proved that address, and otherwise creates an account when allowSignUp is set.
func (*Service) UnlinkProvider ¶
UnlinkProvider refuses to remove the account's last way in: no password, no address an email link can reach, and no other provider.
func (*Service) UpdateProfile ¶
type Session ¶
type Session struct {
ID string
UserID string
// TokenHash is the SHA-256 of the token handed to the browser. The token is never stored, so a
// copy of the database does not hand over live sessions.
TokenHash string
CreatedAt time.Time
// ExpiresAt is absolute, not sliding. A stolen token that is in constant use still dies.
ExpiresAt time.Time
LastSeen time.Time
UserAgent string
IP string
}
type SessionMeta ¶
type SessionRepository ¶
type SessionRepository interface {
Insert(ctx context.Context, session *Session) error
GetByTokenHash(ctx context.Context, tokenHash string) (*Session, error)
Touch(ctx context.Context, id string, lastSeen time.Time) error
DeleteByTokenHash(ctx context.Context, tokenHash string) error
// DeleteByUser removes every session of a user except exceptSessionID, which may be
// empty to remove all of them.
DeleteByUser(ctx context.Context, userID, exceptSessionID string) error
DeleteExpired(ctx context.Context, now time.Time) (int64, error)
}
SessionRepository returns ErrSessionNotFound for an absent session.
type Token ¶
type Token struct {
TokenHash string
Purpose TokenPurpose
// UserID is empty for an email-login token addressed to someone with no account yet.
UserID string
// Email is where the link was sent. For a change-email token it is the new address.
Email string
CreatedAt time.Time
ExpiresAt time.Time
}
Token is a single-use secret mailed as a link. Like a session, only its hash is stored.
type TokenPurpose ¶
type TokenPurpose string
const ( TokenVerifyEmail TokenPurpose = "verify-email" TokenResetPassword TokenPurpose = "reset-password" TokenEmailLogin TokenPurpose = "email-login" TokenChangeEmail TokenPurpose = "change-email" )
type TokenRepository ¶
type TokenRepository interface {
Insert(ctx context.Context, token *Token) error
// Consume deletes the token and returns it, so a link works once however many requests
// race to use it. An expired token or one for another purpose is not found.
Consume(
ctx context.Context,
tokenHash string,
purpose TokenPurpose,
now time.Time,
) (*Token, error)
DeleteByUser(ctx context.Context, userID string, purpose TokenPurpose) error
DeleteExpired(ctx context.Context, now time.Time) (int64, error)
}
TokenRepository returns ErrTokenNotFound for an absent token.
type UpdateProfileRequest ¶
type User ¶
type User struct {
ID string
Username string
// Email is empty only for accounts created before email addresses were collected.
Email string
// EmailVerifiedAt is set once the person follows a link sent to Email, or a provider vouches
// for it.
EmailVerifiedAt *time.Time
Name string
// PasswordHash must never leave this service. Nothing outside auth has a reason to read it.
// It is empty for an account that signs in by email link or a provider only.
PasswordHash string
CreatedAt time.Time
UpdatedAt time.Time
}
func (*User) EmailVerified ¶
func (*User) HasPassword ¶
func (*User) SessionGroups ¶
SessionGroups are the groups of a user who has signed in.
type UserRepository ¶
type UserRepository interface {
Insert(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
Get(ctx context.Context, id string) (*User, error)
GetByUsername(ctx context.Context, username string) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
List(ctx context.Context, limit int) ([]User, error)
Count(ctx context.Context) (int, error)
}
UserRepository returns ErrUserNotFound for an absent user, and ErrUsernameTaken and ErrEmailTaken for collisions. Deleting a user removes its sessions, tokens and identities with it.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package app is the composition root of the identity service running on its own.
|
Package app is the composition root of the identity service running on its own. |
|
Package backend is the one place that maps a storage driver to auth's repositories.
|
Package backend is the one place that maps a storage driver to auth's repositories. |
|
Package client calls the identity service over HTTP.
|
Package client calls the identity service over HTTP. |
|
cmd
|
|
|
auth
command
|
|
|
Package hash turns passwords into storable strings and checks them again.
|
Package hash turns passwords into storable strings and checks them again. |
|
Package httpapi serves the part of the identity service that other services need.
|
Package httpapi serves the part of the identity service that other services need. |
|
Package kit builds the identity service and its pages from configuration, so every binary that runs auth, on its own or inside an application, wires it the same way.
|
Package kit builds the identity service and its pages from configuration, so every binary that runs auth, on its own or inside an application, wires it the same way. |
|
Package oidc signs people in with an OpenID Connect provider, such as Google or Apple, using the authorization code flow with PKCE.
|
Package oidc signs people in with an OpenID Connect provider, such as Google or Apple, using the authorization code flow with PKCE. |
|
oidctest
Package oidctest runs an OpenID Connect provider in the test process, so sign-in with Google or Apple can be tested end to end without either.
|
Package oidctest runs an OpenID Connect provider in the test process, so sign-in with Google or Apple can be tested end to end without either. |
|
Package postgres stores auth's users and sessions in Postgres.
|
Package postgres stores auth's users and sessions in Postgres. |
|
Package repotest is the contract every auth repository must satisfy.
|
Package repotest is the contract every auth repository must satisfy. |
|
Package sqlite stores auth's users and sessions in SQLite.
|
Package sqlite stores auth's users and sessions in SQLite. |
|
Package ui serves the browser pages that must handle a password: sign in, register, first-run setup, and password change.
|
Package ui serves the browser pages that must handle a password: sign in, register, first-run setup, and password change. |