From 2eea4b27109e6f958a31890844e2bb69fbc21a48 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Giovanini" Date: Mon, 28 Oct 2024 15:58:09 +0100 Subject: feat: Add service to handle auth --- pkg/service/auth.go | 117 ++++++++++++++++++++++++++++++++++++++++++++++ pkg/service/auth_test.go | 119 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 pkg/service/auth.go create mode 100644 pkg/service/auth_test.go (limited to 'pkg/service') diff --git a/pkg/service/auth.go b/pkg/service/auth.go new file mode 100644 index 0000000..1fbf4b6 --- /dev/null +++ b/pkg/service/auth.go @@ -0,0 +1,117 @@ +package service + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + "golang.org/x/crypto/bcrypt" +) + +type ( + AuthService struct { + authRepository authRepository + } + + authRepository interface { + GetPassphrase() []byte + GetBase64AesKey() []byte + } +) + +var tokenSeed = []byte("cerrado") + +func (a *AuthService) CheckAuth(username, password string) bool { + passphrase := a.authRepository.GetPassphrase() + pass := []byte(fmt.Sprintf("%s:%s", username, password)) + + err := bcrypt.CompareHashAndPassword(passphrase, pass) + + return err == nil +} + +func (a *AuthService) IssueToken() ([]byte, error) { + // TODO: do this block only once + base := a.authRepository.GetBase64AesKey() + + dbuf, err := base64.StdEncoding.DecodeString(string(base)) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(dbuf) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + ciphertext := gcm.Seal(nonce, nonce, tokenSeed, nil) + + return ciphertext, nil +} + +func (a *AuthService) ValidateToken(token []byte) (bool, error) { + base := a.authRepository.GetBase64AesKey() + + dbuf, err := base64.StdEncoding.DecodeString(string(base)) + if err != nil { + return false, err + } + + block, err := aes.NewCipher(dbuf) + if err != nil { + return false, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return false, err + } + + nonceSize := gcm.NonceSize() + if len(token) < nonceSize { + return false, fmt.Errorf("ciphertext too short") + } + + nonce, ciphertext := token[:nonceSize], token[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return false, err + } + + return bytes.Equal(tokenSeed, plaintext), nil +} + +func GenerateHash(username, password string) (string, error) { + passphrase := fmt.Sprintf("%s:%s", username, password) + bytes, err := bcrypt.GenerateFromPassword([]byte(passphrase), 14) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func GenerateAesKey() (string, error) { + key := make([]byte, 32) + + _, err := rand.Read(key) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(key), nil +} diff --git a/pkg/service/auth_test.go b/pkg/service/auth_test.go new file mode 100644 index 0000000..06bf76f --- /dev/null +++ b/pkg/service/auth_test.go @@ -0,0 +1,119 @@ +// go:build unit + +package service + +import ( + "testing" +) + +func TestCheck(t *testing.T) { + testCases := []struct { + name string + passphrase []byte + username string + password string + wantError bool + }{ + { + name: "generated", + passphrase: nil, + username: "gabrielgio", + password: "adminadmin", + wantError: false, + }, + { + name: "static", + passphrase: []byte("$2a$14$W2yT0E6Zm8nTecqipHUQGOLC6PvNjIQqpQTW/MZmD5oqDfaBJnBV6"), + username: "gabrielgio", + password: "adminadmin", + wantError: false, + }, + { + name: "error", + passphrase: []byte("This is not a valid hash"), + username: "gabrielgio", + password: "adminadmin", + wantError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mock := &mockAuthRepository{ + username: tc.username, + password: tc.password, + passphrase: tc.passphrase, + } + + service := AuthService{authRepository: mock} + + if service.CheckAuth(tc.username, tc.password) == tc.wantError { + t.Errorf("Invalid result, wanted %t got %t", tc.wantError, !tc.wantError) + } + }) + } +} + +func TestValidate(t *testing.T) { + testCases := []struct { + name string + aesKey []byte + }{ + { + name: "generated", + aesKey: nil, + }, + { + name: "static", + aesKey: []byte("RTGkmunKmi5agh7jaqENunG2zI/godnkqhHaHyX/AVg="), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mock := &mockAuthRepository{ + aesKey: tc.aesKey, + } + + service := AuthService{authRepository: mock} + + token, err := service.IssueToken() + if err != nil { + t.Fatalf("Error issuing token: %s", err.Error()) + } + + v, err := service.ValidateToken(token) + if err != nil { + t.Fatalf("Error validating token: %s", err.Error()) + } + + if !v { + t.Error("Invalid token generated") + } + }) + } +} + +type mockAuthRepository struct { + username string + password string + passphrase []byte + + aesKey []byte +} + +func (m *mockAuthRepository) GetPassphrase() []byte { + if m.passphrase == nil { + hash, _ := GenerateHash(m.username, m.password) + m.passphrase = []byte(hash) + } + return m.passphrase +} + +func (m *mockAuthRepository) GetBase64AesKey() []byte { + if m.aesKey == nil { + key, _ := GenerateAesKey() + m.aesKey = []byte(key) + } + return m.aesKey +} -- cgit v1.2.3