fix: harden login for MLPS requirements
This commit is contained in:
@@ -21,6 +21,7 @@ type AuthService struct {
|
||||
jwt *auth.Manager
|
||||
sessions *auth.SessionStore
|
||||
loginGuard *auth.LoginGuard
|
||||
cipher *auth.PasswordCipher
|
||||
}
|
||||
|
||||
func NewAuthService(
|
||||
@@ -45,11 +46,18 @@ func NewAuthService(
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthService) WithPasswordCipher(cipher *auth.PasswordCipher) *AuthService {
|
||||
s.cipher = cipher
|
||||
return s
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
IP string `json:"-"`
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
EncryptedPassword string `json:"encrypted_password"`
|
||||
PasswordKeyID string `json:"password_key_id"`
|
||||
IP string `json:"-"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
@@ -107,6 +115,10 @@ func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginRespon
|
||||
if identifier == "" {
|
||||
return nil, response.ErrBadRequest(40001, "invalid_params", "login identifier is required")
|
||||
}
|
||||
password, err := s.loginPassword(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
permit, err := s.acquireLoginPermit(ctx, identifier, req.IP)
|
||||
if err != nil {
|
||||
@@ -114,6 +126,9 @@ func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginRespon
|
||||
}
|
||||
credentialsVerified := false
|
||||
defer func() {
|
||||
if permit == nil {
|
||||
return
|
||||
}
|
||||
if credentialsVerified {
|
||||
permit.RecordSuccess(ctx)
|
||||
return
|
||||
@@ -127,7 +142,7 @@ func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginRespon
|
||||
return nil, response.ErrUnauthorized(40110, "invalid_credentials", "email or password is incorrect")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
||||
permit.RecordFailure(ctx)
|
||||
return nil, response.ErrUnauthorized(40110, "invalid_credentials", "email or password is incorrect")
|
||||
}
|
||||
@@ -178,6 +193,34 @@ func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginRespon
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) PasswordPublicKey() *auth.PasswordCipherPublicKey {
|
||||
if s == nil || s.cipher == nil {
|
||||
return nil
|
||||
}
|
||||
publicKey := s.cipher.PublicKey()
|
||||
return &publicKey
|
||||
}
|
||||
|
||||
func (s *AuthService) loginPassword(req LoginRequest) (string, error) {
|
||||
if strings.TrimSpace(req.EncryptedPassword) != "" {
|
||||
if s.cipher == nil {
|
||||
return "", response.ErrBadRequest(40002, "password_cipher_unavailable", "encrypted password is not supported by this server")
|
||||
}
|
||||
password, err := s.cipher.Decrypt(req.EncryptedPassword, req.PasswordKeyID)
|
||||
if err != nil {
|
||||
return "", response.ErrBadRequest(40003, "invalid_encrypted_password", "encrypted password is invalid")
|
||||
}
|
||||
if password == "" {
|
||||
return "", response.ErrBadRequest(40001, "invalid_params", "password is required")
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
if req.Password == "" {
|
||||
return "", response.ErrBadRequest(40001, "invalid_params", "password is required")
|
||||
}
|
||||
return req.Password, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) acquireLoginPermit(ctx context.Context, identifier, ip string) (*auth.LoginPermit, error) {
|
||||
if s.loginGuard == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuthServiceLoginPasswordDecryptsEncryptedPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
privatePEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
}))
|
||||
cipher, err := auth.NewPasswordCipher(privatePEM, "tenant-test-key")
|
||||
require.NoError(t, err)
|
||||
|
||||
encrypted, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &key.PublicKey, []byte("Test@123"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
svc := (&AuthService{}).WithPasswordCipher(cipher)
|
||||
got, err := svc.loginPassword(LoginRequest{
|
||||
EncryptedPassword: base64.StdEncoding.EncodeToString(encrypted),
|
||||
PasswordKeyID: "tenant-test-key",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Test@123", got)
|
||||
}
|
||||
|
||||
func TestAuthServiceLoginPasswordAllowsPlainPasswordCompatibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := (&AuthService{}).loginPassword(LoginRequest{Password: "Test@123"})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Test@123", got)
|
||||
}
|
||||
|
||||
func TestAuthServiceLoginPasswordRejectsEncryptedWhenCipherMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := (&AuthService{}).loginPassword(LoginRequest{EncryptedPassword: "abc"})
|
||||
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*response.AppError)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "password_cipher_unavailable", appErr.Message)
|
||||
}
|
||||
Reference in New Issue
Block a user