package auth import ( "crypto/rand" "encoding/hex" "errors" "fmt" "net/http" "time" "github.com/rs/zerolog" "gopkg.in/dgrijalva/jwt-go.v3" ) type ContextIdentityType int const ContextIdentity ContextIdentityType = iota var ( // ErrInvalidCredentials is returned when auth fails because the credentials are // invalid. ErrInvalidCredentials = errors.New("invalid credentials") // ErrAccountidPointofsaleidIncompatible is returned when auth fails because the // credentials contains both a accountid and pointofsaleid. This error will // disappear in a later version. ErrAccountidPointofsaleidIncompatible = errors.New( "credentials cannot contain both accountid and pointofsaleid") // ErrAccountidCobrandidIncompatible is returned when auth fails because the // credentials contains both a accountid and cobrandid. This error will // disappear in a later version. ErrAccountidCobrandidIncompatible = errors.New( "credentials cannot contain both accountid and cobrandid") ) // TokenOptions holds options related to the JWT // //nolint:lll type TokenOptions struct { SecretOpt string `long:"token-secret" env:"AUTH_TOKEN_SECRET" ini-name:"secret" description:"hex HMAC256/AES256 secret for signing/verifying JWT & crypting (32 bytes)"` Secret []byte `no-flag:"t"` Expiration time.Duration `no-flag:"t"` ExpirationOpt func(string) error `long:"token-expiration" ini-name:"expiration" required:"false" description:"Expiration time of the generated tokens" default:"5m"` CookieDomain string `long:"cookie-domain" ini-name:"cookie-domain" description:"The domain used in cookies, must match the public-facing server host name"` CookieSecure bool `long:"cookie-secure" ini-name:"cookie-secure" description:"Set to true if the cookie must be over https only (recommended)"` CacheMaxSize int `long:"token-cache-max-size" ini-name:"cache-max-size" required:"false" description:"Maximum number of entries in the token cache" default:"10000"` CachePurgeDelay time.Duration `no-flag:"t"` CachePurgeDelayOpt func(string) error `long:"token-cache-purge-delay" ini-name:"cache-purge-delay" required:"false" description:"Delay between token cache purges" default:"10m"` cookieBasename string log func() *zerolog.Logger } func durationOption(tgt *time.Duration) func(string) error { return func(duration string) error { d, err := time.ParseDuration(duration) if err != nil { return err } *tgt = d return nil } } // NewTokenOptions creates a TokenOptions. func NewTokenOptions(cookieBasename string, getLog func() *zerolog.Logger) *TokenOptions { options := TokenOptions{ cookieBasename: cookieBasename, log: getLog, } options.ExpirationOpt = durationOption(&options.Expiration) options.CachePurgeDelayOpt = durationOption(&options.CachePurgeDelay) return &options } type Claims interface { jwt.Claims SetExpiresAt(time.Time) } // EnsureSecret makes sure a valid secret is configured. func (t *TokenOptions) EnsureSecret() error { if len(t.Secret) == 32 { return nil } if t.SecretOpt == "" { t.log().Warn().Msg("no secret configured. A random secret will be used, meaning authenticated session will not survice a server restart. To avoid this, please set the token-secret option") t.Secret = make([]byte, 32) _, _ = rand.Read(t.Secret) } else { b, err := hex.DecodeString(t.SecretOpt) if err != nil { return fmt.Errorf("'secret' is not a valid hex string: %w", err) } if len(b) != 32 { return fmt.Errorf("'secret' has in invalid size. Expected %d bytes, got: %d", 32, len(b)) } t.Secret = b } return nil } func (t *TokenOptions) MakeToken( claims Claims, withCookie bool, ) (string, *http.Cookie, error) { if err := t.EnsureSecret(); err != nil { return "", nil, err } expiration := t.Expiration if withCookie { expiration *= 2 } expiresAt := time.Now().Add(expiration) claims.SetExpiresAt(expiresAt) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) stoken, err := token.SignedString(t.Secret) if err != nil { return "", nil, err } var cookie *http.Cookie if withCookie { cookie = &http.Cookie{ Name: t.cookieBasename + "-auth", Value: stoken, Expires: expiresAt, Path: "/", Domain: t.CookieDomain, Secure: t.CookieSecure, } } return stoken, cookie, nil } func (t *TokenOptions) ParseToken(tokenString string, claims Claims) error { if err := t.EnsureSecret(); err != nil { return err } token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (any, error) { return t.Secret, nil }) if err != nil { return fmt.Errorf("invalid token: %w", err) } if !token.Valid { return fmt.Errorf("invalid token") } return nil }