diff --git a/go.mod b/go.mod index c7676d3..0c96d25 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.9 require ( github.com/alecthomas/assert/v2 v2.11.0 github.com/alecthomas/kong v1.15.0 + github.com/db-operator/can-haz-password v0.1.1 github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 github.com/golang-jwt/jwt/v5 v5.2.2 @@ -24,6 +25,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/geozelot/intree v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lib/pq v1.10.9 // indirect diff --git a/go.sum b/go.sum index 7c3a9e9..2045178 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/db-operator/can-haz-password v0.1.1 h1:g0OC+9e4L751aWS9h02p/G4WrAaWgbR9LFjKLLC/hnA= +github.com/db-operator/can-haz-password v0.1.1/go.mod h1:0iO+taMqfWqNF3ltVhDWfIghD4VkXnnaQf06xiEIX8M= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= @@ -51,6 +53,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/geozelot/intree v1.0.0 h1:xUyiXMt0wD9zbPMOjy2rVShiUc3PGMPddPuTmi+Jy2s= +github.com/geozelot/intree v1.0.0/go.mod h1:JrqfsNwe17AgzOM023tCXPyUB89NhaZAb8o5rzfZQ7Q= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index 5789702..8a64e37 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -3,15 +3,28 @@ package controllers import ( "context" "database/sql" + "errors" "time" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/token" + "github.com/google/uuid" ) type TokenController struct { - DB *sql.DB + DB *sql.DB + HashCost int16 } +// Errors +var ( + ErrServerError = errors.New("internal server error") +) + type TokenData struct { UUID string + Name string UserID string CreatedAt time.Time LastUserAt time.Time @@ -23,7 +36,29 @@ type TokenData struct { type Scopes struct{} // Create a new token, store its hash in the database and return the token value -func (ctrl *TokenController) Create(ctx context.Context) (string, error) { +func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (string, error) { + id := uuid.NewString() + log := logger.FromContext(ctx).WithValues("uuid", id) + log.V(2).Info("Creating a new token") + + tokenValue, err := token.GenerateToken() + if err != nil { + log.Error(err, "Couldn't create a token") + return "", ErrServerError + } + + tokenHash, err := hash.HashPassword(tokenValue, int(ctrl.HashCost)) + if err != nil { + log.Error(err, "Couldn't calculate token hash") + return "", ErrServerError + } + + query := "INSERT INTO tokens (uuid, token_hash, user_id, scopes, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6)" + if _, err := ctrl.DB.Query(query, id, tokenHash, "dummy", time.Now(), data.ExpiredAt); err != nil { + log.Error(err, "Couldn't insert a token in the database") + return "", ErrServerError + } + return "", nil } diff --git a/internal/helpers/token/token.go b/internal/helpers/token/token.go new file mode 100644 index 0000000..b37d7d4 --- /dev/null +++ b/internal/helpers/token/token.go @@ -0,0 +1,45 @@ +// Package token should be used to generate secure tokens +package token + +import ( + "fmt" + + "github.com/db-operator/can-haz-password/password" +) + +const ( + TokenPrefix = "sft" +) + +// GenerateToken generates secure password string +func GenerateToken() (string, error) { + generator := password.NewGenerator(newTokenRule()) + password, err := generator.Generate() + if err != nil { + return "", err + } + return fmt.Sprintf("%s_%s", TokenPrefix, password), nil +} + +// Minimum length of 20 characters, maximum length of 30 characters. +// Varied composition including special characters and uppercase and lowercase letters. +// Excludes consecutive dashes (for hybris compatibility) and uses only url safe special characters. +type tokenRule struct{} + +func newTokenRule() *tokenRule { + return &tokenRule{} +} + +func (r *tokenRule) Config() *password.Configuration { + return &password.Configuration{ + Length: 40, + CharacterClasses: []password.CharacterClassConfiguration{ // codespell:ignore + {Characters: password.LowercaseCharacters + password.UppercaseCharacters, Minimum: 10}, + {Characters: password.DigitCharacters, Minimum: 8}, + }, + } +} + +func (r *tokenRule) Valid(password []rune) bool { + return true +} diff --git a/internal/helpers/token/token_test.go b/internal/helpers/token/token_test.go new file mode 100644 index 0000000..6b851b3 --- /dev/null +++ b/internal/helpers/token/token_test.go @@ -0,0 +1,16 @@ +package token_test + +import ( + "strings" + "testing" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/token" + "github.com/stretchr/testify/assert" +) + +func TestUnitTokenGeneration(t *testing.T) { + token, err := token.GenerateToken() + assert.NoError(t, err) + assert.Len(t, token, 44) + assert.True(t, strings.HasPrefix(token, "sft_")) +}