Menu
CoddyTech

토큰 검증하기

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

API 클라이언트는 모든 요청에서 헤더에 토큰을 보냅니다. Authorization: Bearer <token>. decodeJwt로 토큰을 읽으면 누구나 입력한 토큰을 모두 받아들이게 됩니다. jwtVerify(token, key)는 비밀 키로 서명을 다시 계산하고, 서명이 일치하지 않거나 토큰이 만료되었거나 JWT가 아닌 경우 오류를 발생시킵니다. 이 함수가 반환된 후에만 payload를 신뢰할 수 있습니다.

async function requireDevice(req, res, next) {
    const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
    try {
        const { payload } = await jwtVerify(token, deviceKey);
        req.device = payload.sub;
    } catch {
        return res.status(401).json({ error: 'Unknown device' });
    }
    next();
}
challenge icon

챌린지

쉬움

이제 서명한 토큰을 받아들이세요. 앱은 모든 요청의 헤더에 토큰을 보내고, 서버는 토큰 안의 내용을 신뢰하기 전에 서명을 확인합니다.

  1. server.js에서 jose로부터 jwtVerify를 가져오세요.
  2. 미들웨어 requireUser(req, res, next)를 작성하세요. 이 미들웨어는 Authorization 헤더를 읽습니다. 이 헤더에는 Bearer라는 단어, 공백, 그리고 토큰이 들어 있습니다. 여기서 토큰을 꺼내세요.
  3. 서명할 때 사용하는 것과 같은 키로 jwtVerify를 사용해 토큰을 확인하세요. 서명이 일치하지 않거나 토큰이 만료되었거나 텍스트가 토큰이 아닐 때 jwtVerify는 예외를 발생시킵니다. 이 모든 경우와 헤더가 없는 경우에 상태 코드 401과 {"error": "Log in first"}를 응답하세요.
  4. 토큰이 유효하면 토큰의 sub 클레임에서 가져온 id와 username 클레임에서 가져온 username이 포함된 객체를 req.user에 설정하세요. 그런 다음 next()를 호출하세요.
  5. requireUser를 사용하는 GET /me를 추가하세요. req.user를 반환합니다: {"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.

직접 해 보기: ‘플레이’를 누르세요. GET /me를 보내세요: 상태 코드 401과 {"error": "Log in first"}가 반환됩니다. {"username": "ada", "password": "correct horse"}를 사용해 POST /signup을 보낸 다음, 같은 내용으로 POST /token을 보내고 토큰을 복사하세요. Headers 입력란에 이름이 Authorization인 헤더를 추가하고, 값으로 Bearer, 공백, 그리고 토큰을 입력하세요. GET /me를 보내세요: 상태 코드 200과 함께 ID 및 "username": "ada"가 반환됩니다. 이제 토큰의 가운데 부분, 즉 두 점 사이에 있는 글자 하나를 바꾼 다음 GET /me를 다시 보내세요: 서명이 더 이상 일치하지 않으므로 상태 코드 401이 반환됩니다.

직접 해보기

import express from 'express';
import { SignJWT, decodeJwt } 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 });
});

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

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

인증과 보안의 모든 레슨

직접 연습해 보세요: Express 플레이그라운드