mirror of
				https://github.com/elyby/chrly.git
				synced 2025-05-31 14:11:51 +05:30 
			
		
		
		
	Rework security module, replace JWT library, invalidate JWT tokens signed for Chrly v4, generate RSA key in runtime when not provided via configuration
This commit is contained in:
		
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -6,13 +6,13 @@ replace github.com/asaskevich/EventBus v0.0.0-20200330115301-33b3bc6a7ddc => git | ||||
|  | ||||
| // Main dependencies | ||||
| require ( | ||||
| 	github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2 | ||||
| 	github.com/asaskevich/EventBus v0.0.0-20200330115301-33b3bc6a7ddc | ||||
| 	github.com/brunomvsouza/singleflight v0.4.0 | ||||
| 	github.com/defval/di v1.12.0 | ||||
| 	github.com/etherlabsio/healthcheck/v2 v2.0.0 | ||||
| 	github.com/getsentry/raven-go v0.2.1-0.20190419175539-919484f041ea | ||||
| 	github.com/go-playground/validator/v10 v10.17.0 | ||||
| 	github.com/golang-jwt/jwt/v5 v5.2.0 | ||||
| 	github.com/gorilla/mux v1.8.1 | ||||
| 	github.com/jellydator/ttlcache/v3 v3.1.1 | ||||
| 	github.com/mediocregopher/radix/v4 v4.1.4 | ||||
|   | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,5 +1,3 @@ | ||||
| github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2 h1:koK7z0nSsRiRiBWwa+E714Puh+DO+ZRdIyAXiXzL+lg= | ||||
| github.com/SermoDigital/jose v0.9.2-0.20161205224733-f6df55f235c2/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= | ||||
| github.com/brunomvsouza/singleflight v0.4.0 h1:9dNcTeYoXSus3xbZEM0EEZ11EcCRjUZOvVW8rnDMG5Y= | ||||
| github.com/brunomvsouza/singleflight v0.4.0/go.mod h1:8RYo9j5WQRupmsnUz5DlUWZxDLNi+t9Zhj3EZFmns7I= | ||||
| github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= | ||||
| @@ -31,6 +29,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn | ||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||
| github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= | ||||
| github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||
| github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= | ||||
| github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= | ||||
| github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||||
| github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= | ||||
|   | ||||
| @@ -2,9 +2,8 @@ package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
|  | ||||
| 	"ely.by/chrly/internal/http" | ||||
| 	"ely.by/chrly/internal/security" | ||||
|  | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
| @@ -12,20 +11,22 @@ import ( | ||||
| var tokenCmd = &cobra.Command{ | ||||
| 	Use:   "token", | ||||
| 	Short: "Creates a new token, which allows to interact with Chrly API", | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 	RunE: func(cmd *cobra.Command, args []string) error { | ||||
| 		container := shouldGetContainer() | ||||
| 		var auth *http.JwtAuth | ||||
| 		var auth *security.Jwt | ||||
| 		err := container.Resolve(&auth) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		token, err := auth.NewToken(http.SkinScope) | ||||
| 		token, err := auth.NewToken(security.ProfileScope) | ||||
| 		if err != nil { | ||||
| 			log.Fatalf("Unable to create new token. The error is %v\n", err) | ||||
| 			return fmt.Errorf("Unable to create a new token. The error is %v\n", err) | ||||
| 		} | ||||
|  | ||||
| 		fmt.Printf("%s\n", token) | ||||
| 		fmt.Println(token) | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ package di | ||||
| import "github.com/defval/di" | ||||
|  | ||||
| func New() (*di.Container, error) { | ||||
| 	container, err := di.New( | ||||
| 	return di.New( | ||||
| 		config, | ||||
| 		dispatcher, | ||||
| 		logger, | ||||
| @@ -12,11 +12,6 @@ func New() (*di.Container, error) { | ||||
| 		handlers, | ||||
| 		profilesDi, | ||||
| 		server, | ||||
| 		signer, | ||||
| 		securityDiOptions, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return container, nil | ||||
| } | ||||
|   | ||||
| @@ -1,29 +1,36 @@ | ||||
| package di | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"ely.by/chrly/internal/http" | ||||
| 	. "ely.by/chrly/internal/signer" | ||||
| 	"ely.by/chrly/internal/security" | ||||
| 
 | ||||
| 	"github.com/defval/di" | ||||
| 	"github.com/spf13/viper" | ||||
| ) | ||||
| 
 | ||||
| var signer = di.Options( | ||||
| var securityDiOptions = di.Options( | ||||
| 	di.Provide(newTexturesSigner, | ||||
| 		di.As(new(http.TexturesSigner)), | ||||
| 	), | ||||
| ) | ||||
| 
 | ||||
| func newTexturesSigner(config *viper.Viper) (*Signer, error) { | ||||
| func newTexturesSigner(config *viper.Viper) (*security.Signer, error) { | ||||
| 	keyStr := config.GetString("chrly.signing.key") | ||||
| 	if keyStr == "" { | ||||
| 		return nil, errors.New("chrly.signing.key must be set in order to sign textures") | ||||
| 		// TODO: log a message about the generated signing key and the way to specify it permanently | ||||
| 		privateKey, err := rsa.GenerateKey(rand.Reader, 2048) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		return security.NewSigner(privateKey), nil | ||||
| 	} | ||||
| 
 | ||||
| 	var keyBytes []byte | ||||
| @@ -40,10 +47,10 @@ func newTexturesSigner(config *viper.Viper) (*Signer, error) { | ||||
| 	} | ||||
| 
 | ||||
| 	rawPem, _ := pem.Decode(keyBytes) | ||||
| 	key, err := x509.ParsePKCS1PrivateKey(rawPem.Bytes) | ||||
| 	privateKey, err := x509.ParsePKCS1PrivateKey(rawPem.Bytes) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &Signer{Key: key}, nil | ||||
| 	return security.NewSigner(privateKey), nil | ||||
| } | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
| 	"github.com/spf13/viper" | ||||
|  | ||||
| 	. "ely.by/chrly/internal/http" | ||||
| 	"ely.by/chrly/internal/security" | ||||
| ) | ||||
|  | ||||
| var server = di.Options( | ||||
| @@ -19,16 +20,13 @@ var server = di.Options( | ||||
| 	di.Provide(newServer), | ||||
| ) | ||||
|  | ||||
| func newAuthenticator(config *viper.Viper, emitter Emitter) (*JwtAuth, error) { | ||||
| func newAuthenticator(config *viper.Viper) (*security.Jwt, error) { | ||||
| 	key := config.GetString("chrly.secret") | ||||
| 	if key == "" { | ||||
| 		return nil, errors.New("chrly.secret must be set in order to use authenticator") | ||||
| 	} | ||||
|  | ||||
| 	return &JwtAuth{ | ||||
| 		Key:     []byte(key), | ||||
| 		Emitter: emitter, | ||||
| 	}, nil | ||||
| 	return security.NewJwt([]byte(key)), nil | ||||
| } | ||||
|  | ||||
| type serverParams struct { | ||||
|   | ||||
| @@ -86,6 +86,8 @@ type Authenticator interface { | ||||
| 	Authenticate(req *http.Request) error | ||||
| } | ||||
|  | ||||
| // The current middleware implementation doesn't check the scope assigned to the token. | ||||
| // For now there is only one scope and at this moment I don't want to spend time on it | ||||
| func CreateAuthenticationMiddleware(checker Authenticator) mux.MiddlewareFunc { | ||||
| 	return func(handler http.Handler) http.Handler { | ||||
| 		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | ||||
| @@ -137,12 +139,3 @@ func apiForbidden(resp http.ResponseWriter, reason string) { | ||||
| 	}) | ||||
| 	_, _ = resp.Write(result) | ||||
| } | ||||
|  | ||||
| func apiNotFound(resp http.ResponseWriter, reason string) { | ||||
| 	resp.WriteHeader(http.StatusNotFound) | ||||
| 	resp.Header().Set("Content-Type", "application/json") | ||||
| 	result, _ := json.Marshal([]interface{}{ | ||||
| 		reason, | ||||
| 	}) | ||||
| 	_, _ = resp.Write(result) | ||||
| } | ||||
|   | ||||
| @@ -1,78 +0,0 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/SermoDigital/jose/crypto" | ||||
| 	"github.com/SermoDigital/jose/jws" | ||||
| ) | ||||
|  | ||||
| var hashAlg = crypto.SigningMethodHS256 | ||||
|  | ||||
| const scopesClaim = "scopes" | ||||
|  | ||||
| type Scope string | ||||
|  | ||||
| var ( | ||||
| 	SkinScope = Scope("skin") | ||||
| ) | ||||
|  | ||||
| type JwtAuth struct { | ||||
| 	Emitter | ||||
| 	Key []byte | ||||
| } | ||||
|  | ||||
| func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) { | ||||
| 	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(t.Key) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return token, nil | ||||
| } | ||||
|  | ||||
| func (t *JwtAuth) Authenticate(req *http.Request) error { | ||||
| 	if len(t.Key) == 0 { | ||||
| 		return t.emitErr(errors.New("Signing key not set")) | ||||
| 	} | ||||
|  | ||||
| 	bearerToken := req.Header.Get("Authorization") | ||||
| 	if bearerToken == "" { | ||||
| 		return t.emitErr(errors.New("Authentication header not presented")) | ||||
| 	} | ||||
|  | ||||
| 	if !strings.EqualFold(bearerToken[0:7], "BEARER ") { | ||||
| 		return t.emitErr(errors.New("Cannot recognize JWT token in passed value")) | ||||
| 	} | ||||
|  | ||||
| 	tokenStr := bearerToken[7:] | ||||
| 	token, err := jws.ParseJWT([]byte(tokenStr)) | ||||
| 	if err != nil { | ||||
| 		return t.emitErr(errors.New("Cannot parse passed JWT token")) | ||||
| 	} | ||||
|  | ||||
| 	err = token.Validate(t.Key, hashAlg) | ||||
| 	if err != nil { | ||||
| 		return t.emitErr(errors.New("JWT token have invalid signature. It may be corrupted or expired")) | ||||
| 	} | ||||
|  | ||||
| 	t.Emit("authentication:success") | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t *JwtAuth) emitErr(err error) error { | ||||
| 	t.Emit("authentication:error", err) | ||||
| 	return err | ||||
| } | ||||
| @@ -1,127 +0,0 @@ | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| ) | ||||
|  | ||||
| const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNTE2NjU4MTkzIiwic2NvcGVzIjoic2tpbiJ9.agbBS0qdyYMBaVfTZJAZcTTRgW1Y0kZty4H3N2JHBO8" | ||||
|  | ||||
| func TestJwtAuth_NewToken(t *testing.T) { | ||||
| 	t.Run("success", func(t *testing.T) { | ||||
| 		jwt := &JwtAuth{Key: []byte("secret")} | ||||
| 		token, err := jwt.NewToken(SkinScope) | ||||
| 		assert.Nil(t, err) | ||||
| 		assert.NotNil(t, token) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("key not provided", func(t *testing.T) { | ||||
| 		jwt := &JwtAuth{} | ||||
| 		token, err := jwt.NewToken(SkinScope) | ||||
| 		assert.Error(t, err, "signing key not available") | ||||
| 		assert.Nil(t, token) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestJwtAuth_Authenticate(t *testing.T) { | ||||
| 	t.Run("success", func(t *testing.T) { | ||||
| 		emitter := &emitterMock{} | ||||
| 		emitter.On("Emit", "authentication:success") | ||||
|  | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "Bearer "+jwt) | ||||
| 		jwt := &JwtAuth{Key: []byte("secret"), Emitter: emitter} | ||||
|  | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		assert.Nil(t, err) | ||||
|  | ||||
| 		emitter.AssertExpectations(t) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("request without auth header", func(t *testing.T) { | ||||
| 		emitter := &emitterMock{} | ||||
| 		emitter.On("Emit", "authentication:error", mock.MatchedBy(func(err error) bool { | ||||
| 			assert.Error(t, err, "Authentication header not presented") | ||||
| 			return true | ||||
| 		})) | ||||
|  | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		jwt := &JwtAuth{Key: []byte("secret"), Emitter: emitter} | ||||
|  | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		assert.Error(t, err, "Authentication header not presented") | ||||
|  | ||||
| 		emitter.AssertExpectations(t) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("no bearer token prefix", func(t *testing.T) { | ||||
| 		emitter := &emitterMock{} | ||||
| 		emitter.On("Emit", "authentication:error", mock.MatchedBy(func(err error) bool { | ||||
| 			assert.Error(t, err, "Cannot recognize JWT token in passed value") | ||||
| 			return true | ||||
| 		})) | ||||
|  | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "this is not jwt") | ||||
| 		jwt := &JwtAuth{Key: []byte("secret"), Emitter: emitter} | ||||
|  | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		assert.Error(t, err, "Cannot recognize JWT token in passed value") | ||||
|  | ||||
| 		emitter.AssertExpectations(t) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("bearer token but not jwt", func(t *testing.T) { | ||||
| 		emitter := &emitterMock{} | ||||
| 		emitter.On("Emit", "authentication:error", mock.MatchedBy(func(err error) bool { | ||||
| 			assert.Error(t, err, "Cannot parse passed JWT token") | ||||
| 			return true | ||||
| 		})) | ||||
|  | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "Bearer thisIs.Not.Jwt") | ||||
| 		jwt := &JwtAuth{Key: []byte("secret"), Emitter: emitter} | ||||
|  | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		assert.Error(t, err, "Cannot parse passed JWT token") | ||||
|  | ||||
| 		emitter.AssertExpectations(t) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("when secret is not set", func(t *testing.T) { | ||||
| 		emitter := &emitterMock{} | ||||
| 		emitter.On("Emit", "authentication:error", mock.MatchedBy(func(err error) bool { | ||||
| 			assert.Error(t, err, "Signing key not set") | ||||
| 			return true | ||||
| 		})) | ||||
|  | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "Bearer "+jwt) | ||||
| 		jwt := &JwtAuth{Emitter: emitter} | ||||
|  | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		assert.Error(t, err, "Signing key not set") | ||||
|  | ||||
| 		emitter.AssertExpectations(t) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("invalid signature", func(t *testing.T) { | ||||
| 		emitter := &emitterMock{} | ||||
| 		emitter.On("Emit", "authentication:error", mock.MatchedBy(func(err error) bool { | ||||
| 			assert.Error(t, err, "JWT token have invalid signature. It may be corrupted or expired") | ||||
| 			return true | ||||
| 		})) | ||||
|  | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "Bearer "+jwt) | ||||
| 		jwt := &JwtAuth{Key: []byte("this is another secret"), Emitter: emitter} | ||||
|  | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		assert.Error(t, err, "JWT token have invalid signature. It may be corrupted or expired") | ||||
|  | ||||
| 		emitter.AssertExpectations(t) | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										82
									
								
								internal/security/jwt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								internal/security/jwt.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| package security | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
|  | ||||
| 	"ely.by/chrly/internal/version" | ||||
| ) | ||||
|  | ||||
| var now = time.Now | ||||
| var signingMethod = jwt.SigningMethodHS256 | ||||
|  | ||||
| const scopesClaim = "scopes" | ||||
|  | ||||
| type Scope string | ||||
|  | ||||
| const ( | ||||
| 	ProfileScope Scope = "profiles" | ||||
| ) | ||||
|  | ||||
| func NewJwt(key []byte) *Jwt { | ||||
| 	return &Jwt{ | ||||
| 		Key: key, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type Jwt struct { | ||||
| 	Key []byte | ||||
| } | ||||
|  | ||||
| func (t *Jwt) NewToken(scopes ...Scope) (string, error) { | ||||
| 	if len(scopes) == 0 { | ||||
| 		return "", errors.New("you must specify at least one scope") | ||||
| 	} | ||||
|  | ||||
| 	token := jwt.NewWithClaims(signingMethod, jwt.MapClaims{ | ||||
| 		"iss":       "chrly", | ||||
| 		"iat":       now().Unix(), | ||||
| 		scopesClaim: scopes, | ||||
| 	}) | ||||
| 	token.Header["v"] = version.MajorVersion | ||||
|  | ||||
| 	return token.SignedString(t.Key) | ||||
| } | ||||
|  | ||||
| // Keep those names generic in order to reuse them in future for alternative authentication methods | ||||
| var MissingAuthenticationError = errors.New("authentication value not provided") | ||||
| var InvalidTokenError = errors.New("passed authentication value is invalid") | ||||
|  | ||||
| func (t *Jwt) Authenticate(req *http.Request) error { | ||||
| 	bearerToken := req.Header.Get("Authorization") | ||||
| 	if bearerToken == "" { | ||||
| 		return MissingAuthenticationError | ||||
| 	} | ||||
|  | ||||
| 	if !strings.HasPrefix(strings.ToLower(bearerToken), "bearer ") { | ||||
| 		return InvalidTokenError | ||||
| 	} | ||||
|  | ||||
| 	tokenStr := bearerToken[7:] | ||||
| 	token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { | ||||
| 		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||||
| 			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | ||||
| 		} | ||||
|  | ||||
| 		return t.Key, nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return errors.Join(InvalidTokenError, err) | ||||
| 	} | ||||
|  | ||||
| 	if _, vHeaderExists := token.Header["v"]; !vHeaderExists { | ||||
| 		return errors.Join(InvalidTokenError, errors.New("missing v header")) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										75
									
								
								internal/security/jwt_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								internal/security/jwt_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| package security | ||||
|  | ||||
| import ( | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| const jwtString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInYiOjV9.eyJpYXQiOjE3MDY3ODY3NzUsImlzcyI6ImNocmx5Iiwic2NvcGVzIjpbInByb2ZpbGVzIl19.LrXrKo5iRFFHCDlMsVDhmJJheZqxbxuEVXB4XswHFKY" | ||||
|  | ||||
| func TestJwtAuth_NewToken(t *testing.T) { | ||||
| 	jwt := NewJwt([]byte("secret")) | ||||
| 	now = func() time.Time { | ||||
| 		return time.Date(2024, 2, 1, 11, 26, 15, 0, time.UTC) | ||||
| 	} | ||||
|  | ||||
| 	t.Run("with scope", func(t *testing.T) { | ||||
| 		token, err := jwt.NewToken(ProfileScope, "custom-scope") | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInYiOjV9.eyJpYXQiOjE3MDY3ODY3NzUsImlzcyI6ImNocmx5Iiwic2NvcGVzIjpbInByb2ZpbGVzIiwiY3VzdG9tLXNjb3BlIl19.Iq673YyWWkJZjIkBmKYRN8Lx9qoD39S_e-MegG0aORM", token) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("no scopes", func(t *testing.T) { | ||||
| 		token, err := jwt.NewToken() | ||||
| 		require.Error(t, err) | ||||
| 		require.Empty(t, token) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestJwtAuth_Authenticate(t *testing.T) { | ||||
| 	jwt := NewJwt([]byte("secret")) | ||||
| 	t.Run("success", func(t *testing.T) { | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "Bearer "+jwtString) | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		require.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("request without auth header", func(t *testing.T) { | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		require.ErrorIs(t, err, MissingAuthenticationError) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("no bearer token prefix", func(t *testing.T) { | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "trash") | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		require.ErrorIs(t, err, InvalidTokenError) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("bearer token but not jwt", func(t *testing.T) { | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "Bearer seems.like.jwt") | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		require.ErrorIs(t, err, InvalidTokenError) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("invalid signature", func(t *testing.T) { | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "Bearer "+jwtString+"123") | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		require.ErrorIs(t, err, InvalidTokenError) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("missing v header", func(t *testing.T) { | ||||
| 		req := httptest.NewRequest("POST", "http://localhost", nil) | ||||
| 		req.Header.Add("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDY3ODY3NzUsImlzcyI6ImNocmx5Iiwic2NvcGVzIjpbInByb2ZpbGVzIl19.zOX2ZKyU37kjwt1p9uCHxALxWQD2UC0wWcAcNvBXGq0") | ||||
| 		err := jwt.Authenticate(req) | ||||
| 		require.ErrorIs(t, err, InvalidTokenError) | ||||
| 		require.ErrorContains(t, err, "missing v header") | ||||
| 	}) | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package signer | ||||
| package security | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto" | ||||
| @@ -6,37 +6,35 @@ import ( | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/sha1" | ||||
| 	"encoding/base64" | ||||
| 	"errors" | ||||
| ) | ||||
| 
 | ||||
| var randomReader = rand.Reader | ||||
| 
 | ||||
| func NewSigner(key *rsa.PrivateKey) *Signer { | ||||
| 	return &Signer{Key: key} | ||||
| } | ||||
| 
 | ||||
| type Signer struct { | ||||
| 	Key *rsa.PrivateKey | ||||
| } | ||||
| 
 | ||||
| func (s *Signer) SignTextures(textures string) (string, error) { | ||||
| 	if s.Key == nil { | ||||
| 		return "", errors.New("Key is empty") | ||||
| 	} | ||||
| 
 | ||||
| 	message := []byte(textures) | ||||
| 	messageHash := sha1.New() | ||||
| 	_, _ = messageHash.Write(message) | ||||
| 	messageHashSum := messageHash.Sum(nil) | ||||
| 	_, err := messageHash.Write(message) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	messageHashSum := messageHash.Sum(nil) | ||||
| 	signature, err := rsa.SignPKCS1v15(randomReader, s.Key, crypto.SHA1, messageHashSum) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	return base64.StdEncoding.EncodeToString(signature), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Signer) GetPublicKey() (*rsa.PublicKey, error) { | ||||
| 	if s.Key == nil { | ||||
| 		return nil, errors.New("Key is empty") | ||||
| 	} | ||||
| 
 | ||||
| 	return &s.Key.PublicKey, nil | ||||
| } | ||||
							
								
								
									
										43
									
								
								internal/security/signer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								internal/security/signer_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| package security | ||||
|  | ||||
| import ( | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| type ConstantReader struct { | ||||
| } | ||||
|  | ||||
| func (c *ConstantReader) Read(p []byte) (int, error) { | ||||
| 	return 1, nil | ||||
| } | ||||
|  | ||||
| func TestSigner_SignTextures(t *testing.T) { | ||||
| 	randomReader = &ConstantReader{} | ||||
|  | ||||
| 	rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n")) | ||||
| 	key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes) | ||||
|  | ||||
| 	signer := NewSigner(key) | ||||
|  | ||||
| 	signature, err := signer.SignTextures("eyJ0aW1lc3RhbXAiOjE2MTQzMDcxMzQsInByb2ZpbGVJZCI6ImZmYzhmZGM5NTgyNDUwOWU4YTU3Yzk5Yjk0MGZiOTk2IiwicHJvZmlsZU5hbWUiOiJFcmlja1NrcmF1Y2giLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9lbHkuYnkvc3RvcmFnZS9za2lucy82OWM2NzQwZDI5OTNlNWQ2ZjZhN2ZjOTI0MjBlZmMyOS5wbmcifX0sImVseSI6dHJ1ZX0") | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, "IyHCxTP5ITquEXTHcwCtLd08jWWy16JwlQeWg8naxhoAVQecHGRdzHRscuxtdq/446kmeox7h4EfRN2A2ZLL+A==", signature) | ||||
| } | ||||
|  | ||||
| func TestSigner_GetPublicKey(t *testing.T) { | ||||
| 	randomReader = &ConstantReader{} | ||||
|  | ||||
| 	rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n")) | ||||
| 	key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes) | ||||
|  | ||||
| 	signer := NewSigner(key) | ||||
|  | ||||
| 	publicKey, err := signer.GetPublicKey() | ||||
| 	require.NoError(t, err) | ||||
| 	require.IsType(t, &rsa.PublicKey{}, publicKey) | ||||
| } | ||||
| @@ -1,64 +0,0 @@ | ||||
| package signer | ||||
|  | ||||
| import ( | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
|  | ||||
| 	"testing" | ||||
|  | ||||
| 	assert "github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| type ConstantReader struct { | ||||
| } | ||||
|  | ||||
| func (c *ConstantReader) Read(p []byte) (int, error) { | ||||
| 	return 1, nil | ||||
| } | ||||
|  | ||||
| func TestSigner_SignTextures(t *testing.T) { | ||||
| 	randomReader = &ConstantReader{} | ||||
|  | ||||
| 	t.Run("sign textures", func(t *testing.T) { | ||||
| 		rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n")) | ||||
| 		key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes) | ||||
|  | ||||
| 		signer := &Signer{key} | ||||
|  | ||||
| 		signature, err := signer.SignTextures("eyJ0aW1lc3RhbXAiOjE2MTQzMDcxMzQsInByb2ZpbGVJZCI6ImZmYzhmZGM5NTgyNDUwOWU4YTU3Yzk5Yjk0MGZiOTk2IiwicHJvZmlsZU5hbWUiOiJFcmlja1NrcmF1Y2giLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9lbHkuYnkvc3RvcmFnZS9za2lucy82OWM2NzQwZDI5OTNlNWQ2ZjZhN2ZjOTI0MjBlZmMyOS5wbmcifX0sImVseSI6dHJ1ZX0") | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "IyHCxTP5ITquEXTHcwCtLd08jWWy16JwlQeWg8naxhoAVQecHGRdzHRscuxtdq/446kmeox7h4EfRN2A2ZLL+A==", signature) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("empty key", func(t *testing.T) { | ||||
| 		signer := &Signer{} | ||||
|  | ||||
| 		signature, err := signer.SignTextures("hello world") | ||||
| 		assert.Error(t, err, "Key is empty") | ||||
| 		assert.Empty(t, signature) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestSigner_GetPublicKey(t *testing.T) { | ||||
| 	randomReader = &ConstantReader{} | ||||
|  | ||||
| 	t.Run("get public key", func(t *testing.T) { | ||||
| 		rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n")) | ||||
| 		key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes) | ||||
|  | ||||
| 		signer := &Signer{key} | ||||
|  | ||||
| 		publicKey, err := signer.GetPublicKey() | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.IsType(t, &rsa.PublicKey{}, publicKey) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("empty key", func(t *testing.T) { | ||||
| 		signer := &Signer{} | ||||
|  | ||||
| 		publicKey, err := signer.GetPublicKey() | ||||
| 		assert.Error(t, err, "Key is empty") | ||||
| 		assert.Nil(t, publicKey) | ||||
| 	}) | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| package version | ||||
|  | ||||
| const MajorVersion = 5 | ||||
|  | ||||
| var ( | ||||
| 	version = "undefined" | ||||
| 	commit  = "unknown" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user