포스트

TOTP 기반 2FA

TOTP 기반 2FA

2FA는 로그인 화면에 6자리 입력란을 하나 더 추가하는 작업처럼 보인다. 실제로 적용해 보니 TOTP 계산보다 더 오래 고민한 부분은 1차 인증과 최종 세션 사이의 상태를 어떻게 표현할 것인가였다.

기존 외부 Identity Provider 로그인과 OAuth·PKCE 흐름은 1차 인증이 끝나면 바로 Access Token과 Refresh Token을 발급했다. 2FA를 넣으려면 이 경계를 나눠야 했고, 등록 여부와 현재 세션의 인증 수준도 별도로 다루어야 했다.

이 글에서는 TOTP의 원리부터 등록, 로그인 Challenge, 세션 증명, Step-up 인증과 롤아웃 과정까지 정리한다.


I. 2FA와 TOTP

1. 인증 요소를 하나 더 확인한다

인증 요소는 일반적으로 다음과 같이 나눈다.

요소의미예시
지식사용자가 알고 있는 것Password, PIN
소유사용자가 가지고 있는 것Authenticator, Security Key
속성사용자 자신의 특성지문, 얼굴

2FA(Two-Factor Authentication)는 서로 다른 요소 두 개를 확인한다. TOTP Authenticator는 일반적으로 단말이 공유 Secret을 가지고 있음을 보이는 소유 요소로 다룬다.

OAuth 로그인에 TOTP 단계를 추가했다고 항상 엄밀한 의미의 2FA가 되는 것은 아니다. 외부 Identity Provider가 어떤 방법으로 사용자를 인증했는지, TOTP를 표시하는 기기가 실제로 독립된 요소인지까지 보증 수준에 따라 판단해야 한다. 이 글에서는 통상적인 제품 용어에 따라 이 추가 TOTP 인증을 2FA라고 부른다.

2. TOTP는 시간을 Counter로 쓴다

TOTP(Time-Based One-Time Password)는 RFC 6238에 정의된 알고리즘이다. HOTP의 Counter 대신 현재 시각을 일정한 구간으로 나눈 Time Step을 사용한다.

1
2
3
4
5
6
7
T = floor((Current Unix Time - T0) / X)
TOTP = HOTP(K, T)

K  : 서버와 Authenticator가 공유하는 Secret
T  : 현재 Time Step
X  : Time Step의 길이
T0 : 기준 시각

RFC의 기본값인 30초 구간을 사용하면 각 구간 안에서 서버와 Authenticator는 같은 T를 계산한다. 서버와 인증 앱 양쪽이 같은 Secret을 가지고 있다면 코드 생성 단계에서 실시간 통신 없이도 같은 일회용 코드를 만들 수 있다.

flowchart LR
    K[공유 Secret] --> H[HMAC 계산]
    T[현재 Time Step] --> H
    H --> D[Dynamic Truncation]
    D --> O[고정 길이 OTP]

HMAC 결과를 Dynamic Truncation으로 줄인 뒤 사용자가 입력하기 쉬운 자릿수로 변환한다. 기본 HOTP는 HMAC-SHA-1을 사용하며, RFC 6238은 HMAC-SHA-256과 HMAC-SHA-512도 허용한다. 서버와 Authenticator가 같은 매개변수를 사용해야 하므로 알고리즘 선택에서는 호환성도 확인해야 한다. 30초는 RFC 6238의 기본 Time Step이고, 6자리는 인증 앱에서 널리 사용하는 호환 설정이다. 실제 정책은 사용자 경험, 호환성과 보안성을 함께 고려해 선택해야 한다.


II. 1차 인증과 로그인 완료를 분리한다

1. 기존 흐름의 경계를 바꾼다

기존 로그인에서는 외부 Identity Provider의 인증과 계정 확인이 끝나면 바로 정식 세션을 발급했다.

1
2
3
4
5
6
7
8
9
Before
외부 인증 완료 → 계정 확인 → Access/Refresh Session 발급

After
외부 인증 완료 → 계정 확인 → 2FA Challenge 발급
                                     ↓
                                 TOTP 검증
                                     ↓
                              정식 Session 발급

처음에는 1차 인증 후 정상 세션을 먼저 발급하고 mfa_pending과 같은 상태를 넣는 방법도 검토했다. 그러나 이 방법은 OTP 검증 전에 이미 장기 Refresh Session이 생기고, 대기 상태에서 접근할 수 있는 API 목록을 계속 관리해야 한다.

