모각소 3주차
JWT
= 정보 보호 X, 인증 O, 위조 방지 O → 서버는 유효한 토큰인지 확인하는 것이 중요함
-
선택적 서명 및 선택적 암호화를 사용하여 데이터를 만들기 위한 인터넷 표준
-
유저를 인증하고 식별하기 위한 토큰 기반 인증
-
토큰은 세션과 달리 서버가 아닌 클라이언트에 저장됨 → 메모리나 스토리지를 통해 세션을 관리했던 서버의 부담을 줄여줌
-
JWT를 사용하면 RESTful과 같은 Stateless환경에서 사용자 데이터를 주고 받을 수 있다.
-
토큰을 클라이언트에 저장하고 요청시 단순히 HTTP 헤더에 토큰을 첨부하는 것만으로도 단순하게 데이터를 요청하고 응답을 받을 수 있다.
핵심적 특징
- 토큰 자체에 사용자의 권한 정보나 서비스를 사용하기 위한 정보가 포함됨 (self-contained)
- 데이터가 많아지면 토큰이 커질 수 있음 → 토큰이 한 번 발급된 이후 사용자의 정보를 바꾸더라도 토큰을 재발급하지 않는 이상 반영되지 않음
JWT 사용 순서
- 클라이언트 사용자가 아이디, 패스워드를 통해 웹서비스 인증 (로그인)
- 서버에 서명된 JWT를 생성하여 클라이언트에게 응답으로 돌려줌 (클라이언트가 토큰 저장)
- 클라이언트가 서버에 데이터를 추가적으로 요구할 때 JWT를 HTTP Header에 첨부
- 저장해둔 토큰을 포함시켜서 서버에게 요청보냄
- 서버에서 클라이언트로부터 온 JWT 검증
JWT 구조
- Header
- JWT에서 사용할 Type & Hash Algorithm 종류
- Payload
- Signature
- Header, Payload를 Base64 URL-safe Encode를 한 이후
- Header에 명시된 해시함수 적용하고 개인키 (Private Key)로 서명한 전자서명이 담겨있음
ex) 전자서명 알고리즘으로 타원 곡선 암호화 (ECDSA)를 사용한다고 가정
- 전자서명 : 비대칭 암호화 알고리즘 사용
- 암호화를 위한 키 != 복호화를 위한 키
- 암호화(전자서명) = 개인키
- 복호화(검증) = 공개키
Sig = ECDSA (
SHA256(B64(Header).B64(Payload)),
PrivateKey
);
이 값을 JWT로 표현
→ 위에서 만든 전자서명도 Base64 URL-safe Encode로 처리해서 합쳐야 함
JWT = B64(Header).B64(Payload).B64(Sig);
JWT 장점
- Header와 Payload를 사용하여 Signature를 생성 → 데이터 위변조 막을 수 있음
- 인증 정보에 대한 별도의 저장소가 필요없다.
- 필요한 모든 정보를 자체적으로 가지고 있음 (self-contained)
- 토큰에 대한 기본 정보
- 전달할 정보
- 토큰이 검증됨을 증명하는 서명
- 서버는 무상태 (Stateless)가 되어 서버 확장성이 우수해질 수 있음
- 토큰 기반으로 다른 로그인 시스템에 접근 및 권한 공유가 가능함
- OAuth : 페이스북, 구글 등 소셜 계정을 이용하여 다른 웹서비스에서도 로그인 가능
- 모바일 어플리케이션 환경에서도 잘 동작
JWT 단점
- self-contained
- 토큰 자체에 정보를 담고 있으므로 양날의 검이 될 수 있다
- 토큰 길이
- 토큰의 Payload에 3종류의 Claim을 저장함
- 정보가 많아질수록 토큰의 길이가 늘어남 → 네트워크 부하
- Payload 인코딩
- Payload 자체는 암호화된 것이 아님 = Base64로 인코딩 된 것 → 중간에 Payload를 탈취하여 디코딩하면 데이터를 볼 수 있음 → Payload에 중요한 데이터를 넣으면 안됨
- Store Token
- stateless 특징을 가지고 있음 → 클라이언트 측에서 토큰을 관리하고 저장함 → 토큰을 탈취당하면 대처하기 어려움
REST API 인증처리 (JWT 생성 & 검증)
(1) JWT 모듈 설치
// jsonwebtoken 모듈 설치
npm install --save jsonwebtoken
// 모듈 설치하면 자동으로 package.json 파일에 추가됨
"dependencies":{
"jsonwebtoken" : "^8.5.1"
}
(2) 비밀키 모듈 생성
JWT 서명을 생성하기 위해서 비밀키가 필요함 → 외부 노출되면 안됨 → .gitignore
let jwtObj = {};
jwtObj.secret = 'apple';
module.exports = jwtObj
(3) REST API 생성 - JWT Sign (토큰 생성) - sign() 메소드
routes/index.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
jwt.sign(
payload,
secret_Or_privateKey,
[options, callback]
);
jwt.sign(
{foo: 'bar'},
cert,
{algorithm: 'RS2556'}, function(err, token){console.log(token);}
);
let token = jwt.sign(
{
userIdx: userInfo[0].userIdx,
isKeep: isKeep,
},
secret_config.jwtsecret,
{
expiresIn: expiresIn,
subject: "userInfo",
},
);
(4) JWT 권한 확인 - verify() 메소드
routes/index.js
jwt.verify(
token,
secret_Or_privateKey,
[options, callback]
);
- 첫 번째 인자 = token
- 인증된 유효한 토큰인지 확인해야하므로 토큰이 필요함
- 토큰 : 쿠키에 저장되어 있음 → 요청 객체에서 cookies 속성을 참조 → express는 자동으로 cookieparser 미들웨어가 등록되어 있음 → 회사 코드 : 로컬 스토리지에 토큰 저장해둠
- 두 번째 인자 = secret key
- 디코딩하기 위해서 인코딩에서 사용한 secret key가 필요함
!주의!
JavaScript의 비동기 처리로 인해 callback이 호출되기 전에 처리가 완료될 수 있습니다.
→ jwt.sign() 메서드를 Promise로 처리하고 async와 await로 동기 처리되게 해야 합니다.
Access Token & Refresh Token
- JWT token
- 토큰 자체가 HTTPS를 사용하지 않는 환경이거나 기타 보안 취약점으로 인해 노출되었을 때 어떻게 이 문제를 해결할 것인가?
- 토큰 유효 기간 = 보통 1일
- 인증 토큰을 탈취당했을 때, 어떻게 피해를 최소화할 수 있을까? → Refresh token
- 서버의 리소스에 접근할 때 클라이언트 본인을 인증할 수 있는 Access Token으로 동작한다.
문제 1 ) JWT 유출 문제
-
문제점 : JWT는 Stateless한 방식 → 서버측에서 이 토큰을 가지고 있는 클라이언트가 정말 클라이언트 본인인지 토큰을 탈취한 사람인지 확인할 수 없다
-
해결법 : Refresh Token
- 목적 : 사용자 인증 X, Access Token 생성
-
Access Token의 유효 기간은 짧게 설정
-
Refresh Token의 유효 기간은 길게 설정
-
사용자는 Access Token과 Refresh Token 둘 다 서버에 전송 & 전자로 인증 & 만료되었을 시 Refresh Token을 이용하여 새로운 Access Token을 발급받음
-
토큰을 탈취한 사람은 Access Token의 짧은 유효 기간이 지나면 사용할 수 없음
-
정상적인 클라이언트는 Access Token 유효 기간이 지나도 Refresh Token을 이용하여 새로운 Access Token을 생성
→ 짧은 시간동안만 토큰을 사용할 수 있음
→ 주기적으로 재발급 = 토큰이 유출되어도 피해를 최소화
문제 2 ) 정상적인 클라이언트도 짧은 주기마다 다시 로그인해서 Access Token을 재발급받아야 함
- 유효 기간이 긴 Request Token을 사용함
- 정상적인 사용자는 Access Token이 만료되었을 때 → 서버측에 Request Token을 전송
- 다시 로그인 안하고 서버에게 새로운 Access Token을 발급다을 수 있음
문제 3 ) Refresh Token의 탈취
-
Refresh Token 자체가 탈취당함 = 공격자가 Refresh Token의 유효 기간만큼 Access Token을 생성할 수 있음 → 서버의 검증 로직이 필요함
-
DB에 각 사용자의 Acces Token-Refresh Token을 1대1 매핑하여 저장함
-
정상적인 사용자 : 기존의 Access Token으로 접근 → 서버 : DB에 저장된 Access Token과 비교 & 검증
-
공격자 : 탈취한 Refresh Token으로 새로운 Access Token 생성 → 서버 : DB에 저장된 Access Token과 다른 것을 확인
-
DB에 저장된 Access Token이 아직 만료가 안된 상황 & 전송받은 Access Token이 DB에 저장된 Access Token과 다른 경우
- 굳이 새로 발급할 필요가 없는데, 왜 다르지 ? == 탈취당했다고 생각
- Refresh Token이 탈취당했다고 가정 → 두 Token 모두 만료시킴
-
정상적인 사용자 : 자신의 Token이 만료됨 = 다시 로그인
-
공격자 : Refresh Token이 만료되었으므로 정상적인 사용자의 리소스에 접근 불가
문제 4 ) 공격자가 Access Token을 먼저 생성
- 공격자가 Refresh Token을 탈취
- 정상적인 사용자가 Access Token을 다시 발급받기 전에 공격자가 먼저 Access Token을 생성 → Access Token 충돌 → 서버 : 두 토큰 모두 폐기
- ietf 문서의 권장
- Refresh Token 유효 기간 = Access Token 유효 기간
- 사용자가 한 번 Refresh Token으로 Access Token을 발급받았으면 Refresh Token도 다시 발급받기
문제 4 ) Access Token & Refresh Token 둘 다 탈취
- 방법이 없음
- FrontEnd & BackEnd에서 로직을 강화하여 토큰이 유출되지 않도록 보완해야 함
Token의 저장 장소
- 서버 : DB
- 클라이언트 : 쿠키, 로컬 스토리지 등등
- http-only 속성이 부여된 쿠키에 저장하는 것을 권장 → 우리 회사는 로컬 스토리지 → http-only 속성이 부여된 쿠키는 JavaScript 환경에서 접근할 수 없음 → XSS, CSRF 발생해도 쿠키가 노출되지 않음
- 일반적인 쿠키, 로컬 스토리지는 JavaScript에서 자유롭게 접근 가능 → 보안 측면에서 권장되지 않음