feat(auth): implement user and device authentication
This commit is contained in:
@@ -27,12 +27,13 @@ func TestRunnerSupportsUpStatusDownAndIdempotentUp(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Up() error = %v", err)
|
||||
}
|
||||
if applied != 2 {
|
||||
t.Fatalf("Up() applied = %d, want 2", applied)
|
||||
if applied != 3 {
|
||||
t.Fatalf("Up() applied = %d, want 3", applied)
|
||||
}
|
||||
assertStatuses(t, runner, map[int64]bool{
|
||||
1: true,
|
||||
2: true,
|
||||
3: true,
|
||||
})
|
||||
|
||||
applied, err = runner.Up(context.Background())
|
||||
@@ -48,7 +49,8 @@ func TestRunnerSupportsUpStatusDownAndIdempotentUp(t *testing.T) {
|
||||
}
|
||||
assertStatuses(t, runner, map[int64]bool{
|
||||
1: true,
|
||||
2: false,
|
||||
2: true,
|
||||
3: false,
|
||||
})
|
||||
|
||||
applied, err = runner.Up(context.Background())
|
||||
@@ -61,6 +63,7 @@ func TestRunnerSupportsUpStatusDownAndIdempotentUp(t *testing.T) {
|
||||
assertStatuses(t, runner, map[int64]bool{
|
||||
1: true,
|
||||
2: true,
|
||||
3: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unicode/utf8"
|
||||
|
||||
"cmroubao/backend-api/internal/domain"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidPassword = errors.New("password is invalid")
|
||||
ErrPasswordMismatch = errors.New("password does not match")
|
||||
)
|
||||
|
||||
type Bcrypt struct {
|
||||
cost int
|
||||
dummyHash string
|
||||
}
|
||||
|
||||
func NewBcrypt(cost int) (*Bcrypt, error) {
|
||||
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
|
||||
return nil, errors.New("bcrypt cost is out of range")
|
||||
}
|
||||
dummyHash, err := bcrypt.GenerateFromPassword(
|
||||
[]byte("cmroubao-dummy-password"),
|
||||
cost,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("initialize bcrypt dummy hash")
|
||||
}
|
||||
return &Bcrypt{cost: cost, dummyHash: string(dummyHash)}, nil
|
||||
}
|
||||
|
||||
func (manager *Bcrypt) Hash(plain string) (string, error) {
|
||||
if !utf8.ValidString(plain) ||
|
||||
len([]byte(plain)) < domain.MinPasswordBytes ||
|
||||
len([]byte(plain)) > domain.MaxPasswordBytes {
|
||||
return "", ErrInvalidPassword
|
||||
}
|
||||
value, err := bcrypt.GenerateFromPassword([]byte(plain), manager.cost)
|
||||
if err != nil {
|
||||
return "", errors.New("hash password")
|
||||
}
|
||||
return string(value), nil
|
||||
}
|
||||
|
||||
func (manager *Bcrypt) VerifyDummy(plain string) {
|
||||
_ = bcrypt.CompareHashAndPassword(
|
||||
[]byte(manager.dummyHash),
|
||||
[]byte(plain),
|
||||
)
|
||||
}
|
||||
|
||||
func (manager *Bcrypt) Verify(encoded string, plain string) error {
|
||||
if encoded == "" ||
|
||||
plain == "" ||
|
||||
len([]byte(plain)) > domain.MaxPasswordBytes {
|
||||
return ErrPasswordMismatch
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword(
|
||||
[]byte(encoded),
|
||||
[]byte(plain),
|
||||
); err != nil {
|
||||
return ErrPasswordMismatch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestBcryptHashesAndVerifiesWithoutStoringPlaintext(t *testing.T) {
|
||||
manager, err := NewBcrypt(bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatalf("NewBcrypt() error = %v", err)
|
||||
}
|
||||
encoded, err := manager.Hash("correct horse battery staple")
|
||||
if err != nil {
|
||||
t.Fatalf("Hash() error = %v", err)
|
||||
}
|
||||
if encoded == "correct horse battery staple" {
|
||||
t.Fatal("Hash() returned plaintext")
|
||||
}
|
||||
if err := manager.Verify(encoded, "correct horse battery staple"); err != nil {
|
||||
t.Fatalf("Verify(correct) error = %v", err)
|
||||
}
|
||||
if err := manager.Verify(encoded, "wrong"); !errors.Is(
|
||||
err,
|
||||
ErrPasswordMismatch,
|
||||
) {
|
||||
t.Fatalf("Verify(wrong) error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBcryptRejectsInvalidCostAndOversizedPasswords(t *testing.T) {
|
||||
if _, err := NewBcrypt(bcrypt.MinCost - 1); err == nil {
|
||||
t.Fatal("NewBcrypt(invalid) error = nil")
|
||||
}
|
||||
manager, err := NewBcrypt(bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatalf("NewBcrypt() error = %v", err)
|
||||
}
|
||||
oversized := strings.Repeat("x", 73)
|
||||
if _, err := manager.Hash("too-short"); !errors.Is(
|
||||
err,
|
||||
ErrInvalidPassword,
|
||||
) {
|
||||
t.Fatalf("Hash(short) error = %v", err)
|
||||
}
|
||||
if _, err := manager.Hash(oversized); !errors.Is(
|
||||
err,
|
||||
ErrInvalidPassword,
|
||||
) {
|
||||
t.Fatalf("Hash(oversized) error = %v", err)
|
||||
}
|
||||
if _, err := manager.Hash(
|
||||
string([]byte{0xff, 0xfe, 0xfd}),
|
||||
); !errors.Is(err, ErrInvalidPassword) {
|
||||
t.Fatalf("Hash(invalid UTF-8) error = %v", err)
|
||||
}
|
||||
if err := manager.Verify("$2a$04$invalid", oversized); !errors.Is(
|
||||
err,
|
||||
ErrPasswordMismatch,
|
||||
) {
|
||||
t.Fatalf("Verify(oversized) error = %v", err)
|
||||
}
|
||||
manager.VerifyDummy("password-123")
|
||||
}
|
||||
Reference in New Issue
Block a user