방식장점부담
세션 먼저 발급기존 로그인 구조를 덜 바꿀 수 있음대기 Session과 API Allowlist 관리 필요
Challenge 먼저 발급2FA 전에 정식 Session이 없음로그인 응답과 Client 흐름을 변경해야 함

이번에는 TOTP를 이미 등록한 사용자의 새 로그인에서는 2FA 통과 전에 정식 Session을 만들지 않는 Challenge 방식을 선택했다.

2. Challenge는 Session이 아니다

Challenge는 “1차 인증을 통과한 사용자가 지금 2차 인증을 진행 중이다”라는 사실만 표현한다. 일반 API를 호출할 수 있는 Access Token이 아니고, TOTP 검증을 완료하는 용도로만 쓰인다.

sequenceDiagram
    actor User
    participant Client
    participant IdP as Identity Provider
    participant Auth as Auth Server
    participant Store as Challenge Store

    User->>Client: 로그인 시작
    Client->>IdP: 외부 로그인
    IdP-->>Client: Authorization 결과
    Client->>Auth: Callback과 Authorization Code
    Auth->>Auth: 계정과 2FA 등록 상태 확인
    alt 2FA 미등록
        Auth-->>Client: 기본 Session 발급
    else 2FA 등록
        Auth->>Store: 짧은 Challenge 저장
        Auth-->>Client: 2FA Challenge
        User->>Client: TOTP 입력
        Client->>Auth: Challenge + TOTP
        Auth->>Store: 유효한 Challenge 조회
        Auth->>Auth: TOTP 검증
        Auth->>Store: Challenge 원자적 소비
        Auth-->>Client: 2FA 증명이 포함된 Session 발급
    end

2FA 미등록 분기에서 기본 Session을 받는다고 모든 작업이 허용되는 것은 아니다. 2FA가 필수인 민감한 작업은 뒤의 권한 Guard에서 다시 차단하고 등록을 요구한다.

이 흐름에서 Challenge는 다음 속성을 갖도록 했다.

  • 추측하기 어려운 난수로 생성한다.
  • 서버에는 원문 대신 Hash를 저장한다.
  • 수명을 짧게 제한한다.
  • 발급한 Client 유형에 바인딩한다.
  • 실패 횟수를 제한한다.
  • 성공하면 한 번만 소비한다.

동시에 두 요청이 같은 Challenge를 사용해도 하나만 Session을 받도록 실패 횟수 갱신과 Challenge 소비를 각각 원자적으로 처리해야 한다.


III. 등록은 설정과 활성화로 나눈다

1. QR을 보여 줬다고 등록이 끝난 것은 아니다

서버가 Secret과 Provisioning URI를 만들어 QR로 보여 주는 것만으로는 Authenticator에 올바르게 등록되었는지 알 수 없다. 최초 TOTP를 한 번 검증한 후에만 등록을 확정해야 한다.

sequenceDiagram
    actor User
    participant Client
    participant Auth as Auth Server
    participant Store as Credential Store
    participant App as Authenticator

    User->>Client: 2FA 등록 시작
    Client->>Auth: 설정 요청
    Auth->>Auth: 공유 Secret 생성
    Auth->>Store: Secret 암호화 저장, 설정 중
    Auth-->>Client: Provisioning URI
    Client-->>User: QR 표시
    User->>App: QR 스캔
    App-->>User: TOTP 표시
    User->>Client: 최초 TOTP 입력
    Client->>Auth: 활성화 요청
    Auth->>Auth: 현재 Session과 TOTP 검증
    Auth->>Store: 등록 완료로 전환
    Auth-->>Client: 복구 코드와 승격된 Session

등록 상태를 나누면 사용자가 QR 화면을 닫았거나 잘못된 Secret을 스캔했을 때 로그인이 영구적으로 막히는 문제를 피할 수 있다.

flowchart LR
    U[미등록] -->|설정 시작| P[설정 중]
    P -->|최초 TOTP 성공| E[등록 완료]
    P -->|재설정| P2[새 Secret으로 설정 중]
    P2 -->|최초 TOTP 성공| E
    E -->|정책이 허용하면 재인증 후 해제| U

2. Secret은 Hash만으로 보관할 수 없다

Password는 Salt와 비용 계수를 적용한 Password Hashing 또는 KDF 결과만 저장하고 입력값과 비교할 수 있다. 반면 TOTP 서버는 사용자가 보낸 코드가 유효한지 계산하기 위해 원본 Secret을 다시 사용해야 한다. 따라서 TOTP Secret은 Password처럼 단방향 Hash로만 보관할 수 없다.

