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:
ErickSkrauch
2024-02-01 12:11:39 +01:00
parent 11340289ad
commit 10c11bc060
15 changed files with 246 additions and 321 deletions

82
internal/security/jwt.go Normal file
View 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
}

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

View File

@@ -0,0 +1,40 @@
package security
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
)
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) {
message := []byte(textures)
messageHash := sha1.New()
_, 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 {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
func (s *Signer) GetPublicKey() (*rsa.PublicKey, error) {
return &s.Key.PublicKey, nil
}

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