본문 바로가기

web

React 개발자가 구글 oAuth를 이해하기

 

3년 전, NextAuth를 이용해서 구글 로그인을 처리한 적이 있습니다. 그 당시 oAuth를 이해했다고 생각했는데, 이번에 애플과 구글로그인을 클라이언트 사이드에서 구현하면서 어려움을 겪었습니다. 사실 NextAuth가 code 발급과 token, 그리고 JWT를 관리해 줘서 내부에서 어떤 일이 일어나는지 자세히 알지 못했습니다.

서비스의 백엔드 서버에서 토큰을 발급받고, 모바일도 함께 처리하다보니 Access Token과 Refresh Token을 클라이언트에서 관리해야 했습니다. 이걸 어떻게 처리해야 할지 몰라 한동안 헤맸습니다. 이번 기회에 클라이언트에서 토큰을 활용한 로그인 관리와 구글 서버, 프론트엔드, 백엔드 각자의 역할과 토큰들을 정리해 보았습니다.

 

oAuth 시스템이 잘 이해가지 않을 때, 이런 고민이 있었습니다.

  • 굳이 프론트에서 Authorization Code를 받아서 백엔드로 넘기는 이유가 뭐지?
  • 우리 서비스처럼 백엔드에서 Access Token과 Refresh Token을 프론트로 넘기면, 보안이 취약한 프론트에서는 잘 관리할 수 있을까?
  • 모바일도 Access Token과 Refresh Token을 직접 관리하지 말고, 웹처럼 백엔드에게 JWT를 받아서 인증에 쓰면 안 되나?

이러한 고민들은 oAuth 시스템을 이해하면서 자연스레 해소되기 시작했습니다. 

 

우리 서비스에서 구글 oAuth 인증 방식

  1. 사용자가 서비스 페이지에서 구글 로그인 버튼 누름.
  2. 아래 정보와 함께 구글 로그인 페이지로 리디렉션.
    client_id: 서비스의 클라이언트 ID
    redirect_uri: 로그인 후 되돌아올 서비스 URL
    response_type: 인증 후 응답 타입 (Authorization Code, ID Token 등)
    scope: 요청하는 정보
  3. 사용자의 구글 로그인. 처음이라면 서비스가 요청하는 정보에 대해 제공 동의.
  4. Authorization Code와 함께 클라이언트의 Redirect URI로 이동.
  5. Authorization Code를 서버에 전달하고 로그인 요청.
  6. 서버는 구글 서버에 Authorization Code를 보내서 인증하고, Access Token과 Refresh Token을 받아와서 클라이언트에 응답.
  7. 클라이언트는 Access Token을 쿠키에 심어, 서버에 요청할 때마다 사용자 인증.

 

oAuth 시스템에서 각자의 역할

프론트엔드

  • 사용자가 구글에 로그인할 수 있게 에스코트(구글 로그인 페이지 리디렉션).
  • 사용자의 로그인 정보를 서버에 전달.
  • 사용자가 로그인 상태를 유지.

백엔드

  • 클라이언트로부터 전달받은 Authorization Code(사용자 정보 접근 권한)를 구글 서버에서 인증하고, Access Token(사용자의 구글 정보 사용 보증서)과 Refresh Token을 받아서 관리.

구글 서버

  • 구글이 보유하고 있는 사용자의 데이터 및 서비스 접근 권한을, 사용자의 동의하에 해당 서비스가 이용할 수 있도록 인증서(Access Token)를 발급.
※ Access Token, 그리고 서버가 이를 받아온다는 의미
Access Token은 사용자의 구글 데이터를 우리 서비스가 이용할 수 있음을 구글이 인증하는 보증서입니다.
서버가 Authorization Code를 구글에 보내고 Access Token을 받아오는 것은 사용자의 구글 정보를 우리 서비스 서버에서 직접 관리한다는 공식적인 인증 권한을 확보하는 거죠.
서버에서 구글에 Authorization Code를 보낼 때는 서비스의 Client ID와 Secret도 함께 보내서 인증하는데, Code는 해당 사용자 정보에 대한 접근을, 서비스의 Client ID와 Secret은 이용할 서비스 측에서 나왔음을 인증하는 것이라 볼 수 있습니다.

 

 

헷갈리기 쉬운 Authorization Code와 ID Token

클라이언트에서는 사용자의 구글 로그인 후, response_type을 통해 ID Token이나, Authorization Code를 받을 수도 있습니다. 우리 서비스 서버에서는 전달받을 변수 명이 id_token이지만, Access Token 발급을 서버에서 처리하기 때문에 Authorization Code를 전달하였습니다.

// Authorization Code
const authUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${GOOGLE_CLIENT_ID}
&redirect_uri=${GOOGLE_REDIRECT_URI}&response_type=code&scope=email%20profile`;

// ID Token
const authUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${GOOGLE_CLIENT_ID}
&redirect_uri=${GOOGLE_REDIRECT_URI}&response_type=id_token&scope=email%20profile&nonce=random_nonce_value`;
  Authorization Code ID Token
