fix: auth token expiration + constant-time JWT signature comparison

- createToken() добавя exp/iat/nbf в JWT payload
- verifyToken() parse-ва exp/iat/nbf и проверява expiration
- verifyToken() използва constantTimeCompare() за timing-attack resistance
- Добавен import std/times

292 теста, 0 failure-а.
This commit is contained in:
2026-05-12 23:36:17 +03:00
parent a6aeb7801b
commit 5e27a987dd
+33 -4
View File
@@ -2,6 +2,7 @@
import std/strutils
import std/base64
import std/tables
import std/times
import checksums/sha2
type
@@ -82,21 +83,36 @@ proc hmacSha256(key, message: string): string =
proc createToken*(am: AuthManager, claims: JWTClaims): string =
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
let payload = base64UrlEncode(
"{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role &
"\",\"database\":\"" & claims.database & "\"}")
var payloadJson = "{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role &
"\",\"database\":\"" & claims.database & "\""
if claims.exp > 0:
payloadJson &= ",\"exp\":" & $claims.exp
if claims.iat > 0:
payloadJson &= ",\"iat\":" & $claims.iat
if claims.nbf > 0:
payloadJson &= ",\"nbf\":" & $claims.nbf
payloadJson &= "}"
let payload = base64UrlEncode(payloadJson)
let data = header & "." & payload
let signature = hmacSha256(am.secretKey, data)
am.tokens.add(data & "." & base64UrlEncode(signature))
return am.tokens[^1]
proc constantTimeCompare(a, b: string): bool =
if a.len != b.len:
return false
var result = 0
for i in 0..<a.len:
result = result or (ord(a[i]) xor ord(b[i]))
return result == 0
proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
let parts = token.split(".")
if parts.len != 3:
return (false, JWTClaims())
let data = parts[0] & "." & parts[1]
let sig = hmacSha256(am.secretKey, data)
if base64UrlEncode(sig) != parts[2]:
if not constantTimeCompare(base64UrlEncode(sig), parts[2]):
return (false, JWTClaims())
# Parse payload
let payload = base64UrlDecode(parts[1])
@@ -132,10 +148,23 @@ proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
of "database": claims.database = val
of "iss": claims.iss = val
of "aud": claims.aud = val
of "exp": claims.exp = parseInt(val)
of "iat": claims.iat = parseInt(val)
of "nbf": claims.nbf = parseInt(val)
of "jti": claims.jti = val
else: discard
if i < payload.len and payload[i] == ',':
inc i
inc i
# Validate expiration
let now = int64(getTime().toUnix())
if claims.exp > 0 and now > claims.exp:
return (false, JWTClaims())
if claims.nbf > 0 and now < claims.nbf:
return (false, JWTClaims())
if claims.iat > 0 and now < claims.iat - 60:
# Allow 60 seconds clock skew
return (false, JWTClaims())
return (true, claims)
proc validateCredentials*(am: AuthManager, creds: AuthCredentials): AuthResult =