mirror of
https://github.com/elyby/chrly.git
synced 2025-03-10 09:59:26 +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:
parent
11340289ad
commit
10c11bc060
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user