auth

package
v0.0.0-...-b504301 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Index

Constants

View Source
const (

	// RecoveryCodeCount is how many single-use recovery codes are issued when
	// a user enables two-factor authentication.
	RecoveryCodeCount = 10
)

Variables

View Source
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

func GenerateRecoveryCodes(n int) ([]string, error)

GenerateRecoveryCodes returns n formatted single-use recovery codes. Only the SHA-256 hash of NormaliseRecoveryCode(code) is ever persisted.

func GenerateResetToken

func GenerateResetToken() (string, error)

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

func HashPassword(password string) (string, error)

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

func HashToken(raw string) string

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

func NewTOTPKey(email string) (secret, otpauthURL string, err error)

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

func NormaliseRecoveryCode(code string) string

NormaliseRecoveryCode canonicalises user input so a code is accepted whether or not the dashes and original case are preserved.

func ValidateTOTPCode

func ValidateTOTPCode(secret, code string) bool

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

func VerifyPassword(password, stored string) bool

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

func (s *AuthService) IssueTokenPairForUser(ctx context.Context, user *db.User) (*TokenPair, error)

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

func (s *AuthService) LogoutAllSessions(ctx context.Context, userID uuid.UUID) error

LogoutAllSessions revokes all active refresh tokens for a user.

func (*AuthService) RefreshToken

func (s *AuthService) RefreshToken(ctx context.Context, rawToken string) (*TokenPair, error)

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

func (d *Denylist) Add(jti string, expiresAt time.Time)

Add revokes the token identified by jti until expiresAt. After expiresAt the entry is pruned automatically.

func (*Denylist) IsRevoked

func (d *Denylist) IsRevoked(jti string) bool

IsRevoked reports whether the given JTI has been explicitly revoked and has not yet expired. Expired entries are considered not revoked because the token itself would have failed signature validation first.

func (*Denylist) Stop

func (d *Denylist) Stop()

Stop terminates the background cleanup goroutine. The Denylist must not be used after Stop is called.

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

func (p *LocalAuthProvider) IssueTokenPair(ctx context.Context, user *db.User) (*TokenPair, error)

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

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

func (p *LocalAuthProvider) RefreshToken(ctx context.Context, rawToken string) (*TokenPair, error)

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

type LoginRequest struct {
	Email    string
	Password string
}

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

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

func (p *OIDCAuthProvider) RefreshToken(ctx context.Context, rawToken string) (*TokenPair, error)

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

type TokenValidator interface {
	ValidateAccessToken(tokenString string) (*Claims, error)
}

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

type TwoFactorRequiredError struct {
	UserID uuid.UUID
}

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

Jump to

Keyboard shortcuts

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