The configuration file was deleted in favor of using environment variables.

Token generation functionality remove. Secret token now provided via CHRLY_SECRET env variable.
This commit is contained in:
ErickSkrauch 2018-02-15 14:20:17 +03:00
parent 235f65f11c
commit 778bc615aa
No known key found for this signature in database
GPG Key ID: 669339FCBB30EE0E
10 changed files with 48 additions and 293 deletions

28
Gopkg.lock generated
View File

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

View File

@ -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]]

View File

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

View File

@ -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()
}

View File

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

View File

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

View File

@ -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")
}

View File

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

View File

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

View File

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