사용 방식 서버에서 Authorization code를 인증 처리하고, 받아온 Access Token으로 구글의 데이터 및 API를 사용 가능. 클라이언트에서 구글 서버로부터 사용자의 구글 데이터를 받아서 활용하는 방식.
구글의 API를 사용할 수 없음.
구글 사용자 정보 접근 직접 사용자 정보에 접근 불가 사용자 정보를 포함 (JWT 형식)
목적 서버에서 Access Token으로 교환 사용자의 신원을 검증

 

 

 

 

Access Token과 Refresh Token

서비스를 개발하면서 두 가지 문제가 발생했습니다. 하나는 서버에서 Access Token 뿐만 아니라 Refresh Token을 클라이언트로 전달하는 문제, 그리고 또 하나는 로그인 지속시간문제였습니다.

 

Refresh Token 관리 문제

주섬 서버 API는 모바일과 웹을 모두 대응합니다. 모바일을 먼저 개발되면서 로그인 응답으로 JWT가 아닌, Access Token과 Refresh Token을 보냅니다. 이때 웹은 어떻게 처리하면 좋을까요?

 

일반적으로 모바일과 웹은 차이가 있습니다.

  웹 (SPA) 모바일 (Android / IOS)
토큰 저장 방식 Refresh Token은 서버에서 관리하고
Access Token은 클라이언트의 httpOnly Secure 쿠키나
localStorage로 관리
Refresh Token과 Access Token을
클라이언트의 Secure Storage에서 관리
토큰 재발급 방식 클라이언트가 서버에 Refresh Token 요청 클라이언트가 직접 Refresh Token 사용

 

모바일에서 토큰을 직접 관리하는 이유는 모바일 앱은 httpOnly Secure 쿠키를 사용할 수 없고, 서버 세션을 유지하기에 어려움이 있기 때문입니다. 클라이언트 사이드의 Secure Storage(Keychain, EncryptedSharedPreferences)에 저장하여 보안을 강화합니다.

 

현재 개발된 상태를 최대한 활용하고 싶어서, 웹에서도 클라이언트에서 Refresh Token을 관리하는 방법을 고민했습니다.
그리고 그에 대한 힌트는 NextAuth 인증에서 얻었습니다.  생각해 보니 Next.js도 라우트 서버가 있으니 백엔드 서버처럼 못할 게 없었습니다.

 

저는 토큰을 httpOnly 쿠키에 저장해서 서버와 통신할 때만 토큰을 사용할 수 있도록 했습니다.

// Refresh Token 관리
function storeRefreshToken(refreshToken: string) {
  cookies().set({
    name: "refreshToken",
    value: refreshToken,
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    path: "/",
    maxAge: 60 * 60 * 24 * 30,
  });
}

서버로부터 Refresh Token을 받으면 httpOnly secure 쿠키로 토큰을 저장했습니다. 이제 토큰에 JS로 접근할 수 없고 프로덕트 환경에서는 HTTPS에서만 전송됩니다. 또한 sameSite 설정으로 우리 서비스 이외에는 사용이 불가합니다. 

 

로그인 지속시간문제

Refresh Token을 활용해서 직접 처리한 경험이 없었습니다. 그래서 개발 초기에는 Access Token의 유효 시간을 기존 1시간에서 24시간으로 변경했습니다. 하지만 당연하게도 사용자 경험이 좋지 않았습니다. 요즘 대부분의 서비스는 웹에서 한 번만 로그인하면 다시 로그인할 필요가 없는데, 개발 중인 서비스는 뜬금없는 타이밍에 로그인 페이지로 튕기는 사용자 경험이 있었습니다. 그래서 이 두 개의 토큰을 제대로 이해하고 적용시켜 보기로 했습니다.

  Access Token Refresh Token
유효 시간 기본 1시간 오랫동안 지속
용도 클라이언트가 서버에 API 요청 보낼 때 사용 만료된 Access Token을 새로 발급받는 용도
보안 유출시 보안상 문제가 적음 유출시 보안상 위험

 

개념을 정리하는 와중에 이런 의문이 들었습니다.

서버에서 이미 사용자 인증을 하고 있는데,
클라이언트에서 Refresh Token과 Client ID 그리고 Secret을 가지고 Access Token을 직접 갱신한다고?

서비스를 함께 개발하던 동료 개발자에게 관련 질문을 했고,
우리 서비스는 Access Token의 만료기한을 없앴다는 이야기를 들었습니다. 

액세스 토큰에 만료기한이 없다네... 젠장 할 수 있는 게 없잖아.

아직 결제나 민감한 개인정보가 없고, 보안이 크게 중요하지 않은 시기라 판단했다고 했습니다. 서비스 수요자 규모가 매우 작은 상황이고, 서비스 특성상 보안에 민감하지 않았기에 납득이 갔습니다.

아쉽지만 추후에 보안을 강화할 필요가 있는 시기가 온다면, 그때 Refresh Token을 활용해 Access Token을 갱신하고 Refresh Token을 안전하게 관리하는 방법을 적용해서 개발하기로 했습니다.

결과적으로 토큰은 클라이언트에서 Access Token만 만료기한 없이 HttpOnly 쿠키로 사용하였습니다.

 

'web' 카테고리의 다른 글

챗 봇 스트리밍 통신  (2) 2024.06.06
Core Web Vitals  (0) 2023.07.28
DOM (Document Object Model)  (0) 2023.07.09