홈 › 인증·로그인 › 03 / 3

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

섹션 7진행 0 / 3

4. 응용 변형 예제

변형 1: SHA-256, 8자리, 60초 — 파라미터를 URI 에 싣는다

알고리즘·자릿수·주기는 모두 URI 파라미터로 앱에 전달됩니다. 서버와 앱이 같은 값을 쓰기만 하면 됩니다.

java
Mac mac = Mac.getInstance("HmacSHA256");                     // RFC 6238 은 SHA-256/512 도 허용
// otpauth://totp/…?secret=…&algorithm=SHA256&digits=8&period=60

단, Google Authenticator 는 오랫동안 SHA1·6자리·30초 외 값을 무시했고 지금도 앱마다 지원이 다릅니다. 기본값을 바꾸면 사용자가 다른 코드를 보게 되는 문의가 옵니다. 바꿀 이유가 없으면 기본값을 유지하세요. SHA1 이 충돌 공격에 약한 것과 HMAC-SHA1 의 안전성은 별개입니다.

변형 2: 신뢰하는 기기 기억 (30일간 코드 생략)

매번 코드를 넣게 하면 사용자는 2FA 를 끕니다. "이 기기 기억" 체크는 2FA 통과 시 서버가 난수 토큰을 발급해 HttpOnly 쿠키로 주고 해시를 사용자별로 저장하는 것입니다.

java
// /login/2fa 성공 시 remember=true 면
String device = tokens.randomToken();
u.trustedDevices.put(sha256(device), now + 30 * 86400);      // 해시 + 만료
ex.getResponseHeaders().add("Set-Cookie", Http.refreshCookie("mfa_device", device, "/login", 30 * 86400));

// /login 에서 비밀번호 통과 후
String d = Http.cookies(ex).get("mfa_device");
if (d != null && u.trustedDevices.getOrDefault(sha256(d), 0L) > now) { issue(ex, u, List.of("pwd", "otp")); return; }   // amr 에 otp 유지

쿠키 Path 를 /login 으로 좁히면 다른 요청에는 실리지 않습니다. 비밀번호 변경·2FA 재등록 시 trustedDevices 를 비우는 것이 짝입니다.

변형 3: 민감 작업 재인증 (step-up)

로그인은 비밀번호만으로 하고, 정산 승인·비밀번호 변경 같은 순간에만 코드를 요구하는 방식입니다. Access 의 amr 과 발급 시각을 리소스 서버가 검사합니다.

java
Map<String, Object> c = tokens.verifyAccess(bearer, AUDIENCE);
List<?> amr = (List<?>) c.get("amr");
long iat = ((Number) c.get("iat")).longValue();
if (!amr.contains("otp") || now() - iat > 300)                // 5분 안에 2FA 로 발급된 토큰만
    { Http.json(ex, 403, Map.of("error", "step_up_required")); return; }

클라이언트는 403 을 받으면 코드 입력창을 띄우고 /login/2fa 를 다시 거쳐 amr 에 otp 가 있는 새 Access 를 받습니다. 은행 앱이 이체 직전에만 추가 인증을 요구하는 구조입니다.

변형 4: DB 스키마와 시크릿 암호화

sql
ALTER TABLE users ADD (
  totp_secret_enc     RAW(64),       -- AES-GCM 으로 암호화한 시크릿. 키는 KMS/HSM 또는 별도 설정
  totp_pending_enc    RAW(64),
  totp_last_step      NUMBER(12),
  mfa_failures        NUMBER(2) DEFAULT 0,
  mfa_locked_until    TIMESTAMP
);
CREATE TABLE backup_codes (
  user_id    NUMBER       NOT NULL REFERENCES users(id),
  code_hash  VARCHAR2(64) NOT NULL,          -- SHA-256 hex
  used_at    TIMESTAMP,
  CONSTRAINT pk_backup PRIMARY KEY (user_id, code_hash)
);

TOTP 시크릿은 비밀번호 해시와 달리 복호화가 필요한 값입니다(서버도 코드를 계산해야 하므로). 따라서 해시가 아니라 암호화이고, DB 덤프가 유출됐을 때 시크릿이 그대로 읽히면 비밀번호 없이도 2FA 가 뚫립니다. 암호화 키를 DB 와 다른 곳에 두는 것이 이 컬럼의 존재 이유입니다. used_at 은 지우는 대신 남겨 "언제 백업 코드로 들어왔는지" 감사 기록으로 씁니다.