fix: check a password even when the username does not exist
This commit is contained in:
1 parent
d5e1de09db
commit
27a98e990a
4 files changed
+47
No files matched your search
@@ -117,6 +117,13 @@ func (a *Accounts) Login(ctx context.Context, current member.Member, currentToke
|
||||
func (a *Accounts) verify(ctx context.Context, username, password string) (port.Account, error) {
|
||||
account, err := a.members.AccountByUsername(ctx, username)
|
||||
if errors.Is(err, port.ErrMemberNotFound) {
|
||||
dummy, err := dummyPasswordHash()
|
||||
if err != nil {
|
||||
return port.Account{}, err
|
||||
}
|
||||
if _, err := checkPassword(ctx, dummy, password); err != nil {
|
||||
return port.Account{}, err
|
||||
}
|
||||
return port.Account{}, ErrInvalidCredentials
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -450,3 +450,21 @@ func TestLoginRejectedWhenGuestSessionOrPasswordChangesMeanwhile(t *testing.T) {
|
||||
t.Error("登录失败时游客的 token 应仍然有效")
|
||||
}
|
||||
}
|
||||
|
||||
// 名额占满、请求已经取消时,要做密码计算的请求以 context.Canceled 结束,不做的直接返回结果。
|
||||
func TestLoginDoesPasswordWorkForUnknownUsername(t *testing.T) {
|
||||
a, s, _, _, _ := newAccounts(t)
|
||||
registerAccount(t, a, s, "alice", "correct horse")
|
||||
guest := createMember(t, s)
|
||||
if _, err := dummyPasswordHash(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
occupyArgonSlots(t)
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
for _, username := range []string{"alice", "nobody"} {
|
||||
if _, _, err := a.Login(ctx, guest.Member, guest.MemberToken, clientIP, username, "wrong horse"); !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("用户名 %s:Login = %v,期望同样等待密码计算的名额", username, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
@@ -52,6 +53,12 @@ func HashPassword(ctx context.Context, password string) (string, error) {
|
||||
return passwordPrefix + base64.RawStdEncoding.EncodeToString(salt) + "$" + base64.RawStdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
// 用户名不存在时拿它做一次同样的计算,响应时间不暴露用户名是否存在。密码是随机值,不会与任何人输入的密码匹配。
|
||||
var dummyPasswordHash = sync.OnceValues(func() (string, error) {
|
||||
password, _ := newSecret()
|
||||
return HashPassword(context.Background(), password)
|
||||
})
|
||||
|
||||
// 只接受按当前参数生成的哈希:存储中的密码哈希都由 HashPassword 生成。
|
||||
func checkPassword(ctx context.Context, hash, password string) (bool, error) {
|
||||
encoded, ok := strings.CutPrefix(hash, passwordPrefix)
|
||||
|
||||
@@ -87,3 +87,18 @@ func TestPasswordWorkReturnsItsSlot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDummyPasswordHashMatchesNoPassword(t *testing.T) {
|
||||
hash, err := dummyPasswordHash()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if again, _ := dummyPasswordHash(); again != hash {
|
||||
t.Error("假哈希应只生成一次")
|
||||
}
|
||||
for _, password := range []string{"", "correct horse", "12345678"} {
|
||||
if ok, err := checkPassword(t.Context(), hash, password); err != nil || ok {
|
||||
t.Errorf("checkPassword(假哈希, %q) = %v, %v,期望能解析且不匹配", password, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user