fix(security): legacy /api/auth/login bypassed lockout and OTP enforcement
Delegate legacy login to TwoFactorService.secureLogin so the legacy path inherits login-failure lockout and OTP policy. OTP-required accounts get OTP_REQUIRED (no token, no challenge token) and must use /login/secure. Web UI already uses secureLogin; mobile scaffold (uncommitted track) must adopt the 2FA flow for staff roles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8509787016
commit
a657f9516a
@ -7,7 +7,8 @@ import com.zioinfo.kintex.auth.dto.WorkspaceDto;
|
|||||||
import com.zioinfo.kintex.auth.mapper.UserMapper;
|
import com.zioinfo.kintex.auth.mapper.UserMapper;
|
||||||
import com.zioinfo.kintex.common.error.ApiException;
|
import com.zioinfo.kintex.common.error.ApiException;
|
||||||
import com.zioinfo.kintex.common.error.ErrorCode;
|
import com.zioinfo.kintex.common.error.ErrorCode;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import com.zioinfo.kintex.security.TwoFactorService;
|
||||||
|
import com.zioinfo.kintex.security.dto.SecureLoginResponse;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@ -17,36 +18,34 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* 인증 서비스 — UserMapper(인증행·행사 역할) 배선 완료. 로그인·워크스페이스·초대 수락을 처리한다.
|
* 인증 서비스 — UserMapper(인증행·행사 역할) 배선 완료. 로그인·워크스페이스·초대 수락을 처리한다.
|
||||||
*
|
*
|
||||||
* <p>보안(계약 §0-3): 비밀번호는 BCrypt 해시와 대조({@link PasswordEncoder})하며, 해시·내부 식별자는 응답에 미노출.
|
* <p>보안(계약 §0-3): 비밀번호 대조는 위임된 secureLogin(BCrypt)이 수행하며, 해시·내부 식별자는 응답에 미노출.
|
||||||
* 실패 메시지는 이메일/비밀번호를 구분하지 않는 일반화 문구만 반환한다.
|
* 실패 메시지는 이메일/비밀번호를 구분하지 않는 일반화 문구만 반환한다.
|
||||||
*
|
*
|
||||||
* <p>2차 인증(OTP/TOTP)·로그인 실패 잠금은 common-dev(B-1) 레인이 별도로 얹으며 본 서비스는 건드리지 않는다.
|
* <p>레거시 /api/auth/login 도 {@link TwoFactorService#secureLogin}에 위임한다 — 위임하지 않으면
|
||||||
|
* 이 경로가 로그인 실패 잠금·OTP 강제를 모두 우회하는 구멍이 된다. OTP 대상 계정은 토큰 없이
|
||||||
|
* OTP_REQUIRED 로 거절되며(챌린지 토큰 미반환), 보안 로그인(/login/secure) 플로우만 허용된다.
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class AuthServiceImpl implements AuthService {
|
public class AuthServiceImpl implements AuthService {
|
||||||
|
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final TwoFactorService twoFactorService;
|
||||||
|
|
||||||
public AuthServiceImpl(JwtService jwtService, UserMapper userMapper, PasswordEncoder passwordEncoder) {
|
public AuthServiceImpl(JwtService jwtService, UserMapper userMapper, TwoFactorService twoFactorService) {
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
this.userMapper = userMapper;
|
this.userMapper = userMapper;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.twoFactorService = twoFactorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginResponse login(LoginRequest request) {
|
public LoginResponse login(LoginRequest request) {
|
||||||
Map<String, Object> row = userMapper.findAuthByEmail(request.email());
|
SecureLoginResponse secured = twoFactorService.secureLogin(request.email(), request.password());
|
||||||
String hash = row == null ? null : str(row.get("passwordHash"));
|
if (secured.login() == null) {
|
||||||
if (row == null || hash == null || !passwordEncoder.matches(request.password(), hash)) {
|
throw new ApiException(ErrorCode.OTP_REQUIRED,
|
||||||
// 이메일/비밀번호 구분 없이 일반화(계정 열거 방지).
|
"2차 인증이 필요한 계정입니다. 보안 로그인 절차를 이용해 주세요.");
|
||||||
throw new ApiException(ErrorCode.UNAUTHORIZED, "이메일 또는 비밀번호가 올바르지 않습니다.");
|
|
||||||
}
|
}
|
||||||
String userId = str(row.get("userId"));
|
return secured.login();
|
||||||
String displayName = str(row.get("displayName"));
|
|
||||||
boolean hallManager = Boolean.TRUE.equals(bool(row.get("hallManager")));
|
|
||||||
return buildLoginResponse(userId, displayName, hallManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user