배경
기존에 운영하던 서비스의 인증 방식을 바꾸며 Google oAuth와 관련된 작업을 하게 되었습니다.
여러 고민과 결정된 사안에 대해 기록을 남기고 공유하고자 합니다.
상황
기존 방식은 클라이언트에서 Firebase 기반으로 로그인을 구현해 두었습니다.
최초 인증만 자체 서버에서 실행하고, Firebase custom token을 반환하여 해당 token을 통해 인가가 수행되도록 구현했습니다.
당시에는 빠르게 개발할 수 있고 custom token을 firebase 대시보드에서 관리할 수 있다는 이점으로 인해 위와 같은 결정을 했지만, 다른 oAuth 인증 공급자(ex. google, apple, kakao 등)가 추가될수록 어려움을 겪었습니다.
기존 구조에서는 최초 인증 이후로 Firebase 인가 토큰이 만료될 때까지, oAuth 인증 공급자와 의사소통이 전무했습니다.
따라서, oAuth 인증 공급자로부터 access token과 refresh token이 만료되어도 알 수 없었고, 개인정보의 변경사항도 파악이 어렵게 되었습니다.
여정
위 문제를 해결하기 위해 Firebase custom token 기반의 인가를 제거하고 직접 관리하는 리소스 서버에서 인가를 위한 access token을 발행하는 방식으로 poc를 진행했습니다. (가장 일반적인 방식의 oAuth 구현이라고 할 수 있겠습니다.)
앱과 웹의 공존
저희 앱은 Flutter로 구현되어 있습니다.
Flutter로 Google 로그인을 구현하기 위해서는, GoogleSignIn이라는 패키지를 사용하는 것이 일반적입니다.
GoogleSignIn.signIn() 메서드는 구글 인증 서버에서 사용자에 대한 인증이 완료되면, 인증 정보를 반환합니다.
그중 중요한 정보는 다음 두 가지입니다.
- serverAuthCode
- 다른 oAuth 문서에서는 Authorization Code(인가 코드)라고 부르는 코드입니다.
- idToken
- 구글에서 제공하는 jwt 토큰으로, 사용자에 대한 정보를 가지고 있습니다.
- Future<GoogleSignInAuthentication> 타입인 authentication 필드에 접근해야 확인 가능한 필드입니다.
여기서 2가지 선택지가 존재합니다.
- serverAuthCode을 서버로 보내서 인증을 처리한다.
- 앱 내부에서 idToken을 검증하여 인증을 처리한 후 결과를 서버로 보낸다.
결론적으로는 전자를 선택했는데, 그 이유는 다음과 같습니다.
- 웹에서 구글 로그인을 연동할 때, 인증 로직을 재사용 가능해야 한다.
- 구글 서버로 요청을 보내는 주체를 자체 서버로 국한할 수 있다.
- 상대적으로 기기 자체 보안이 있는 앱과 달리, 웹은 보안 강도가 약하다.
위와 같은 의사결정을 가지고 구현 단계로 넘어가게 되었습니다.
구현
앞서, Flutter에서의 구글 로그인 구현 방식은 간단하게 설명드렸습니다.
oAuth에 대한 이해가 없어도 충분히 구현할 수 있는 부분이기에 추가 설명은 생략하겠습니다.
먼저 serverAuthCode를 서버로 보냅니다.
서버는 클라이언트가 전해준 serverAuthCode (인가코드)가 정상적인 코드인지 확인하기 위해 구글 인증 서버에 요청을 전송합니다.
request [get] https://oauth2.googleapis.com/token
위 요청이 성공적으로 처리되었다면, 응답 body에서 id_token 필드를 찾을 수 있습니다.
위에서 언급한 바와 같이 idToken은 jwt이므로 verify 절차가 필요합니다.
python에서는 google.oauth2 패키지에서 verify_oauth2_token 메서드를 제공합니다.
verify_oauth_token의 사용법은 다음 문서를 참고했습니다.
[GOOGLE] Authenticate with a backend server
검증이 완료된 id_token의 payload는 다음과 같은 형태를 가집니다.
{
"iss": "공급자 (ex. <https://accounts.google.com>)",
"azp": "인증을 위임한 Client ID",
"aud": "인증을 요청한 Client ID",
"sub": "사용자의 고유 ID",
"email": "사용자의 email",
"email_verified": "이메일 인증 여부",
"at_hash": "/token api에서 반환된 access_token의 무결성 검사를 위한 필드",
"name": "사용자의 이름",
"picture": "사용자의 프로필 이미지 url",
"given_name": "?",
"iat": "토큰 발급일",
"exp": "토큰 만료일"
}
위 payload에서 서비스에 필요한 사용자 정보만 추려서 자체 db에 적재합니다.
또한 구글 사용자의 고유 ID인 sub 필드도 놓쳐서는 안 되겠습니다.
인가를 위한 access_token, refresh_token 발급 방식은 일반적인 방식과 크게 다르지 않게 구현했습니다.
마무리
google oauth 기능을 개편하며 고민한 내용에 대해서 남겨보았습니다.
redirect를 고려해야 하는 웹 서비스와는 달리 앱 서비스는 상대적으로 어렵지 않게 구현이 가능합니다.
이후 apple 로그인과 웹 사이트와의 연동도 남겨두고 있는 만큼 이후 oauth 시리즈로 또 찾아뵙겠습니다.