여기서 TOTP Secret과 이를 DB에서 보호하는 암호화 Key를 구분해야 한다.

값역할보관 방식
TOTP Secret S인증 앱과 서버가 공유하는 장기 비밀값AEAD 암호화
암호화 Key EDB에 저장한 S를 암·복호화하는 서버 전용 KeyDB 밖의 Secret Store
OTPS와 현재 시간으로 만드는 일회용 코드저장하지 않음
복구 코드인증 앱을 잃었을 때 사용하는 별도의 일회성 값Hash

인증 앱은 S를 직접 가지고 있다. 서버는 DB의 암호문을 E로 복호화해 S를 얻은 뒤 같은 OTP를 계산한다.

1
2
3
Authenticator: OTP      = TOTP(S, 현재 시간)
Server:        S        = AEAD_DECRYPT(E, ciphertext, Context)
               expected = TOTP(S, 현재 시간)

DB에 Hash(S)만 남기면 서버는 S를 복구할 수 없다. TOTP(Hash(S), 현재 시간)을 계산해도 인증 앱이 만든 TOTP(S, 현재 시간)과는 다른 값이 된다.

구현에서는 다음과 같이 관리했다.

  • TOTP Secret은 AEAD 방식으로 암호화하고 DB에는 암호문, Nonce와 Key ID만 저장한다.
  • 암호화 Key는 TOTP Secret과 같은 DB에 두지 않는다.
  • 사용 목적과 사용자 식별자를 AEAD의 AAD(Additional Authenticated Data)에 바인딩한다.
  • TOTP를 계산하는 등 Secret이 필요한 요청에서는 애플리케이션 메모리로 복호화한다.
  • 복구 코드는 등록 완료 시 원문을 한 번만 보여 주고 Hash만 저장한다.
  • 사용한 복구 코드는 즉시 폐기한다.
  • 복구 코드는 인증 앱을 잃은 로그인에만 허용하고, 등록·Step-up·해제에는 TOTP를 요구한다.
  • Provisioning URI와 QR은 Server Log와 Analytics Event에 남기지 않고, 사용자에게도 화면 캡처·공유에 주의하도록 안내한다.

복구 코드가 충분한 Entropy를 가진 난수라면 일반 암호학적 Hash로도 비교할 수 있다. 사용자가 직접 입력하기 위해 짧게 만든 코드라면 Salt와 느린 Hash, 강한 시도 제한까지 함께 고려해야 한다.

AAD는 암호문을 다른 사용자나 용도로 옮기는 변조를 막지만, 암호화 Key 노출을 대신 방어하지는 않는다. 암호문과 암호화 Key가 함께 노출되면 서버와 동일한 OTP를 만들 수 있으므로, Key를 DB와 분리하고 접근·회전 정책을 관리해야 한다.


IV. 등록 상태와 세션의 인증 수준을 분리한다

1. “등록됨”과 “인증됨”은 다르다

2FA를 구현하면 비슷해 보이는 세 가지 상태가 생긴다.

상태질문귀속 대상
enrolled이 사용자가 TOTP를 등록했는가?사용자 계정
verified이 세션이 TOTP를 통과했는가?각 Session
required이 작업에 TOTP가 필수인가?권한 정책

이 세 가지를 하나의 Boolean으로 뭉치면 예외 흐름을 설명하기 어려워진다. 예를 들어 TOTP를 등록한 사용자라도 새로운 기기의 Session은 아직 verified=false일 수 있다. 반대로 2FA가 선택인 일반 작업과 필수인 민감한 작업도 구분해야 한다.

특히 verified를 사용자 계정에 기록하면 안 된다. 한 기기에서 TOTP를 통과했다는 이유로 다른 기기에서 이미 열려 있던 Session까지 승격되어서는 안 된다.

1
2
3
4
5
6
7
8
사용자 계정
└─ TOTP 등록 완료

Session A
└─ TOTP 통과

Session B
└─ TOTP 미통과

2. Session에 인증 방법을 남긴다

RFC 8176은 amr(Authentication Methods References) Claim에서 사용할 인증 방법 참조값의 Registry와 초기 값을 정의한다. OTP를 통과한 Session에는 otp라는 인증 방법을 담을 수 있다. amr은 어떤 방법으로 인증했는지를 나타낼 뿐, 그 자체가 보증 수준이나 권한 정책을 의미하는 것은 아니다. 권한 Guard가 작업별 정책과 amr을 함께 보고 요구 조건이 충족됐는지 판단한다.

