Menu
CoddyTech

토큰 서명하기

Coddy Node.js 여정의 인증과 보안 섹션에 포함된 레슨. 17개 중 10번째.

토큰은 다른 누구도 만들 수 없을 때만 가치가 있습니다. jose는 HS256으로 토큰에 서명합니다. 이는 서버만 아는 비밀 키를 사용하는 헤더와 페이로드의 HMAC입니다.

비밀 키는 환경 변수인 process.env.JWT_SECRET에서 가져오며, 코드에서 가져오지 않습니다. 코드는 공유되고, 복사되고, 커밋되기 때문입니다. 이 강의에서는 JWT_SECRET을 설정합니다. jose는 이를 바이트로 받습니다.

const key = new TextEncoder().encode(process.env.INVITE_SECRET);

const invite = await new SignJWT({ room: 'design' })
    .setProtectedHeader({ alg: 'HS256' })
    .setSubject(guestId)
    .setIssuedAt()
    .setExpirationTime('2d')
    .sign(key);

setSubject에는 텍스트를 전달하고, setExpirationTime에는 '30m' 또는 '2d'와 같은 기간을 전달합니다.

challenge icon

챌린지

쉬움
  • POST /token에 { username, password }를 보내면 { token }이 반환됩니다. HS256으로 서명되며, JWT_SECRET을 사용하고, username 클레임과 텍스트 형식의 사용자 ID를 subject로 포함하며, 1시간 후 만료됩니다.
  • 로그인 정보가 틀리면 401과 { "error": "Wrong username or password" }가 반환됩니다.

가입한 뒤 토큰을 받아 GET /inspect?token=에 붙여 넣으세요.

직접 해보기

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

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 });
});

// 토큰의 내용을 보여줍니다: 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' });
    }
});

// 관리자 페이지입니다. 서명을 검증하기 전에는 어떤 클레임도 신뢰할 수 없습니다.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

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

인증과 보안의 모든 레슨