Skip to content
Snippets Groups Projects
auth.go 4.19 KiB
Newer Older
package auth

import (
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"time"

	"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          func(string) error `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"`
}

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
	}
}

func hexBytesOption(size int, tgt *[]byte) func(string) error {
	return func(s string) error {
		b, err := hex.DecodeString(s)
		if err != nil {
			return fmt.Errorf("invalid hex string: %w", err)
		}
		if len(b) != size {
			return fmt.Errorf("invalid option size. Expected %d bytes, got: %d", size, len(b))
		}
		*tgt = b

		return nil
	}
}

// NewTokenOptions creates a TokenOptions.
func NewTokenOptions(cookieBasename string) *TokenOptions {
	options := TokenOptions{
		cookieBasename: cookieBasename,
	}
	options.ExpirationOpt = durationOption(&options.Expiration)
	options.CachePurgeDelayOpt = durationOption(&options.CachePurgeDelay)
	options.SecretOpt = hexBytesOption(32, &options.Secret)

	return &options
}

type Claims interface {
	jwt.Claims
	SetExpiresAt(time.Time)
}

func (t TokenOptions) MakeToken(
	claims Claims, withCookie bool,
) (string, *http.Cookie, error) {
	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 {
	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
}