복구 코드는 NIST 분류상 OTP가 아니라 Look-up Secret이다. 복구 로그인을 지원한다면 이를 otp로 표현하지 말고 실제 사용한 인증 방법과 내부 정책 충족 여부를 구분하는 편이 낫다. 이 글의 amr=otp 흐름은 TOTP를 직접 검증한 경우를 기준으로 한다.

1
2
3
4
5
6
7
8
9
TOTP 검증 성공
    ↓
Refresh Session에 검증 시각 기록
    ↓
Access Token의 amr에 otp 포함
    ↓
Token Refresh
    ↓
새 Session에도 검증 상태 승계

Access Token만 보고 2FA 정책 충족 여부를 판단하더라도, Refresh 후의 새 Token이 같은 상태를 유지하려면 서버의 Refresh Session도 해당 증명을 알고 있어야 한다. 로그아웃하면 추가 Token 발급에 필요한 Refresh Session과 그 안의 2FA 인증 기록은 폐기된다. 다만 이미 발급된 Stateless Access Token의 amr Claim은 별도의 폐기 장치가 없다면 Token 만료 시점까지 남아 있다.

또한 2FA가 필수인 작업에서는 정책 도입 전 Session이나 amr 값이 없는 Token을 미통과로 보는 Fail-closed가 자연스럽다.

3. Gateway 뒤에서도 위조를 막아야 한다

Access Token의 amr을 여러 서비스가 사용한다면, 외부 Client가 같은 이름의 Header를 직접 만들어 낼 수 없어야 한다. Gateway가 JWT 서명을 검증한 후에만 내부 인증 Context를 재구성하고, 외부에서 들어온 동일한 Header는 먼저 제거해야 한다.

flowchart LR
    C[Untrusted Client] --> G[API Gateway]
    G --> V[JWT 서명 검증과 amr 추출]
    G --> R[외부 인증 Header 제거]
    V --> T[검증된 내부 Auth Context]
    R --> T
    T --> P[권한 정책에서 otp 확인]
    P --> A[보호된 Application API]

토큰이 올바르더라도 해당 작업이 2FA를 필수로 요구한다면 권한 Guard가 otp 여부를 최종적으로 확인한다.


V. Step-up은 기존 Session을 승격한다

1. 로그인 Challenge만으로는 부족하다

로그인할 때 Challenge를 요구하면 새 Session은 통제할 수 있다. 그러나 모든 Session이 로그인 Challenge를 거치는 것은 아니다.

  • 2FA 정책을 배포하기 전부터 존재하던 Session
  • 로그인 후 민감한 권한을 새로 얻은 Session

이런 경우마다 로그아웃을 강요하기보다 현재 Session을 2FA가 확인된 Session으로 승격하는 Step-up 흐름이 필요하다. 등록을 처음 활성화하는 경로는 활성화 과정 안에서 기존 Refresh Session을 교체하므로 별도 Step-up이 필요하지 않다.

sequenceDiagram
    actor User
    participant Client
    participant Auth as Auth Server
    participant Guard as Policy Guard

    Client->>Guard: 보호된 작업 요청
    Guard-->>Client: 2FA 증명 필요
    Client-->>User: TOTP 요청
    User->>Client: TOTP 입력
    Client->>Auth: 현재 Session + TOTP
    Auth->>Auth: Session 소유자와 TOTP 검증
    Auth->>Auth: 기존 Refresh Session 폐기
    Auth-->>Client: 2FA 증명이 포함된 새 Session
    Client->>Guard: 작업 1회 재시도
    Guard-->>Client: 작업 완료

Session을 그대로 둔 채 Access Token만 교체하는 것보다 기존 Refresh Session을 폐기하고 새 Session을 만드는 편이 명확하다. 이렇게 하면 이전 Refresh Token으로 2FA 전의 세션을 다시 만들 수 없다.

2. 로그인과 권한 경계에서 두 번 확인한다

로그인 Challenge와 API 권한 Guard는 서로 대체하는 기능이 아니다.

1
2
3
4
5
등록된 사용자의 새 로그인
→ Challenge 검증을 통과한 뒤에만 정식 Session을 발급한다.

기존 Session 또는 권한이 변경된 Session
→ 민감한 API의 Guard가 인증 수준을 다시 확인한다.

