스프링 시큐리티 - OAUTH2
[ 용어 사항 ]
엔드 유저
서비스 요청을 하는 실제 어플리케이션 단에서 사용자로 서비스 이용 고객으로 생각하면 된다.
클라이언트 서버
React나 Flutter 등으로 만들어진 UI 어플리케이션으로 직접 엔드 유저와 상호작용하는 어플리케이션 서버라고 생각하면 된다.
인증 서버
OAUTH2 인증 시스템에서 인증 관련 정보만 처리해주는 서버라고 생각하면 된다.
리소스 서버
OAUTH2 인증 시스템에서 사용자에 관한 데이터를 가지고 있는 서버이다. 예를 들면 티스토리라면 사용자의 모든 글을 가지고 있는 서버라고 생각하면 된다.
[ OAUTH2가 필요한 이유 ]
같은 기업 내의 서비스에서 OAUTH2의 장점
한 기업 내에 OAUTH2를 사용하면 서비스가 확장될 때마다 사용자가 각각의 서비스 별 인증을 위해 ID, PASSWORD로 회원가입을 할 필요가 없다. 예를 들어 구글에서는 구글에 가입만 하고 로그인만 하면 YouTube와 Gmail 등을 추가적인 로그인 없이 이용할 수 있다. 그 이유는 OAUTH2 기술을 이용해 공통 AUTH 서버를 두고 각각의 리소스별 서버를 두었기 때문이다. 따라서 사용자들에게 편리함을 줄 수 있다.
다른 기업과의 서비스에서 OAUTH2의 장점
만약 누군가가 인스타그램 글 분석을 해주는 어플리케이션을 만들었다고 해보자. OAUTH2가 없었을 때에는 인스타그램의 글을 가져오기 위해서는 엔드 유저가 믿을 수 있는 글 분석 어플리케이션 서비스 제공자에게 인증서를 제공하고 서비스 제공자는 받은 인증서를 통해 리소스 서버에 요청을 보낸다. 이는 실제로 엔드 유저가 인스타그램에 접속한 것과 동일한 권한을 가지며 믿을 수 없는 제 3자가 이런 서비스를 제공하는 경우 인증서를 통해 계정을 마음대로 사용할 수 있는 위험이 있다.
그러나 OAUTH2를 사용하면 이런 문제에서 벗어날 수 있다. 엔드 유저가 클라이언트에게 요청을 하면 클라이언트 서버는 인증 서버로 엔드 유저를 리다이렉션 시켜버린다. 이 때, 인증 서버에서 적절하면 클라이언트에 리소스 서버에 접근할 수 있는 권한이 사항과 접근이 가능한 엑세스 토큰, 엑세스 토큰이 만료됐을 때 재발급이 가능한 리프레시 토큰을 준다. 이를 실제 예시로 생각해보면 깃허브 사이트에서 로그인할 때 구글로 할 수 있다. 이 때 깃허브 사이트는 구글로 리다이렉션 시키고 로그인을 해본 경험이 있을 것이다. 이 때 일어나는 과정을 설명했다고 보면 된다.
[ OAUTH2 프레임워크 grant types ]
OAUTH2 프레임워크에서는 어떻게 위에서 설명한 내용을 어떻게 제공하고 있나가 궁금할 것이다. OAUTH2에서는 다양한 grant types를 제공하는데 이 각각의 grant types 방법을 알아보자.
AUTHORIZATION CODE GRANT TYPE
엔드 유저가 직접적으로 참여하는 형태로 특징은 인증과정에 엔드 유저, 클라이언트 서버, 인증 서버, 리소스 서버가 모두 참여한다. 아래와 같은 그림의 과정으로 전체 프로세스가 일어난다. 자료는 직접 들었던 강의에서 가지고 왔다. 난 이 과정을 3단계로 나누어 보려고 한다.
1. 엔드 유저의 리소스 요청과 인증서 제공
엔드 유저가 리소스에 접근하고 싶다고 클라이언트 서버에 요청을 한다. 그러면 클라이언트 서버는 인증 서버에서 인증을 해야 하니 인증 서버로 리다이렉션 시켜버린다. 이 때 아래와 같은 정보를 보낸다. 인증 서버에 서비스 등록을 할 때 발급 받은 client_id, 인증 서버에서 인증서를 입력할 수 있는 url, 접근 권한 정보가 담긴 scope, 인터넷 브라우저 내에서 이루어지므로 csrf 공격을 방지하기 위한 토큰인 state가 있다.
그러면 사용자는 인증 정보를 username과 password로 리다이렉션된 페이지에 입력함으로써 인증서를 인증 서버에 제공한다.
2. 인증서버와 클라이언트 사이의 검증
csrf 때의 설명처럼 웹 브라우저 내에서 엔드 유저가 요청하는 내용은 어떤 csrf 공격이 있을 수 있다. 중간에 누가 이상한 사이트를 만들어서 접근하는 것인지 아닌지 확인을 하기 위해서 받은 URL 내의 쿼리 내부에 있는 csrf 토큰과 함께 비교하는 과정이 일어난다. 또한 클라이언트가 인증 서버에 등록할 때 받은 인증서를 제공해서 신원이 확인된 클라이언트의 요청임을 확인하는 과정이 일어난다. 그리고 임시 발급 인증 코드까지 확인한다. 아래의 이미지는 임시 인증 코드 타입, 임시 정보 클라이언트 인증서 정보, 이전에 받은 인증 정보가 담겨 있다.
이 모든 교환이 끝나고 모든 테스트가 통과하면 인증서버에서는 리소스 서버로 접근할 수 있는 엑세스 토큰과 리프레시 토큰을 제공한다. 엑세스 토큰은 임시적인 시간 동안 사용할 수 있는 토큰으로 인증 서버에서 설정한 시간이 지나면 리프레시 토큰을 통해 재발급 받아야 한다.
{
"token_type": "Bearer",
"expires_in": 86400,
"access_token": "lo_Bko9Ag5Q7l4U1X1f_YF279OUciU5A1BBZZII4FvE3ZGsKMUhDgrou9TNINCkgepYiRc6w",
"scope": "photo offline_access",
"refresh_token": "ctgXq7vSmUsTTMapy9IZzBDC"
}
3. 리소스 서버와 클라이언트 서버 사이의 정보 교환
엑세스 토큰을 가지고 클라이언트 서버는 리소스 서버에 본인이 가진 scope에 해당하는 권한만큼만 행동을 할 수 있다. 리소스 자원에 접근해 이제 권한 내의 모든 데이터를 받을 수 있다.
개인 의문
그런데 여기서 조금 궁금한 내용이 생겼었다. 그러면 이 토큰이 노출되면 보안적인 문제가 발생하는거 아닌가? 특히 엑세스 토큰은 시간이 짧아서 만료되면 문제가 없지만 리프레시 토큰이 뻇기면 리프레시 토큰으로 새로 엑세스 토큰을 발급 받으면 보안적으로 문제가 있는게 아닌가 싶었다.
보안적으로 생각을 해봤을 때 엑세스 토큰이나 리프레시 토큰 중 하나가 뺏기는 건 상관이 없다고 한다. 이는 레디스나 MongoDB를 인증 서버에 사용하는 이유이기도 한데, 인증 서버에서는 엑세스 토큰과 리프레시 토큰이 쌍으로 저장이 된다고 한다. 그래서 처음 인증 서버에서 제공할 때의 쌍이 함께 DB에 저장이 된다. 만약 엑세스 토큰이 뺏긴거라면? 인증 시간이 지나면 무효화되고 시간이 짧기 때문에 그 시간 이후에는 엑세스 토큰이 무효화되므로 어느정도의 안전을 보장한다. 만약에 리프레시 토큰이 뺏기면? 리프레시 토큰으로 엑세스 토큰을 인증 서버에서 재발급까지는 가능하다. 하지만 리소스 서버 내로 접근을 했을 때 DB 내에 저장된 엑세스 토큰과 내용이 다르기 때문에 공격자의 행동일 거라 생각하고 요청을 거부한다.
엑세스 토큰 만료 시 동작 과정
아마도 아래의 방식으로 인증을 제공받기 때문이 아닐까 싶다. 아래처럼 엑세스 토큰이 만료된 상태로 클라이언트 서버가 리소스 서버에 요청을 하면 클라이언트는 요청 거부를 받는다. 이 때 클라이언트는 즉시 본인이 가진 리프레시 토큰을 인증 서버로 요청을 보내고 새로 리프레시 토큰과 엑세스 토큰을 발급 받는다. 이렇게 만료가 되더라도 재발급 받은 토큰으로 리소스 서버에 접근할 수 있다.
인증 서버와 리소스 서버 간의 데이터 교환
그럼 어떻게 리소스 서버가 인증 서버에서 발급한 내용을 알 수 있을까 싶다. 둘이 직접 연결하는 방법도 있지만 극러면 인증 서버에 과도한 부하가 일어난다. 또한 DB를 같이 쓰는 방법도 있기는 하나 이 또한 DB에 부담이 많이 간다. 따라서 리소스 서버가 시작할 때 인증 서버와 함께 공개키를 서로 주고 받는다. 그러면 둘이 같은 공개키를 가지고 있으니 JWT 토큰으로 날아오는 엑세스 토큰을 같은 키로 검증할 수 있다. 그래서 서로 같은 DB를 쓸 필요도 없고 계속 서로 요청을 주고 받을 필요도 없다. 이 방식이 제일 많이 쓰인다고 한다.
IMPLICIT GRANT TYPE
위에서 여러 csrf 공격 등을 막기 위해 중간 과정이 있는데 이걸 생략하면 어떻게 될까? 결과만 말하면 그 방식이 IMPLICIT GRANT TYPE이며 보안에 취약해지고 정보가 탈취당할 가능성이 매우 높아진다. 옛 레거시 프로젝트에서는 쓰였겠지만 보안상의 이슈로 권장하지 않기에 OAUTH 2.1부터는 완전히 삭제되는 방식이라고 한다. 또한 AUTHORIZATION CODE 방식과 다르게 URL의 파라미터로 엑세스 토큰이 날아오기 때문에 보안에 매우 취약하다. 결과창 URL이 GET 요청에 의한 응답으로 다음처럼 나오기 때문이다.
https://oauth.com/playground/implicit.html
#state=rkOHCa2YcMZ7tmCQ
&access_token=Jost79WePxoCNuFnmhwtgt8gFwQMJiupQRxA2yd-Ly9EMqie2L6w-NqeYvGDMvCIeRVWK8Ah
&token_type=Bearer
&expires_in=86400&scope=photos
IMPLICIT GRANT TYPE처럼 GET으로 요청을 해서 받아오면 문제가 URL은 데이터가 노출되고 암호화가 제대로 되어있지 않다. 또한 브라우저 내에 캐싱이 되거나 브라우저 히스토리에 남는 등의 문제가 발생한다. 하지만 AUTHORIZATION CODE에서는 POST 요청으로 데이터를 보낸다. 또한 HTTPS 프로토콜을 통해 전송을 하면 헤더와 바디를 암호화해서 전송할 수 있기 때문에 상대적으로 안전하고 POST 요청은 따로 브라우저 내에 저장되는 게 없어서 추가적으로 더 안전하다.
CLIENT CREDENTIALS GRANT TYPE
엔드 유저 없이 클라이언트 서버가 자체적으로 인증 서버에게 리소스 서버의 자료를 쓰고 싶다고 요청을 할 수 있다. 이 때는 CLIENT CREDENTIALS 방식을 사용하며 엔드 유저가 딱히 관여하지 않고 웹 브라우저를 사용하지 않기 때문에 따로 보안과정을 크게 추가하지 않아도 돼서 과정이 훨신 짧다. 엔드 유저와 중간 보안과정이 빠졌을 뿐 매우 비슷하므로 추가적인 설명은 하지 않도록 하겠다.
[ 참고 자료 ]
Spring Security 6 초보에서 마스터되기 최신 강의! - Eazy Bytes