Initial commit

This commit is contained in:
Yaser
2026-08-13 20:20:47 +03:30
commit 78a0c832d7
33 changed files with 3399 additions and 0 deletions

47
pkg/auth/hmac.go Normal file
View File

@@ -0,0 +1,47 @@
package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"time"
)
// APIKeyHMAC contains API keys credential to authenticate to HTTP API using HMAC
type APIKeyHMAC struct {
AccessKey string
SecretKey string
}
// NewAPIKeyHMAC creates an instance of APIKeyHMAC
func NewAPIKeyHMAC(accessKey, secretKey string) *APIKeyHMAC {
return &APIKeyHMAC{
AccessKey: accessKey,
SecretKey: secretKey,
}
}
// GetSignature return a signature for the given nonce, if nonce is zero it use the current time in millisecond
func (key *APIKeyHMAC) GetSignature(nonce int64) string {
if nonce == 0 {
nonce = int64(time.Now().UnixNano() * 1000000)
}
mac := hmac.New(sha256.New, []byte(key.SecretKey))
mac.Write([]byte(fmt.Sprintf("%d%s", nonce, key.AccessKey)))
return hex.EncodeToString(mac.Sum(nil))
}
// GetSignedHeader returns a header with valid HMAC authorization fields
func (key *APIKeyHMAC) GetSignedHeader(nonce int64) http.Header {
if nonce == 0 {
nonce = int64(time.Now().UnixNano() * 1000000)
}
return http.Header{
"X-Auth-Apikey": {key.AccessKey},
"X-Auth-Nonce": {fmt.Sprintf("%d", nonce)},
"X-Auth-Signature": {key.GetSignature(nonce)},
}
}

34
pkg/auth/hmac_test.go Normal file
View File

@@ -0,0 +1,34 @@
package auth
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAPIKeyHMACGetSignature(t *testing.T) {
accessKey := "61d025b8573501c2"
secretKey := "2d0b4979c7fe6986daa8e21d1dc0644f"
k := NewAPIKeyHMAC(accessKey, secretKey)
nonce := int64(1584524005143)
signature := k.GetSignature(nonce)
assert.Equal(t, "bd42b945e095880e28d046846dbecf655fdf09d95a396a24fe6fe1df42f15d13", signature)
}
func TestAPIKeyHMACGetSignedHeader(t *testing.T) {
accessKey := "61d025b8573501c2"
secretKey := "2d0b4979c7fe6986daa8e21d1dc0644f"
k := NewAPIKeyHMAC(accessKey, secretKey)
nonce := int64(1584524005143)
headers := k.GetSignedHeader(nonce)
assert.Equal(t,
http.Header{
"X-Auth-Apikey": {accessKey},
"X-Auth-Nonce": {"1584524005143"},
"X-Auth-Signature": {"bd42b945e095880e28d046846dbecf655fdf09d95a396a24fe6fe1df42f15d13"},
}, headers)
}

31
pkg/auth/jwt.go Normal file
View File

@@ -0,0 +1,31 @@
package auth
import (
"crypto/rsa"
"encoding/json"
"github.com/golang-jwt/jwt"
)
// Auth struct represents parsed jwt information.
type Auth struct {
UID string `json:"uid"`
State string `json:"state"`
Email string `json:"email"`
Role string `json:"role"`
ReferralID json.Number `json:"referral_id"`
Level json.Number `json:"level"`
Audience []string `json:"aud,omitempty"`
jwt.StandardClaims
}
// ParseAndValidate parses token and validates RS256 signature with Barong RSA public key.
func ParseAndValidate(token string, key *rsa.PublicKey) (Auth, error) {
auth := Auth{}
_, err := jwt.ParseWithClaims(token, &auth, func(t *jwt.Token) (interface{}, error) {
return key, nil
})
return auth, err
}

150
pkg/auth/key_store.go Normal file
View File