Client가 미리 상태를 조회하고 등록이나 Step-up 화면으로 이동하면 UX는 좋아진다. 그러나 Client Routing은 보안 경계가 아니며, 최종 강제는 항상 서버의 권한 Guard가 담당해야 한다.


VI. 6자리 코드 밖의 보안 장치

1. 시계 오차와 Replay를 함께 다룬다

단말과 서버의 시계가 정확히 같지 않을 수 있으므로 인접한 Time Step을 허용하는 유예 구간을 둘 수 있다. 하지만 허용 구간을 넓힐수록 공격자가 시도할 수 있는 유효 코드도 늘어난다.

더 중요한 문제는 같은 Time Step의 코드를 여러 번 제출하는 Replay다. NIST SP 800-63B도 유효 기간 안의 OTP라도 한 번만 받아들여 Replay에 대응할 것을 요구한다.

이를 위해 마지막으로 성공한 Time Step을 저장하고, 그보다 같거나 이전인 코드는 거부했다. 동시에 같은 코드를 검증해도 하나만 성공하도록 검증과 Time Step 갱신을 직렬화했다.

2. 실패 횟수는 여러 경로에서 제한한다

6자리 OTP는 가능한 값이 적기 때문에 온라인 추측 공격을 방치해서는 안 된다. Challenge 하나의 실패 횟수를 제한하는 것에서 끝내면 새 Challenge를 반복 발급해 제한을 우회할 수 있다. 따라서 Challenge 단위 제한과 재발급까지 합산하는 사용자 단위 제한을 함께 두고, 등록 활성화·Step-up·해제 경로도 같은 정책 안에 포함해야 한다.

위험대응
OTP Brute ForceChallenge와 사용자 단위 시도 제한
OTP Replay마지막 성공 Time Step 저장
Challenge 동시 사용원자적 소비
Challenge 저장소 유출난수 Token의 Hash만 저장
Challenge 노출 시간짧은 TTL
Challenge 재사용성공 시 원자적 소비
채널 간 Challenge 오용Web·Native와 같은 Client 유형 바인딩
Secret 유출Context와 결합한 AEAD 암호화
복구 코드 유출Hash 저장, 1회 사용
인증 Context 위조Gateway에서 Token 검증 후 재구성
민감한 값의 로그 노출OTP, Secret, QR, 복구 코드를 로그·분석 수집에서 제외

3. Web과 Native는 Challenge 전달 방식이 다르다

Browser에서 Challenge Token을 JavaScript가 읽을 필요가 없다면 HttpOnly Cookie에 담고 CSRF 방어를 함께 적용할 수 있다. Native App은 OAuth Code 교환 응답으로 Challenge Token을 받되 영구 저장하지 않고 인증 흐름 동안 메모리에만 유지할 수 있다.

또한 2FA Challenge 상태의 인증 실패를 일반 Session 만료와 동일하게 처리하면 안 된다. Challenge 단계에는 Refresh Session이 없으므로, 공통 인터셉터가 자동 Refresh를 반복하지 않도록 인증 단계를 구분해야 한다.


VII. 구현 순서와 검증

1. 먼저 모든 Session 발급 경로를 찾는다

2FA는 특정 Endpoint 하나를 추가하는 기능이 아니다. 웹 OAuth Callback, Native Code 교환, Refresh, 등록 활성화와 Step-up처럼 Session이 생성되거나 교체되는 모든 경로를 먼저 파악해야 한다.

실제 작업은 다음 순서로 진행했다.

  1. 2FA가 필수인 작업과 선택인 사용자 범위를 정의했다.
  2. 모든 로그인·Session 발급·Refresh 경로를 추적했다.
  3. 영속적인 Credential과 단기 Challenge를 별도 상태로 모델링했다.
  4. 등록, 활성화, Challenge 검증, Step-up과 해제 Use Case를 구현했다.
  5. Session에 인증 방법을 기록하고 Gateway와 권한 Guard까지 전파했다.
  6. Web과 Native Client의 응답, Cookie, CSRF, 오류 계약을 정리했다.
  7. 동시성과 기존 Session을 포함한 예외 흐름을 검증했다.

요청을 받는 계층과 외부 기술을 다루는 계층 사이에서 인증 Use Case가 전체 흐름을 소유하도록 책임도 나누었다.

경계주요 책임
PresentationWeb·Native 입출력, Cookie, CSRF
Application등록·검증·Step-up·Session 교체 순서
DomainCredential, Challenge, Session 상태와 규칙
Infrastructure영속 저장소, TTL 저장소, 암호화, 시도 제한
GatewayToken 검증, 신뢰할 수 있는 인증 Context 전파

