fix: harden login for MLPS requirements

This commit is contained in:
2026-05-14 11:05:20 +08:00
parent ecf3ee1ef0
commit 879677516f
39 changed files with 1230 additions and 71 deletions
+49 -6
View File
@@ -117,6 +117,7 @@ type AuthService struct {
audits *AuditService
issuer *TokenIssuer
guard *sharedauth.LoginGuard
cipher *sharedauth.PasswordCipher
}
func NewAuthService(
@@ -132,12 +133,27 @@ func NewAuthService(
return &AuthService{accounts: accounts, audits: audits, issuer: issuer, guard: guard}
}
func (s *AuthService) WithPasswordCipher(cipher *sharedauth.PasswordCipher) *AuthService {
s.cipher = cipher
return s
}
func (s *AuthService) PasswordPublicKey() *sharedauth.PasswordCipherPublicKey {
if s == nil || s.cipher == nil {
return nil
}
publicKey := s.cipher.PublicKey()
return &publicKey
}
type LoginInput struct {
Username string
Password string
IP string
UserAgent string
RequestID string
Username string
Password string
EncryptedPassword string
PasswordKeyID string
IP string
UserAgent string
RequestID string
}
type LoginResult struct {
@@ -176,6 +192,10 @@ func (s *AuthService) Login(ctx context.Context, in LoginInput) (*LoginResult, e
if in.Username == "" {
return nil, response.ErrBadRequest(40000, "invalid_payload", "username is required")
}
password, err := s.loginPassword(in)
if err != nil {
return nil, err
}
permit, err := s.acquireLoginPermit(ctx, in)
if err != nil {
@@ -183,6 +203,9 @@ func (s *AuthService) Login(ctx context.Context, in LoginInput) (*LoginResult, e
}
credentialsVerified := false
defer func() {
if permit == nil {
return
}
if credentialsVerified {
permit.RecordSuccess(ctx)
return
@@ -203,7 +226,7 @@ func (s *AuthService) Login(ctx context.Context, in LoginInput) (*LoginResult, e
return nil, response.ErrForbidden(40310, "account_disabled", "账号已停用")
}
if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(in.Password)); err != nil {
if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(password)); err != nil {
permit.RecordFailure(ctx)
s.recordLoginFailure(ctx, in, "wrong_password")
return nil, response.ErrUnauthorized(40110, "invalid_credentials", "账号或密码错误")
@@ -237,6 +260,26 @@ func (s *AuthService) Login(ctx context.Context, in LoginInput) (*LoginResult, e
}, nil
}
func (s *AuthService) loginPassword(in LoginInput) (string, error) {
if strings.TrimSpace(in.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(in.EncryptedPassword, in.PasswordKeyID)
if err != nil {
return "", response.ErrBadRequest(40003, "invalid_encrypted_password", "encrypted password is invalid")
}
if password == "" {
return "", response.ErrBadRequest(40000, "invalid_payload", "password is required")
}
return password, nil
}
if in.Password == "" {
return "", response.ErrBadRequest(40000, "invalid_payload", "password is required")
}
return in.Password, nil
}
func (s *AuthService) acquireLoginPermit(ctx context.Context, in LoginInput) (*sharedauth.LoginPermit, error) {
if s.guard == nil {
return nil, nil
+60
View File
@@ -0,0 +1,60 @@
package app
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"testing"
sharedauth "github.com/geo-platform/tenant-api/internal/shared/auth"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/stretchr/testify/require"
)
func TestOpsAuthLoginPasswordDecryptsEncryptedPassword(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 := sharedauth.NewPasswordCipher(privatePEM, "ops-test-key")
require.NoError(t, err)
encrypted, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &key.PublicKey, []byte("Ops@1234"), nil)
require.NoError(t, err)
svc := (&AuthService{}).WithPasswordCipher(cipher)
got, err := svc.loginPassword(LoginInput{
EncryptedPassword: base64.StdEncoding.EncodeToString(encrypted),
PasswordKeyID: "ops-test-key",
})
require.NoError(t, err)
require.Equal(t, "Ops@1234", got)
}
func TestOpsAuthLoginPasswordAllowsPlainPasswordCompatibility(t *testing.T) {
t.Parallel()
got, err := (&AuthService{}).loginPassword(LoginInput{Password: "Ops@1234"})
require.NoError(t, err)
require.Equal(t, "Ops@1234", got)
}
func TestOpsAuthLoginPasswordRejectsEncryptedWhenCipherMissing(t *testing.T) {
t.Parallel()
_, err := (&AuthService{}).loginPassword(LoginInput{EncryptedPassword: "abc"})
require.Error(t, err)
appErr, ok := err.(*response.AppError)
require.True(t, ok)
require.Equal(t, "password_cipher_unavailable", appErr.Message)
}