@@ -0,0 +1,150 @@
package auth
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io/ioutil"
"os"
"github.com/golang-jwt/jwt"
)
type KeyStore struct {
PublicKey *rsa.PublicKey
PrivateKey *rsa.PrivateKey
}
func fileExist(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func (ks *KeyStore) LoadPublicKeyFromFile(path string) error {
pemBytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}
key, err := jwt.ParseRSAPublicKeyFromPEM(pemBytes)
if err != nil {
return err
}
ks.PublicKey = key
return nil
}
func (ks *KeyStore) LoadPublicKeyFromString(str string) error {
pemBytes, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return err
}
key, err := jwt.ParseRSAPublicKeyFromPEM(pemBytes)
if err != nil {
return err
}
ks.PublicKey = key
return nil
}
func (ks *KeyStore) LoadPrivateKey(path string) error {
pemBytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}
key, err := jwt.ParseRSAPrivateKeyFromPEM(pemBytes)
if err != nil {
return err
}
ks.PrivateKey = key
return nil
}
func (ks *KeyStore) GenerateKeys() error {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
ks.PrivateKey = key
ks.PublicKey = &key.PublicKey
return nil
}
func (ks *KeyStore) SavePrivateKey(path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
block := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(ks.PrivateKey),
}
return pem.Encode(file, block)
}
func (ks *KeyStore) SavePublicKey(path string) error {
bytes, err := x509.MarshalPKIXPublicKey(ks.PublicKey)
if err != nil {
return err
}
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
block := &pem.Block{
Type: "PUBLIC KEY",
Bytes: bytes,
}
return pem.Encode(file, block)
}
func LoadOrGenerateKeys(privPath, pubPath string) (*KeyStore, error) {
ks := &KeyStore{}
if fileExist(privPath) {
if err := ks.LoadPrivateKey(privPath); err != nil {
return ks, err
}
} else {
if err := ks.GenerateKeys(); err != nil {
return ks, err
}
if err := ks.SavePrivateKey(privPath); err != nil {
return ks, err
}
}
if fileExist(pubPath) {
if err := ks.LoadPublicKeyFromFile(pubPath); err != nil {
return ks, err
}
} else {
if ks.PublicKey == nil {
ks.PublicKey = &ks.PrivateKey.PublicKey
}
if err := ks.SavePublicKey(pubPath); err != nil {
return ks, err
}
}
return ks, nil
}

83
pkg/auth/validator.go Normal file
View File

@@ -0,0 +1,83 @@
package auth
import (
"crypto/ed25519"
"crypto/rsa"
"fmt"
"os"
pkgjwt "github.com/openware/pkg/jwt"
)
const (
ValidatorRSA = "rsa"
ValidatorEdDSA = "eddsa"
)
// Validator verifies Barong JWT tokens using RS256 or EdDSA depending on configured key material.
type Validator struct {
Mode string
RSA *rsa.PublicKey
EdDSA ed25519.PublicKey
}
type Claims struct {
UID string
Role string
}
// LoadValidator reads JWT_PUBLIC_KEY (base64 PEM) or -pubKey file path.
// OpenDAX / Barong 2.6 uses RS256; upstream Rango 3.1.2+ defaults to EdDSA.
func LoadValidator(pubKeyPath string) (Validator, error) {
if encPem := os.Getenv("JWT_PUBLIC_KEY"); encPem != "" {
rsaStore := KeyStore{}
if err := rsaStore.LoadPublicKeyFromString(encPem); err == nil && rsaStore.PublicKey != nil {
return Validator{Mode: ValidatorRSA, RSA: rsaStore.PublicKey}, nil
}
edStore := pkgjwt.KeyStoreEdDSA{}
if err := edStore.LoadPublicKeyFromString(encPem); err == nil && edStore.PublicKey != nil {
return Validator{Mode: ValidatorEdDSA, EdDSA: edStore.PublicKey}, nil
}
return Validator{}, fmt.Errorf("JWT_PUBLIC_KEY is neither a valid RSA nor EdDSA public key")
}
rsaStore := KeyStore{}
if err := rsaStore.LoadPublicKeyFromFile("config/rsa-key.pub"); err == nil && rsaStore.PublicKey != nil {
return Validator{Mode: ValidatorRSA, RSA: rsaStore.PublicKey}, nil
}
edStore := pkgjwt.KeyStoreEdDSA{}
if err := edStore.LoadPublicKeyFromFile(pubKeyPath); err != nil {
return Validator{}, fmt.Errorf("load public key from %q: %w", pubKeyPath, err)
}
if edStore.PublicKey == nil {
return Validator{}, fmt.Errorf("public key not loaded from %q", pubKeyPath)
}
return Validator{Mode: ValidatorEdDSA, EdDSA: edStore.PublicKey}, nil
}
func (v Validator) ParseAndValidate(token string) (Claims, error) {
if token == "" {
return Claims{}, fmt.Errorf("missing token")
}
switch v.Mode {
case ValidatorRSA:
auth, err := ParseAndValidate(token, v.RSA)
if err != nil {
return Claims{}, err
}
return Claims{UID: auth.UID, Role: auth.Role}, nil
case ValidatorEdDSA:
auth, err := pkgjwt.ParseAndValidateEdDSA(token, v.EdDSA)
if err != nil {
return Claims{}, err
}
return Claims{UID: auth.UID, Role: auth.Role}, nil
default:
return Claims{}, fmt.Errorf("unknown validator mode %q", v.Mode)
}
}

View File

@@ -0,0 +1,31 @@
package auth
import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadValidator_RSAFromEnv(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
store := KeyStore{PublicKey: &key.PublicKey}
path := t.TempDir() + "/pub.pem"
require.NoError(t, store.SavePublicKey(path))
pemBytes, err := os.ReadFile(path)
require.NoError(t, err)
t.Setenv("JWT_PUBLIC_KEY", base64.StdEncoding.EncodeToString(pemBytes))
validator, err := LoadValidator("config/ed25519-key.pub")
require.NoError(t, err)
assert.Equal(t, ValidatorRSA, validator.Mode)
assert.NotNil(t, validator.RSA)
}