diff --git a/auth/auth.go b/auth/auth.go index 8f17e465eba0a4bc37efba5b4e9dc145408ec939_YXV0aC9hdXRoLmdv..866dbada1e4d3ab9e21e7b6a09187fe693f8c2b2_YXV0aC9hdXRoLmdv 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,9 +1,10 @@ package auth import ( + "crypto/rand" "encoding/hex" "errors" "fmt" "net/http" "time" @@ -4,9 +5,10 @@ "encoding/hex" "errors" "fmt" "net/http" "time" + "github.com/rs/zerolog" "gopkg.in/dgrijalva/jwt-go.v3" ) @@ -36,7 +38,7 @@ // //nolint:lll type TokenOptions struct { - SecretOpt func(string) error `long:"token-secret" env:"AUTH_TOKEN_SECRET" ini-name:"secret" required:"t" description:"hex HMAC256/AES256 secret for signing/verifying JWT & crypting (32 bytes)"` + 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"` @@ -47,6 +49,7 @@ 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 { @@ -61,19 +64,4 @@ } } -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. @@ -79,4 +67,4 @@ // NewTokenOptions creates a TokenOptions. -func NewTokenOptions(cookieBasename string) *TokenOptions { +func NewTokenOptions(cookieBasename string, getLog func() *zerolog.Logger) *TokenOptions { options := TokenOptions{ cookieBasename: cookieBasename, @@ -81,5 +69,6 @@ options := TokenOptions{ cookieBasename: cookieBasename, + log: getLog, } options.ExpirationOpt = durationOption(&options.Expiration) options.CachePurgeDelayOpt = durationOption(&options.CachePurgeDelay) @@ -83,7 +72,6 @@ } options.ExpirationOpt = durationOption(&options.Expiration) options.CachePurgeDelayOpt = durationOption(&options.CachePurgeDelay) - options.SecretOpt = hexBytesOption(32, &options.Secret) return &options } @@ -93,6 +81,30 @@ SetExpiresAt(time.Time) } -func (t TokenOptions) MakeToken( +// 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) { @@ -97,5 +109,8 @@ claims Claims, withCookie bool, ) (string, *http.Cookie, error) { + if err := t.EnsureSecret(); err != nil { + return "", nil, err + } expiration := t.Expiration if withCookie { expiration *= 2 @@ -123,7 +138,10 @@ return stoken, cookie, nil } -func (t TokenOptions) ParseToken(tokenString string, claims Claims) error { +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 }) diff --git a/cmd/program.go b/cmd/program.go index 8f17e465eba0a4bc37efba5b4e9dc145408ec939_Y21kL3Byb2dyYW0uZ28=..866dbada1e4d3ab9e21e7b6a09187fe693f8c2b2_Y21kL3Byb2dyYW0uZ28= 100644 --- a/cmd/program.go +++ b/cmd/program.go @@ -168,15 +168,6 @@ }) } -func WithTokenOptions[E any]() Option[E] { - return func(program *Program[E]) { - program.TokenOptions = auth.NewTokenOptions(program.Name) - if _, err := program.Parser.AddGroup("Token", "Token Options", program.TokenOptions); err != nil { - panic(err) - } - } -} - func WithDatabase[E any](migrateSource source.Driver) Option[E] { return func(program *Program[E]) { program.hasDB = true diff --git a/cmd/program_auth.go b/cmd/program_auth.go index 8f17e465eba0a4bc37efba5b4e9dc145408ec939_Y21kL3Byb2dyYW1fYXV0aC5nbw==..866dbada1e4d3ab9e21e7b6a09187fe693f8c2b2_Y21kL3Byb2dyYW1fYXV0aC5nbw== 100644 --- a/cmd/program_auth.go +++ b/cmd/program_auth.go @@ -1,4 +1,8 @@ package cmd import ( + "crypto/rand" + "encoding/hex" + "fmt" + "github.com/justinas/alice" @@ -4,5 +8,6 @@ "github.com/justinas/alice" + "github.com/rs/zerolog" "orus.io/orus-io/go-orusapi/auth" ) @@ -5,7 +10,23 @@ "orus.io/orus-io/go-orusapi/auth" ) +func WithTokenOptions[E any]() Option[E] { + return func(program *Program[E]) { + program.TokenOptions = auth.NewTokenOptions( + program.Name, + func() *zerolog.Logger { + return &program.Logger + }, + ) + if _, err := program.Parser.AddGroup("Token", "Token Options", program.TokenOptions); err != nil { + panic(err) + } + + SetupGenerateAuthSecretCmd(program) + } +} + func WithAuthMiddleware[E any](middleware any) Option[E] { return func(program *Program[E]) { program.authMiddlewares = append(program.authMiddlewares, getMiddleware[E](middleware)) @@ -22,3 +43,32 @@ }, ) } + +type GenerateAuthSecretCmd[E any] struct { + program *Program[E] +} + +func (cmd *GenerateAuthSecretCmd[E]) Execute([]string) error { + secret := make([]byte, 32) + if _, err := rand.Read(secret); err != nil { + panic(err) + } + + _, err := fmt.Println(hex.EncodeToString(secret)) + + return err +} + +func SetupGenerateAuthSecretCmd[E any](program *Program[E]) *GenerateAuthSecretCmd[E] { + cmd := GenerateAuthSecretCmd[E]{ + program: program, + } + + if _, err := program.Parser.AddCommand( + "generate-auth-secret", "Generate a proper auth secret", "", &cmd, + ); err != nil { + program.Logger.Fatal().Err(err).Msg("could not init generate-auth-secret command") + } + + return &cmd +} diff --git a/cmd/serve.go b/cmd/serve.go index 8f17e465eba0a4bc37efba5b4e9dc145408ec939_Y21kL3NlcnZlLmdv..866dbada1e4d3ab9e21e7b6a09187fe693f8c2b2_Y21kL3NlcnZlLmdv 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -38,6 +38,9 @@ } func (cmd *ServeCmd[E]) Execute([]string) error { + if err := cmd.program.TokenOptions.EnsureSecret(); err != nil { + return err + } if err := cmd.program.EnsureDB(cmd.AutoMigrate); err != nil { return err }