feat(auth): JWT iat + users.password_changed_at invalidation (PR-Docsrv-JWT-Invalidation-1)

PR-Infra-Sec-1H Phase 0 audit 에서 DS jwt invalidation 정책 부재 확정.
password rotation 으로 구 365d JWT (voice-memo-bot 등) invalidate 안 되는
hard gate STOP 진입 → 선행 PR 분리.

- migration 269: users.password_changed_at timestamptz NULL (legacy 호환)
- create_access_token / create_refresh_token: payload 에 iat (int 초) 추가
- verify_password_changed_at helper: int(password_changed_at.timestamp()) > int(iat) 시 401
- get_current_user + refresh_token route: verify helper 호출
- change_password / setup signup / seed_admin INSERT+UPDATE: password_changed_at 갱신

NULL = 검증 skip (migration 직후 운영 영향 0). 첫 password 변경 후만 iat
검증 활성. Sec-1H 의 G-token-old hard gate 통과 path 확보.
This commit is contained in:
hyungi
2026-05-17 06:20:18 +00:00
parent b8575084b1
commit 74876b674c
6 changed files with 35 additions and 7 deletions
+3 -3
View File
@@ -55,14 +55,14 @@ async def seed_admin():
if result.scalar_one_or_none():
print(f"'{username}' 계정이 이미 존재합니다. 비밀번호를 업데이트합니다.")
await session.execute(
text("UPDATE users SET password_hash = :hash WHERE username = :username"),
text("UPDATE users SET password_hash = :hash, password_changed_at = NOW() WHERE username = :username"),
{"hash": password_hash, "username": username},
)
else:
await session.execute(
text(
"INSERT INTO users (username, password_hash, is_active) "
"VALUES (:username, :hash, TRUE)"
"INSERT INTO users (username, password_hash, is_active, password_changed_at) "
"VALUES (:username, :hash, TRUE, NOW())"
),
{"username": username, "hash": password_hash},
)