Newer
Older
"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
}