CodeGym /Kursy /Spring Security /Time in JWT: iat, nbf, exp

Time in JWT: iat, nbf, exp

Spring Security
Poziom 22, Lekcja 3
Dostępny

1. Time — JWT validity boundaries

If JWT could not “age,” it would be very convenient... for an attacker. Imagine an apartment key that was issued once and then forgotten: you might not have lived in that apartment for five years, but the key still opens the door. In the security world, that is not allowed, so a token almost always needs time boundaries. These boundaries solve two practical problems at once: they reduce the risk window if a token is leaked and give the system a “control lever” — you can change the token lifetime policy without rewriting the entire access mechanism.

As soon as iat, nbf, and exp appear in the token contract, the question becomes very practical: when does this pass even start working, when is it still too early to accept it, and when is it already too late? Without that, JWT remains a string of numbers, not a manageable access window.

In our project, Secure Content Platform API JWT will play the role of a “pass” for access to private zones (/api/me, draft management, editor/admin areas). But even if the token is perfectly signed, it remains a bearer token: whoever holds the token in their hands gets in as you. That is why time limits are a way to say: “Yes, this is a pass... but it works only for a short time.”

To make this more than a slogan and turn it into an engineering picture, let us fix a simple model of the token “validity window.”

flowchart TD
    A["iat: token issued"] --> B["nbf: token becomes valid"]
    B --> C["exp: token stops being valid"]

In the ideal “simple” scenario, nbf matches iat, meaning the token is valid immediately after issuance. But the very fact that JWT has a lower bound (nbf) and an upper bound (exp) makes it a manageable object, not an eternal plaque reading “user = Vasya.”

2. NumericDate: time in seconds

When a beginner first sees exp: 1773828900, they usually suspect one of two things: either it is a cipher, or JWT developers are a secret society of calculator enthusiasts. In fact, it is simpler: the JWT standard describes time in NumericDate format, which is essentially the number of seconds since the Unix epoch (1970-01-01T00:00:00Z). This representation is convenient for machines, does not depend on time zones, and does not require an agreement on how to write the date as a string.

The key idea for a backend developer: in JWT, time is almost always stored in seconds, not milliseconds. And this is where one of the most annoying bugs lives: the server writes milliseconds, but the validator thinks they are seconds — and the token suddenly becomes “valid until the end of time” (well, roughly until the Sun burns out, and it is still holding up for now).

Let us look at this in Java. We are intentionally using a fixed time so that the output is predictable.

import java.time.Instant;

public class EpochDemo {
    public static void main(String[] args) {
        // NumericDate in JWT is seconds since the Unix epoch, not milliseconds
        long expSeconds = 1773828900L;

        // Convert epoch-seconds to Instant (UTC moment in time)
        Instant exp = Instant.ofEpochSecond(expSeconds);

        // For debugging, it is convenient to print Instant: the time is visible in UTC
        System.out.println(exp); // 2026-03-17T??:??:??Z (depends on the number)
    }
}

If you want to see the “human-friendly” representation even more clearly, you can format it through ZonedDateTime, but for security logic Instant is usually enough: it is always in UTC and does not make you think about time zones where that is unnecessary.

It is also important to understand that NumericDate is not “server time.” It is just an absolute point on the timeline. The server may live in New York, the client in Tokyo, and the token in UTC. And that is good: the token should not depend on where you physically are.

3. iat, nbf, exp: token validity window

iat: token issuance moment

iat (issued at) sounds very straightforward: “issued at such and such time.” But this straightforwardness has an important practical value. The iat field helps answer questions you will inevitably start asking while debugging: “When did this token even appear?”, “Why is it still valid?”, “Why does the user have a token ‘from the future’?” This field also helps distinguish “old” tokens from “new” ones at least conceptually, even if you are not building a complex token management infrastructure.

In the educational project, iat is also useful as a point from which to calculate TTL: usually TTL is exp - iat. We are not turning this into cryptographic magic; we are turning it into time bookkeeping.

Example: calculate iat and exp for a token that lives for 15 minutes. The important thing here is to remember that JWT values are in seconds.

import java.time.Duration;
import java.time.Instant;

