Menu
CoddyTech

Token Süresinin Dolması

Coddy'nin Node.js Journey'sinin Kimlik Doğrulama ve Güvenlik bölümünün bir parçası. Ders 17 / 24.

Bir sunucu bir tokenı geri alamaz: hiçbir şey saklanmadığından, çalınan bir tokenın süresi dolana kadar kullanılabilir. Tokenların ömrünü kısa tut; günler yerine dakikalar olsun ve istemcinin yeni bir token isteyebilmesini sağla.

jwtVerify, süresi dolmuş bir exp değerine sahip tokenı zaten reddeder. exp, 1 Ocak 1970'ten bu yana saniye cinsinden sayılan bir zamanı ifade eder. errors.JWTExpired hatasını fırlatır. err instanceof errors.JWTExpired bu tür bir hata için true olur; böylece süresi dolmuş bir tokenı sahte bir tokenı ayırt edebilir ve bunu belirtebilirsin: Token süresi doldu mesajını okuyan bir istemci, yeniden giriş yapması gerektiğini anlar.

import { errors, jwtVerify } from 'jose';

try {
    await jwtVerify(inviteToken, inviteKey);
} catch (err) {
    if (err instanceof errors.JWTExpired) {
        return res.status(410).json({ error: 'This invite has run out' });
    }
    return res.status(400).json({ error: 'Not a real invite' });
}
challenge icon

Görev

Kolay

Bir sunucu tokenı geri alamaz; bu nedenle çalınan bir token süresi dolana kadar çalışmaya devam eder. Tokenları kısa süreli yapın ve tokenının süresi dolduğunda uygulamaya bunu açıkça bildirin, böylece uygulama yeniden giriş yapması gerektiğini anlar.

  1. server.js dosyasında, POST /token içinde tokenların oluşturulduktan bir saat sonra değil, 15 dakika sonra süresinin dolmasını sağlayın.
  2. requireUser içinde, jwtVerify işlevinin fırlattığı hatayı inceleyin. Süresi dolmuş bir token için jose, bir errors.JWTExpired hatası fırlatır. jose içinden errors öğesini içe aktarın. Hata bu türdeyse 401 durum koduyla ve {"error": "Token expired"} yanıtını verin.
  3. Diğer tüm sorunlarda 401 durum kodu ve {"error": "Log in first"} yanıtı verilmeye devam etsin: başlık olmaması, değiştirilmiş bir token veya başka bir anahtarla imzalanmış bir token.

Deneyin: “Oyna” düğmesine basın. POST /signup, ardından POST /token isteği gönderin; her ikisinde de {"username": "ada", "password": "correct horse"} kullanın. Yeni tokenın ardından GET /inspect?token= isteğini gönderin: exp artık iat değerinden 900 saniye sonrasıdır. Her ikisi de 1 Ocak 1970'ten itibaren geçen saniyeleri sayar. Sırada, bu sunucunun gizli anahtarıyla imzalanmış ve süresi 1 Ocak 2026'da dolmuş bir token var: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkYSIsInN1YiI6IjY2ZjFjMmE5ZTRiMGExYjJjM2Q0ZTVmNiIsImlhdCI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI2NTAwfQ.PL8VCHpGNwBJ-7EawZ60veEEE-ZyosNC7xYQQpgT2uE Headers alanında Authorization değerini Bearer, bir boşluk ve bu token olacak şekilde ayarlayın; ardından GET /me isteğini gönderin: 401 durum kodu ve {"error": "Token expired"}. Orta kısmındaki bir harfi değiştirip isteği yeniden gönderin: 401 durum kodu ve {"error": "Log in first"}. Değiştirilmiş bir tokenın imzası, tarihi okunmadan önce doğrulanamaz.

Kendin dene

import express from 'express';
import { SignJWT, decodeJwt, jwtVerify } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const app = express();
app.use(express.json());

app.post('/signup', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const password = String(req.body.password ?? '');
    if (!username || password.length < 8) {
        return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
    }
    if (await users.findOne({ username })) {
        return res.status(409).json({ error: 'That username is taken' });
    }
    await users.insertOne({ username, passwordHash: await hashPassword(password) });
    res.status(201).json({ username });
});

app.post('/login', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const user = await users.findOne({ username });
    if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
        return res.status(401).json({ error: 'Wrong username or password' });
    }
    res.json({ username: user.username });
});

app.post('/token', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const user = await users.findOne({ username });
    if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
        return res.status(401).json({ error: 'Wrong username or password' });
    }
    const token = await new SignJWT({ username: user.username })
        .setProtectedHeader({ alg: 'HS256' })
        .setSubject(user._id.toHexString())
        .setIssuedAt()
        .setExpirationTime('1h')
        .sign(secret);
    res.json({ token });
});

// Authorization başlığındaki token'ı kontrol eder; geçerli değilse 401 yanıtı verir.
async function requireUser(req, res, next) {
    const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
    let payload;
    try {
        ({ payload } = await jwtVerify(token, secret));
    } catch {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: payload.sub, username: payload.username };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json(req.user);
});

// Bir token'ın içeriğini gösterir: GET /inspect?token=...
app.get('/inspect', (req, res) => {
    try {
        res.json(decodeJwt(String(req.query.token ?? '')));
    } catch {
        res.status(400).json({ error: 'Not a JWT' });
    }
});

// Yönetici sayfası. İmzası doğrulanmadan hiçbir beyana güvenilemez.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

Kimlik Doğrulama ve Güvenlik bölümündeki tüm dersler

Kendi başına pratik yap: Express playground