3. Token
3-1. Token기반 인증
- 세션기반 인증은 서버(혹은 DB)에 유저 정보를 담는 방식 → 서버의 부담을 덜어내는 방법
- 클라이언트에서 인증 정보를 보관하는 방법으로 토큰기반 인증이 고안
- 대표적인 토큰기반 인증 : JWT(JSON Web Token)
- 토큰은 유저 정보를 암호화 하기 때문에 클라이언트에 담을 수 있음
3-2. JWT
JWT의 종류
- JWT : json 포맷으로 사용자에 대한 속성을 저장하는 웹 토큰
- 액세스 토큰 : 보호된 정보들에 접근할 수 있는 권한부여에 사용
- 클라이언트가 처음 인증을 받게 될 때(로그인 시) 액세스 토큰, 리프레시 토큰 두가지를 다 받지만, 실제로 권한을 얻는 데 사용하는 토큰은 액세스 토큰
- 액세스 토큰에는 짧은 유효기간을 주어 토큰을 탈취하더라도 오랫동안 사용할 수 없도록 하는것이 좋음
- 리프레시 토큰 : 액세스 토큰의 유효기간이 만료되면 새로운 액세스 토큰 발급할 때 사용
- 리프레시 토큰은 유효기간이 길어 보안을 위해 사용하지 않는 곳이 많음
JWT 구조
- Header : 토큰 종류, 시그니처를 암호화하는 알고리즘 → JSON 객체를 base64 방식으로 인코딩
- Payload : 서버에서 활용할 수 있는 유저의 정보 → JSON 객체를 base64로 인코딩
- Signature : 서버의 비밀 키(암호화에 추가할 salt)와 헤더에서 지정한 알고리즘을 사용하여 해싱
3-3. Token기반 인증 절차
- 클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청 보냄
- 아이디/비밀번호가 일치하는지 확인, 클라이언트에게 보낼 암호화된 토큰 생성
- access/refresh 토큰을 모두 생성
- 토큰에 담길 정보(payload) : 유저를 식별할 정보, 권한이 부여된 카테고리(사진, 연락처)
- 두 종류의 토큰이 같은 정보를 담을 필요 없음
- 서버가 토큰을 클라이언트에게 보내주면, 클라이언트는 토큰을 저장
- 저장하는 위치 : Local Storage, Session Storage, Cookie 등 다양
- 클라이언트가 HTTP 헤더(Authorizition 헤더)또는 쿠키에 토큰을 담아 보냄
쿠키에는 리프레시 토큰, 헤더 또는 바디에는 액세스 토큰을 담는 등 다양한 방법으로 구현
- Authorizition 헤더를 사용한다면 Bearer Authorizition을 이용
- 서버는 토큰을 해독하여 발급한 토큰이 맞으면, 클라이언트의 요청을 처리한 후 응답 보냄
3-2. Token 튜토리얼
ACCESS_SECRET=codestates
REFRESH_SECRET=codestates
const express = require('express');
const cors = require('cors');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const fs = require('fs');
const https = require('https');
const controllers = require('./controllers');
const app = express();
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const HTTPS_PORT = process.env.HTTPS_PORT || 4000;
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(
cors({
origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'OPTIONS'],
credentials: true,
})
);
app.post('/login', controllers.login);
app.post('/logout', controllers.logout);
app.get('/userinfo', controllers.userInfo);
let server;
if (fs.existsSync('./key.pem') && fs.existsSync('./cert.pem')) {
const privateKey = fs.readFileSync(__dirname + '/key.pem', 'utf8');
const certificate = fs.readFileSync(__dirname + '/cert.pem', 'utf8');
const credentials = {
key: privateKey,
cert: certificate,
};
server = https.createServer(credentials, app);
server.listen(HTTPS_PORT, () => console.log(`🚀 HTTPS Server is starting on ${HTTPS_PORT}`));
} else {
server = app.listen(HTTPS_PORT, () => console.log(`🚀 HTTP Server is starting on ${HTTPS_PORT}`));
}
module.exports = server;
require("dotenv").config();
const {sign, verify} = require("jsonwebtoken");
module.exports = {
generateToken: async (user, checkedKeepLogin) => {
const payload = {
id: user.id,
email: user.email,
};
let result = {
accessToken: sign(payload, process.env.ACCESS_SECRET, {
expiresIn: "1d",
}),
};
if (checkedKeepLogin) {
result.refreshToken = sign(payload, process.env.REFRESH_SECRET, {
expiresIn: "7d",
});
}
return result;
},
verifyToken: async (type, token) => {
let secretKey, decoded;
switch (type) {
case "access":
secretKey = process.env.ACCESS_SECRET;
break;
case "refresh":
secretKey = process.env.REFRESH_SECRET;
break;
default:
return null;
}
try {
decoded = await verify(token, secretKey);
} catch (err) {
console.log(`JWT Error: ${err.message}`);
return null;
}
return decoded;
},
};
const {USER_DATA} = require("../../db/data");
const {generateToken} = require("../helper/tokenFunctions");
module.exports = async (req, res) => {
const {userId, password} = req.body.loginInfo;
const {checkedKeepLogin} = req.body;
const userInfo = {
...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0],
};
if (!userInfo.id) {
res.status(401).send("Not Authorized");
} else {
const {accessToken, refreshToken} = await generateToken(userInfo, checkedKeepLogin);
const cookiesOption = {
domain: "localhost",
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
};
res.cookie("access_jwt", accessToken, cookiesOption);
if (checkedKeepLogin) {
cookiesOption.maxAge = 1000 * 60 * 60 * 24 * 7;
res.cookie("refresh_jwt", refreshToken, cookiesOption);
}
res.redirect("/userinfo");
}
};
module.exports = (req, res) => {
const {access_jwt, refresh_jwt} = req.cookies;
const cookiesOption = {
domain: "localhost",
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
};
res.clearCookie("access_jwt", cookiesOption);
if (refresh_jwt) {
res.clearCookie("refresh_jwt", cookiesOption);
}
res.status(205).send("logout");
};
const {USER_DATA} = require("../../db/data");
const {verifyToken, generateToken} = require("../helper/tokenFunctions");
module.exports = async (req, res) => {
const {access_jwt, refresh_jwt} = req.cookies;
const accessPayload = await verifyToken("access", access_jwt);
if (accessPayload) {
const userInfo = {
...USER_DATA.filter((user) => user.id === accessPayload.id)[0],
};
if (!userInfo.id) {
res.status(401).send("Not Authorized");
}
delete userInfo.password;
res.send(userInfo);
} else if (refresh_jwt) {
const refreshPayload = await verifyToken("refresh", refresh_jwt);
if (!refreshPayload) {
res.status(401).send("Not Authorized");
}
const userInfo = {
...USER_DATA.filter((user) => user.id === refreshPayload.id)[0],
};
if (!userInfo.id) {
res.status(401).send("Not Authorized");
}
const {accessToken} = await generateToken(userInfo);
const cookiesOption = {
domain: "localhost",
path: "/",
httpOnly: true,
sameSite: "none",
secure: true,
};
res.cookie("access_jwt", accessToken, cookiesOption);
res.redirect("/userinfo");
} else {
res.status(401).send("Not Authorized");
}
};