Daeho's Dev Blog

JWT nbf 에러 — 서버 간 시계 차이로 토큰이 거부될 때

#PHP#JWT#인증

A 서버에서 JWT 발급하고 B 서버에서 검증하는데 nbf 에러가 남. 확인해보니 A랑 B 서버 시계가 1초 차이 나 있었음. 매일 자정마다 동기화하는데도 통신하는 그 순간엔 1초 정도 어긋나는 듯.

nbf 클레임

JWT 페이로드에 들어가는 표준 클레임 중 하나임.

  • iat (issued at) — 토큰 발행 시각
  • nbf (not before) — 이 시각 이전에는 토큰을 쓸 수 없음
  • exp (expiration) — 이 시각 이후에는 만료

nbf는 ’이 시각부터 유효’라는 뜻. 검증하는 서버의 현재 시각이 nbf보다 이르면 토큰이 거부됨.

토큰이 거부되는 과정

A 서버가 nbf = 발행 시각(현재)으로 토큰을 만듦. 근데 검증하는 B 서버 시계가 A보다 1초 느렸음. B 입장에선 토큰 nbf아직 안 온 미래라서 ’아직 못 쓰는 토큰’으로 보고 거부함.

firebase/php-jwt 쓰면 이때 BeforeValidException 터지고, 메시지는 대략 이렇게 나옴.

Cannot handle token prior to 2024-05-07T13:44:00+0900

서버 간 시계 차이

근본 원인은 두 서버 시계가 미세하게 다른 거. 매일 자정에 동기화해도 하루 동안 조금씩 밀리고(드리프트), 요청 오가는 찰나에 1초 차이가 그대로 드러남. 서버 시각 동기화 자체 맞추는 법은 따로 정리해둠 → Ubuntu 22.04 서버 시간 맞추기.

동기화를 아무리 잘 해도 초 단위 오차는 남으니까, JWT 쪽에서도 오차를 견디게 만들어주는 게 실전 해법임.

해결 1. 발급 시 토큰을 몇 초 과거로 (백데이팅)

발행 시각이랑 nbf를 현재보다 몇 초 앞당겨두면, 검증 서버가 좀 느려도 nbf가 이미 지난 시각이라 통과함.

$issuer = "localhost";
$audience = "localhost";
$issuedAt = time() - 5;            // 발행 시각을 5초 과거로
$notBefore = $issuedAt;            // 즉시 사용 (nbf = iat)
$expirationTime = $issuedAt + (60 * G5_JWT_ACCESS_MTIME);

$payload = array(
    "id"  => $id,
    "iss" => $issuer,
    "aud" => $audience,
    "iat" => $issuedAt,
    "nbf" => $notBefore,
    "exp" => $expirationTime,
);

포인트는 time() - 5. nbf를 5초 과거로 밀어두면 서버 간 1초 정도 차이는 흡수됨. 코드에 이미 이게 들어 있다면 그게 바로 이 에러 대응임.

해결 2. 검증 시 오차 허용 (leeway) — 더 표준적

firebase/php-jwt엔 시계 오차를 허용하는 leeway 옵션이 있음. 검증하는 쪽에서 한 줄만 넣으면 됨.

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

JWT::$leeway = 5; // 5초 시계 오차 허용

$decoded = JWT::decode($jwt, new Key($key, 'HS256'));

백데이팅은 발급 측에서 nbf만 앞당기는데, leeway는 검증 측에서 iat·nbf·exp 세 검사 다에 오차를 적용해줌. 그래서 만료(exp) 직전 요청이 시계 차이로 잘못 거부되는 것까지 같이 막아줌.

주의점

leeway를 너무 크게 잡으면 만료 검사도 그만큼 느슨해져서 보안이 약해짐. 서버 간 실제 오차 수준(보통 몇 초, 5초 내외)이면 충분함.

정리

  • nbf 에러 = 검증 서버 현재 시각이 토큰 nbf보다 이르다 = 서버 간 시계 차이.
  • 코드의 time() - 5는 발급 시각을 과거로 미는 백데이팅 워크어라운드.
  • firebase/php-jwt면 검증 측에서 JWT::$leewayiat·nbf·exp 오차를 한 번에 허용하는 게 더 깔끔함.
  • 근본적으론 NTP로 서버 시각을 상시 동기화해두는 게 좋음.