exp (expiration)
Must not be accepted after this instant. If the current time equals exp exactly, the token is still valid. It is not a required claim, so its absence yields a token that never expires — usually by mistake.
브라우저 내부 처리
A "token expired" log usually is not an expiry. It is a misread number, a missing verification step, or two servers whose clocks disagree. Here is how to tell them apart.
JWT has three time-related claims and all three are Unix epoch seconds, not milliseconds. That single line explains half of the bugs in practice.
Must not be accepted after this instant. If the current time equals exp exactly, the token is still valid. It is not a required claim, so its absence yields a token that never expires — usually by mistake.
Must not be accepted before this instant. Used to issue a token that becomes valid later. Rarely needed, but if the issuing server's clock runs slightly fast, a freshly minted token gets rejected as "not yet valid".
When the token was issued. Not used for validity directly, but for your own policies such as "is this token too old" and for audit logs. Using iat for expiry alongside exp creates two competing rules.
Paste a token into the JWT decoder to inspect the values, and use the Unix timestamp converter to turn epoch numbers into readable times.
Missing this distinction produces security that looks present but is not. It accounts for the largest share of JWT incidents.
A JWT's header and payload are Base64URL encoded, not encrypted. Anyone can read them — this site's decoder can, and so can one line in a browser console. Therefore never put secrets in a JWT payload. National ID numbers, internal identifiers or detailed permissions in a token are a disclosure by themselves.
Only by checking the third segment against a secret or public key can you conclude "we issued this and it has not been altered". Skip verification and read the payload, and an attacker's token with {"role":"admin"} is trusted as-is.
Most JWT libraries expose both decode() and verify(). decode() does not check the signature. The shorter name surfaces first in autocomplete and works fine in local tests, which is how it reaches production. Grepping for decode in review often catches it.
Because decoding is trivial, a token in a log file is a usable credential until it expires. If you log whole request headers, mask Authorization.
The JWT spec includes a none algorithm meaning "unsigned". If verification follows the header's alg, an attacker can set it to none, strip the signature, and pass. This was a real vulnerability in older libraries.
RS256 signs with a private key and verifies with the public key. If an attacker switches alg to HS256, the verifier uses that public key as an HMAC secret. The public key is public, so anyone can forge a valid signature.
Pin the expected algorithm in code. Most libraries accept an argument like algorithms: ['RS256']. Allow only that, whatever the header claims. A token's header is input, not configuration.
If the issuing server's clock runs a few seconds ahead, iat and nbf are in the future from the verifier's perspective, so the token is refused as "not yet valid". A few seconds of drift is common when NTP sync is loose in containers or VMs.
Conversely, a fast verifier sees a still-valid token as expired. This is a frequent cause of intermittent 401s that will not reproduce. If it only happens on certain instances, check the clocks first.
Most libraries offer clockTolerance or leeway; 30–60 seconds is conventional. Raising it to minutes keeps expired tokens alive that much longer, so it is no substitute for fixing NTP.
A long access-token exp means a stolen token is usable for that long. Because JWT verification is stateless, there is no built-in way to revoke an issued token. Hence short lifetimes plus refresh tokens. This is also the source of "I logged out but the token still works".
date command is enough.decode instead of verify, and whether the allowed algorithm is pinned.Next: read claims in the JWT decoder, cross-check times with the timestamp converter, and confirm for yourself that the payload is merely Base64 using the Base64 encoder/decoder.