A JSON Web Token is a signed, self-contained piece of data a server hands a client after login, and trusts on every later request — without needing to check a session store each time.
A JWT is three base64url-encoded segments joined by dots: header.payload.signature. The payload holds claims like the user's ID and an expiry time — readable by anyone, but not forgeable, because the signature is computed with a secret only the server knows.
{
"sub": "user_42",
"role": "editor",
"exp": 1735689600
}
const jwt = require('jsonwebtoken')
app.post('/login', (req, res) => {
const user = authenticate(req.body.email, req.body.password)
if (!user) return res.status(401).json({ error: 'Invalid credentials' })
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
)
res.json({ token })
})
The secret must never be checked into source control or hardcoded — it should live in an environment variable, and rotating it invalidates every previously issued token at once.
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization
if (!authHeader) return res.status(401).json({ error: 'No token' })
try {
const payload = jwt.verify(authHeader.split(' ')[1], process.env.JWT_SECRET)
req.user = payload
next()
} catch {
res.status(401).json({ error: 'Invalid or expired token' })
}
}
app.get('/api/profile', requireAuth, (req, res) => {
res.json({ userId: req.user.sub })
})
jwt.verify checks the signature and expiry in one call, throwing if either fails — which is why it's wrapped in try/catch.
A JWT stored in localStorage is readable by any JavaScript on the page, including an injected XSS script. An httpOnly cookie is safer against that specific attack but needs CSRF protection instead. Neither option is risk-free — pick based on which threat matters more for your app, and don't treat JWTs as inherently secure by default.
Comments