Documentation
¶
Index ¶
- Constants
- Variables
- func GenerateRecoveryCodes(n int) ([]string, error)
- func GenerateResetToken() (string, error)
- func HashPassword(password string) (string, error)
- func HashToken(raw string) string
- func NewTOTPKey(email string) (secret, otpauthURL string, err error)
- func NormaliseRecoveryCode(code string) string
- func ValidateTOTPCode(secret, code string) bool
- func VerifyPassword(password, stored string) bool
- type AuthProvider
- type AuthService
- func (s *AuthService) AuthorizationURL(ctx context.Context, providerID uuid.UUID, callbackURL string) (url, state, codeVerifier string, err error)
- func (s *AuthService) ExchangeCode(ctx context.Context, req OIDCCallbackRequest) (*TokenPair, error)
- func (s *AuthService) IssueTokenPairForUser(ctx context.Context, user *db.User) (*TokenPair, error)
- func (s *AuthService) JWTManager() *JWTManager
- func (s *AuthService) ListEnabledProviders(ctx context.Context) ([]*db.OIDCProvider, error)
- func (s *AuthService) LoginLocal(ctx context.Context, req LoginRequest) (*TokenPair, error)
- func (s *AuthService) Logout(ctx context.Context, rawToken string) error
- func (s *AuthService) LogoutAllSessions(ctx context.Context, userID uuid.UUID) error
- func (s *AuthService) RefreshToken(ctx context.Context, rawToken string) (*TokenPair, error)
- func (s *AuthService) RevokeAccessToken(jti string, expiresAt time.Time)
- func (s *AuthService) ValidateAccessToken(tokenString string) (*Claims, error)
- type Claims
- type Denylist
- type JWTManager
- type LocalAuthProvider
- func (p *LocalAuthProvider) IssueTokenPair(ctx context.Context, user *db.User) (*TokenPair, error)
- func (p *LocalAuthProvider) Login(ctx context.Context, req LoginRequest) (*TokenPair, error)
- func (p *LocalAuthProvider) Logout(ctx context.Context, rawToken string) error
- func (p *LocalAuthProvider) ProviderType() string
- func (p *LocalAuthProvider) RefreshToken(ctx context.Context, rawToken string) (*TokenPair, error)
- type LoginRequest
- type OIDCAuthProvider
- func (p *OIDCAuthProvider) AuthorizationURL(ctx context.Context, providerID uuid.UUID, callbackURL string) (url, state, codeVerifier string, err error)
- func (p *OIDCAuthProvider) ExchangeCode(ctx context.Context, req OIDCCallbackRequest) (*TokenPair, error)
- func (p *OIDCAuthProvider) ListEnabledProviders(ctx context.Context) ([]*db.OIDCProvider, error)
- func (p *OIDCAuthProvider) Login(_ context.Context, _ LoginRequest) (*TokenPair, error)
- func (p *OIDCAuthProvider) Logout(ctx context.Context, rawToken string) error
- func (p *OIDCAuthProvider) ProviderType() string
- func (p *OIDCAuthProvider) RefreshToken(ctx context.Context, rawToken string) (*TokenPair, error)
- type OIDCCallbackRequest
- type OIDCFlowProvider
- type TokenPair
- type TokenValidator
- type TwoFactorRequiredError
Constants ¶
const ( // RecoveryCodeCount is how many single-use recovery codes are issued when // a user enables two-factor authentication. RecoveryCodeCount = 10 )
Variables ¶
var ( // ErrInvalidCredentials is returned when email/password do not match. ErrInvalidCredentials = errors.New("auth: invalid credentials") // ErrUserNotFound is returned when no user exists for the given identifier. ErrUserNotFound = errors.New("auth: user not found") // ErrUserDisabled is returned when the user account is inactive. ErrUserDisabled = errors.New("auth: user account is disabled") // ErrTokenExpired is returned when a JWT or refresh token has expired. ErrTokenExpired = errors.New("auth: token expired") // ErrTokenInvalid is returned when a token cannot be parsed or verified. ErrTokenInvalid = errors.New("auth: token invalid") // ErrRefreshTokenNotFound is returned when the provided refresh token // does not exist or has already been rotated out. ErrRefreshTokenNotFound = errors.New("auth: refresh token not found") // ErrProviderNotFound is returned when no OIDC provider matches the given ID. ErrProviderNotFound = errors.New("auth: oidc provider not found") // ErrOIDCStateMismatch is returned when the OAuth2 state parameter does // not match the value stored in the session cookie (CSRF protection). ErrOIDCStateMismatch = errors.New("auth: oidc state mismatch") // ErrOIDCCodeVerifierMissing is returned when the PKCE code verifier is // absent from the session during the callback phase. ErrOIDCCodeVerifierMissing = errors.New("auth: oidc code verifier missing") // ErrTokenRevoked is returned when a syntactically valid access token has // been explicitly revoked via the denylist (e.g. after logout). ErrTokenRevoked = errors.New("auth: token has been revoked") )
Sentinel errors returned by auth providers and the auth service. Callers should use errors.Is for comparison.
Functions ¶
func GenerateRecoveryCodes ¶
GenerateRecoveryCodes returns n formatted single-use recovery codes. Only the SHA-256 hash of NormaliseRecoveryCode(code) is ever persisted.
func GenerateResetToken ¶
GenerateResetToken returns a cryptographically random hex-encoded token suitable for single-use links such as password reset. It uses the same entropy source and length as refresh tokens.
func HashPassword ¶
HashPassword returns an Argon2id hash of the given plaintext password. Exported so the user registration handler can hash passwords without depending on the full auth provider.
Format: saltHex:hashHex
func HashToken ¶
HashToken returns the SHA-256 hex digest of an opaque token. Only the hash is persisted; the raw token is delivered to the user (cookie or email link).
func NewTOTPKey ¶
NewTOTPKey creates a fresh TOTP shared secret for the given account and returns the base32 secret together with the otpauth:// URL the GUI renders as a QR code. The secret must be stored encrypted (db.EncryptedString).
func NormaliseRecoveryCode ¶
NormaliseRecoveryCode canonicalises user input so a code is accepted whether or not the dashes and original case are preserved.
func ValidateTOTPCode ¶
ValidateTOTPCode reports whether code is a valid six-digit TOTP value for secret. totp.Validate applies the Google Authenticator defaults — 30 second period, SHA-1, six digits — and a skew of one period either side, which is the clock-drift tolerance we want.
func VerifyPassword ¶
VerifyPassword checks a plaintext password against a stored Argon2id hash. Exported so handlers outside this package (e.g. the two-factor disable/ regenerate endpoints) can re-verify the current password without depending on the full auth provider.
Types ¶
type AuthProvider ¶
type AuthProvider interface {
// Login authenticates a user and returns a token pair on success.
// The access token is a signed JWT; the refresh token is an opaque string
// that must be stored in an httpOnly cookie by the caller.
Login(ctx context.Context, req LoginRequest) (*TokenPair, error)
// RefreshToken validates a refresh token, rotates it, and returns a new
// token pair. The old refresh token is invalidated after this call.
RefreshToken(ctx context.Context, refreshToken string) (*TokenPair, error)
// Logout invalidates the given refresh token so it cannot be used again.
// Access tokens remain valid until expiry — their short TTL (15 min) is
// the revocation mechanism for those.
Logout(ctx context.Context, refreshToken string) error
// ProviderType returns a string identifier for this provider.
// Used for logging and to route OIDC callbacks to the correct provider.
ProviderType() string
}
AuthProvider is the interface that every authentication backend must implement. Currently two implementations exist: LocalAuthProvider (email/password) and OIDCAuthProvider (external identity provider via OpenID Connect).
New providers (SAML, LDAP, etc.) can be added by implementing this interface without changes to the auth service or API layer.
type AuthService ¶
type AuthService struct {
// contains filtered or unexported fields
}
AuthService is the entry point for all authentication operations. It holds references to both providers and delegates to the appropriate one based on the operation requested.
The REST API layer depends on AuthService, never on individual providers directly.
func NewAuthService ¶
func NewAuthService( local *LocalAuthProvider, oidc *OIDCAuthProvider, tokenRepo repositories.RefreshTokenRepository, jwtManager *JWTManager, denylist *Denylist, ) *AuthService
NewAuthService creates an AuthService with the given providers and dependencies. Both local and oidc providers are required even if OIDC is not configured — OIDCAuthProvider will return ErrProviderNotFound at runtime if the provider ID does not exist in the database.
func (*AuthService) AuthorizationURL ¶
func (s *AuthService) AuthorizationURL(ctx context.Context, providerID uuid.UUID, callbackURL string) (url, state, codeVerifier string, err error)
AuthorizationURL generates the OIDC authorization URL for the given provider. callbackURL is the server-computed redirect URI (base_url + /api/v1/auth/oidc/callback). Returns the URL to redirect the user to, plus state and codeVerifier that the caller must store in short-lived session cookies before redirecting.
func (*AuthService) ExchangeCode ¶
func (s *AuthService) ExchangeCode(ctx context.Context, req OIDCCallbackRequest) (*TokenPair, error)
ExchangeCode completes the OIDC Authorization Code flow and returns a token pair.
func (*AuthService) IssueTokenPairForUser ¶
IssueTokenPairForUser completes a login for a user whose credentials have already been verified. Used to finish two-factor authentication once the second factor has been accepted.
func (*AuthService) JWTManager ¶
func (s *AuthService) JWTManager() *JWTManager
JWTManager exposes the underlying JWTManager for direct access.
func (*AuthService) ListEnabledProviders ¶
func (s *AuthService) ListEnabledProviders(ctx context.Context) ([]*db.OIDCProvider, error)
ListEnabledProviders returns all enabled OIDC provider configurations. Used by the public login endpoint to build the per-provider SSO button list.
func (*AuthService) LoginLocal ¶
func (s *AuthService) LoginLocal(ctx context.Context, req LoginRequest) (*TokenPair, error)
LoginLocal authenticates a user via email and password.
func (*AuthService) Logout ¶
func (s *AuthService) Logout(ctx context.Context, rawToken string) error
Logout invalidates the given refresh token.
func (*AuthService) LogoutAllSessions ¶
LogoutAllSessions revokes all active refresh tokens for a user.
func (*AuthService) RefreshToken ¶
RefreshToken validates and rotates a refresh token issued by either provider. Refresh tokens are provider-agnostic once issued.
func (*AuthService) RevokeAccessToken ¶
func (s *AuthService) RevokeAccessToken(jti string, expiresAt time.Time)
RevokeAccessToken adds the given JTI to the denylist until expiresAt. Called on logout so the current access token is rejected immediately, rather than remaining valid until its 15-minute TTL expires.
func (*AuthService) ValidateAccessToken ¶
func (s *AuthService) ValidateAccessToken(tokenString string) (*Claims, error)
ValidateAccessToken parses and verifies a JWT access token, then checks the denylist to reject tokens that were explicitly revoked (e.g. after logout).
type Claims ¶
type Claims struct {
jwt.RegisteredClaims
// UserID is the UUID of the authenticated user.
UserID string `json:"uid"`
// Email is included for convenience so the frontend does not need to
// fetch the user profile just to display the logged-in identity.
Email string `json:"email"`
// Role is the user's role at token issuance time.
// Access tokens are short-lived so role staleness is acceptable.
Role string `json:"role"`
}
Claims holds the custom JWT claims embedded in every access token. Standard claims (exp, iat, iss) are included via jwt.RegisteredClaims.
type Denylist ¶
type Denylist struct {
// contains filtered or unexported fields
}
Denylist is a thread-safe in-memory set of revoked JWT IDs (JTIs). Entries are pruned automatically after their natural expiry time so the set never grows beyond the number of tokens that have been revoked within the current access token TTL window (15 minutes by default).
Trade-off: the denylist is not persisted — server restarts clear it. This is acceptable because all access tokens expire within 15 minutes, so the worst case after a restart is a 15-minute window during which a token revoked just before the restart is still accepted. Refresh tokens are revoked in the database and are not affected by this limitation.
Call Stop to release the background cleanup goroutine.
func NewDenylist ¶
func NewDenylist() *Denylist
NewDenylist creates a Denylist and starts its background cleanup goroutine.
func (*Denylist) Add ¶
Add revokes the token identified by jti until expiresAt. After expiresAt the entry is pruned automatically.
type JWTManager ¶
type JWTManager struct {
// contains filtered or unexported fields
}
JWTManager handles RS256 signing and verification of access tokens. It holds the RSA key pair in memory after initialization.
func NewJWTManagerFromFiles ¶
func NewJWTManagerFromFiles(privateKeyPath, publicKeyPath, issuer string) (*JWTManager, error)
NewJWTManagerFromFiles loads an RSA key pair from PEM files on disk. privateKeyPath must point to a PKCS#8 or PKCS#1 PEM-encoded private key. publicKeyPath must point to the corresponding PEM-encoded public key.
Use this in production where keys are mounted as secrets (Docker, Kubernetes).
func NewJWTManagerGenerated ¶
func NewJWTManagerGenerated(issuer string) (*JWTManager, error)
NewJWTManagerGenerated creates a JWTManager with a freshly generated RSA key pair. The keys are ephemeral — they are not persisted anywhere. This means all existing tokens are invalidated on server restart.
Suitable for development and single-instance deployments where token invalidation on restart is acceptable.
func (*JWTManager) GenerateAccessToken ¶
func (m *JWTManager) GenerateAccessToken(userID, email, role string) (string, error)
GenerateAccessToken creates a signed RS256 JWT for the given user. The token expires after accessTokenDuration (15 minutes).
func (*JWTManager) PublicKeyPEM ¶
func (m *JWTManager) PublicKeyPEM() ([]byte, error)
PublicKeyPEM returns the public key in PEM-encoded PKIX format. Useful for exposing a JWKS endpoint or sharing the key with other services.
func (*JWTManager) ValidateAccessToken ¶
func (m *JWTManager) ValidateAccessToken(tokenString string) (*Claims, error)
ValidateAccessToken parses and verifies a JWT string. Returns the embedded Claims on success, or a sentinel error on failure.
Callers should use errors.Is(err, auth.ErrTokenExpired) to distinguish expired tokens from tampered/malformed ones.
type LocalAuthProvider ¶
type LocalAuthProvider struct {
// contains filtered or unexported fields
}
LocalAuthProvider authenticates users via email/password stored in the database. Passwords are hashed with Argon2id and stored as EncryptedString (AES-256-GCM at rest). Refresh tokens are stored as SHA-256 hashes so the raw token is never persisted.
func NewLocalAuthProvider ¶
func NewLocalAuthProvider( userRepo repositories.UserRepository, tokenRepo repositories.RefreshTokenRepository, jwtManager *JWTManager, logger *zap.Logger, ) *LocalAuthProvider
NewLocalAuthProvider creates a LocalAuthProvider with the given dependencies.
func (*LocalAuthProvider) IssueTokenPair ¶
IssueTokenPair completes authentication for an already-verified user: it stamps LastLoginAt and issues the token pair. Exported so the two-factor login handler can finish a login it started via Login.
func (*LocalAuthProvider) Login ¶
func (p *LocalAuthProvider) Login(ctx context.Context, req LoginRequest) (*TokenPair, error)
Login validates email/password and returns a token pair on success. The password is verified against the Argon2id hash stored in the database and encrypted at rest via EncryptedString.
func (*LocalAuthProvider) Logout ¶
func (p *LocalAuthProvider) Logout(ctx context.Context, rawToken string) error
Logout invalidates the given refresh token. If the token does not exist the call is a no-op — the client should clear its cookie regardless.
func (*LocalAuthProvider) ProviderType ¶
func (p *LocalAuthProvider) ProviderType() string
ProviderType implements AuthProvider.
func (*LocalAuthProvider) RefreshToken ¶
RefreshToken validates a refresh token, rotates it, and issues a new token pair. The old token is deleted before issuing the new one — if the issue fails the user must log in again. This prevents replay attacks even on partial failures.
type LoginRequest ¶
LoginRequest carries credentials for a local email/password login attempt. OIDC logins use OIDCCallbackRequest instead and bypass Login entirely.
type OIDCAuthProvider ¶
type OIDCAuthProvider struct {
// contains filtered or unexported fields
}
OIDCAuthProvider implements OIDCFlowProvider using coreos/go-oidc. It handles the Authorization Code flow with PKCE for multiple configured OIDC providers. Provider configuration is loaded from the database on each call to allow runtime updates without server restart.
func NewOIDCAuthProvider ¶
func NewOIDCAuthProvider( providerRepo repositories.OIDCProviderRepository, userRepo repositories.UserRepository, tokenRepo repositories.RefreshTokenRepository, jwtManager *JWTManager, logger *zap.Logger, ) *OIDCAuthProvider
NewOIDCAuthProvider creates an OIDCAuthProvider with the given dependencies.
func (*OIDCAuthProvider) AuthorizationURL ¶
func (p *OIDCAuthProvider) AuthorizationURL(ctx context.Context, providerID uuid.UUID, callbackURL string) (url, state, codeVerifier string, err error)
AuthorizationURL generates the OIDC authorization URL for the given provider. callbackURL is the redirect URI registered with the identity provider (computed server-side as {base_url}/api/v1/auth/oidc/callback). The caller must store state and codeVerifier in short-lived session cookies before redirecting the user.
func (*OIDCAuthProvider) ExchangeCode ¶
func (p *OIDCAuthProvider) ExchangeCode(ctx context.Context, req OIDCCallbackRequest) (*TokenPair, error)
ExchangeCode completes the OIDC Authorization Code flow. It verifies the state parameter, exchanges the code for tokens, validates the ID token, and either retrieves the existing user or provisions a new one (JIT provisioning).
func (*OIDCAuthProvider) ListEnabledProviders ¶
func (p *OIDCAuthProvider) ListEnabledProviders(ctx context.Context) ([]*db.OIDCProvider, error)
ListEnabledProviders returns all enabled OIDC provider configurations. Used by the public login endpoint to build the SSO button list.
func (*OIDCAuthProvider) Login ¶
func (p *OIDCAuthProvider) Login(_ context.Context, _ LoginRequest) (*TokenPair, error)
Login is not used for OIDC — the flow goes through AuthorizationURL and ExchangeCode. This satisfies the AuthProvider interface but always returns an error to prevent accidental misuse.
func (*OIDCAuthProvider) Logout ¶
func (p *OIDCAuthProvider) Logout(ctx context.Context, rawToken string) error
Logout invalidates the given refresh token. No OIDC back-channel logout is performed — the session at the identity provider remains active.
func (*OIDCAuthProvider) ProviderType ¶
func (p *OIDCAuthProvider) ProviderType() string
ProviderType implements AuthProvider.
func (*OIDCAuthProvider) RefreshToken ¶
RefreshToken delegates to the same logic as LocalAuthProvider — refresh tokens are provider-agnostic once issued.
type OIDCCallbackRequest ¶
type OIDCCallbackRequest struct {
// ProviderID identifies which OIDC provider configuration to use.
ProviderID string
// CallbackURL is the redirect URI that was used in AuthorizationURL.
// Must match exactly what was sent to the identity provider.
CallbackURL string
// Code is the authorization code returned by the identity provider.
Code string
// State must match the value generated in AuthorizationURL (CSRF protection).
State string
// SessionState is the state value stored in the session cookie, used to
// verify the State parameter from the identity provider.
SessionState string
// CodeVerifier is the PKCE verifier stored in the session cookie.
CodeVerifier string
}
OIDCCallbackRequest carries the parameters received in the OAuth2 callback.
type OIDCFlowProvider ¶
type OIDCFlowProvider interface {
AuthProvider
// AuthorizationURL generates the OIDC authorization URL for the given provider
// and returns the state and code verifier (PKCE) that must be stored server-side
// in short-lived session cookies before redirecting the user.
AuthorizationURL(ctx context.Context, providerID uuid.UUID, callbackURL string) (url, state, codeVerifier string, err error)
// ExchangeCode completes the OIDC flow by exchanging the authorization code
// for tokens. state and codeVerifier must match the values from AuthorizationURL.
ExchangeCode(ctx context.Context, req OIDCCallbackRequest) (*TokenPair, error)
}
OIDCFlowProvider extends AuthProvider with the two-step OAuth2 flow. Only OIDCAuthProvider implements this interface.
The split from AuthProvider is intentional: the REST API layer can type-assert to OIDCFlowProvider when handling /auth/oidc/* routes, keeping the base AuthProvider interface clean and implementable by non-OIDC providers.
type TokenPair ¶
type TokenPair struct {
AccessToken string
// AccessTokenExpiresAt is used by the HTTP layer to compute expires_in
// in the response body so the frontend can schedule proactive refreshes.
AccessTokenExpiresAt time.Time
// RefreshToken is the raw opaque token string. The HTTP handler is
// responsible for setting it as a cookie; this struct does not carry
// cookie metadata (path, domain, SameSite) to keep the auth layer
// decoupled from HTTP concerns.
RefreshToken string
// RefreshTokenExpiresAt is used by the HTTP layer to set the cookie
// Max-Age / Expires attribute correctly.
RefreshTokenExpiresAt time.Time
}
TokenPair is returned after a successful login or token refresh. AccessToken is meant to be returned in the response body (or Authorization header). RefreshToken is meant to be set as an httpOnly Secure cookie by the HTTP layer — it is never included in API responses directly.
type TokenValidator ¶
TokenValidator abstracts access token validation. Both *JWTManager and *AuthService satisfy this interface; middleware and handlers should accept TokenValidator so they automatically benefit from denylist checking when wired with AuthService in production.
type TwoFactorRequiredError ¶
TwoFactorRequiredError is returned by LocalAuthProvider.Login when the password is correct but the account has two-factor authentication enabled. It carries the user ID so the handler can create a challenge without re-reading the user. Authentication is not complete: no token is issued and LastLoginAt is not stamped.
func (*TwoFactorRequiredError) Error ¶
func (e *TwoFactorRequiredError) Error() string