From 778bc615aa89e78e3fc494664586c2fa2493f9c9 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 15 Feb 2018 14:20:17 +0300 Subject: [PATCH] The configuration file was deleted in favor of using environment variables. Token generation functionality remove. Secret token now provided via CHRLY_SECRET env variable. --- Gopkg.lock | 28 +---------- Gopkg.toml | 11 ---- auth/jwt.go | 112 ++++------------------------------------- auth/jwt_test.go | 85 +++---------------------------- bootstrap/bootstrap.go | 9 ---- cmd/root.go | 19 ++----- cmd/serve.go | 9 +++- cmd/token.go | 48 +++--------------- http/api.go | 4 +- http/api_test.go | 16 +++--- 10 files changed, 48 insertions(+), 293 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 751a168..264c1ed 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -55,12 +55,6 @@ packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] revision = "8f6b1344a92ff8877cf24a5de9177bf7d0a2a187" -[[projects]] - branch = "master" - name = "github.com/howeyc/gopass" - packages = ["."] - revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8" - [[projects]] name = "github.com/inconshreveable/mousetrap" packages = ["."] @@ -79,12 +73,6 @@ packages = ["cluster","pool","redis","util"] revision = "d234cfb904a91daafa4e1f92599a893b349cc0c2" -[[projects]] - branch = "master" - name = "github.com/mitchellh/go-homedir" - packages = ["."] - revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" - [[projects]] branch = "master" name = "github.com/mitchellh/mapstructure" @@ -121,12 +109,6 @@ revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" -[[projects]] - branch = "master" - name = "github.com/segmentio/go-prompt" - packages = ["."] - revision = "f0d19b6901ade831d5a3204edc0d6a7d6457fbb2" - [[projects]] branch = "master" name = "github.com/spf13/afero" @@ -176,16 +158,10 @@ revision = "59055296916bb3c6ad9cf3b21d5f2cf7059f8e76" source = "https://github.com/erickskrauch/govalidator.git" -[[projects]] - branch = "master" - name = "golang.org/x/crypto" - packages = ["ssh/terminal"] - revision = "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8" - [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix","windows"] + packages = ["unix"] revision = "7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce" [[projects]] @@ -203,6 +179,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "85c318cc67a4e78dd3608297ae189cc70b07968ba6e0e1a04cc21b264fddf1eb" + inputs-digest = "e6bd87f630333e3e5b03bea33720c3281a9094551bd5ced436062157fe51ab71" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 2364f3a..9e6ac7d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -24,22 +24,11 @@ ignored = ["elyby/minecraft-skinsystem"] name = "github.com/SermoDigital/jose" version = "~1.1.0" -[[constraint]] - name = "github.com/mitchellh/go-homedir" - -[[constraint]] - name = "github.com/segmentio/go-prompt" - branch = "master" - [[constraint]] name = "github.com/thedevsaddam/govalidator" source = "https://github.com/erickskrauch/govalidator.git" branch = "issue-18" -[[constraint]] - branch = "master" - name = "github.com/spf13/afero" - # Testing dependencies [[constraint]] diff --git a/auth/jwt.go b/auth/jwt.go index fcb3365..7d86618 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -1,25 +1,17 @@ package auth import ( - "encoding/base64" - "math" - "math/rand" + "errors" "net/http" - "os" "strings" "time" "github.com/SermoDigital/jose/crypto" "github.com/SermoDigital/jose/jws" - "github.com/mitchellh/go-homedir" - "github.com/spf13/afero" ) -var fs = afero.NewOsFs() - var hashAlg = crypto.SigningMethodHS256 -const appHomeDirName = ".minecraft-skinsystem" const scopesClaim = "scopes" type Scope string @@ -29,20 +21,19 @@ var ( ) type JwtAuth struct { - signingKey []byte + Key []byte } func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) { - key, err := t.getSigningKey() - if err != nil { - return nil, err + if len(t.Key) == 0 { + return nil, errors.New("signing key not available") } claims := jws.Claims{} claims.Set(scopesClaim, scopes) claims.SetIssuedAt(time.Now()) encoder := jws.NewJWT(claims, hashAlg) - token, err := encoder.Serialize(key) + token, err := encoder.Serialize(t.Key) if err != nil { return nil, err } @@ -50,20 +41,11 @@ func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) { return token, nil } -func (t *JwtAuth) GenerateSigningKey() error { - if err := createAppHomeDir(); err != nil { - return err - } - - key := generateRandomBytes(64) - if err := afero.WriteFile(fs, getKeyPath(), key, 0600); err != nil { - return err - } - - return nil -} - func (t *JwtAuth) Check(req *http.Request) error { + if len(t.Key) == 0 { + return &Unauthorized{"Signing key not set"} + } + bearerToken := req.Header.Get("Authorization") if bearerToken == "" { return &Unauthorized{"Authentication header not presented"} @@ -79,79 +61,14 @@ func (t *JwtAuth) Check(req *http.Request) error { return &Unauthorized{"Cannot parse passed JWT token"} } - signKey, err := t.getSigningKey() + err = token.Validate(t.Key, hashAlg) if err != nil { - return err - } - - err = token.Validate(signKey, hashAlg) - if err != nil { - return &Unauthorized{"JWT token have invalid signature. It corrupted or expired."} + return &Unauthorized{"JWT token have invalid signature. It may be corrupted or expired."} } return nil } -func (t *JwtAuth) getSigningKey() ([]byte, error) { - if t.signingKey == nil { - path := getKeyPath() - if _, err := fs.Stat(path); err != nil { - if os.IsNotExist(err) { - return nil, &SigningKeyNotAvailable{} - } - - return nil, err - } - - key, err := afero.ReadFile(fs, path) - if err != nil { - return nil, err - } - - t.signingKey = key - } - - return t.signingKey, nil -} - -func createAppHomeDir() error { - path := getAppHomeDirPath() - if _, err := fs.Stat(path); os.IsNotExist(err) { - err := fs.Mkdir(path, 0755) // rwx r-x r-x - if err != nil { - return err - } - } - - return nil -} - -func getAppHomeDirPath() string { - path, err := homedir.Expand("~/" + appHomeDirName) - if err != nil { - panic(err) - } - - return path -} - -func getKeyPath() string { - return getAppHomeDirPath() + "/jwt-key" -} - -func generateRandomBytes(n int) []byte { - // base64 will increase length in 1.37 times - // +1 is needed to ensure, that after base64 we will do not have any '===' characters - randLen := int(math.Ceil(float64(n) / 1.37)) + 1 - randBytes := make([]byte, randLen) - rand.Read(randBytes) - // +5 is needed to have additional buffer for the next set of XX=== characters - resBytes := make([]byte, n + 5) - base64.URLEncoding.Encode(resBytes, randBytes) - - return resBytes[:n] -} - type Unauthorized struct { Reason string } @@ -163,10 +80,3 @@ func (e *Unauthorized) Error() string { return "Unauthorized" } - -type SigningKeyNotAvailable struct { -} - -func (*SigningKeyNotAvailable) Error() string { - return "Signing key not available" -} diff --git a/auth/jwt_test.go b/auth/jwt_test.go index 5b4e701..00fc65a 100644 --- a/auth/jwt_test.go +++ b/auth/jwt_test.go @@ -2,88 +2,36 @@ package auth import ( "net/http/httptest" - "strings" "testing" - "github.com/spf13/afero" - testify "github.com/stretchr/testify/assert" ) const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNTE2NjU4MTkzIiwic2NvcGVzIjoic2tpbiJ9.agbBS0qdyYMBaVfTZJAZcTTRgW1Y0kZty4H3N2JHBO8" func TestJwtAuth_NewToken_Success(t *testing.T) { - clearFs() assert := testify.New(t) - fs.Mkdir(getAppHomeDirPath(), 0755) - afero.WriteFile(fs, getKeyPath(), []byte("secret"), 0600) - - jwt := &JwtAuth{} + jwt := &JwtAuth{[]byte("secret")} token, err := jwt.NewToken(SkinScope) assert.Nil(err) assert.NotNil(token) } func TestJwtAuth_NewToken_KeyNotAvailable(t *testing.T) { - clearFs() assert := testify.New(t) - fs = afero.NewMemMapFs() - jwt := &JwtAuth{} token, err := jwt.NewToken(SkinScope) - assert.IsType(&SigningKeyNotAvailable{}, err) + assert.Error(err, "signing key not available") assert.Nil(token) } -func TestJwtAuth_GenerateSigningKey_KeyNotExists(t *testing.T) { - clearFs() - assert := testify.New(t) - - jwt := &JwtAuth{} - err := jwt.GenerateSigningKey() - assert.Nil(err) - if _, err := fs.Stat(getAppHomeDirPath()); err != nil { - assert.Fail("directory not created") - } - - if _, err := fs.Stat(getKeyPath()); err != nil { - assert.Fail("signing file not created") - } - - content, _ := afero.ReadFile(fs, getKeyPath()) - assert.Len(content, 64) -} - -func TestJwtAuth_GenerateSigningKey_KeyExists(t *testing.T) { - clearFs() - assert := testify.New(t) - - fs.Mkdir(getAppHomeDirPath(), 0755) - afero.WriteFile(fs, getKeyPath(), []byte("secret"), 0600) - - jwt := &JwtAuth{} - err := jwt.GenerateSigningKey() - assert.Nil(err) - if _, err := fs.Stat(getAppHomeDirPath()); err != nil { - assert.Fail("directory not created") - } - - if _, err := fs.Stat(getKeyPath()); err != nil { - assert.Fail("signing file not created") - } - - content, _ := afero.ReadFile(fs, getKeyPath()) - assert.NotEqual([]byte("secret"), content) -} - func TestJwtAuth_Check_EmptyRequest(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) - jwt := &JwtAuth{} + jwt := &JwtAuth{[]byte("secret")} err := jwt.Check(req) assert.IsType(&Unauthorized{}, err) @@ -91,12 +39,11 @@ func TestJwtAuth_Check_EmptyRequest(t *testing.T) { } func TestJwtAuth_Check_NonBearer(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) req.Header.Add("Authorization", "this is not jwt") - jwt := &JwtAuth{} + jwt := &JwtAuth{[]byte("secret")} err := jwt.Check(req) assert.IsType(&Unauthorized{}, err) @@ -104,12 +51,11 @@ func TestJwtAuth_Check_NonBearer(t *testing.T) { } func TestJwtAuth_Check_BearerButNotJwt(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) req.Header.Add("Authorization", "Bearer thisIs.Not.Jwt") - jwt := &JwtAuth{} + jwt := &JwtAuth{[]byte("secret")} err := jwt.Check(req) assert.IsType(&Unauthorized{}, err) @@ -117,7 +63,6 @@ func TestJwtAuth_Check_BearerButNotJwt(t *testing.T) { } func TestJwtAuth_Check_SecretNotAvailable(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) @@ -125,11 +70,10 @@ func TestJwtAuth_Check_SecretNotAvailable(t *testing.T) { jwt := &JwtAuth{} err := jwt.Check(req) - assert.IsType(&SigningKeyNotAvailable{}, err) + assert.Error(err, "Signing key not set") } func TestJwtAuth_Check_SecretInvalid(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) @@ -138,11 +82,10 @@ func TestJwtAuth_Check_SecretInvalid(t *testing.T) { err := jwt.Check(req) assert.IsType(&Unauthorized{}, err) - assert.EqualError(err, "JWT token have invalid signature. It corrupted or expired.") + assert.EqualError(err, "JWT token have invalid signature. It may be corrupted or expired.") } func TestJwtAuth_Check_Valid(t *testing.T) { - clearFs() assert := testify.New(t) req := httptest.NewRequest("POST", "http://localhost", nil) @@ -152,17 +95,3 @@ func TestJwtAuth_Check_Valid(t *testing.T) { err := jwt.Check(req) assert.Nil(err) } - -func TestJwtAuth_generateRandomBytes(t *testing.T) { - assert := testify.New(t) - lengthMap := []int{12, 20, 24, 30, 32, 48, 50, 64} - for _, length := range lengthMap { - bytes := generateRandomBytes(length) - assert.Len(bytes, length) - assert.False(strings.HasSuffix(string(bytes), "="), "secret key should not ends with '=' character") - } -} - -func clearFs() { - fs = afero.NewMemMapFs() -} diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index ad86505..d8774ee 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -62,12 +62,3 @@ func CreateLogger(statsdAddr string, sentryAddr string) (wd.Watchdog, error) { return wd.New("", "").WithParams(rays.Host), nil } - -type RabbitMQConfig struct { - Username string - Password string - Host string - Port int - Vhost string -} - diff --git a/cmd/root.go b/cmd/root.go index 8d65df6..de5fcf5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "strings" "elyby/minecraft-skinsystem/bootstrap" @@ -10,8 +11,6 @@ import ( "github.com/spf13/viper" ) -var cfgFile string - var RootCmd = &cobra.Command{ Use: "chrly", Short: "Implementation of Minecraft skins system server", @@ -29,22 +28,10 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) - RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.test.yaml)") } func initConfig() { - if cfgFile != "" { - viper.SetConfigFile(cfgFile) - } else { - viper.SetConfigName("config") - viper.AddConfigPath("/etc/minecraft-skinsystem") - viper.AddConfigPath(".") - } - viper.AutomaticEnv() - - if err := viper.ReadInConfig(); err == nil { - // TODO: show only on verbose mode - fmt.Println("Using config file:", viper.ConfigFileUsed()) - } + replacer := strings.NewReplacer(".", "_") + viper.SetEnvKeyReplacer(replacer) } diff --git a/cmd/serve.go b/cmd/serve.go index 29f2cea..689afbc 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -47,7 +47,7 @@ var serveCmd = &cobra.Command{ SkinsRepo: skinsRepo, CapesRepo: capesRepo, Logger: logger, - Auth: &auth.JwtAuth{}, + Auth: &auth.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))}, } if err := cfg.Run(); err != nil { @@ -58,4 +58,11 @@ var serveCmd = &cobra.Command{ func init() { RootCmd.AddCommand(serveCmd) + viper.SetDefault("server.host", "") + viper.SetDefault("server.port", 80) + viper.SetDefault("storage.redis.host", "localhost") + viper.SetDefault("storage.redis.port", 6379) + viper.SetDefault("storage.redis.poll", 10) + viper.SetDefault("storage.filesystem.basePath", "data") + viper.SetDefault("storage.filesystem.capesDirName", "capes") } diff --git a/cmd/token.go b/cmd/token.go index 70c8724..b74829e 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -6,60 +6,24 @@ import ( "elyby/minecraft-skinsystem/auth" - "github.com/segmentio/go-prompt" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var tokenCmd = &cobra.Command{ Use: "token", - Short: "API tokens manipulation", -} - -var createCmd = &cobra.Command{ - Use: "create", Short: "Creates a new token, which allows to interact with Chrly API", Run: func(cmd *cobra.Command, args []string) { - jwtAuth := &auth.JwtAuth{} - for { - token, err := jwtAuth.NewToken(auth.SkinScope) - if err != nil { - if _, ok := err.(*auth.SigningKeyNotAvailable); !ok { - log.Fatalf("Unable to create new token. The error is %v\n", err) - } - - log.Println("Signing key not available. Creating...") - err := jwtAuth.GenerateSigningKey() - if err != nil { - log.Fatalf("Unable to generate new signing key. The error is %v\n", err) - } - - continue - } - - fmt.Printf("%s\n", token) - } - }, -} - -var resetCmd = &cobra.Command{ - Use: "reset", - Short: "Re-creates the secret key, which invalidate all tokens", - Run: func(cmd *cobra.Command, args []string) { - if !prompt.Confirm("Do you really want to invalidate all exists tokens?") { - fmt.Println("Aboart.") - return + jwtAuth := &auth.JwtAuth{Key: []byte(viper.GetString("chrly.secret"))} + token, err := jwtAuth.NewToken(auth.SkinScope) + if err != nil { + log.Fatalf("Unable to create new token. The error is %v\n", err) } - jwtAuth := &auth.JwtAuth{} - if err := jwtAuth.GenerateSigningKey(); err != nil { - log.Fatalf("Unable to generate new signing key. The error is %v\n", err) - } - - fmt.Println("Token successfully regenerated.") + fmt.Printf("%s\n", token) }, } func init() { - tokenCmd.AddCommand(createCmd, resetCmd) RootCmd.AddCommand(tokenCmd) } diff --git a/http/api.go b/http/api.go index 37a43f6..f81c6e3 100644 --- a/http/api.go +++ b/http/api.go @@ -236,8 +236,8 @@ func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string) func apiForbidden(resp http.ResponseWriter, reason string) { resp.WriteHeader(http.StatusForbidden) resp.Header().Set("Content-Type", "application/json") - result, _ := json.Marshal([]interface{}{ - reason, + result, _ := json.Marshal(map[string]interface{}{ + "error": reason, }) resp.Write(result) } diff --git a/http/api_test.go b/http/api_test.go index a2a9fde..34fc532 100644 --- a/http/api_test.go +++ b/http/api_test.go @@ -345,9 +345,9 @@ func TestConfig_PostSkin_Unauthorized(t *testing.T) { defer resp.Body.Close() assert.Equal(403, resp.StatusCode) response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`[ - "Cannot parse passed JWT token" - ]`, string(response)) + assert.JSONEq(`{ + "error": "Cannot parse passed JWT token" + }`, string(response)) } func TestConfig_DeleteSkinByUserId_Success(t *testing.T) { @@ -475,18 +475,20 @@ func TestConfig_Authenticate_SignatureKeyNotSet(t *testing.T) { req := httptest.NewRequest("POST", "http://localhost", nil) w := httptest.NewRecorder() - mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.SigningKeyNotAvailable{}) - mocks.Log.EXPECT().Error("Unknown error on validating api request: :err", gomock.Any()) + mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"signing key not available"}) mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) + mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1)) res := config.Authenticate(http.HandlerFunc(func (resp http.ResponseWriter, req *http.Request) {})) res.ServeHTTP(w, req) resp := w.Result() defer resp.Body.Close() - assert.Equal(500, resp.StatusCode) + assert.Equal(403, resp.StatusCode) response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(response) + assert.JSONEq(`{ + "error": "signing key not available" + }`, string(response)) } // base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png