public class IatExpDemo {
    public static void main(String[] args) {
        // Fix the token issuance moment (iat) in UTC
        Instant issuedAt = Instant.parse("2026-03-18T10:00:00Z");

        // exp is usually calculated as iat + TTL
        Instant expiresAt = issuedAt.plus(Duration.ofMinutes(15));

        // JWT stores seconds (NumericDate), so we use epochSecond
        System.out.println(issuedAt.getEpochSecond()); // 1773828000 (example)
        System.out.println(expiresAt.getEpochSecond()); // 1773828900 (example)
    }
}

Why does iat not guarantee that the token “already works”? Because that is what nbf is for. In a normal scenario they match, but the standard leaves room to say: “the token is issued now, but it will start working a bit later.” And that brings us to the next field.

nbf: token activation moment

nbf (not before) is the lower bound of validity. The logic is simple: even if the token is perfectly signed and looks “correct,” the server must not accept it before the specified moment. In practice, many systems do not use nbf at all and either set it equal to iat or leave it out of the token. That is fine: there is no need to complicate the contract if you do not need delayed validity.

But nbf is worth understanding because it explains the typical “the token exists, but the server says it is not valid yet” scenario. Beginners often interpret this as “something is broken,” when sometimes it is just correct nbf behavior.

Let us show a mini check. We are not validating the signature (that is a separate story); we are purely looking at the time window.

import java.time.Instant;

public class NbfDemo {
    public static void main(String[] args) {
        // "Current time" (for example, on the server) — we fix it for the example
        Instant now = Instant.parse("2026-03-18T10:00:10Z");

        // Lower bound of token validity: it cannot be accepted before this moment
        Instant notBefore = Instant.parse("2026-03-18T10:00:30Z");

        // usableNow = now >= notBefore
        boolean usableNow = !now.isBefore(notBefore);

        // At this moment the token has not "taken effect" yet
        System.out.println(usableNow); // false
    }
}

In plain language: the token “exists,” but it has not “taken effect” yet.

When might this be useful? For example, in systems where a token is issued ahead of time, or during migrations between services, or for some kinds of clock skew (we will talk about that a bit later). But even if you never intentionally use nbf, you should be able to read it and not be alarmed.

exp: token “death” moment

exp (expiration time) is the upper bound of validity. After this moment, the token must not be accepted. It is exp that turns an access token into a “temporary pass,” not an “eternal passport.” And this is one of the most important anti-chaos mechanisms in the stateless model: if you cannot instantly “take back” an already issued token, at least you can guarantee that it will die on its own.

It is important to remember a simple validation rule: if now >= exp, then the token is expired. Not “roughly,” not “almost,” not “well, fine, it is only one second late” — formally it is already invalid. In the real world, however, clock skew will still show up, and we will discuss that next.

Mini check in Java:

import java.time.Instant;

public class ExpDemo {
    public static void main(String[] args) {
        // Fix the "current time"
        Instant now = Instant.parse("2026-03-18T10:15:00Z");

        // Token death time
        Instant exp = Instant.parse("2026-03-18T10:15:00Z");

        // Formal rule: now >= exp => token expired
        boolean expired = !now.isBefore(exp); // now >= exp

        // On the "exactly at exp" boundary, the token is already considered expired
        System.out.println(expired); // true
    }
}

Yes, on the boundary at exactly exp, the token is already considered expired. That can be surprising, but it makes the rule unambiguous. Security generally likes unambiguity: it is worse for romance, but better for operations.

Another important point: exp is time, not “number of minutes.” Therefore TTL is a derived value: we calculate it by subtracting iat from exp. And that brings us to TTL as an engineering and product decision.

4. Token TTL: a trade-off between convenience and risk

TTL (time to live) is “how long the token lives.” In JWT it is usually not stored as a separate field (although sometimes people add it as a custom claim, which is often redundant). TTL is almost always expressed through iat and exp: TTL = exp - iat. And TTL is what turns “abstract security” into a concrete compromise between convenience and risk.

If TTL is very short, it may be inconvenient for the user: the token expires often, and the client has to obtain access again. If TTL is very long, it is convenient for everyone... except your security system and the on-call developer who will have to figure out why “we stole the token yesterday, but it still works today.”