2. 정상 흐름보다 재사용과 동시성을 더 많이 테스트한다

단순히 “올바른 OTP가 통과한다”는 테스트만으로는 세션 경계의 문제를 찾기 어렵다. 다음 범위를 함께 검증했다.

범위확인한 내용
TOTP정상 코드, 시간 경계, 시계 오차, Replay
Challenge만료, Client 불일치, 반복 실패, 중복 소비
Session등록·Step-up 후 교체, Refresh 승계, 기존 Session의 Fail-closed
Recovery복구 코드의 1회성과 허용 경로 제한
Gateway서명 검증, 외부 Auth Context 위조 제거
Client 계약Web·Native 응답, Cookie, CSRF, 오류 분기
Migration기존 데이터와 이전 Session의 처리

특히 Challenge 소비와 Replay 방지는 단일 요청 테스트로 충분하지 않다. 같은 토큰과 OTP를 사용한 여러 요청을 동시에 보내어 하나만 성공하는지 확인해야 한다.


VIII. 배포는 기능보다 흐름의 문제다

1. Server와 Client의 배포 순서를 함께 본다

Server와 Client를 별도 작업으로 나누더라도 배포 순서는 하나의 인증 흐름으로 설계해야 한다. Server 계약을 먼저 안정화할 수는 있지만, Client가 준비되기 전에 정책을 강제하면 기존 사용자가 다음 단계로 이동하지 못한다.

따라서 안전한 롤아웃은 다음 의존성을 따라야 한다.

flowchart LR
    S[Schema와 Server 흐름] --> C[Client 등록·Challenge·Step-up]
    C --> O[등록과 복구 운영 절차]
    O --> E[민감한 권한에 2FA 강제]
    E --> M[모니터링과 Audit]

특히 기존 Session에는 2FA 증명이 없다. 서버가 이를 Fail-closed로 처리하는 순간, Client에는 반드시 등록 또는 Step-up 화면으로 이동할 길이 있어야 한다.

2. TOTP는 완성형이 아니다

TOTP는 최초 Secret 등록 후에는 SMS나 Email처럼 로그인할 때마다 코드를 전달하는 채널에 의존하지 않고, 널리 사용되는 Authenticator와 호환된다는 장점이 있다. 하지만 사용자가 피싱 사이트에 코드를 직접 입력하면 공격자가 유효 시간 안에 그 코드를 재전송할 수 있다. NIST도 OTP 인증을 Phishing-resistant로 분류하지 않는다.

따라서 TOTP를 도입한 후에도 다음 항목은 별도로 검토해야 한다.

  • 분실한 기기와 Authenticator를 해제하는 복구 절차
  • 복구 코드 재발급과 사용 이력
  • 2FA 등록·해제·실패에 대한 Audit Log와 경보
  • 권한 변경처럼 더 민감한 작업에 대한 최근 재인증
  • 암호화 Key의 분리, 회전과 접근 권한
  • WebAuthn과 Passkey 같은 Phishing-resistant 인증 수단

OWASP MFA Cheat Sheet도 TOTP의 널리 쓰이는 호환성과 함께 Phishing, 기기 분실, 같은 기기에 Authenticator를 두는 경우의 한계를 지적한다. TOTP는 현실적인 보안 강화 수단이지만, 모든 인증 위협을 해결하는 종착점은 아니다.


IX. 정리

TOTP 2FA에서 코드 생성과 검증은 표준 라이브러리로 비교적 쉽게 구현할 수 있다. 실제 보안 경계를 만드는 것은 그 주변의 상태와 Session 설계다.

이번 구현을 통해 얻은 결론은 다음과 같다.

  1. TOTP를 등록한 사용자에게 1차 인증 성공은 로그인 완료가 아니다.
  2. 이 사용자의 새 로그인에서는 OTP 전에 정식 Session 대신 제한된 1회성 Challenge를 사용한다.
  3. 2FA 등록은 계정에, 2FA 통과는 각 Session에 귀속시킨다.
  4. 로그인 Challenge와 권한 Guard를 함께 적용한다.
  5. Replay, 동시성, Refresh, 복구와 배포 순서까지 하나의 흐름으로 검증한다.

2FA의 핵심은 6자리 코드가 아니라, 부분적으로 인증된 상태가 정식 Session의 권한으로 넘어가지 못하게 만드는 것이다.


X. 참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.