diff --git a/server/constants/env.go b/server/constants/env.go index e36c5e3..213a125 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -20,6 +20,8 @@ const ( EnvKeyAuthorizerURL = "AUTHORIZER_URL" // EnvKeyPort key for env variable PORT EnvKeyPort = "PORT" + // EnvKeyClientID key for env variable CLIENT_ID + EnvKeyClientID = "CLIENT_ID" // EnvKeyAdminSecret key for env variable ADMIN_SECRET EnvKeyAdminSecret = "ADMIN_SECRET" diff --git a/server/crypto/ecdsa.go b/server/crypto/ecdsa.go new file mode 100644 index 0000000..b76813b --- /dev/null +++ b/server/crypto/ecdsa.go @@ -0,0 +1,137 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "errors" +) + +// NewECDSAKey to generate new ECDSA Key if env is not set +func NewECDSAKey() (*ecdsa.PrivateKey, string, string, error) { + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, "", "", err + } + + privateKey, publicKey, err := AsECDSAStr(key, &key.PublicKey) + if err != nil { + return nil, "", "", err + } + + return key, privateKey, publicKey, err +} + +// IsECDSA checks if given string is valid ECDSA algo +func IsECDSA(algo string) bool { + switch algo { + case "ES256", "ES384", "ES512": + return true + default: + return false + } +} + +// ExportEcdsaPrivateKeyAsPemStr to get ECDSA private key as pem string +func ExportEcdsaPrivateKeyAsPemStr(privkey *ecdsa.PrivateKey) (string, error) { + privkeyBytes, err := x509.MarshalECPrivateKey(privkey) + if err != nil { + return "", err + } + privkeyPem := pem.EncodeToMemory( + &pem.Block{ + Type: "ECDSA PRIVATE KEY", + Bytes: privkeyBytes, + }, + ) + return string(privkeyPem), nil +} + +// ExportEcdsaPublicKeyAsPemStr to get ECDSA public key as pem string +func ExportEcdsaPublicKeyAsPemStr(pubkey *ecdsa.PublicKey) (string, error) { + pubkeyBytes, err := x509.MarshalPKIXPublicKey(pubkey) + if err != nil { + return "", err + } + pubkeyPem := pem.EncodeToMemory( + &pem.Block{ + Type: "ECDSA PUBLIC KEY", + Bytes: pubkeyBytes, + }, + ) + + return string(pubkeyPem), nil +} + +// ParseEcdsaPrivateKeyFromPemStr to parse ECDSA private key from pem string +func ParseEcdsaPrivateKeyFromPemStr(privPEM string) (*ecdsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(privPEM)) + if block == nil { + return nil, errors.New("failed to parse PEM block containing the key") + } + + priv, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + + return priv, nil +} + +// ParseEcdsaPublicKeyFromPemStr to parse ECDSA public key from pem string +func ParseEcdsaPublicKeyFromPemStr(pubPEM string) (*ecdsa.PublicKey, error) { + block, _ := pem.Decode([]byte(pubPEM)) + if block == nil { + return nil, errors.New("failed to parse PEM block containing the key") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + switch pub := pub.(type) { + case *ecdsa.PublicKey: + return pub, nil + default: + break // fall through + } + return nil, errors.New("Key type is not ECDSA") +} + +// AsECDSAStr returns private, public key string or error +func AsECDSAStr(privateKey *ecdsa.PrivateKey, publicKey *ecdsa.PublicKey) (string, string, error) { + // Export the keys to pem string + privPem, err := ExportEcdsaPrivateKeyAsPemStr(privateKey) + if err != nil { + return "", "", err + } + pubPem, err := ExportEcdsaPublicKeyAsPemStr(publicKey) + if err != nil { + return "", "", err + } + + // Import the keys from pem string + privParsed, err := ParseEcdsaPrivateKeyFromPemStr(privPem) + if err != nil { + return "", "", err + } + pubParsed, err := ParseEcdsaPublicKeyFromPemStr(pubPem) + if err != nil { + return "", "", err + } + + // Export the newly imported keys + privParsedPem, err := ExportEcdsaPrivateKeyAsPemStr(privParsed) + if err != nil { + return "", "", err + } + pubParsedPem, err := ExportEcdsaPublicKeyAsPemStr(pubParsed) + if err != nil { + return "", "", err + } + + return privParsedPem, pubParsedPem, nil +} diff --git a/server/crypto/hmac.go b/server/crypto/hmac.go new file mode 100644 index 0000000..cb349b5 --- /dev/null +++ b/server/crypto/hmac.go @@ -0,0 +1,19 @@ +package crypto + +import "github.com/google/uuid" + +// NewHMAC key returns new key that can be used to ecnrypt data using HMAC algo +func NewHMACKey() string { + key := uuid.New().String() + return key +} + +// IsHMACValid checks if given string is valid HMCA algo +func IsHMACA(algo string) bool { + switch algo { + case "HS256", "HS384", "HS512": + return true + default: + return false + } +} diff --git a/server/crypto/rsa.go b/server/crypto/rsa.go new file mode 100644 index 0000000..34edad5 --- /dev/null +++ b/server/crypto/rsa.go @@ -0,0 +1,127 @@ +package crypto + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" +) + +// NewRSAKey to generate new RSA Key if env is not set +func NewRSAKey() (*rsa.PrivateKey, string, string, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, "", "", err + } + + privateKey, publicKey, err := AsRSAStr(key, &key.PublicKey) + if err != nil { + return nil, "", "", err + } + + return key, privateKey, publicKey, err +} + +// IsRSA checks if given string is valid RSA algo +func IsRSA(algo string) bool { + switch algo { + case "RS256", "RS384", "RS512": + return true + default: + return false + } +} + +// ExportRsaPrivateKeyAsPemStr to get RSA private key as pem string +func ExportRsaPrivateKeyAsPemStr(privkey *rsa.PrivateKey) string { + privkeyBytes := x509.MarshalPKCS1PrivateKey(privkey) + privkeyPem := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privkeyBytes, + }, + ) + return string(privkeyPem) +} + +// ExportRsaPublicKeyAsPemStr to get RSA public key as pem string +func ExportRsaPublicKeyAsPemStr(pubkey *rsa.PublicKey) (string, error) { + pubkeyBytes, err := x509.MarshalPKIXPublicKey(pubkey) + if err != nil { + return "", err + } + pubkeyPem := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: pubkeyBytes, + }, + ) + + return string(pubkeyPem), nil +} + +// ParseRsaPrivateKeyFromPemStr to parse RSA private key from pem string +func ParseRsaPrivateKeyFromPemStr(privPEM string) (*rsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(privPEM)) + if block == nil { + return nil, errors.New("failed to parse PEM block containing the key") + } + + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + + return priv, nil +} + +// ParseRsaPublicKeyFromPemStr to parse RSA public key from pem string +func ParseRsaPublicKeyFromPemStr(pubPEM string) (*rsa.PublicKey, error) { + block, _ := pem.Decode([]byte(pubPEM)) + if block == nil { + return nil, errors.New("failed to parse PEM block containing the key") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + switch pub := pub.(type) { + case *rsa.PublicKey: + return pub, nil + default: + break // fall through + } + return nil, errors.New("Key type is not RSA") +} + +// AsRSAStr returns private, public key string or error +func AsRSAStr(privateKey *rsa.PrivateKey, publickKey *rsa.PublicKey) (string, string, error) { + // Export the keys to pem string + privPem := ExportRsaPrivateKeyAsPemStr(privateKey) + pubPem, err := ExportRsaPublicKeyAsPemStr(publickKey) + if err != nil { + return "", "", err + } + + // Import the keys from pem string + privParsed, err := ParseRsaPrivateKeyFromPemStr(privPem) + if err != nil { + return "", "", err + } + pubParsed, err := ParseRsaPublicKeyFromPemStr(pubPem) + if err != nil { + return "", "", err + } + + // Export the newly imported keys + privParsedPem := ExportRsaPrivateKeyAsPemStr(privParsed) + pubParsedPem, err := ExportRsaPublicKeyAsPemStr(pubParsed) + if err != nil { + return "", "", err + } + + return privParsedPem, pubParsedPem, nil +} diff --git a/server/env/env.go b/server/env/env.go index 11f213d..03ad1de 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -1,22 +1,80 @@ package env import ( + "fmt" "log" "os" "strings" "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/utils" "github.com/gin-gonic/gin" - "github.com/google/uuid" "github.com/joho/godotenv" ) +// InitRequiredEnv to initialize EnvData and through error if required env are not present +func InitRequiredEnv() { + envPath := os.Getenv(constants.EnvKeyEnvPath) + + if envPath == "" { + envPath = `.env` + } + + if envstore.ARG_ENV_FILE != nil && *envstore.ARG_ENV_FILE != "" { + envPath = *envstore.ARG_ENV_FILE + } + + err := godotenv.Load(envPath) + if err != nil { + log.Printf("using OS env instead of %s file", envPath) + } + + dbURL := os.Getenv(constants.EnvKeyDatabaseURL) + dbType := os.Getenv(constants.EnvKeyDatabaseType) + dbName := os.Getenv(constants.EnvKeyDatabaseName) + + if dbType == "" { + if envstore.ARG_DB_TYPE != nil && *envstore.ARG_DB_TYPE != "" { + dbType = *envstore.ARG_DB_TYPE + } + + if dbType == "" { + panic("DATABASE_TYPE is required") + } + } + + if dbURL == "" { + if envstore.ARG_DB_URL != nil && *envstore.ARG_DB_URL != "" { + dbURL = *envstore.ARG_DB_URL + } + + if dbURL == "" { + panic("DATABASE_URL is required") + } + } + + if dbName == "" { + if dbName == "" { + dbName = "authorizer" + } + } + + envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEnvPath, envPath) + envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL) + envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseType, dbType) + envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseName, dbName) +} + // InitEnv to initialize EnvData and through error if required env are not present -func InitEnv() { - // get clone of current store - envData := envstore.EnvInMemoryStoreObj.GetEnvStoreClone() +func InitAllEnv() { + envData, err := GetEnvData() + if err != nil { + log.Println("No env data found in db, using local clone of env data") + // get clone of current store + envData = envstore.EnvInMemoryStoreObj.GetEnvStoreClone() + } if envData.StringEnv[constants.EnvKeyEnv] == "" { envData.StringEnv[constants.EnvKeyEnv] = os.Getenv(constants.EnvKeyEnv) @@ -36,19 +94,6 @@ func InitEnv() { envData.StringEnv[constants.EnvKeyAppURL] = os.Getenv(constants.EnvKeyAppURL) } - if envData.StringEnv[constants.EnvKeyEnvPath] == "" { - envData.StringEnv[constants.EnvKeyEnvPath] = `.env` - } - - if envstore.ARG_ENV_FILE != nil && *envstore.ARG_ENV_FILE != "" { - envData.StringEnv[constants.EnvKeyEnvPath] = *envstore.ARG_ENV_FILE - } - - err := godotenv.Load(envData.StringEnv[constants.EnvKeyEnvPath]) - if err != nil { - log.Printf("using OS env instead of %s file", envData.StringEnv[constants.EnvKeyEnvPath]) - } - if envData.StringEnv[constants.EnvKeyPort] == "" { envData.StringEnv[constants.EnvKeyPort] = os.Getenv(constants.EnvKeyPort) if envData.StringEnv[constants.EnvKeyPort] == "" { @@ -60,37 +105,6 @@ func InitEnv() { envData.StringEnv[constants.EnvKeyAdminSecret] = os.Getenv(constants.EnvKeyAdminSecret) } - if envData.StringEnv[constants.EnvKeyDatabaseType] == "" { - envData.StringEnv[constants.EnvKeyDatabaseType] = os.Getenv(constants.EnvKeyDatabaseType) - - if envstore.ARG_DB_TYPE != nil && *envstore.ARG_DB_TYPE != "" { - envData.StringEnv[constants.EnvKeyDatabaseType] = *envstore.ARG_DB_TYPE - } - - if envData.StringEnv[constants.EnvKeyDatabaseType] == "" { - panic("DATABASE_TYPE is required") - } - } - - if envData.StringEnv[constants.EnvKeyDatabaseURL] == "" { - envData.StringEnv[constants.EnvKeyDatabaseURL] = os.Getenv(constants.EnvKeyDatabaseURL) - - if envstore.ARG_DB_URL != nil && *envstore.ARG_DB_URL != "" { - envData.StringEnv[constants.EnvKeyDatabaseURL] = *envstore.ARG_DB_URL - } - - if envData.StringEnv[constants.EnvKeyDatabaseURL] == "" { - panic("DATABASE_URL is required") - } - } - - if envData.StringEnv[constants.EnvKeyDatabaseName] == "" { - envData.StringEnv[constants.EnvKeyDatabaseName] = os.Getenv(constants.EnvKeyDatabaseName) - if envData.StringEnv[constants.EnvKeyDatabaseName] == "" { - envData.StringEnv[constants.EnvKeyDatabaseName] = "authorizer" - } - } - if envData.StringEnv[constants.EnvKeySmtpHost] == "" { envData.StringEnv[constants.EnvKeySmtpHost] = os.Getenv(constants.EnvKeySmtpHost) } @@ -111,32 +125,83 @@ func InitEnv() { envData.StringEnv[constants.EnvKeySenderEmail] = os.Getenv(constants.EnvKeySenderEmail) } - if envData.StringEnv[constants.EnvKeyJwtSecret] == "" { - envData.StringEnv[constants.EnvKeyJwtSecret] = os.Getenv(constants.EnvKeyJwtSecret) - if envData.StringEnv[constants.EnvKeyJwtSecret] == "" { - envData.StringEnv[constants.EnvKeyJwtSecret] = uuid.New().String() - } - } - - if envData.StringEnv[constants.EnvKeyCustomAccessTokenScript] == "" { - envData.StringEnv[constants.EnvKeyCustomAccessTokenScript] = os.Getenv(constants.EnvKeyCustomAccessTokenScript) - } - - if envData.StringEnv[constants.EnvKeyJwtPrivateKey] == "" { - envData.StringEnv[constants.EnvKeyJwtPrivateKey] = os.Getenv(constants.EnvKeyJwtPrivateKey) - } - - if envData.StringEnv[constants.EnvKeyJwtPublicKey] == "" { - envData.StringEnv[constants.EnvKeyJwtPublicKey] = os.Getenv(constants.EnvKeyJwtPublicKey) - } - + algo := "" if envData.StringEnv[constants.EnvKeyJwtType] == "" { envData.StringEnv[constants.EnvKeyJwtType] = os.Getenv(constants.EnvKeyJwtType) if envData.StringEnv[constants.EnvKeyJwtType] == "" { - envData.StringEnv[constants.EnvKeyJwtType] = "HS256" + envData.StringEnv[constants.EnvKeyJwtType] = "RS256" + algo = envData.StringEnv[constants.EnvKeyJwtType] + } else { + algo = envData.StringEnv[constants.EnvKeyJwtType] + if !crypto.IsHMACA(algo) && !crypto.IsRSA(algo) && !crypto.IsECDSA(algo) { + panic("JWT_TYPE is invalid") + } } } + if envData.StringEnv[constants.EnvKeyJwtSecret] == "" && crypto.IsHMACA(algo) { + envData.StringEnv[constants.EnvKeyJwtSecret] = os.Getenv(constants.EnvKeyJwtSecret) + if envData.StringEnv[constants.EnvKeyJwtSecret] == "" { + envData.StringEnv[constants.EnvKeyJwtSecret] = crypto.NewHMACKey() + } + } + + if crypto.IsRSA(algo) || crypto.IsECDSA(algo) { + privateKey, publicKey := "", "" + + if envData.StringEnv[constants.EnvKeyJwtPrivateKey] == "" { + privateKey = os.Getenv(constants.EnvKeyJwtPrivateKey) + } + + if envData.StringEnv[constants.EnvKeyJwtPublicKey] == "" { + publicKey = os.Getenv(constants.EnvKeyJwtPublicKey) + } + + // if algo is RSA / ECDSA, then we need to have both private and public key + // if either of them is not present generate new keys + if privateKey == "" || publicKey == "" { + if crypto.IsRSA(algo) { + _, privateKey, publicKey, err = crypto.NewRSAKey() + if err != nil { + panic(err) + } + } else if crypto.IsECDSA(algo) { + _, privateKey, publicKey, err = crypto.NewECDSAKey() + if err != nil { + panic(err) + } + } + } else { + // parse keys to make sure they are valid + if crypto.IsRSA(algo) { + _, err = crypto.ParseRsaPrivateKeyFromPemStr(privateKey) + if err != nil { + panic(err) + } + + _, err = crypto.ParseRsaPublicKeyFromPemStr(publicKey) + if err != nil { + panic(err) + } + } else if crypto.IsECDSA(algo) { + _, err = crypto.ParseEcdsaPrivateKeyFromPemStr(privateKey) + if err != nil { + panic(err) + } + + _, err = crypto.ParseEcdsaPublicKeyFromPemStr(publicKey) + if err != nil { + panic(err) + } + } + fmt.Println("=> keys parsed successfully") + } + fmt.Println(privateKey) + fmt.Println(publicKey) + envData.StringEnv[constants.EnvKeyJwtPrivateKey] = privateKey + envData.StringEnv[constants.EnvKeyJwtPublicKey] = publicKey + } + if envData.StringEnv[constants.EnvKeyJwtRoleClaim] == "" { envData.StringEnv[constants.EnvKeyJwtRoleClaim] = os.Getenv(constants.EnvKeyJwtRoleClaim) @@ -145,6 +210,10 @@ func InitEnv() { } } + if envData.StringEnv[constants.EnvKeyCustomAccessTokenScript] == "" { + envData.StringEnv[constants.EnvKeyCustomAccessTokenScript] = os.Getenv(constants.EnvKeyCustomAccessTokenScript) + } + if envData.StringEnv[constants.EnvKeyRedisURL] == "" { envData.StringEnv[constants.EnvKeyRedisURL] = os.Getenv(constants.EnvKeyRedisURL) } diff --git a/server/env/persist_env.go b/server/env/persist_env.go index de2fc49..cd44e95 100644 --- a/server/env/persist_env.go +++ b/server/env/persist_env.go @@ -15,6 +15,40 @@ import ( "github.com/google/uuid" ) +// GetEnvData returns the env data from database +func GetEnvData() (envstore.Store, error) { + var result envstore.Store + env, err := db.Provider.GetEnv() + // config not found in db + if err != nil { + return result, err + } + + encryptionKey := env.Hash + decryptedEncryptionKey, err := utils.DecryptB64(encryptionKey) + if err != nil { + return result, err + } + + envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, decryptedEncryptionKey) + b64DecryptedConfig, err := utils.DecryptB64(env.EnvData) + if err != nil { + return result, err + } + + decryptedConfigs, err := utils.DecryptAES([]byte(b64DecryptedConfig)) + if err != nil { + return result, err + } + + err = json.Unmarshal(decryptedConfigs, &result) + if err != nil { + return result, err + } + + return result, err +} + // PersistEnv persists the environment variables to the database func PersistEnv() error { env, err := db.Provider.GetEnv() @@ -29,22 +63,16 @@ func PersistEnv() error { if err != nil { return err } - // configData, err := json.Marshal() - // if err != nil { - // return err - // } - - // encryptedConfig, err := utils.EncryptAES(configData) - // if err != nil { - // return err - // } env = models.Env{ Hash: encodedHash, EnvData: encryptedConfig, } - db.Provider.AddEnv(env) + env, err = db.Provider.AddEnv(env) + if err != nil { + return err + } } else { // decrypt the config data from db // decryption can be done using the hash stored in db @@ -134,6 +162,7 @@ func PersistEnv() error { } envstore.EnvInMemoryStoreObj.UpdateEnvStore(storeData) + if hasChanged { encryptedConfig, err := utils.EncryptEnvData(storeData) if err != nil { @@ -147,8 +176,11 @@ func PersistEnv() error { return err } } - } + // ID of env is used to identify the config and declared as client id + // this client id can be used in `aud` section of JWT token + envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyClientID, env.ID) + return nil } diff --git a/server/main.go b/server/main.go index 377f454..f41ccd3 100644 --- a/server/main.go +++ b/server/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "log" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/db" @@ -22,13 +23,20 @@ func main() { envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, VERSION) - env.InitEnv() + // initialize required envs (mainly db env & env file path) + env.InitRequiredEnv() + // initialize db provider db.InitDB() - env.PersistEnv() + // initialize all envs + env.InitAllEnv() + // persist all envs + err := env.PersistEnv() + if err != nil { + log.Println("Error persisting env:", err) + } sessionstore.InitSession() oauth.InitOAuth() - router := routes.InitRouter() router.Run(":" + envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyPort)) diff --git a/server/test/env_file_test.go b/server/test/env_file_test.go index 8a6a838..50f8e74 100644 --- a/server/test/env_file_test.go +++ b/server/test/env_file_test.go @@ -11,7 +11,7 @@ import ( func TestEnvs(t *testing.T) { envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEnvPath, "../../.env.sample") - env.InitEnv() + env.InitAllEnv() store := envstore.EnvInMemoryStoreObj.GetEnvStoreClone() assert.Equal(t, store.StringEnv[constants.EnvKeyEnv], "production") diff --git a/server/test/test.go b/server/test/test.go index e0451a9..e0ff189 100644 --- a/server/test/test.go +++ b/server/test/test.go @@ -78,7 +78,7 @@ func testSetup() TestSetup { envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeySmtpPassword, "test") envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeySenderEmail, "info@yopmail.com") envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.SliceStoreIdentifier, constants.EnvKeyProtectedRoles, []string{"admin"}) - env.InitEnv() + env.InitAllEnv() sessionstore.InitSession() w := httptest.NewRecorder()