When the requested email does not exist, the function returned in microseconds, while a valid email with wrong password took ~100ms (bcrypt). An attacker could enumerate valid accounts by measuring response latency. Add a dummy bcrypt.CompareHashAndPassword call on the not-found path so both branches take a comparable amount of time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
106 lines
3.4 KiB
Go
106 lines
3.4 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2025 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package usecase
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"git.happydns.org/happyDomain/internal/usecase/authuser"
|
|
"git.happydns.org/happyDomain/internal/usecase/user"
|
|
"git.happydns.org/happyDomain/model"
|
|
)
|
|
|
|
type AuthenticationStorage interface {
|
|
authuser.AuthUserStorage
|
|
user.UserStorage
|
|
}
|
|
|
|
type loginUsecase struct {
|
|
config *happydns.Options
|
|
store AuthenticationStorage
|
|
userService happydns.UserUsecase
|
|
}
|
|
|
|
func NewAuthenticationUsecase(cfg *happydns.Options, store AuthenticationStorage, userService happydns.UserUsecase) happydns.AuthenticationUsecase {
|
|
return &loginUsecase{
|
|
config: cfg,
|
|
store: store,
|
|
userService: userService,
|
|
}
|
|
}
|
|
|
|
func (lu *loginUsecase) CompleteAuthentication(uinfo happydns.UserInfo) (*happydns.User, error) {
|
|
// Check if the user already exists
|
|
user, err := lu.store.GetUser(uinfo.GetUserId())
|
|
if err != nil {
|
|
// Create the user
|
|
user, err = lu.userService.CreateUser(uinfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create user account: %w", err)
|
|
}
|
|
} else if (uinfo.GetEmail() != "" && user.Email != uinfo.GetEmail()) || time.Since(user.LastSeen) > time.Hour*12 {
|
|
if uinfo.GetEmail() != "" {
|
|
user.Email = uinfo.GetEmail()
|
|
}
|
|
user.LastSeen = time.Now()
|
|
|
|
err = lu.store.CreateOrUpdateUser(user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("has a correct JWT, user has been found, but an error occured when trying to update the user's information: %w", err)
|
|
}
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (lu *loginUsecase) AuthenticateUserWithPassword(request happydns.LoginRequest) (*happydns.User, error) {
|
|
// Retrieve the given user
|
|
user, err := lu.store.GetAuthUserByEmail(request.Email)
|
|
if err != nil {
|
|
// Perform a dummy bcrypt comparison to equalize timing with the
|
|
// valid-user path and prevent email enumeration via response time.
|
|
bcrypt.CompareHashAndPassword([]byte("$2a$12$dummy.hash.that.never.matches.any.real.password.value"), []byte(request.Password))
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
|
|
if !user.CheckPassword(request.Password) {
|
|
return nil, fmt.Errorf("invalid password")
|
|
}
|
|
|
|
// Ensure the account is enabled
|
|
if !lu.config.NoMail && user.EmailVerification == nil {
|
|
return nil, fmt.Errorf("account email not verified")
|
|
}
|
|
|
|
// Record the successful login time and transparently upgrade the hash cost if needed
|
|
now := time.Now()
|
|
user.LastLoggedIn = &now
|
|
if user.NeedsRehash() {
|
|
user.DefinePassword(request.Password)
|
|
}
|
|
lu.store.UpdateAuthUser(user)
|
|
|
|
return lu.CompleteAuthentication(user)
|
|
}
|