JWT 토큰 탈취시 대처법에 대한 고찰
AS-IS
사용자에게 다가가는 서비스를 만들기 위해선 보안도 반드시 고려해보아야 한다고 생각합니다
이번에 보안을 적용해볼 프로젝트는 MVVM의 단순 DB I/O 작업을 하는 서버이며, 아래와 같은 구조로 동작합니다
로그인을 통해 인가된 사용자 보낸 요청(일정 CRUD)을 서버가 응답합니다
저는 사용자를 인증하는 부분:기밀성에서의 위험을 최소화 할 수 있는 방법을 고민해 보았습니다
기존 토큰 인증 방식
로그인 - access-token 발급 - 서비스 요청 - 서버에서 access-token 검증 - 서비스 응답
//토큰 검증기
public JwtCode validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return JwtCode.ACCESS;
} catch (ExpiredJwtException e){
return JwtCode.EXPIRED;
} catch (JwtException | IllegalArgumentException e) {
log.info("jwtException : {}", e);
}
return JwtCode.DENIED;
}
위험! access-token만 탈취 당해도 공격자는 모든 서비스를 이용할 수 있다
기존 토큰 인증 방식 개선하기
토큰 탈취시 받을 수 있는 데미지를 최소화하기 위한 방법은 토큰 유효시간을 짧게 설정하는 것입니다
유효시간이 짧으면 짧을 수록 좋겠지만, 사용자는 계속해서 로그인을 해야하는 불편함이 발생합니다
이런 불편함은 리프레시 토큰을 활용해 해결할 수 있습니다
보안이 적용된 서비스의 궁극적인 목적은 사용자이기 때문에 사용자의 불편함은 고려 대상 1순위 라고 생각합니다
- Access Token의 유효 기간을 짧게 설정한다.
- Refresh Token의 유효 기간은 길게 설정한다.
- 사용자는 Access Token과 Refresh Token을 둘 다 서버에 전송하여 전자로 인증하고 만료됐을 시 후자로 새로운 Access Token을 발급받는다.
- 공격자는 Access Token을 탈취하더라도 짧은 유효 기간이 지나면 사용할 수 없다.
- 정상적인 클라이언트는 유효 기간이 지나더라도 Refresh Token을 사용하여 새로운 Access Token을 생성, 사용할 수 있음.
//사용자에게 Access Token과 Refresh Token을 함께 발급
private Token createToken() {
//Access Token
String accessToken = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + accessTokenValidTime))
.signWith(SignatureAlgorithm.HS256, accessSecretKey)
.compact();
//Refresh Token
String refreshToken = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + refreshTokenValidTime))
.signWith(SignatureAlgorithm.HS256, refreshSecretKey)
.compact();
return Token.builder().accessToken(accessToken).refreshToken(refreshToken).key(userEmail).build();
}
//access-token 재발급
public String recreationAccessToken(){
//Access Token
String accessToken = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + accessTokenValidTime))
.signWith(SignatureAlgorithm.HS256, accessSecretKey)
.compact();
return accessToken;
}
차라리 탈취를 당해버리면 어떨까?
다양한 변칙 공격이 존재하는 지금, 탈취 시도 자체를 막는 것은 한계가 있다고 생각하기 때문에, 탈취를 당한다는 가정하에 이에 대비하는 시나리오를 구축해 보았습니다
Refresh Token이 탈취 대비 시나리오
user-pk-id access-token refresh-token
1 | aaa.aaa.aaa | bbb.bbb.bbb |
2 | ccc.ccc.ccc | ddd.ddd.ddd |
- 데이터베이스에 각 사용자에 1대1로 맵핑되는 현재 사용되는 Access Token, Refresh Token 쌍을 저장한다.
- 공격자가 탈취한 Refresh Token으로 Access Token 발급 요청을 한다
- 데이터베이스에 저장된 Access Token 토큰이 아직 만료되지 않은 경우, 서버는 Refresh Token이 탈취당했다고 가정하고 두 토큰을 모두 폐기시킨다.
- 사용자는 재 로그인을 통해 다시 토큰을 발급 받는다
Access Token, Refresh Token쌍 탈취 대비 시나리오
Access Token, Refresh Token을 둘 다 탈취한다면 어떻게 할까?
이렇게 모든 경우의 수를 대비하기 위한 로직을 추가하다 보면 서버 리소스 부하는 점점 증가하게 될 것 입니다
때문에 이때는 아래 소개할 내용처럼 데이터베이스 자체 보안을 강화하는 쪽이 현명할 것이라 여겨집니다
다양한 의견을 댓글에 남겨주시면 감사하겠습니다