For the educational Secure Content Platform API, it makes sense to think of TTL as a “short, manageable window.” For example, 15 minutes is a good learning target: long enough not to be annoying, and short enough that the point of the restriction is noticeable.

Let us calculate TTL directly in code so we do not leave it to guesswork.

import java.time.Duration;
import java.time.Instant;

public class TtlDemo {
    public static void main(String[] args) {
        // Set iat and exp explicitly so we can see TTL without tying it to "now"
        Instant iat = Instant.parse("2026-03-18T10:00:00Z");
        Instant exp = Instant.parse("2026-03-18T10:15:00Z");

        // TTL = exp - iat
        Duration ttl = Duration.between(iat, exp);

        // It is convenient to verify expectations in minutes
        System.out.println(ttl.toMinutes()); // 15
    }
}

When you discuss TTL with your team (or with yourself from the future), it is useful to ask not “let us set exp,” but “what TTL do we need and why.” That makes the decision meaningful: you choose a risk window, not a random number.

And one more important practical detail: TTL should almost always be configurable, because “15 minutes” in a dev environment and “15 minutes” in production can have different costs. But within this lecture we are only fixing the mental model: TTL is part of the contract and part of security, not a default parameter.

And here it is easy to overestimate the power of exp. A short TTL reduces the window during which a stolen token or stale claims remain dangerous. But TTL does not rewrite the contents of an already issued payload and does not provide instant revocation: it only limits the damage in time.

5. clock skew: allowance for clock drift

Clock skew is the difference in clocks between systems. Even if you love precision, your servers and clients may live in different time realities. One server synced via NTP, another slightly behind, a virtual machine with drifting time, a container that started and behaves strangely for the first few seconds, and somewhere else a client phone decided it is 2017 because the user wanted it that way. As a result, a “correct” token may look “from the future” or “already expired” if you check time without any allowance.

That is why many systems introduce a tolerance: for example, they allow a small difference like 10–30 seconds. This is not “weakening security out of laziness,” it is engineering insurance against false rejections. The main thing is not to turn clock skew into “everything is allowed”: the allowance should be small, otherwise you are actually widening the token validity window.

Let us show the idea: the token is formally “5 seconds expired,” but we allow 10 seconds of drift.

import java.time.Duration;
import java.time.Instant;

public class ClockSkewDemo {
    public static void main(String[] args) {
        // Current time (at the validator)
        Instant now = Instant.parse("2026-03-18T10:15:05Z");

        // exp came from the token
        Instant exp = Instant.parse("2026-03-18T10:15:00Z");

        // Allowance for clock drift
        Duration skew = Duration.ofSeconds(10);

        // Strict check: now >= exp => expired
        boolean expiredStrict = !now.isBefore(exp);

        // Check with allowance: as if "our clocks are a bit fast"
        boolean expiredWithSkew = !now.minus(skew).isBefore(exp);

        System.out.println(expiredStrict);   // true
        System.out.println(expiredWithSkew); // false
    }
}

Notice the idea: we do not “move exp forward,” we move current time backward by a small allowance. This is a convenient mental model: “let us check what happens if our clock is a little fast.”

You can apply skew to nbf in the same way: if the token is “not yet active,” but only by a couple of seconds, and we allow drift, it can be accepted.

6. Checking token time without cryptography

It is very easy to mix things up: “JWT validation” sounds like signatures, keys, algorithms, parsing, and error handling all at once. But the time-related part is checked by a separate, simple layer. And it is useful to be able to describe this layer separately, because it later helps with debugging: at least you know whether the token failed because of time or because of everything else.

Mentally, time validation looks like a “validity window”: the token must be “not earlier than nbf” and also “not later than exp.” If nbf is missing, we assume there is no lower bound (or that it equals iat, if that is what you decided in the contract). If iat is missing, that is not always critical for validity, but it is usually strange for a contract. In the educational project, we will stick to a simple, readable contract where iat and exp are always present.

Below is a small “time validator” example. It does not make JWT secure (the signature is not checked), but it shows exactly the piece of logic related to time.

import java.time.Duration;
import java.time.Instant;

