From bf211c21f1a372f657f137e32d56ff0addd79261 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Thu, 19 Dec 2024 19:24:14 +0500 Subject: [PATCH 01/15] added password hasher --- api/admin/cli/main.go | 3 +- api/routing_fuzzing_test.go | 9 +++- application/application_test.go | 13 +++-- data/keycloak_user.go | 58 +++++++++++++++++----- data/keycloak_user_test.go | 21 +++++--- data/realm.go | 1 + data/user.go | 5 +- managers/files/manager.go | 3 +- managers/files/manager_test.go | 5 +- managers/redis/manager_realm_operations.go | 2 + managers/redis/manager_test.go | 27 ++++++---- managers/redis/manager_user_operations.go | 11 ++-- services/token_based_security.go | 15 ++++-- utils/hasher/b64hasher.go | 53 ++++++++++++++++++++ utils/hasher/b64hasher_test.go | 22 ++++++++ 15 files changed, 198 insertions(+), 50 deletions(-) create mode 100644 utils/hasher/b64hasher.go create mode 100644 utils/hasher/b64hasher_test.go diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 897387c..f62af1b 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -6,9 +6,10 @@ import ( "encoding/json" "flag" "fmt" - "github.com/wissance/Ferrum/managers" "log" + "github.com/wissance/Ferrum/managers" + "github.com/wissance/Ferrum/api/admin/cli/operations" "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" diff --git a/api/routing_fuzzing_test.go b/api/routing_fuzzing_test.go index fe333ca..5eb59ba 100644 --- a/api/routing_fuzzing_test.go +++ b/api/routing_fuzzing_test.go @@ -14,6 +14,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" + b64hasher "github.com/wissance/Ferrum/utils/hasher" sf "github.com/wissance/stringFormatter" "github.com/stretchr/testify/assert" @@ -29,6 +30,8 @@ const ( ) var ( + testSalt = "salt" + hashedPassword = b64hasher.HashPassword("1234567890", testSalt) testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ @@ -39,16 +42,18 @@ var ( Type: data.ClientIdAndSecrets, Value: testClient1Secret, }}, - }, Users: []interface{}{ + }, + Users: []interface{}{ map[string]interface{}{ "info": map[string]interface{}{ "sub": "667ff6a7-3f6b-449b-a217-6fc5d9ac0723", "name": "vano", "preferred_username": "vano", "given_name": "vano ivanov", "family_name": "ivanov", "email_verified": true, }, - "credentials": map[string]interface{}{"password": "1234567890"}, + "credentials": map[string]interface{}{"password": hashedPassword}, }, }, + PasswordSalt: testSalt, }, }, } diff --git a/application/application_test.go b/application/application_test.go index 7faec27..09a14a9 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -17,6 +17,7 @@ import ( "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" "github.com/wissance/Ferrum/errors" + b64hasher "github.com/wissance/Ferrum/utils/hasher" "github.com/wissance/stringFormatter" ) @@ -29,8 +30,10 @@ const ( ) var ( - testKey = []byte("qwerty1234567890") - testServerData = data.ServerData{ + testSalt = "salt" + testHashedPassowrd = b64hasher.HashPassword("1234567890", testSalt) + testKey = []byte("qwerty1234567890") + testServerData = data.ServerData{ Realms: []data.Realm{ { Name: testRealm1, TokenExpiration: testAccessTokenExpiration, RefreshTokenExpiration: testRefreshTokenExpiration, @@ -39,16 +42,18 @@ var ( Type: data.ClientIdAndSecrets, Value: testClient1Secret, }}, - }, Users: []interface{}{ + }, + Users: []interface{}{ map[string]interface{}{ "info": map[string]interface{}{ "sub": "667ff6a7-3f6b-449b-a217-6fc5d9ac0723", "name": "vano", "preferred_username": "vano", "given_name": "vano ivanov", "family_name": "ivanov", "email_verified": true, }, - "credentials": map[string]interface{}{"password": "1234567890"}, + "credentials": map[string]interface{}{"password": testHashedPassowrd}, }, }, + PasswordSalt: testSalt, }, }, } diff --git a/data/keycloak_user.go b/data/keycloak_user.go index 6f57de7..89c748e 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -6,6 +6,7 @@ import ( "github.com/google/uuid" "github.com/ohler55/ojg/jp" + b64hasher "github.com/wissance/Ferrum/utils/hasher" ) const ( @@ -42,23 +43,36 @@ func (user *KeyCloakUser) GetUsername() string { return getPathStringValue[string](user.rawData, "info.preferred_username") } -// GetPassword returns password -/* this function use internal map to navigate over credentials.password keys to retrieve a password +// GetPasswordHash returns hash of password +/* this function use internal map to navigate over credentials.password keys to retrieve a hash of password * Parameters: no - * Returns: password + * Returns: hash of password */ -// todo(UMV): this function should be changed to GetPasswordHash also we should consider case when User is External -func (user *KeyCloakUser) GetPassword() string { - return getPathStringValue[string](user.rawData, pathToPassword) +// todo(UMV): we should consider case when User is External +func (user *KeyCloakUser) GetPasswordHash() string { + password := getPathStringValue[string](user.rawData, pathToPassword) + if !b64hasher.IsPasswordHashed(password) { + // todo (YuriShang): think about actions if the password is not hashed + } + return password } -func (user *KeyCloakUser) SetPassword(password string) error { - mask, err := jp.ParseString(pathToPassword) - if err != nil { - return fmt.Errorf("jp.ParseString failed: %w", err) - } - if err := mask.Set(user.rawData, password); err != nil { - return fmt.Errorf("jp.Set failed: %w", err) +// HashPassword +/* this function changes a raw password to its hash in the user's rawData and jsonRawData + * Parameters: salt - salt for make password hash more strong + */ +func (user *KeyCloakUser) HashPassword(salt string) { + password := getPathStringValue[string](user.rawData, pathToPassword) + hashedPassword := b64hasher.HashPassword(password, salt) + setPathStringValue(user.rawData, pathToPassword, hashedPassword) + jsonData, _ := json.Marshal(&user.rawData) + user.jsonRawData = string(jsonData) +} + +func (user *KeyCloakUser) SetPassword(password, salt string) error { + hashedPassword := b64hasher.HashPassword(password, salt) + if err := setPathStringValue(user.rawData, pathToPassword, hashedPassword); err != nil { + return err } jsonData, _ := json.Marshal(user.rawData) user.jsonRawData = string(jsonData) @@ -131,3 +145,21 @@ func getPathStringValue[T any](rawData interface{}, path string) T { } return result } + +// setPathStringValue is a function to search data by path and set data by key, key represents as a jsonpath navigation property +/* this function uses json path to navigate over nested maps and set data + * Parameters: + * - rawData - json object + * - path - json path to retrieve part of json + * - value - value to be set to rawData + */ +func setPathStringValue(rawData interface{}, path string, value string) error { + mask, err := jp.ParseString(path) + if err != nil { + return fmt.Errorf("jp.ParseString failed: %w", err) + } + if err := mask.Set(rawData, value); err != nil { + return fmt.Errorf("jp.Set failed: %w", err) + } + return nil +} diff --git a/data/keycloak_user_test.go b/data/keycloak_user_test.go index 18f9055..7f3e3ad 100644 --- a/data/keycloak_user_test.go +++ b/data/keycloak_user_test.go @@ -2,9 +2,10 @@ package data import ( "encoding/json" + "testing" + "github.com/stretchr/testify/assert" sf "github.com/wissance/stringFormatter" - "testing" ) func TestInitUserWithJsonAndCheck(t *testing.T) { @@ -16,12 +17,18 @@ func TestInitUserWithJsonAndCheck(t *testing.T) { userTemplate string federationId string }{ - {name: "simple_user", userName: "admin", preferredUsername: "Administrator", isFederated: false, - userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}}`}, - {name: "federated_user", userName: `m.ushakov`, preferredUsername: "m.ushakov", isFederated: true, federationId: "Wissance_test_domain", - userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}, "federation":{"name":"Wissance_test_domain"}}`}, - {name: "federated_user", userName: `root`, preferredUsername: "root", isFederated: false, - userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}, "federation":{"cfg":{}}}`}, + { + name: "simple_user", userName: "admin", preferredUsername: "Administrator", isFederated: false, + userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}}`, + }, + { + name: "federated_user", userName: `m.ushakov`, preferredUsername: "m.ushakov", isFederated: true, federationId: "Wissance_test_domain", + userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}, "federation":{"name":"Wissance_test_domain"}}`, + }, + { + name: "federated_user", userName: `root`, preferredUsername: "root", isFederated: false, + userTemplate: `{"info":{"name":"{0}", "preferred_username": "{1}"}, "federation":{"cfg":{}}}`, + }, } for _, tCase := range testCases { diff --git a/data/realm.go b/data/realm.go index 71721ff..269ce24 100644 --- a/data/realm.go +++ b/data/realm.go @@ -12,4 +12,5 @@ type Realm struct { TokenExpiration int `json:"token_expiration"` RefreshTokenExpiration int `json:"refresh_expiration"` UserFederationServices []UserFederationServiceConfig `json:"user_federation_services"` + PasswordSalt string } diff --git a/data/user.go b/data/user.go index 228ee93..5149466 100644 --- a/data/user.go +++ b/data/user.go @@ -6,8 +6,9 @@ import "github.com/google/uuid" // because Password is not an only method for authentication type User interface { GetUsername() string - GetPassword() string - SetPassword(password string) error + GetPasswordHash() string + SetPassword(password, salt string) error + HashPassword(salt string) GetId() uuid.UUID GetUserInfo() interface{} GetRawData() interface{} diff --git a/managers/files/manager.go b/managers/files/manager.go index e28fc58..4feda15 100644 --- a/managers/files/manager.go +++ b/managers/files/manager.go @@ -2,9 +2,10 @@ package files import ( "encoding/json" - "github.com/wissance/Ferrum/config" "os" + "github.com/wissance/Ferrum/config" + "github.com/wissance/Ferrum/errors" "github.com/google/uuid" diff --git a/managers/files/manager_test.go b/managers/files/manager_test.go index aa2fa49..48d107c 100644 --- a/managers/files/manager_test.go +++ b/managers/files/manager_test.go @@ -2,13 +2,14 @@ package files import ( "encoding/json" + "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/logging" - "testing" ) const testDataFile = "test_data.json" @@ -160,5 +161,5 @@ func checkUsers(t *testing.T, expected *[]data.User, actual *[]data.User) { func checkUser(t *testing.T, expected *data.User, actual *data.User) { assert.Equal(t, (*expected).GetId(), (*actual).GetId()) assert.Equal(t, (*expected).GetUsername(), (*actual).GetUsername()) - assert.Equal(t, (*expected).GetPassword(), (*actual).GetPassword()) + assert.Equal(t, (*expected).GetPasswordHash(), (*actual).GetPasswordHash()) } diff --git a/managers/redis/manager_realm_operations.go b/managers/redis/manager_realm_operations.go index dce169c..8f9752e 100755 --- a/managers/redis/manager_realm_operations.go +++ b/managers/redis/manager_realm_operations.go @@ -7,6 +7,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" appErrs "github.com/wissance/Ferrum/errors" + b64hasher "github.com/wissance/Ferrum/utils/hasher" sf "github.com/wissance/stringFormatter" ) @@ -123,6 +124,7 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { Users: []any{}, TokenExpiration: newRealm.TokenExpiration, RefreshTokenExpiration: newRealm.RefreshTokenExpiration, + PasswordSalt: b64hasher.GenerateSalt(), } jsonShortRealm, err := json.Marshal(shortRealm) if err != nil { diff --git a/managers/redis/manager_test.go b/managers/redis/manager_test.go index 73f3f60..6de8914 100644 --- a/managers/redis/manager_test.go +++ b/managers/redis/manager_test.go @@ -461,12 +461,14 @@ func TestGetClientFailsNonExistingClient(t *testing.T) { func TestGetUsersSuccessfully(t *testing.T) { // 1. Create Realm manager := createTestRedisDataManager(t) - realm := data.Realm{ + r := data.Realm{ Name: sf.Format("realm_4_get_multiple_users_{0}", uuid.New().String()), TokenExpiration: 3600, RefreshTokenExpiration: 1800, } - err := manager.CreateRealm(realm) + err := manager.CreateRealm(r) + assert.NoError(t, err) + realm, err := manager.getRealmObject(r.Name) assert.NoError(t, err) // 2. Create multiple users users := make([]data.User, 3) @@ -479,8 +481,8 @@ func TestGetUsersSuccessfully(t *testing.T) { err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) user := data.CreateUser(rawUser) - users[i] = user err = manager.CreateUser(realm.Name, user) + users[i] = user assert.NoError(t, err) } // 3. Get all related to realm users @@ -792,19 +794,21 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { Value: uuid.New().String(), }, } - realm.Clients = append([]data.Client{client}) + realm.Clients = append(realm.Clients, client) + err := manager.CreateRealm(realm) + assert.NoError(t, err) + + createdRealm, err := manager.getRealmObject(realm.Name) + assert.NoError(t, err) userName := "new_app_user" userTemplate := `{"info":{"preferred_username":"{0}"}, "credentials":{"password": "{1}"}}` userJson := sf.Format(userTemplate, userName, "123") var rawUser interface{} - err := json.Unmarshal([]byte(userJson), &rawUser) - assert.NoError(t, err) - realm.Users = append([]interface{}{rawUser}) - - err = manager.CreateRealm(realm) + err = json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - _, err = manager.GetRealm(realm.Name) + user := data.CreateUser(rawUser) + err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) // 2. Reset Password and check ... @@ -816,6 +820,7 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { err = json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) expectedUser := data.CreateUser(rawUser) + expectedUser.HashPassword(createdRealm.PasswordSalt) u, err := manager.GetUser(realm.Name, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &u) @@ -1057,7 +1062,7 @@ func checkUsers(t *testing.T, expected *[]data.User, actual *[]data.User) { func checkUser(t *testing.T, expected *data.User, actual *data.User) { assert.Equal(t, (*expected).GetId(), (*actual).GetId()) assert.Equal(t, (*expected).GetUsername(), (*actual).GetUsername()) - assert.Equal(t, (*expected).GetPassword(), (*actual).GetPassword()) + assert.Equal(t, (*expected).GetPasswordHash(), (*actual).GetPasswordHash()) } func checkUserFederationConfigs(t *testing.T, expected *[]data.UserFederationServiceConfig, actual *[]data.UserFederationServiceConfig) { diff --git a/managers/redis/manager_user_operations.go b/managers/redis/manager_user_operations.go index d3fec05..46b7f80 100644 --- a/managers/redis/manager_user_operations.go +++ b/managers/redis/manager_user_operations.go @@ -3,6 +3,7 @@ package redis import ( "encoding/json" "errors" + "github.com/google/uuid" "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" @@ -128,7 +129,7 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro } // TODO(SIA) Add transaction // TODO(SIA) use function isExists - _, err := mn.getRealmObject(realmName) + realm, err := mn.getRealmObject(realmName) if err != nil { mn.logger.Warn(sf.Format("CreateUser: GetRealmObject failed, error: {0}", err.Error())) return err @@ -143,7 +144,7 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro mn.logger.Warn(sf.Format("CreateUser: GetUser failed, error: {0}", err.Error())) return err } - + userNew.HashPassword(realm.PasswordSalt) upsertUserErr := mn.upsertUserObject(realmName, userName, userNew.GetJsonString()) if upsertUserErr != nil { mn.logger.Error(sf.Format("CreateUser: addUserToRealm failed, error: {0}", upsertUserErr.Error())) @@ -241,7 +242,11 @@ func (mn *RedisDataManager) SetPassword(realmName string, userName string, passw if err != nil { return errors2.NewUnknownError("GetUser", "RedisDataManager.SetPassword", err) } - if setPasswordErr := user.SetPassword(password); setPasswordErr != nil { + realm, err := mn.getRealmObject(realmName) + if err != nil { + return errors2.NewUnknownError("getRealmObject", "RedisDataManager.SetPassword", err) + } + if setPasswordErr := user.SetPassword(password, realm.PasswordSalt); setPasswordErr != nil { return errors2.NewUnknownError("SetPassword", "RedisDataManager.SetPassword", setPasswordErr) } if upsertUserErr := mn.upsertUserObject(realmName, userName, user.GetJsonString()); upsertUserErr != nil { diff --git a/services/token_based_security.go b/services/token_based_security.go index ef4118d..2517b00 100644 --- a/services/token_based_security.go +++ b/services/token_based_security.go @@ -1,15 +1,17 @@ package services import ( - sf "github.com/wissance/stringFormatter" "time" + sf "github.com/wissance/stringFormatter" + "github.com/google/uuid" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" "github.com/wissance/Ferrum/errors" "github.com/wissance/Ferrum/logging" "github.com/wissance/Ferrum/managers" + b64hasher "github.com/wissance/Ferrum/utils/hasher" ) // TokenBasedSecurityService structure that implements SecurityService @@ -73,15 +75,20 @@ func (service *TokenBasedSecurityService) CheckCredentials(tokenIssueData *dto.T return &data.OperationError{Msg: errors.InvalidUserCredentialsMsg, Description: errors.InvalidUserCredentialsDesc} } + realm, err := (*service.DataProvider).GetRealm(realmName) + if err != nil { + service.logger.Trace("Credential check: failed to get realm") + return &data.OperationError{Msg: "failed to get realm", Description: err.Error()} + } + if user.IsFederatedUser() { msg := sf.Format("User \"{0}\" configured as federated, currently it is not fully supported, wait for future releases", user.GetUsername()) service.logger.Warn(msg) return &data.OperationError{Msg: "federated user not supported", Description: msg} } else { - // todo(UMV): use hash instead raw passwords - password := user.GetPassword() - if password != tokenIssueData.Password { + oldPasswordHash := user.GetPasswordHash() + if !b64hasher.IsPasswordsMatch(tokenIssueData.Password, realm.PasswordSalt, oldPasswordHash) { service.logger.Trace("Credential check: password mismatch") return &data.OperationError{Msg: errors.InvalidUserCredentialsMsg, Description: errors.InvalidUserCredentialsDesc} } diff --git a/utils/hasher/b64hasher.go b/utils/hasher/b64hasher.go new file mode 100644 index 0000000..5e2ff1a --- /dev/null +++ b/utils/hasher/b64hasher.go @@ -0,0 +1,53 @@ +package b64hasher + +import ( + "crypto/sha512" + "encoding/base64" + "math/rand" +) + +func GenerateSalt() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+=" + salt := make([]byte, 32) + for i := range salt { + salt[i] = charset[rand.Intn(len(charset))] + } + return string(salt) +} + +func HashPassword(password, salt string) string { + passwordBytes := []byte(password + salt) + + sha512Hasher := sha512.New() + sha512Hasher.Write(passwordBytes) + + hashedPasswordBytes := sha512Hasher.Sum(nil) + b64encoded := b64Encode(hashedPasswordBytes) + return b64encoded +} + +func IsPasswordsMatch(password, salt, hash string) bool { + currPasswordHash := HashPassword(password, salt) + return b64Decode(hash) == b64Decode(currPasswordHash) +} + +func IsPasswordHashed(password string) bool { + decoded := b64Decode(password) + if len(decoded) == 0 { + return false + } + return true +} + +func b64Encode(encoded []byte) string { + cstr := base64.URLEncoding.EncodeToString(encoded) + return cstr +} + +func b64Decode(encoded string) string { + cstr, err := base64.URLEncoding.DecodeString(encoded) + if err != nil { + return "" + } + return string(cstr) +} diff --git a/utils/hasher/b64hasher_test.go b/utils/hasher/b64hasher_test.go new file mode 100644 index 0000000..c0d72f9 --- /dev/null +++ b/utils/hasher/b64hasher_test.go @@ -0,0 +1,22 @@ +package b64hasher + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_HashPassword(t *testing.T) { + t.Run("success", func(t *testing.T) { + // Arrange + pwd := "qwerty" + salt := "salt" + + // Act + hashedPwd := HashPassword(pwd, salt) + isMatch := IsPasswordsMatch(pwd, salt, hashedPwd) + + // Assert + assert.True(t, isMatch) + }) +} From ac298e0c553dcb76ebe002a9821073180dd54dd8 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Fri, 20 Dec 2024 09:20:06 +0500 Subject: [PATCH 02/15] added password hashing check to HashPassword method --- data/keycloak_user.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/data/keycloak_user.go b/data/keycloak_user.go index 89c748e..cb054ab 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -63,10 +63,12 @@ func (user *KeyCloakUser) GetPasswordHash() string { */ func (user *KeyCloakUser) HashPassword(salt string) { password := getPathStringValue[string](user.rawData, pathToPassword) - hashedPassword := b64hasher.HashPassword(password, salt) - setPathStringValue(user.rawData, pathToPassword, hashedPassword) - jsonData, _ := json.Marshal(&user.rawData) - user.jsonRawData = string(jsonData) + if !b64hasher.IsPasswordHashed(password) { + hashedPassword := b64hasher.HashPassword(password, salt) + setPathStringValue(user.rawData, pathToPassword, hashedPassword) + jsonData, _ := json.Marshal(&user.rawData) + user.jsonRawData = string(jsonData) + } } func (user *KeyCloakUser) SetPassword(password, salt string) error { From 004b277fb90d98bc6e63ea3f224bf1d84146a33c Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Wed, 8 Jan 2025 23:41:49 +0500 Subject: [PATCH 03/15] implemented PasswordJsonEncoder --- api/admin/cli/main.go | 18 +++++++- api/routing_fuzzing_test.go | 13 +++--- application/application_test.go | 5 +- data/keycloak_user.go | 34 ++++++-------- data/keycloak_user_test.go | 4 +- data/realm.go | 5 +- data/user.go | 8 ++-- managers/files/manager.go | 5 +- managers/files/manager_test.go | 8 +++- managers/files/test_data.json | 3 +- managers/redis/manager_realm_operations.go | 13 ++++-- managers/redis/manager_test.go | 42 ++++++++++------- managers/redis/manager_user_operations.go | 21 ++++++--- services/token_based_security.go | 3 +- .../b64hasher.go => encoding/encoding.go} | 46 +++++++++++++------ .../encoding_test.go} | 7 +-- 16 files changed, 148 insertions(+), 87 deletions(-) rename utils/{hasher/b64hasher.go => encoding/encoding.go} (54%) rename utils/{hasher/b64hasher_test.go => encoding/encoding_test.go} (61%) diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index f62af1b..410ec52 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -122,7 +122,14 @@ func main() { if err := json.Unmarshal(value, &userNew); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - user := data.CreateUser(userNew) + if resourceId == "" { + log.Fatalf("Realm name not specified") + } + realm, err := manager.GetRealm(resourceId) + if err != nil { + log.Fatalf("GetRealm failed: %s", err) + } + user := data.CreateUser(userNew, realm.Encoder) if err := manager.CreateUser(params, user); err != nil { log.Fatalf("CreateUser failed: %s", err) } @@ -203,7 +210,14 @@ func main() { if err := json.Unmarshal(value, &newUser); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - user := data.CreateUser(newUser) + if resourceId == "" { + log.Fatalf("Realm name not specified") + } + realm, err := manager.GetRealm(resourceId) + if err != nil { + log.Fatalf("GetRealm failed: %s", err) + } + user := data.CreateUser(newUser, realm.Encoder) if err := manager.UpdateUser(params, resourceId, user); err != nil { log.Fatalf("UpdateUser failed: %s", err) } diff --git a/api/routing_fuzzing_test.go b/api/routing_fuzzing_test.go index 5eb59ba..2e7c717 100644 --- a/api/routing_fuzzing_test.go +++ b/api/routing_fuzzing_test.go @@ -14,7 +14,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" - b64hasher "github.com/wissance/Ferrum/utils/hasher" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" "github.com/stretchr/testify/assert" @@ -30,10 +30,11 @@ const ( ) var ( - testSalt = "salt" - hashedPassword = b64hasher.HashPassword("1234567890", testSalt) - testKey = []byte("qwerty1234567890") - testServerData = data.ServerData{ + testSalt = "salt" + encoder = encoding.NewPasswordJsonEncoder(testSalt) + testHashedPassowrd = encoder.HashPassword("1234567890") + testKey = []byte("qwerty1234567890") + testServerData = data.ServerData{ Realms: []data.Realm{ { Name: testRealm1, TokenExpiration: testAccessTokenExpiration, RefreshTokenExpiration: testRefreshTokenExpiration, @@ -50,7 +51,7 @@ var ( "name": "vano", "preferred_username": "vano", "given_name": "vano ivanov", "family_name": "ivanov", "email_verified": true, }, - "credentials": map[string]interface{}{"password": hashedPassword}, + "credentials": map[string]interface{}{"password": testHashedPassowrd}, }, }, PasswordSalt: testSalt, diff --git a/application/application_test.go b/application/application_test.go index 09a14a9..7138db1 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -17,7 +17,7 @@ import ( "github.com/wissance/Ferrum/data" "github.com/wissance/Ferrum/dto" "github.com/wissance/Ferrum/errors" - b64hasher "github.com/wissance/Ferrum/utils/hasher" + "github.com/wissance/Ferrum/utils/encoding" "github.com/wissance/stringFormatter" ) @@ -31,7 +31,8 @@ const ( var ( testSalt = "salt" - testHashedPassowrd = b64hasher.HashPassword("1234567890", testSalt) + encoder = encoding.NewPasswordJsonEncoder(testSalt) + testHashedPassowrd = encoder.HashPassword("1234567890") testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ diff --git a/data/keycloak_user.go b/data/keycloak_user.go index cb054ab..d5c4409 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -6,7 +6,7 @@ import ( "github.com/google/uuid" "github.com/ohler55/ojg/jp" - b64hasher "github.com/wissance/Ferrum/utils/hasher" + "github.com/wissance/Ferrum/utils/encoding" ) const ( @@ -26,9 +26,11 @@ type KeyCloakUser struct { * - rawData - any json * Return: instance of User as KeyCloakUser */ -func CreateUser(rawData interface{}) User { +func CreateUser(rawData interface{}, encoder encoding.PasswordJsonEncoder) User { jsonData, _ := json.Marshal(&rawData) kcUser := &KeyCloakUser{rawData: rawData, jsonRawData: string(jsonData)} + password := getPathStringValue[string](kcUser.rawData, pathToPassword) + kcUser.SetPassword(password, encoder) user := User(kcUser) return user } @@ -51,32 +53,24 @@ func (user *KeyCloakUser) GetUsername() string { // todo(UMV): we should consider case when User is External func (user *KeyCloakUser) GetPasswordHash() string { password := getPathStringValue[string](user.rawData, pathToPassword) - if !b64hasher.IsPasswordHashed(password) { + if !encoding.IsPasswordHashed(password) { // todo (YuriShang): think about actions if the password is not hashed } return password } -// HashPassword -/* this function changes a raw password to its hash in the user's rawData and jsonRawData - * Parameters: salt - salt for make password hash more strong +// SetPassword +/* this function changes a raw password to its hash in the user's rawData and jsonRawData and sets it + * Parameters: + * - password - new password + * - encoder - encoder object with salt and hasher */ -func (user *KeyCloakUser) HashPassword(salt string) { - password := getPathStringValue[string](user.rawData, pathToPassword) - if !b64hasher.IsPasswordHashed(password) { - hashedPassword := b64hasher.HashPassword(password, salt) - setPathStringValue(user.rawData, pathToPassword, hashedPassword) - jsonData, _ := json.Marshal(&user.rawData) - user.jsonRawData = string(jsonData) - } -} - -func (user *KeyCloakUser) SetPassword(password, salt string) error { - hashedPassword := b64hasher.HashPassword(password, salt) - if err := setPathStringValue(user.rawData, pathToPassword, hashedPassword); err != nil { +func (user *KeyCloakUser) SetPassword(password string, encoder encoding.PasswordJsonEncoder) error { + hashed := encoder.HashPassword(password) + if err := setPathStringValue(user.rawData, pathToPassword, hashed); err != nil { return err } - jsonData, _ := json.Marshal(user.rawData) + jsonData, _ := json.Marshal(&user.rawData) user.jsonRawData = string(jsonData) return nil } diff --git a/data/keycloak_user_test.go b/data/keycloak_user_test.go index 7f3e3ad..510a121 100644 --- a/data/keycloak_user_test.go +++ b/data/keycloak_user_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" ) @@ -38,7 +39,8 @@ func TestInitUserWithJsonAndCheck(t *testing.T) { var rawUserData interface{} err := json.Unmarshal([]byte(jsonStr), &rawUserData) assert.NoError(t, err) - user := CreateUser(rawUserData) + encoder := encoding.NewPasswordJsonEncoder("salt") + user := CreateUser(rawUserData, encoder) assert.Equal(t, tCase.preferredUsername, user.GetUsername()) assert.Equal(t, tCase.isFederated, user.IsFederatedUser()) if user.IsFederatedUser() { diff --git a/data/realm.go b/data/realm.go index 269ce24..3317127 100644 --- a/data/realm.go +++ b/data/realm.go @@ -1,5 +1,7 @@ package data +import "github.com/wissance/Ferrum/utils/encoding" + // Realm is a struct that describes typical Realm /* It was originally designed to efficiently work in memory with small amount of data therefore it contains relations with Clients and Users * But in a systems with thousands of users working at the same time it is too expensive to fetch Realm with all relations therefore @@ -12,5 +14,6 @@ type Realm struct { TokenExpiration int `json:"token_expiration"` RefreshTokenExpiration int `json:"refresh_expiration"` UserFederationServices []UserFederationServiceConfig `json:"user_federation_services"` - PasswordSalt string + PasswordSalt string `json:"password_salt"` + Encoder encoding.PasswordJsonEncoder } diff --git a/data/user.go b/data/user.go index 5149466..7b49f2d 100644 --- a/data/user.go +++ b/data/user.go @@ -1,14 +1,16 @@ package data -import "github.com/google/uuid" +import ( + "github.com/google/uuid" + "github.com/wissance/Ferrum/utils/encoding" +) // User is a common user interface with all Required methods to get information about user, in future we probably won't have GetPassword method // because Password is not an only method for authentication type User interface { GetUsername() string GetPasswordHash() string - SetPassword(password, salt string) error - HashPassword(salt string) + SetPassword(password string, encoder encoding.PasswordJsonEncoder) error GetId() uuid.UUID GetUserInfo() interface{} GetRawData() interface{} diff --git a/managers/files/manager.go b/managers/files/manager.go index 4feda15..c38a753 100644 --- a/managers/files/manager.go +++ b/managers/files/manager.go @@ -5,6 +5,7 @@ import ( "os" "github.com/wissance/Ferrum/config" + "github.com/wissance/Ferrum/utils/encoding" "github.com/wissance/Ferrum/errors" @@ -78,6 +79,7 @@ func (mn *FileDataManager) GetRealm(realmName string) (*data.Realm, error) { // case-sensitive comparison, myapp and MyApP are different realms if e.Name == realmName { e.Users = nil + e.Encoder = encoding.NewPasswordJsonEncoder(e.PasswordSalt) return &e, nil } } @@ -101,8 +103,9 @@ func (mn *FileDataManager) GetUsers(realmName string) ([]data.User, error) { return nil, errors.ErrZeroLength } users := make([]data.User, len(e.Users)) + e.Encoder = encoding.NewPasswordJsonEncoder(e.PasswordSalt) for i, u := range e.Users { - user := data.CreateUser(u) + user := data.CreateUser(u, e.Encoder) users[i] = user } return users, nil diff --git a/managers/files/manager_test.go b/managers/files/manager_test.go index 48d107c..c0d7806 100644 --- a/managers/files/manager_test.go +++ b/managers/files/manager_test.go @@ -68,7 +68,9 @@ func TestGetUserSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser) + r, err := manager.GetRealm(realm) + assert.NoError(t, err) + expectedUser := data.CreateUser(rawUser, r.Encoder) user, err := manager.GetUser(realm, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &user) @@ -97,7 +99,9 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser) + r, err := manager.GetRealm(realm) + assert.NoError(t, err) + expectedUser := data.CreateUser(rawUser, r.Encoder) user, err := manager.GetUserById(realm, userId) assert.NoError(t, err) checkUser(t, &expectedUser, &user) diff --git a/managers/files/test_data.json b/managers/files/test_data.json index eb262a6..dda6580 100644 --- a/managers/files/test_data.json +++ b/managers/files/test_data.json @@ -49,7 +49,8 @@ "password": "qwerty_user" } } - ] + ], + "password_salt": "super_strong_salt" } ] } diff --git a/managers/redis/manager_realm_operations.go b/managers/redis/manager_realm_operations.go index 8f9752e..e5f3f1b 100755 --- a/managers/redis/manager_realm_operations.go +++ b/managers/redis/manager_realm_operations.go @@ -7,7 +7,7 @@ import ( "github.com/wissance/Ferrum/config" "github.com/wissance/Ferrum/data" appErrs "github.com/wissance/Ferrum/errors" - b64hasher "github.com/wissance/Ferrum/utils/hasher" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" ) @@ -48,6 +48,7 @@ func (mn *RedisDataManager) GetRealm(realmName string) (*data.Realm, error) { } } realm.UserFederationServices = configs + realm.Encoder = encoding.NewPasswordJsonEncoder(realm.PasswordSalt) return realm, nil } @@ -70,7 +71,7 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { } // TODO(SIA) Add transaction // TODO(SIA) use function isExists - _, err := mn.getRealmObject(newRealm.Name) + _, err := mn.GetRealm(newRealm.Name) if err == nil { return appErrs.NewObjectExistsError(string(Realm), newRealm.Name, "") } @@ -99,10 +100,13 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { } } + salt := encoding.GenerateRandomSalt() + if len(newRealm.Users) != 0 { realmUsers := make([]data.ExtendedIdentifier, len(newRealm.Users)) + encoder := encoding.NewPasswordJsonEncoder(salt) for i, user := range newRealm.Users { - newUser := data.CreateUser(user) + newUser := data.CreateUser(user, encoder) newUserName := newUser.GetUsername() if upsertUserErr := mn.upsertUserObject(newRealm.Name, newUserName, newUser.GetJsonString()); upsertUserErr != nil { return appErrs.NewUnknownError("upsertUserObject", "RedisDataManager.CreateRealm", upsertUserErr) @@ -124,7 +128,8 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { Users: []any{}, TokenExpiration: newRealm.TokenExpiration, RefreshTokenExpiration: newRealm.RefreshTokenExpiration, - PasswordSalt: b64hasher.GenerateSalt(), + PasswordSalt: salt, + Encoder: encoding.PasswordJsonEncoder{}, } jsonShortRealm, err := json.Marshal(shortRealm) if err != nil { diff --git a/managers/redis/manager_test.go b/managers/redis/manager_test.go index 6de8914..51722a1 100644 --- a/managers/redis/manager_test.go +++ b/managers/redis/manager_test.go @@ -12,6 +12,7 @@ import ( "github.com/wissance/Ferrum/data" appErrs "github.com/wissance/Ferrum/errors" "github.com/wissance/Ferrum/logging" + "github.com/wissance/Ferrum/utils/encoding" sf "github.com/wissance/stringFormatter" ) @@ -76,7 +77,7 @@ func TestCreateRealmSuccessfully(t *testing.T) { expectedUsers := make([]data.User, len(realm.Users)) if len(realm.Users) > 0 { for i := range realm.Users { - expectedUsers[i] = data.CreateUser(realm.Users[i]) + expectedUsers[i] = data.CreateUser(realm.Users[i], realm.Encoder) } } checkUsers(t, &expectedUsers, &users) @@ -465,10 +466,11 @@ func TestGetUsersSuccessfully(t *testing.T) { Name: sf.Format("realm_4_get_multiple_users_{0}", uuid.New().String()), TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(r) assert.NoError(t, err) - realm, err := manager.getRealmObject(r.Name) + realm, err := manager.GetRealm(r.Name) assert.NoError(t, err) // 2. Create multiple users users := make([]data.User, 3) @@ -480,7 +482,7 @@ func TestGetUsersSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, r.Encoder) err = manager.CreateUser(realm.Name, user) users[i] = user assert.NoError(t, err) @@ -530,6 +532,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { Name: "realm_4_test_get_user_by_id", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -540,7 +543,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -603,7 +606,7 @@ func TestCreateUserSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, r.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) storedUser, err := manager.GetUser(realm.Name, tCase.userName) @@ -622,6 +625,7 @@ func TestCreateUserFailsDuplicateUser(t *testing.T) { Name: "realm_4_test_user_create_fails_duplicate", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -631,7 +635,7 @@ func TestCreateUserFailsDuplicateUser(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -650,6 +654,7 @@ func TestUpdateUserSuccessfully(t *testing.T) { Name: "realm_4_test_user_update", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -660,7 +665,7 @@ func TestUpdateUserSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -668,7 +673,7 @@ func TestUpdateUserSuccessfully(t *testing.T) { jsonStr = sf.Format(jsonTemplate, "pppetrov", "67890", "00000000-0000-0000-0000-000000000001") err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user = data.CreateUser(rawUser) + user = data.CreateUser(rawUser, realm.Encoder) err = manager.UpdateUser(realm.Name, userName, user) assert.NoError(t, err) @@ -687,6 +692,7 @@ func TestUpdateUserFailsNonExistingUser(t *testing.T) { Name: "realm_4_test_user_update_fails_non_existing_user", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -697,7 +703,7 @@ func TestUpdateUserFailsNonExistingUser(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.UpdateUser(realm.Name, userName, user) assert.Error(t, err) assert.True(t, errors.As(err, &appErrs.EmptyNotFoundErr)) @@ -713,6 +719,7 @@ func TestDeleteUserSuccessfully(t *testing.T) { Name: "realm_4_test_user_delete", TokenExpiration: 3600, RefreshTokenExpiration: 1800, + Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -723,7 +730,7 @@ func TestDeleteUserSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) u, err := manager.GetUser(realm.Name, userName) @@ -779,7 +786,7 @@ func TestGetUserFailsNonExistingUser(t *testing.T) { func TestChangeUserPasswordSuccessfully(t *testing.T) { manager := createTestRedisDataManager(t) // 1. Create Realm+Client+User - realm := data.Realm{ + realm := &data.Realm{ Name: sf.Format("app_4_user_pwd_change_check_{0}", uuid.New().String()), TokenExpiration: 3600, RefreshTokenExpiration: 1800, @@ -795,10 +802,10 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { }, } realm.Clients = append(realm.Clients, client) - err := manager.CreateRealm(realm) + err := manager.CreateRealm(*realm) assert.NoError(t, err) - createdRealm, err := manager.getRealmObject(realm.Name) + realm, err = manager.GetRealm(realm.Name) assert.NoError(t, err) userName := "new_app_user" @@ -807,7 +814,7 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser) + user := data.CreateUser(rawUser, realm.Encoder) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -816,11 +823,12 @@ func TestChangeUserPasswordSuccessfully(t *testing.T) { err = manager.SetPassword(realm.Name, userName, newPassword) assert.NoError(t, err) + var rawUser2 interface{} userJson = sf.Format(userTemplate, userName, newPassword) - err = json.Unmarshal([]byte(userJson), &rawUser) + err = json.Unmarshal([]byte(userJson), &rawUser2) + assert.NoError(t, err) + expectedUser := data.CreateUser(rawUser2, realm.Encoder) assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser) - expectedUser.HashPassword(createdRealm.PasswordSalt) u, err := manager.GetUser(realm.Name, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &u) diff --git a/managers/redis/manager_user_operations.go b/managers/redis/manager_user_operations.go index 46b7f80..de4339a 100644 --- a/managers/redis/manager_user_operations.go +++ b/managers/redis/manager_user_operations.go @@ -57,8 +57,12 @@ func (mn *RedisDataManager) GetUsers(realmName string) ([]data.User, error) { } userData := make([]data.User, len(realmUsersData)) + realm, err := mn.GetRealm(realmName) + if err != nil { + return []data.User{}, nil + } for i, u := range realmUsersData { - userData[i] = data.CreateUser(u) + userData[i] = data.CreateUser(u, realm.Encoder) } return userData, nil } @@ -83,7 +87,11 @@ func (mn *RedisDataManager) GetUser(realmName string, userName string) (data.Use } return nil, errors2.NewUnknownError("getSingleRedisObject", "RedisDataManager.GetUser", err) } - user := data.CreateUser(*rawUser) + realm, err := mn.GetRealm(realmName) + if err != nil { + return nil, err + } + user := data.CreateUser(*rawUser, realm.Encoder) return user, nil } @@ -129,7 +137,7 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro } // TODO(SIA) Add transaction // TODO(SIA) use function isExists - realm, err := mn.getRealmObject(realmName) + _, err := mn.GetRealm(realmName) if err != nil { mn.logger.Warn(sf.Format("CreateUser: GetRealmObject failed, error: {0}", err.Error())) return err @@ -144,7 +152,6 @@ func (mn *RedisDataManager) CreateUser(realmName string, userNew data.User) erro mn.logger.Warn(sf.Format("CreateUser: GetUser failed, error: {0}", err.Error())) return err } - userNew.HashPassword(realm.PasswordSalt) upsertUserErr := mn.upsertUserObject(realmName, userName, userNew.GetJsonString()) if upsertUserErr != nil { mn.logger.Error(sf.Format("CreateUser: addUserToRealm failed, error: {0}", upsertUserErr.Error())) @@ -242,11 +249,11 @@ func (mn *RedisDataManager) SetPassword(realmName string, userName string, passw if err != nil { return errors2.NewUnknownError("GetUser", "RedisDataManager.SetPassword", err) } - realm, err := mn.getRealmObject(realmName) + realm, err := mn.GetRealm(realmName) if err != nil { - return errors2.NewUnknownError("getRealmObject", "RedisDataManager.SetPassword", err) + return errors2.NewUnknownError("GetRealm", "RedisDataManager.SetPassword", err) } - if setPasswordErr := user.SetPassword(password, realm.PasswordSalt); setPasswordErr != nil { + if setPasswordErr := user.SetPassword(password, realm.Encoder); setPasswordErr != nil { return errors2.NewUnknownError("SetPassword", "RedisDataManager.SetPassword", setPasswordErr) } if upsertUserErr := mn.upsertUserObject(realmName, userName, user.GetJsonString()); upsertUserErr != nil { diff --git a/services/token_based_security.go b/services/token_based_security.go index 2517b00..a9bf1e1 100644 --- a/services/token_based_security.go +++ b/services/token_based_security.go @@ -11,7 +11,6 @@ import ( "github.com/wissance/Ferrum/errors" "github.com/wissance/Ferrum/logging" "github.com/wissance/Ferrum/managers" - b64hasher "github.com/wissance/Ferrum/utils/hasher" ) // TokenBasedSecurityService structure that implements SecurityService @@ -88,7 +87,7 @@ func (service *TokenBasedSecurityService) CheckCredentials(tokenIssueData *dto.T return &data.OperationError{Msg: "federated user not supported", Description: msg} } else { oldPasswordHash := user.GetPasswordHash() - if !b64hasher.IsPasswordsMatch(tokenIssueData.Password, realm.PasswordSalt, oldPasswordHash) { + if !realm.Encoder.IsPasswordsMatch(tokenIssueData.Password, oldPasswordHash) { service.logger.Trace("Credential check: password mismatch") return &data.OperationError{Msg: errors.InvalidUserCredentialsMsg, Description: errors.InvalidUserCredentialsDesc} } diff --git a/utils/hasher/b64hasher.go b/utils/encoding/encoding.go similarity index 54% rename from utils/hasher/b64hasher.go rename to utils/encoding/encoding.go index 5e2ff1a..a48e5d5 100644 --- a/utils/hasher/b64hasher.go +++ b/utils/encoding/encoding.go @@ -1,36 +1,52 @@ -package b64hasher +package encoding import ( "crypto/sha512" "encoding/base64" + "hash" "math/rand" ) -func GenerateSalt() string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+=" - salt := make([]byte, 32) - for i := range salt { - salt[i] = charset[rand.Intn(len(charset))] - } - return string(salt) +type PasswordJsonEncoder struct { + salt string + hasher hash.Hash } -func HashPassword(password, salt string) string { - passwordBytes := []byte(password + salt) +func NewPasswordJsonEncoder(salt string) PasswordJsonEncoder { + encoder := PasswordJsonEncoder{ + hasher: sha512.New(), + salt: salt, + } + return encoder +} - sha512Hasher := sha512.New() - sha512Hasher.Write(passwordBytes) +func (e *PasswordJsonEncoder) HashPassword(password string) string { + if IsPasswordHashed(password) { + return password + } + passwordBytes := []byte(password + e.salt) + e.hasher.Write(passwordBytes) + hashedPasswordBytes := e.hasher.Sum(nil) + e.hasher.Reset() - hashedPasswordBytes := sha512Hasher.Sum(nil) b64encoded := b64Encode(hashedPasswordBytes) return b64encoded } -func IsPasswordsMatch(password, salt, hash string) bool { - currPasswordHash := HashPassword(password, salt) +func (e *PasswordJsonEncoder) IsPasswordsMatch(password, hash string) bool { + currPasswordHash := e.HashPassword(password) return b64Decode(hash) == b64Decode(currPasswordHash) } +func GenerateRandomSalt() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+=" + salt := make([]byte, 32) + for i := range salt { + salt[i] = charset[rand.Intn(len(charset))] + } + return string(salt) +} + func IsPasswordHashed(password string) bool { decoded := b64Decode(password) if len(decoded) == 0 { diff --git a/utils/hasher/b64hasher_test.go b/utils/encoding/encoding_test.go similarity index 61% rename from utils/hasher/b64hasher_test.go rename to utils/encoding/encoding_test.go index c0d72f9..7bbfc2a 100644 --- a/utils/hasher/b64hasher_test.go +++ b/utils/encoding/encoding_test.go @@ -1,4 +1,4 @@ -package b64hasher +package encoding import ( "testing" @@ -11,10 +11,11 @@ func Test_HashPassword(t *testing.T) { // Arrange pwd := "qwerty" salt := "salt" + encoder := NewPasswordJsonEncoder(salt) // Act - hashedPwd := HashPassword(pwd, salt) - isMatch := IsPasswordsMatch(pwd, salt, hashedPwd) + hashedPwd := encoder.HashPassword(pwd) + isMatch := encoder.IsPasswordsMatch(pwd, hashedPwd) // Assert assert.True(t, isMatch) From 9ab845e4bba9dfa397b2e549d4831057d1960b72 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Fri, 10 Jan 2025 21:16:32 +0500 Subject: [PATCH 04/15] Method rename && CreateUser anti panic err omit --- api/routing_fuzzing_test.go | 2 +- application/application_test.go | 2 +- data/keycloak_user.go | 5 +++-- utils/encoding/encoding.go | 4 ++-- utils/encoding/encoding_test.go | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/api/routing_fuzzing_test.go b/api/routing_fuzzing_test.go index 2e7c717..280e31c 100644 --- a/api/routing_fuzzing_test.go +++ b/api/routing_fuzzing_test.go @@ -32,7 +32,7 @@ const ( var ( testSalt = "salt" encoder = encoding.NewPasswordJsonEncoder(testSalt) - testHashedPassowrd = encoder.HashPassword("1234567890") + testHashedPassowrd = encoder.GetB64PasswordHash("1234567890") testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ diff --git a/application/application_test.go b/application/application_test.go index 7138db1..f2f680d 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -32,7 +32,7 @@ const ( var ( testSalt = "salt" encoder = encoding.NewPasswordJsonEncoder(testSalt) - testHashedPassowrd = encoder.HashPassword("1234567890") + testHashedPassowrd = encoder.GetB64PasswordHash("1234567890") testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ diff --git a/data/keycloak_user.go b/data/keycloak_user.go index d5c4409..a097bda 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -30,7 +30,8 @@ func CreateUser(rawData interface{}, encoder encoding.PasswordJsonEncoder) User jsonData, _ := json.Marshal(&rawData) kcUser := &KeyCloakUser{rawData: rawData, jsonRawData: string(jsonData)} password := getPathStringValue[string](kcUser.rawData, pathToPassword) - kcUser.SetPassword(password, encoder) + // todo(UMV): handle CreateUser errors in the future + _ = kcUser.SetPassword(password, encoder) user := User(kcUser) return user } @@ -66,7 +67,7 @@ func (user *KeyCloakUser) GetPasswordHash() string { * - encoder - encoder object with salt and hasher */ func (user *KeyCloakUser) SetPassword(password string, encoder encoding.PasswordJsonEncoder) error { - hashed := encoder.HashPassword(password) + hashed := encoder.GetB64PasswordHash(password) if err := setPathStringValue(user.rawData, pathToPassword, hashed); err != nil { return err } diff --git a/utils/encoding/encoding.go b/utils/encoding/encoding.go index a48e5d5..544c4c1 100644 --- a/utils/encoding/encoding.go +++ b/utils/encoding/encoding.go @@ -20,7 +20,7 @@ func NewPasswordJsonEncoder(salt string) PasswordJsonEncoder { return encoder } -func (e *PasswordJsonEncoder) HashPassword(password string) string { +func (e *PasswordJsonEncoder) GetB64PasswordHash(password string) string { if IsPasswordHashed(password) { return password } @@ -34,7 +34,7 @@ func (e *PasswordJsonEncoder) HashPassword(password string) string { } func (e *PasswordJsonEncoder) IsPasswordsMatch(password, hash string) bool { - currPasswordHash := e.HashPassword(password) + currPasswordHash := e.GetB64PasswordHash(password) return b64Decode(hash) == b64Decode(currPasswordHash) } diff --git a/utils/encoding/encoding_test.go b/utils/encoding/encoding_test.go index 7bbfc2a..8402c24 100644 --- a/utils/encoding/encoding_test.go +++ b/utils/encoding/encoding_test.go @@ -14,7 +14,7 @@ func Test_HashPassword(t *testing.T) { encoder := NewPasswordJsonEncoder(salt) // Act - hashedPwd := encoder.HashPassword(pwd) + hashedPwd := encoder.GetB64PasswordHash(pwd) isMatch := encoder.IsPasswordsMatch(pwd, hashedPwd) // Assert From a5086ac338cfab256a446f8003e8f9a3c5f664e9 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Sat, 11 Jan 2025 23:57:10 +0500 Subject: [PATCH 05/15] fixed user create|update --- api/admin/cli/main.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 410ec52..6ff127d 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -122,10 +122,7 @@ func main() { if err := json.Unmarshal(value, &userNew); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - if resourceId == "" { - log.Fatalf("Realm name not specified") - } - realm, err := manager.GetRealm(resourceId) + realm, err := manager.GetRealm(params) if err != nil { log.Fatalf("GetRealm failed: %s", err) } @@ -210,10 +207,7 @@ func main() { if err := json.Unmarshal(value, &newUser); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - if resourceId == "" { - log.Fatalf("Realm name not specified") - } - realm, err := manager.GetRealm(resourceId) + realm, err := manager.GetRealm(params) if err != nil { log.Fatalf("GetRealm failed: %s", err) } From b6c83a36078cbb31187749802f400c4bc8001f0a Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Sun, 12 Jan 2025 00:04:07 +0500 Subject: [PATCH 06/15] more detailed args --- api/admin/cli/main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 6ff127d..2fced84 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -20,12 +20,12 @@ import ( const defaultConfig = "./config_w_redis.json" var ( - argConfigFile = flag.String("config", defaultConfig, "") - argOperation = flag.String("operation", "", "") - argResource = flag.String("resource", "", "") - argResourceId = flag.String("resource_id", "", "") - argParams = flag.String("params", "", "This is the name of the realm for operations on client or user resources") - argValue = flag.String("value", "", "Json object") + argConfigFile = flag.String("config", defaultConfig, "Application config for working with a persistent data store") + argOperation = flag.String("operation", "", "One of the available operations read|create|update|delete or user specific change/reset password") + argResource = flag.String("resource", "", "\"realm\", \"client\" or \"user\" or maybe other in future") + argResourceId = flag.String("resource_id", "", "resource object identifier, id required for the update|delete or read operation") + argParams = flag.String("params", "", "Name of a realm for operations on client or user resources") + argValue = flag.String("value", "", "Json encoded resource itself") ) func main() { From 53c4913bc7a2957bc971768c499462ce604baee0 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Sun, 12 Jan 2025 20:02:47 +0500 Subject: [PATCH 07/15] added dada for additional Realm --- tools/create_wissance_demo_users_docker.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/create_wissance_demo_users_docker.sh b/tools/create_wissance_demo_users_docker.sh index ccb1db5..fd7ed59 100644 --- a/tools/create_wissance_demo_users_docker.sh +++ b/tools/create_wissance_demo_users_docker.sh @@ -1,3 +1,8 @@ +# Realm WissanceFerrumDemo ./ferrum-admin --config=config_docker_w_redis.json --resource=realm --operation=create --value='{"name": "WissanceFerrumDemo", "user_federation_services":[], "token_expiration": 600, "refresh_expiration": 300}' ./ferrum-admin --config=config_docker_w_redis.json --resource=client --operation=create --value='{"id": "d4dc483d-7d0d-4d2e-a0a0-2d34b55e6667", "name": "WissanceWebDemo", "type": "confidential", "auth": {"type": 1, "value": "fb6Z4RsOadVycQoeQiN57xpu8w8w1111"}}' --params="WissanceFerrumDemo" ./ferrum-admin --config=config_docker_w_redis.json --resource=user --operation=create --value='{"info": {"sub": "667ff6a7-3f6b-449b-a217-6fc5d9ac6891", "email_verified": true, "roles": ["admin"], "name": "M.V.Ushakov", "preferred_username": "umv", "given_name": "Michael", "family_name": "Ushakov"}, "credentials": {"password": "1s2d3f4g90xs"}}' --params="WissanceFerrumDemo" +# Realm WissanceFerrumDemo2 +./ferrum-admin --config=config_docker_w_redis.json --resource=realm --operation=create --value='{"name": "WissanceFerrumDemo2", "user_federation_services":[], "token_expiration": 600, "refresh_expiration": 300}' +./ferrum-admin --config=config_docker_w_redis.json --resource=client --operation=create --value='{"id": "d4dc483d-7d0d-4d2e-a0a0-2d34b55e6668", "name": "WissanceWebDemo2", "type": "confidential", "auth": {"type": 1, "value": "fb6Z4RsOadVycQoeQiN57xpu8w8w2222"}}' --params="WissanceFerrumDemo2" +./ferrum-admin --config=config_docker_w_redis.json --resource=user --operation=create --value='{"info": {"sub": "667ff6a7-3f6b-449b-a217-6fc5d9ac6892", "email_verified": true, "roles": ["manager"], "name": "A.Petrov", "preferred_username": "paa", "given_name": "Alex", "family_name": "Petrov"}, "credentials": {"password": "12345678"}}' --params="WissanceFerrumDemo2" From f7c10945de207df3b031ae8008fdad49868e1422 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Sun, 12 Jan 2025 20:24:53 +0500 Subject: [PATCH 08/15] clarification --- api/admin/cli/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 2fced84..9726551 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -251,7 +251,7 @@ func main() { } // TODO(SIA) Moving password verification to another location if len(value) < 8 { - log.Fatalf("Password length must be greater than 8") + log.Fatalf("Password length must be greater than 7") } password := string(value) passwordManager := manager.(PasswordManager) From c9c6765520aa668a72b71df9f520c0711b9573a1 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Mon, 13 Jan 2025 14:38:19 +0500 Subject: [PATCH 09/15] add nil check for encoder in data.CreateUser this is necessary to avoid multiple hashing of passwords --- .gitignore | 2 ++ api/admin/cli/main.go | 6 +----- data/keycloak_user.go | 13 ++++++------- data/realm.go | 2 +- data/user.go | 2 +- managers/files/manager.go | 3 +-- managers/files/manager_test.go | 8 ++------ managers/redis/manager_realm_operations.go | 2 +- managers/redis/manager_test.go | 14 +++++--------- managers/redis/manager_user_operations.go | 12 ++---------- utils/encoding/encoding.go | 15 ++------------- 11 files changed, 24 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index f3d7fbd..15c0050 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ extTestApps/Wissance.Auth.FerrumChecker/.vs/ extTestApps/Wissance.Auth.FerrumChecker/Wissance.Auth.FerrumChecker/bin/ extTestApps/Wissance.Auth.FerrumChecker/Wissance.Auth.FerrumChecker/obj/ + +.vscode/ \ No newline at end of file diff --git a/api/admin/cli/main.go b/api/admin/cli/main.go index 2fced84..188b52e 100644 --- a/api/admin/cli/main.go +++ b/api/admin/cli/main.go @@ -207,11 +207,7 @@ func main() { if err := json.Unmarshal(value, &newUser); err != nil { log.Fatalf("json.Unmarshal failed: %s", err) } - realm, err := manager.GetRealm(params) - if err != nil { - log.Fatalf("GetRealm failed: %s", err) - } - user := data.CreateUser(newUser, realm.Encoder) + user := data.CreateUser(newUser, nil) if err := manager.UpdateUser(params, resourceId, user); err != nil { log.Fatalf("UpdateUser failed: %s", err) } diff --git a/data/keycloak_user.go b/data/keycloak_user.go index a097bda..78d3935 100644 --- a/data/keycloak_user.go +++ b/data/keycloak_user.go @@ -26,12 +26,14 @@ type KeyCloakUser struct { * - rawData - any json * Return: instance of User as KeyCloakUser */ -func CreateUser(rawData interface{}, encoder encoding.PasswordJsonEncoder) User { +func CreateUser(rawData interface{}, encoder *encoding.PasswordJsonEncoder) User { jsonData, _ := json.Marshal(&rawData) kcUser := &KeyCloakUser{rawData: rawData, jsonRawData: string(jsonData)} password := getPathStringValue[string](kcUser.rawData, pathToPassword) - // todo(UMV): handle CreateUser errors in the future - _ = kcUser.SetPassword(password, encoder) + if encoder != nil { + // todo(UMV): handle CreateUser errors in the future + _ = kcUser.SetPassword(password, encoder) + } user := User(kcUser) return user } @@ -54,9 +56,6 @@ func (user *KeyCloakUser) GetUsername() string { // todo(UMV): we should consider case when User is External func (user *KeyCloakUser) GetPasswordHash() string { password := getPathStringValue[string](user.rawData, pathToPassword) - if !encoding.IsPasswordHashed(password) { - // todo (YuriShang): think about actions if the password is not hashed - } return password } @@ -66,7 +65,7 @@ func (user *KeyCloakUser) GetPasswordHash() string { * - password - new password * - encoder - encoder object with salt and hasher */ -func (user *KeyCloakUser) SetPassword(password string, encoder encoding.PasswordJsonEncoder) error { +func (user *KeyCloakUser) SetPassword(password string, encoder *encoding.PasswordJsonEncoder) error { hashed := encoder.GetB64PasswordHash(password) if err := setPathStringValue(user.rawData, pathToPassword, hashed); err != nil { return err diff --git a/data/realm.go b/data/realm.go index 3317127..cea2012 100644 --- a/data/realm.go +++ b/data/realm.go @@ -15,5 +15,5 @@ type Realm struct { RefreshTokenExpiration int `json:"refresh_expiration"` UserFederationServices []UserFederationServiceConfig `json:"user_federation_services"` PasswordSalt string `json:"password_salt"` - Encoder encoding.PasswordJsonEncoder + Encoder *encoding.PasswordJsonEncoder } diff --git a/data/user.go b/data/user.go index 7b49f2d..26ba1ca 100644 --- a/data/user.go +++ b/data/user.go @@ -10,7 +10,7 @@ import ( type User interface { GetUsername() string GetPasswordHash() string - SetPassword(password string, encoder encoding.PasswordJsonEncoder) error + SetPassword(password string, encoder *encoding.PasswordJsonEncoder) error GetId() uuid.UUID GetUserInfo() interface{} GetRawData() interface{} diff --git a/managers/files/manager.go b/managers/files/manager.go index c38a753..3999d50 100644 --- a/managers/files/manager.go +++ b/managers/files/manager.go @@ -103,9 +103,8 @@ func (mn *FileDataManager) GetUsers(realmName string) ([]data.User, error) { return nil, errors.ErrZeroLength } users := make([]data.User, len(e.Users)) - e.Encoder = encoding.NewPasswordJsonEncoder(e.PasswordSalt) for i, u := range e.Users { - user := data.CreateUser(u, e.Encoder) + user := data.CreateUser(u, nil) users[i] = user } return users, nil diff --git a/managers/files/manager_test.go b/managers/files/manager_test.go index c0d7806..ebb077f 100644 --- a/managers/files/manager_test.go +++ b/managers/files/manager_test.go @@ -68,9 +68,7 @@ func TestGetUserSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - r, err := manager.GetRealm(realm) - assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser, r.Encoder) + expectedUser := data.CreateUser(rawUser, nil) user, err := manager.GetUser(realm, userName) assert.NoError(t, err) checkUser(t, &expectedUser, &user) @@ -99,9 +97,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err := json.Unmarshal([]byte(userJson), &rawUser) assert.NoError(t, err) - r, err := manager.GetRealm(realm) - assert.NoError(t, err) - expectedUser := data.CreateUser(rawUser, r.Encoder) + expectedUser := data.CreateUser(rawUser, nil) user, err := manager.GetUserById(realm, userId) assert.NoError(t, err) checkUser(t, &expectedUser, &user) diff --git a/managers/redis/manager_realm_operations.go b/managers/redis/manager_realm_operations.go index e5f3f1b..86773ef 100755 --- a/managers/redis/manager_realm_operations.go +++ b/managers/redis/manager_realm_operations.go @@ -129,7 +129,7 @@ func (mn *RedisDataManager) CreateRealm(newRealm data.Realm) error { TokenExpiration: newRealm.TokenExpiration, RefreshTokenExpiration: newRealm.RefreshTokenExpiration, PasswordSalt: salt, - Encoder: encoding.PasswordJsonEncoder{}, + Encoder: nil, } jsonShortRealm, err := json.Marshal(shortRealm) if err != nil { diff --git a/managers/redis/manager_test.go b/managers/redis/manager_test.go index 51722a1..838a79f 100644 --- a/managers/redis/manager_test.go +++ b/managers/redis/manager_test.go @@ -466,7 +466,6 @@ func TestGetUsersSuccessfully(t *testing.T) { Name: sf.Format("realm_4_get_multiple_users_{0}", uuid.New().String()), TokenExpiration: 3600, RefreshTokenExpiration: 1800, - Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(r) assert.NoError(t, err) @@ -482,7 +481,7 @@ func TestGetUsersSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser, r.Encoder) + user := data.CreateUser(rawUser, nil) err = manager.CreateUser(realm.Name, user) users[i] = user assert.NoError(t, err) @@ -532,7 +531,6 @@ func TestGetUserByIdSuccessfully(t *testing.T) { Name: "realm_4_test_get_user_by_id", TokenExpiration: 3600, RefreshTokenExpiration: 1800, - Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -543,7 +541,7 @@ func TestGetUserByIdSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser, realm.Encoder) + user := data.CreateUser(rawUser, nil) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) @@ -673,7 +671,7 @@ func TestUpdateUserSuccessfully(t *testing.T) { jsonStr = sf.Format(jsonTemplate, "pppetrov", "67890", "00000000-0000-0000-0000-000000000001") err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user = data.CreateUser(rawUser, realm.Encoder) + user = data.CreateUser(rawUser, nil) err = manager.UpdateUser(realm.Name, userName, user) assert.NoError(t, err) @@ -692,7 +690,6 @@ func TestUpdateUserFailsNonExistingUser(t *testing.T) { Name: "realm_4_test_user_update_fails_non_existing_user", TokenExpiration: 3600, RefreshTokenExpiration: 1800, - Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -703,7 +700,7 @@ func TestUpdateUserFailsNonExistingUser(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser, realm.Encoder) + user := data.CreateUser(rawUser, nil) err = manager.UpdateUser(realm.Name, userName, user) assert.Error(t, err) assert.True(t, errors.As(err, &appErrs.EmptyNotFoundErr)) @@ -719,7 +716,6 @@ func TestDeleteUserSuccessfully(t *testing.T) { Name: "realm_4_test_user_delete", TokenExpiration: 3600, RefreshTokenExpiration: 1800, - Encoder: encoding.NewPasswordJsonEncoder("salt"), } err := manager.CreateRealm(realm) assert.NoError(t, err) @@ -730,7 +726,7 @@ func TestDeleteUserSuccessfully(t *testing.T) { var rawUser interface{} err = json.Unmarshal([]byte(jsonStr), &rawUser) assert.NoError(t, err) - user := data.CreateUser(rawUser, realm.Encoder) + user := data.CreateUser(rawUser, nil) err = manager.CreateUser(realm.Name, user) assert.NoError(t, err) u, err := manager.GetUser(realm.Name, userName) diff --git a/managers/redis/manager_user_operations.go b/managers/redis/manager_user_operations.go index de4339a..ac4dbbd 100644 --- a/managers/redis/manager_user_operations.go +++ b/managers/redis/manager_user_operations.go @@ -57,12 +57,8 @@ func (mn *RedisDataManager) GetUsers(realmName string) ([]data.User, error) { } userData := make([]data.User, len(realmUsersData)) - realm, err := mn.GetRealm(realmName) - if err != nil { - return []data.User{}, nil - } for i, u := range realmUsersData { - userData[i] = data.CreateUser(u, realm.Encoder) + userData[i] = data.CreateUser(u, nil) } return userData, nil } @@ -87,11 +83,7 @@ func (mn *RedisDataManager) GetUser(realmName string, userName string) (data.Use } return nil, errors2.NewUnknownError("getSingleRedisObject", "RedisDataManager.GetUser", err) } - realm, err := mn.GetRealm(realmName) - if err != nil { - return nil, err - } - user := data.CreateUser(*rawUser, realm.Encoder) + user := data.CreateUser(*rawUser, nil) return user, nil } diff --git a/utils/encoding/encoding.go b/utils/encoding/encoding.go index 544c4c1..d513de0 100644 --- a/utils/encoding/encoding.go +++ b/utils/encoding/encoding.go @@ -12,18 +12,15 @@ type PasswordJsonEncoder struct { hasher hash.Hash } -func NewPasswordJsonEncoder(salt string) PasswordJsonEncoder { +func NewPasswordJsonEncoder(salt string) *PasswordJsonEncoder { encoder := PasswordJsonEncoder{ hasher: sha512.New(), salt: salt, } - return encoder + return &encoder } func (e *PasswordJsonEncoder) GetB64PasswordHash(password string) string { - if IsPasswordHashed(password) { - return password - } passwordBytes := []byte(password + e.salt) e.hasher.Write(passwordBytes) hashedPasswordBytes := e.hasher.Sum(nil) @@ -47,14 +44,6 @@ func GenerateRandomSalt() string { return string(salt) } -func IsPasswordHashed(password string) bool { - decoded := b64Decode(password) - if len(decoded) == 0 { - return false - } - return true -} - func b64Encode(encoded []byte) string { cstr := base64.URLEncoding.EncodeToString(encoded) return cstr From 7d1c29b6df3bfdc396b38d32d54b541e5d09b274 Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Thu, 16 Jan 2025 18:15:12 +0500 Subject: [PATCH 10/15] File was configured to be working with hashes insead of raw passwords --- application/application_test.go | 4 ++-- data.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/application/application_test.go b/application/application_test.go index f2f680d..6a02e82 100644 --- a/application/application_test.go +++ b/application/application_test.go @@ -32,7 +32,7 @@ const ( var ( testSalt = "salt" encoder = encoding.NewPasswordJsonEncoder(testSalt) - testHashedPassowrd = encoder.GetB64PasswordHash("1234567890") + testHashedPassword = encoder.GetB64PasswordHash("1234567890") testKey = []byte("qwerty1234567890") testServerData = data.ServerData{ Realms: []data.Realm{ @@ -51,7 +51,7 @@ var ( "name": "vano", "preferred_username": "vano", "given_name": "vano ivanov", "family_name": "ivanov", "email_verified": true, }, - "credentials": map[string]interface{}{"password": testHashedPassowrd}, + "credentials": map[string]interface{}{"password": testHashedPassword}, }, }, PasswordSalt: testSalt, diff --git a/data.json b/data.json index eb262a6..0d9c4dc 100644 --- a/data.json +++ b/data.json @@ -4,6 +4,7 @@ "name": "myapp", "token_expiration": 330, "refresh_expiration": 200, + "password_salt": "1234567890", "clients": [ { "id": "d4dc483d-7d0d-4d2e-a0a0-2d34b55e5a14", @@ -29,7 +30,7 @@ "family_name": "sys" }, "credentials": { - "password": "1s2d3f4g90xs" + "password": "AcMWCBu5AQDN8IvSRExUUSQq7H3RH6IzsxZJqyIoEmPFtJwGknUUvzet0vhS95hgkrKLNM66v0mUB5xji8zdqA==" } }, { From cdee5034bbb4d4c044b123d102fb2445f5bf84ac Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Thu, 16 Jan 2025 18:50:23 +0500 Subject: [PATCH 11/15] added changes in readme.md --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a583314..34286f9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Ferrum is a **better** Authorization Server, this is a Community version. ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/wissance/Ferrum?style=plastic) ![GitHub issues](https://img.shields.io/github/issues/wissance/Ferrum?style=plastic) ![GitHub Release Date](https://img.shields.io/github/release-date/wissance/Ferrum) -![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/Ferrum/v0.9.1/total?style=plastic) +![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/Ferrum/v0.9.2/total?style=plastic) ![Ferrum: A better Auth Server](/img/ferrum_cover.png) @@ -43,6 +43,8 @@ it has `endpoints` SIMILAR to `Keycloak`, at present time we are having followin ## 3. How to use +`Ferrum` is thoroughly developing with maximal quality of code and solution; we are working using a `git-flow` approach; even `master` branch is a stable release branch, but `develop` is also highly stable, therefore develop version could also be used in a production. + ### 3.1 Build First of all build is simple run `go build` from application root directory. Additionally it is possible @@ -193,7 +195,68 @@ Since version `0.9.1` it is possible to use `CLI Admin` [See](api/admin/cli/READ ![Use CLI Admin from docker](/img/additional/cli_from_docker.png) -## 6. Contributors +## 6. Changes + +Brief info about changes in releases. + +### 6.1 Changes in 0.0.1 + +Features: +* `Keycloak` compatible HTTP-endpoints to issue a new `token` and to get `userinfo` + +### 6.2 Changes in 0.1.0 + +Features: +* documentation (`readme.md` file) +* integration tests + +### 6.3 Changes in 0.1.1 + +Features: +* fixed modules names + +### 6.4 Changes in 0.1.2 + +Features: +* changed module names to make it available to embed `Ferrum` in an other applications + +### 6.5 Changes in 0.1.3 + +Features: +* `Keycloak` compatible HTTP-endpoint for token introspect + +### 6.6 Changes in 0.1.4 + +Features: +* removed `/` therefore it is possible to interact with `Ferrum` using `go-cloak` package + +### 6.7 Changes in 0.9.0 + +Features +* logging +* implemented token refresh +* better docs + +### 6.8 Changes in 0.9.1 + +Features: +* `docker` && `docker-compose` for app running +* admin `CLI` `API` +* `Redis` as a production data storage + +### 6.9 Changes in 0.9.2 + +Features: +* admin cli added to docker +* test on `Redis` data manger +* used different config to run locally and in docker +* newer `Keycloak` versions support +* checked stability if `Redis` is down, `Ferrum` does not crushes and wait until `Redis` is ready +* swagger (-devmode) and `Keycloak` compatible HTTP endpoint `openid-configuration` +* support for federated user (without full providers impl, just preliminary) +* store password as a hashes + +## 7. Contributors From 8e869bd90d62e2b50b0f4601fa9a78f5c73bfb4b Mon Sep 17 00:00:00 2001 From: Ushakov Michale Date: Thu, 16 Jan 2025 18:51:11 +0500 Subject: [PATCH 12/15] some clarifications --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34286f9..9eb5a72 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ Features: * used different config to run locally and in docker * newer `Keycloak` versions support * checked stability if `Redis` is down, `Ferrum` does not crushes and wait until `Redis` is ready -* swagger (-devmode) and `Keycloak` compatible HTTP endpoint `openid-configuration` +* `swagger` (`-devmode` option in cmd line) and `Keycloak` compatible HTTP endpoint `openid-configuration` * support for federated user (without full providers impl, just preliminary) * store password as a hashes From 60d87820864ee58dbc7868a44e0a9a91424c24da Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Fri, 17 Jan 2025 11:04:19 +0500 Subject: [PATCH 13/15] workflow -> workflows --- .github/{workflow => workflows}/ci.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflow => workflows}/ci.yml (100%) diff --git a/.github/workflow/ci.yml b/.github/workflows/ci.yml similarity index 100% rename from .github/workflow/ci.yml rename to .github/workflows/ci.yml From 3a52f86de4fb91c86c573d1e1274c22f78619d44 Mon Sep 17 00:00:00 2001 From: Yuri Shangaraev Date: Fri, 17 Jan 2025 11:07:59 +0500 Subject: [PATCH 14/15] testing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9eb5a72..bed7018 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Ferrum +# Ferrum . Ferrum is a **better** Authorization Server, this is a Community version. From 3f782ac5ae104c2154e5cbae0d054167c8919488 Mon Sep 17 00:00:00 2001 From: Yuri <95570582+YuriShang@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:13:21 +0500 Subject: [PATCH 15/15] Update ci.yml --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abc620f..1f400fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,9 @@ name: Go CI on: push: branches: - - 'main' - - 'develop' + - main + - develop + - master jobs: build-linux: