회사에서 혼자 독학하고 작업까지 하는지라 누구한테 물어 볼 사람도 없고하니 항상 이곳에 신세를 지게 되네요.
혹시나 짐작가시는 원인이 있으시면 도움 부탁드립니다.
구글 OAuth 인증을 통해 AccessToken 을 발급 받고 이를 이용해 구글캘린더와 연동하는 페이지를 작업하고 있습니다.
환경
앱: ReactNative(RN)를 이용한 하이브리드앱
내부 웹: Vue.js 3
상황
RN의 웹뷰의 본 사이트에서 버튼 클릭으로 추가적인 웹뷰 컴포넌트 하나 띄움
(본 사이트에서 벗어나지 않고, 인증 후 리디렉션 시 현재페이지를 새로고침 하지 않은채 UI의 일관성 유지를 위함)
추가로 뜬 웹뷰컴포넌트의 인증 페이지에서 구글 OAuth 인증 진행해서 AccessToken 과 RefreshToken 받아 옴
받은 AccessToken과 RefreshToken을 postMessage를 통해 RN로 전송
RN은 추가적으로 뜬 인증용 웹뷰컴퍼넌트를 닫고 message이벤트로 AccessToken 과 RefreshToken 기본 웹뷰의 본사이트로 넘김
기본웹뷰의 본 사이트에서 RN으로 부터 넘겨받은 AccessToken 과 RefreshToken 을 localStorage에 저장하고
AccessToken을 이용해 구글캘린더 API로 캘린더 목록을 요청해서 받아 옴.
여기 까지 문제 없음.
(혹시나 추가적인 웹뷰를 띄우고 거기에서 인증을 진행해서 문제가 발생하나 생각해봤지만 그렇다면 애초에 이 과정에서 문제가 생겨야 할 것 같은 생각이 듭니다. 여기서 구글캘린더API를 요청하는 페이지는 기본웹뷰의 사이트이기 때문)
문제 발생 상황
페이지를 나갔다가 다시 돌아와서(1~20초정도 후) 저장된 AccessToken을 이용해 구글캘린더 API로 캘린더 목록을 요청하면 인증 오류 발생
(오류코드: { "error": { "code": 401, "message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", "errors": [ { "message": "Login Required.", "domain": "global", "reason": "required", "location": "Authorization", "locationType": "header" } ], "status": "UNAUTHENTICATED", "details": [ { "@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "CREDENTIALS_MISSING", "domain": "googleapis.com", "metadata": { "service": "calendar-json.googleapis.com", "method": "calendar.v3.CalendarList.List" } } ] } })
테스트 1
처음에 AccessToken 으로 캘린더 목록을 한 번 받아오는데 사용해서 AccessToken 이 만료된건 아닌지 의심스러워서 구글 OAuth 인증을 통해 최초에 받아 온 AccessToken 을 바로 사용하지 않고,
페이지를 나갔다가 다시 돌아와서(1~20초정도 후) localStorage 저장된 AccessToken 으로 캘린더 목록 요청
-> 동일한 오류 발생
테스트 2
무조건 AccessToken 이 만료되었고 생각하고 RefreshToken 을 이용해 AccessToken 재발급 요청
-> { "error": "invalid_grant", "error_description": "Bad Request" } 오류 발생으로 재발급 안됨.
***
구글 OAuth 인증을 위한 사이트 URI나 승인된 리디렉션 URI는 모두 등록한 상태입니다.
에러메시지를 보면, access token이 누락되었다는 걸 보니 먼저 디버깅을 통해 http 요청에서 access token이 포함되어 있는지 확인해야 할거 같습니다.
코드를 직접 본게 아니라 확신을 드릴순 없지만, 가장 기초적인 부분부터 의심하자면
페이지를 나갔다가 다시 돌아와서(1~20초정도 후) ''저장된 AccessToken을 이용해'' 이 부분이 제일 의심됩니다.
구글 프레임워크 쓰라고 할거에요. 아마 RN 용으로 포팅된 것도 있지 않을까 싶네요
2. authorization code 를 이용하여 access token 을 발급받는 방법이 있고, 바로 access token 을 발급 받는 방법(google 은 implicit grant model 이라고 표기)이 있습니다. 사용하신 방법은 후자이며, 이 경우 토큰 수명이 극단적으로 짧으며 저장하면서 사용하는게 아닙니다.
전자는 "추가로 뜬 웹뷰컴포넌트의 인증 페이지에서 구글 OAuth 인증 진행해서" token 이 아니라 code 란걸 받아옵니다.
이 code 를 먼저 Tigirls 님의 서버로 전송하고, 서버에서 code 와 oauth 인증 정보를 만들면 구글이 주는 secret 정보와 함께, 구글에 다시 전송해서 access_token 을 발급 받을 수 있습니다.
이 토큰은 수명이 길며 저장해서 반복 사용할 수 있습니다.
https://developers.google.com/identity/oauth2/web/guides/choose-authorization-model?hl=ko
구글 문서에서 비교한걸 참고해보세요.
참고로 서버가 없이 브라우저 혹은 RN에서 돌아가는 코드로 Code -> token 발급 절차를 밟는건 보안상의 이유로 하면 안될겁니다. 서버에서 발급받은걸 클라이언트에 다시 내려주는건... 잘 모르겠네요. 상관없나?
여튼 지금 방식대로 쓰시려면 문서를 봤을 때 api 요청을 보낼 때마다 토큰을 발급받는게 구글의 의도인 것 같습니다.
바로 access token을 받는 방법이 있었군요
무심코 그 부분이 생략되었다고 생각 했었네요
좋은걸 배워가네요!
클라이언트에서 발급받아 클라이언트에서 바로 사용할 수 있다보니 보안 이슈로 인해 이 토큰은 되는게 별로 없는 편이죠...ㅋㅋㅋ
(내용: 현재 OAuth 2.0 클라이언트 ID당 Google 계정당 갱신 토큰 100개로 제한됩니다. 한도에 도달하면 새 갱신 토큰을 만들면 경고 없이 자동으로 가장 오래된 갱신 토큰이 무효화됩니다. 이 한도는 서비스 계정에는 적용되지 않습니다.)
이게 어플리케이션 1개당인지 사용자 1명당 인지 애매한데...
만약 어플리케이션 1개당이면 여러 사용자가 쓰기엔 무리일것 같아서 말씀해주신 방법으로 변경을 해보고 싶은데 질문좀 더 드려도 될까 모르겠네요^^;
구글 인증 진행해서 code란걸 받아와서 서버로 던져주면 서버에서 최종적으로 accessToken만 발급받아서 클라이언트에 던져주고 이걸로 클라이언트에서 필요한 api통신에 이용하나요?
아니면 서버에서 accessToken으로 필요한 api통신까지 모두 하고 최종적인 데이터만 클라이언트로 전송하나요?
“클라이언트 ID당 Google 계정당 갱신 토큰 100개” 면 상관없는거 아닌가요?
영어 원문으로 한번 확인해보세요!
보통은 서버에서 api호출을 해서 결과만 내려주는 방법이 일반적이고 많이 쓰입니다.
토큰이 탈취될 보안 위험을 줄이기 위함이라던가,,
cors 문제를 겪지 않기 위해서라던가,,
그래서 토큰이나 api 호출에 필요한 키를 서비스에 따라서는 클라이언트에게 내려주면 안되는 경우도 있구요.
애초에 클라이언트에서 바로 access_token 이 바로 발급되는 구현은 이러한 문제가 발생할 수 있어서 그만큼 권한이 부족하거나, 시간이 짧거나 등으로 구성됩니다.
하지만 반대로, 괜히 서버에 사용자 계정에 대한 권한을 주지 않고 모든 작업은 내 폰에서만 일어나는게 오히려 보안상 좋을 수도 있겠죠.
토큰을 저장한 서버가 털릴 수도 있으니까요.
그래서 상황에 맞게 쓰시면 됩니다.
자답입니다.
받아온 토큰을 localStorage 에 저장했다가 가져오는 과정에서 토큰 앞뒤에 따옴표(")가 붙어서 토큰값이 달라져서 생기는 일이었네요.ㅜㅜ
하룻동안 해멨는데 이런 문제라니...;;;
혹시나 저 같이 바보같은 실수를 하는분이 없으시길 바라면서 질문은 지우지 않고 두겠습니다.