public class JwtTimeValidator {
    public static boolean isTimeValid(Instant now, Instant nbf, Instant exp, Duration skew) {
        // For nbf, allow a small skew in the direction of "our clock may be a little behind"
        // Check: now + skew >= nbf
        boolean notTooEarly = !now.plus(skew).isBefore(nbf);

        // For exp, allow a small skew in the direction of "our clock may be a little fast"
        // Check: now - skew < exp
        boolean notTooLate = now.minus(skew).isBefore(exp);

        // Time-wise, the token is valid only if it is already active and has not expired yet
        return notTooEarly && notTooLate;
    }

    public static void main(String[] args) {
        // Fix the time in the example so behavior is predictable
        Instant now = Instant.parse("2026-03-18T10:00:10Z");
        Instant nbf = Instant.parse("2026-03-18T10:00:00Z");
        Instant exp = Instant.parse("2026-03-18T10:15:00Z");

        // In real code, skew comes from configuration
        System.out.println(isTimeValid(now, nbf, exp, Duration.ofSeconds(10))); // true
    }
}

There are two small “magics” here (actually anti-magics). For nbf, we check “not too early” with allowance: now + skew >= nbf. For exp, we check “not too late” with allowance: now - skew < exp. This way you allow a small clock error in both directions and avoid unexpected rejections for no reason.

If you want to reinforce the picture visually, here is the same algorithm as a flowchart.

flowchart TD
    A["There is now, nbf, exp, skew"] --> B{"now + skew < nbf ?"}
    B -- "yes" --> E["Too early: token not active"]
    B -- "no" --> C{"now - skew >= exp ?"}
    C -- "yes" --> F["Too late: token expired"]
    C -- "no" --> D["Time part is valid"]

This is useful because in logs and debugging you will be able to explain to yourself and your colleagues: “We rejected the token because it is not active yet” or “because it is expired,” instead of the vague “JWT is invalid.”

7. Typical mistakes when working with JWT time

Mistake #1: confusing seconds and milliseconds.
The most common and most toxic mistake. JWT expects NumericDate in seconds, but the developer habitually grabs System.currentTimeMillis() and stores it as is. As a result, exp ends up 1000 times larger than expected, the token looks “valid” for centuries, and you have accidentally invented an immortality pass. If you want to stay sane, use Instant.now().getEpochSecond() and Instant.ofEpochSecond(...), and keep milliseconds far away from claims.

Mistake #2: using local time (LocalDateTime) without a time zone.
LocalDateTime looks friendly, but in security it often becomes a trap: it has no zone, which means it is not an “absolute moment in time.” In tokens and time checks, you almost always need Instant or ZonedDateTime in UTC. Otherwise you risk getting tokens that are “yesterday” on one server and “tomorrow” on another, especially during daylight saving transitions and with different JVM settings.

Mistake #3: treating nbf as “optional, so it can be ignored.”
If you agreed that nbf exists in your contract and has meaning, then you cannot ignore it: otherwise you are destroying the token’s time model yourself. Even if in the educational project nbf equals iat, it is important not to lose the meaning of the field: it is responsible for the lower bound, and its absence or wrong interpretation leads to strange “the token is not active yet” behavior or, conversely, to premature acceptance of the token.

Mistake #4: making TTL huge “for convenience,” especially at the start.
It is very tempting to set TTL to “one day” or “one month” so nothing expires during development. But that quietly breaks the mental model: you stop feeling that the token is limited in time at all and start designing the system as if the token were eternal. It is better to make TTL reasonable and handle development inconvenience consciously (for example, by configuring TTL through settings) than to learn the wrong habit.

Mistake #5: forgetting about clock skew and getting “random” rejections for no reason.
In a learning environment, everything often runs on one laptop, and it seems that clocks always match. But once you have several services, containers, or even just a virtual machine, suddenly some tokens start “not working yet” or “already expired” by a few seconds. If you check time strictly, you get instability. If you introduce a small skew allowance (within a few seconds), you turn this into predictable behavior.

1
Zadanie
Spring Security, poziom 22, lekcja 3
Niedostępne
Obliczanie TTL tokena na podstawie iat i exp
Obliczanie TTL tokena na podstawie iat i exp
1
Zadanie
Spring Security, poziom 22, lekcja 3
Niedostępne
Walidacja okna czasowego z uwzglednieniem clock skew
Walidacja okna czasowego z uwzglednieniem clock skew
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION