2022. 4. 14. 02:24ㆍ고민거리
안녕하세요
Spring Security 를 사용하면서 OAuth2.0 처리를 하시는 분들이 많을거고,
이때 Client server를 별도로 둔다고 한다면, (혹은 application 을 )
OAuth 의 redirect 처리를 어떻게 해야할까 고민하시는 분들이 많을겁니다!
저는 오늘 이 고민에 대해서 명쾌한 답변을 드리고자 블로그포스팅을 합니다 :)
(사실 제가 많은 레퍼런스를 찾아봤는데, 외부 client application 간의 OAuth 처리 redirect 관련
처리흐름을 명확히 이해해주는 글이 없었어서 많은 분들께 저처럼 깊은 삽질을 막아드리고자.. 글을 작성합니다.)
우선 현재 인증 시나리오와 몇가지 고민 예시로 독자분의 상황과 맞다면 관심있게 봐주시고,
그렇지 않다면 참고만 해주시면 될것같습니다!
시나리오 조건
Spring Security, OAuth 2.0, Cookie&Session 방식의 인증 처리,
React 와 같이 외부 Client Application 에서의 인증 요청
고민 예시
+ 추가로 별도의 OAuth 인증 필터를 커스텀하는 것이 아니라,
Spring Security 에서 제공하는 OAuth2LoginAuthenticationFilter 를 활용하시는 분들 중에
redirect 요청에 대해서 internal error 가 발생하시는 분들도 읽으시면 도움이 될겁니다
( state param 관련 에러 )
답변과 해결 방법
1) OAuth2.0 을 활용한 인증을 처리하는데, redirect 를 server 로 받게되면, client 로 다시 요청을 어떻게 보내주지?
(Q1 과 Q2 해결)
우선 이 문제가 가장 핵심입니다.
사실 이 질문에는 애초에 잘못된 가정이 설정되어있습니다.
우선 kakao 에서 제공되어지는 문서를 한번 보면
Client 가 Kakao에 로그인을 요청하고, redirect 를 서버로 주는데요 ? 라고 생각할 수 있습니다.
심지어 문서에도 "rediect_uri 로 redirect 되며" 라고 명시 되어있습니다.
위 두 정보를 통해서 우리는
" 아 Client 에서 kakao로 인증 요청을 보내면, Kakao 에서 Server 로 redirect 를 보내는구나 "
라고 생각할 수 있습니다. 물론 저도 그랬었구요.
정답은
Kakao 에서는 Client 로 resposne 를 보내게되고,
HTTP 302 요청에 Location 에 우리가 입력한 redirect 주소가 담겨져서 보내지게됩니다.
이렇게 kakao에서는 response 를 client 로 보내게되고,
이때 302 redirect 로 요청이 와서,
client 에서 주어진 Location 으로 redirect 를 처리하게되는 것입니다.
즉, 위의 Q1 질문에서는 1번이 답이 될것이고,
그림으로 표현하자면
이런 식의 처리 흐름이 될것입니다.
그리고 저는 이 결과를 client 의 loading 페이지로 redirect 시켜서 클라이언트에게 값을 전달하게 수행했습니다.
(Q2 의 답변)
2) OAuth2LoginAuthenticationFilter 에서 자꾸 에러가 발생합니다.
분명히 정상적인 인증 처리가 되어 code 가 redirect 로인해 서버로 전송되었는데,
server에서 kakao 로 code 바탕의 token 인증 요청시 자꾸 내부적으로 에러가 발생되게됩니다.
이제는 Spring Security OAuth2 관련 인증 Filter를 직접 커스텀하여 구현해야하는 경우와 아닌 경우로 나뉘어집니다.
우선 이 에러의 발생원인부터 살펴보겠습니다.
OAuth2LoginAuthenticationFilter 내부의 attemptAuthentication 메서드를 살펴보시면,
request 의 params 를 가져오고, isAuthorizationResponse() 메서드를 통해
해당 요청의 param 을 검증하는 로직을 거칩니다.
해당 메서드를 파고들어가보면
이렇게 해당 요청의 code 가 존재하는지, state 가 존재하는지 확인하고
이는 AND 연산으로, code 와 state 모두가 존재해야지 Response 를 성공했다고 판단합니다.
이때 둘 중 하나라도 없으면 다시 위의 이미지에서 if 분기 안으로 들어가기 때문에
INVALID_REQUEST 가 발생하게 됩니다.
근데 code 는 주어지니까 넘어가지는데,
대부분은 state가 뭐지? 싶으실 겁니다.
우선 state 는 csrf 를 막기 위해 별도로 제공되어지는 검증 파라미터로,
자세한 내용은 공식문서를 참고해주세요!
https://spring.io/blog/2011/11/30/cross-site-request-forgery-and-oauth2
Cross Site Request Forgery and OAuth2
<p>In this short article we look at Cross Site Request Forgery in the context of <a href="http://en.wikipedia.org/wiki/OAuth#OAuth_2.0">OAuth2</a>, looking at possible attacks and how they can be countered when OAuth2 is being used to protect web resources
spring.io
결론은 state 를 명시해주어 csrf 를 막고자 하는 Spring Security의 고정적인 Filter 때문에,
첫번째 에러 발생의 원인이 된것입니다.
(참고로 로그인 요청할때 state를 담아주면, 해당 문제는 해결이됩니다.)
다음으로는
authorization_request_not_found_error_code 에러를 만나시게 될겁니다.
해당 분기를 넘어가지 못하게 되는 것인데,
removeAuthorizationRequest 를 통해 request 와관련된 정보가 나오는데,
이때 이 값이 null 인 경우에 에러가 발생합니다.
이유는 다시 한번 파헤쳐 들어가보면
이렇게 두 메서드를 접하게 되는데요, 아래 메서드에 집중해주시면 될것같습니다.
parameter 에서 위 분기에서 해결한 state 관련한 값을 받아오는데,
값을 넣어줬다는 가정이라면, 처음 null 검증 분기는 넘어갈 것이고,
다음으로 request에서 해당 파라미터에 해당하는 value 를 지우면서 지워진 값을 originalRequest 로 받게되는데,
이때 state 가 명확하지 않다면, 혹은 state에 대한 명확한 정책이 없다면
여기서 null이 발생되어 에러가 발생하게 됩니다.
그래서 어떻게 해결하는데?
저는 server 에서 server 로 다시 redirect 하는 방식으로 이 문제를 해결했습니다.
저 인증과정에서 에러가 발생했다면,
failureHandler 를 거치게 될것이고, 이때 response 에 token 을 발급받는 요청으로 redirect 시켜주는 것 입니다.
이렇게 되면 다시 정상적으로 kakao에 code 를 바탕으로 token 발급을 요청하게되고,
이때부터는 정상적인 사용자 정보 조회처리가 이루어집니다.
그리고 이때 처리완료된 결과는 1번 질문에서의 답변처럼 redirect 를 요청한 client 에게 response 로 주어지게 됩니다.
3) cookie 의 정책은 어떻게 가져가지?
위 두 질문을 통해서는 Spring Security OAuth2 를 기반으로 발생한 에러를 해결한 것이라면,
해당 질문은 통신에 대한 위와 다른 분야의 질문입니다.
우선 chorme 에서는 현재 SameSite : LAX 를 기본값으로 권장하고 있습니다.
이때, 문제가 발생되는 상황이 있습니다.
#1 Client 와 Server 간의 도메인이 다른 경우
즉 각각의 application 서로 다른 호스트에서 실행이 되는 경우입니다.
이때 서로 다른 도메인에 대해서는 쿠키를 공유할 수 없게 됩니다.
따라서 이때 해결방법은
1. 도메인을 사고 서브 도메인을 바탕으로 쿠키를 주고받는다.
예를 들어 naver.com 이라는 도메인을 바탕으로
api.naver.com 이라는 서브 도메인을 부여한다면, 두 도메인에 대한 쿠키는 공유가 가능하게됩니다.
2. 한 인스턴스에 각각의 application을 함께 올린다.
즉 port 만 변화하여 요청을 처리하게된다. ( client : 3000, server : 8080)
#2 server 와 client 가 cookie 를 공유하려면 CORS 설정에서 credential에 대한 공유가 true 로 설정되어야합니다!!
( 추가로 여기서 제가 오래 삽질한 부분은....
credential을 true 로 설정하게되면 Origins 는 "*" 와 같이 와일드카드로 설정할 수 없다는 것입니다.
이거는 로그도 안뜨는 400에러여서 원인을 찾는데 시간이 꽤 걸렸습니다...)
결론 및 기대 효과
결론은
첫째로 Spring Security OAtuth2 의 인증 필터를 새롭게 커스텀하듯 구현하는 경우라면
redirect 에 관한 이해만 되면 oauth 인증에는 문제가 없을것입니다.
하지만 개인적인 견해로는,
여러 예외에 대한 형식적인 처리와 정형적인 로직은 오히려 서비스의 안정감을 줄 수 있으므로,
Spring Security 에서 제공하는 OAuth Authentication Filter 를 사용하는 것도 좋은 방법이라고 생각합니다.
(즉, 꼭 커스텀이 좋다라는것은 아닙니다. 별도의 handler 나 OAuth UserService 로직에 대한 커스텀만으로도 충분)
둘째로는 OAuth2LoginAuthenticationFilter를 사용하신다면,
state 와 관련되어 발생되는 예외에 대한 처리도 고민해보셔야합니다.
제가 구현한 방식이 무조건 옳다는 아니지만,
저 나름의 해결방법이라 생각되어 제 방법도 공유해드립니다.
( 저 로직에 추가로 실제 인증이 되지 않은 경우에 대한 예외처리도 Failure Handler 에서 처리를 해줘야합니다.)
셋째로 client 와의 쿠키 정책은 유심히 살펴볼 필요가 있는것같습니다.
또한 많은 분들이 JWT 토큰을 바탕으로 인증처리를 하시는데,
저는 개인적으로 보안을 생각한다면, 쿠키 세션 방식이 더 올바른 방향이지 않을까 생각합니다 :)
(Client <-> Auth Server : cookie, session / API server <-> API server : Bearer Token)
생각보다 삽질을 좀 했던 부분이었어서, 많은 분들께 도움이 되는 정보가 되었으면 합니다.
긴 글 읽어주셔서 감사합니다 🙂
'고민거리' 카테고리의 다른 글
[Spring] 첫 스프링 공식 문서 기여 🎉 (1) | 2022.06.12 |
---|---|
[Spring boot] 문제를 발견하고 공식 github에 issue를 올린 경험 (0) | 2022.04.01 |
[Spring Boot] 대용량 데이터를 처리해야하는 경우 (0) | 2022.03.05 |
[SpringBoot] 수정 기능에 대한 고찰 (0) | 2021.12.21 |