공공부하자개발 · 영어 학습 노트
자바
인증·로그인JWT · OAuth2 · 2FA 로 로그인 구현0/3 완료
  • 011단계: 비밀번호 해시와 JWT 로그인
  • 022단계: Access/Refresh 토큰과 OAuth2 인가 코드
  • 033단계: 2FA(TOTP) 와 로그인 흐름 통합
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 인증·로그인 › 03 / 3

3단계: 2FA(TOTP) 와 로그인 흐름 통합

섹션 7진행 0 / 3
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

6. 연습 문제

문제 1

POST /2fa/backup-codes 를 추가해 로그인된 사용자가 백업 코드를 재발급받을 수 있게 하세요. 조건: (1) 재발급하면 이전 코드는 전부 무효, (2) 재발급 요청 본문에 현재 TOTP 코드를 요구해 세션 탈취자가 백업 코드를 뽑아가지 못하게, (3) /login/2fa 성공 응답에 backup_codes_left 를 넣어 2개 이하면 클라이언트가 경고할 수 있게.

정답 보기
java
private void backupCodes(HttpExchange ex) throws IOException {
    UserStore.User u = requireUser(ex);
    if (!u.mfaEnabled()) { Http.json(ex, 400, Map.of("error", "mfa_not_enabled")); return; }
    long step = Totp.verify(u.totpSecret, String.valueOf(Http.body(ex).get("code")), u.lastUsedStep);   // (2) 재인증
    if (step < 0) { fail(ex, u); return; }                                                                // 실패 카운터 공유
    u.lastUsedStep = step;
    Http.json(ex, 200, Map.of("backup_codes", users.newBackupCodes(u)));                                  // (1) newBackupCodes 가 clear() 로 이전 것 무효화
}

// /login/2fa 성공 시 issue() 대신
Http.json(ex, 200, Map.of("access_token", …, "amr", amr, "backup_codes_left", users.backupCodesLeft(u)));   // (3)

재발급에 TOTP 코드를 요구하는 이유: Access 토큰만 탈취한 공격자가 백업 코드를 발급받아 두면, 나중에 비밀번호만 알아내도 2FA 를 우회할 수 있습니다. "2FA 설정을 바꾸는 작업은 2FA 로 재인증" 이 원칙입니다.

문제 2

변형 2 의 "이 기기 기억" 을 구현하되, 다음 조건을 지키세요. (1) 기기 토큰은 사용자당 최대 5개, 오래된 것부터 밀어냄, (2) 비밀번호가 바뀌면 전부 무효, (3) 기억된 기기로 로그인해도 amr 에는 otp 대신 remembered 를 넣어 step-up 규칙(변형 3) 이 구분할 수 있게.

정답 보기
java
// UserStore.User
final LinkedHashMap<String, Long> trustedDevices = new LinkedHashMap<>();   // hash → expiresAt, 삽입 순서 유지

void trustDevice(String hash, long expiresAt) {
    trustedDevices.put(hash, expiresAt);
    while (trustedDevices.size() > 5) trustedDevices.remove(trustedDevices.keySet().iterator().next());   // (1) 가장 오래된 것 제거
}
void changePassword(String newHash) { passwordHash = newHash; trustedDevices.clear(); }                     // (2)

// AuthServer.login, 비밀번호 통과 후
String d = Http.cookies(ex).get("mfa_device");
if (u.mfaEnabled() && d != null && u.trustedDevices.getOrDefault(UserStore.sha256(d), 0L) > TokenService.now()) {
    issue(ex, u, List.of("pwd", "remembered")); return;                                                   // (3)
}

변형 3 의 step-up 검사는 amr.contains("otp") 이므로 remembered 세션은 정산 승인 때 코드를 다시 넣게 됩니다. "편의는 로그인까지, 위험한 순간은 진짜 요소로" 가 이 설계의 요지입니다. passwordHash 를 final 에서 풀어야 하는 것도 잊지 마세요.

문제 3

하드웨어 토큰(버튼을 누를 때마다 카운터가 1 오르는 HOTP 기기) 을 지원하려면 시간 대신 서버가 기억하는 카운터로 검증해야 합니다. 사용자가 버튼을 몇 번 헛눌러 기기 카운터가 서버보다 앞서 있을 수 있습니다. verifyHotp(secret, code, serverCounter) 를 구현하세요. 조건은 셋입니다.

  1. 서버 카운터부터 앞으로 최대 10개(look-ahead)를 검사한다.
  2. 일치하면 일치한 카운터 + 1 을 돌려줘 서버가 저장하게 한다.
  3. 뒤로는 절대 검사하지 않는다.
정답 보기
java
public static long verifyHotp(byte[] secret, String code, long serverCounter) {
    for (long c = serverCounter; c < serverCounter + 10; c++) {                       // (1) 앞으로만, (3) 뒤는 재생이므로 금지
        if (MessageDigest.isEqual(generate(secret, c).getBytes(US_ASCII), code.getBytes(US_ASCII))) return c + 1;   // (2)
    }
    return -1;
}
// 호출: long next = verifyHotp(u.hotpSecret, code, u.hotpCounter); if (next > 0) u.hotpCounter = next;

generate 는 TOTP 와 완전히 같은 함수입니다. TOTP 가 HOTP 의 특수한 경우라는 것이 코드로 드러납니다. look-ahead 를 10 으로 두면 유효 코드가 10개라 실패 횟수 제한이 더 중요해지고, 기기가 10번 넘게 헛눌렸으면 관리자가 재동기화(연속 코드 2개 입력) 해야 합니다. RFC 4226 7.4 가 이 절차입니다.

연습 문제
  • 문제 1
  • 문제 2
  • 문제 3
이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리