Initial commit
This commit is contained in:
47
pkg/auth/hmac.go
Normal file
47
pkg/auth/hmac.go
Normal 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
34
pkg/auth/hmac_test.go
Normal 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
31
pkg/auth/jwt.go
Normal 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
150
pkg/auth/key_store.go
Normal 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
83
pkg/auth/validator.go
Normal 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)
|
||||
}
|
||||
}
|
||||
31
pkg/auth/validator_test.go
Normal file
31
pkg/auth/validator_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user