OAuth 2.0 동작 원리: 소셜 로그인은 어떻게 작동하나

‘구글로 로그인’, ‘카카오로 시작하기’ 버튼은 이제 거의 모든 서비스에 붙어 있다. 그런데 새 서비스에 구글 계정으로 로그인할 때, 정작 그 서비스에 구글 비밀번호를 입력한 적은 없다. 비밀번호를 넘겨주지 않았는데도 서비스는 사용자의 이름과 이메일을 받아 가입을 끝낸다. 이 매끄러운 과정 뒤에서 일하는 표준이 바로 OAuth 2.0이다. 이 글은 OAuth 2.0 동작 원리를 네 가지 역할, 인가 코드 그랜트 흐름, PKCE, 토큰 구조 순서로 풀어 본다.

OAuth 2.0이 풀려는 문제: 비밀번호를 넘기지 않는 위임

OAuth 2.0의 정식 명칭은 ‘OAuth 2.0 Authorization Framework’이며, 2012년 IETF가 RFC 6749로 표준화했다. 이름에 ‘인증(Authentication)’이 아니라 ‘인가(Authorization)’가 들어간다는 점이 핵심이다. OAuth 2.0은 “누가 로그인했는가”를 증명하는 기술이 아니라, “이 앱이 내 데이터에 어디까지 접근하도록 허락할 것인가”를 위임하는 프레임워크다.

문제를 구체화해 보자. 어떤 사진 인쇄 서비스가 사용자의 구글 포토 사진을 가져오려 한다. 가장 단순한 방법은 사용자에게 구글 아이디와 비밀번호를 그대로 입력받는 것이다. 하지만 이렇게 하면 인쇄 서비스가 비밀번호를 저장하게 되고, 메일·드라이브·캘린더까지 사실상 전부 접근할 수 있게 된다. 비밀번호 하나가 유출되면 피해 범위가 계정 전체로 번진다.

OAuth 2.0은 이 ‘전부 아니면 전무’ 구조를 깬다. 비밀번호 대신 범위가 제한된 토큰을 발급해, 인쇄 서비스에는 “사진 읽기”만 허용하고 그 외에는 막는다. 호텔에서 객실 카드키만 주고 마스터키는 주지 않는 것과 같은 원리다.

OAuth 2.0의 네 가지 역할

OAuth 2.0 동작 원리를 이해하려면 먼저 등장인물을 구분해야 한다. RFC 6749는 네 가지 역할을 정의한다.

역할 영문 실제 예시
자원 소유자 Resource Owner 로그인하는 사용자 본인
클라이언트 Client 접근을 요청하는 앱(사진 인쇄 서비스)
인가 서버 Authorization Server 동의를 받고 토큰을 발급(구글 계정 서버)
자원 서버 Resource Server 보호된 데이터를 보관(구글 포토 API)

소셜 로그인에서 인가 서버와 자원 서버는 같은 회사(구글, 카카오 등)가 함께 운영하는 경우가 많지만, 표준상으로는 별개의 역할이다. 이 분리 덕분에 토큰 발급의 책임과 데이터 제공의 책임이 깔끔하게 나뉜다.

인가 코드 그랜트: 가장 표준적인 흐름

OAuth 2.0에는 여러 흐름(grant type)이 있지만, 웹·모바일 로그인에서 가장 널리 쓰이는 것은 인가 코드 그랜트(Authorization Code Grant)다. oauth.net 기준으로 단계는 다음과 같다.

  1. 인가 요청 — 사용자가 ‘구글로 로그인’을 누르면, 클라이언트는 사용자를 인가 서버로 리다이렉트한다. 이때 요청에는 클라이언트 ID, 돌려받을 주소(redirect URI), 요청 권한(scope)이 담긴다.
  2. 사용자 동의 — 사용자는 구글 화면에서 직접 로그인하고 “이 앱이 사진을 보려고 합니다. 허용하시겠습니까?”라는 동의 화면을 본다. 비밀번호는 구글에만 입력되며 클라이언트는 보지 못한다.
  3. 인가 코드 발급 — 동의하면 인가 서버는 사용자를 redirect URI로 돌려보내며 짧은 수명의 인가 코드(authorization code)를 함께 전달한다. 이 코드는 토큰이 아니라 토큰으로 교환할 일회성 표일 뿐이다.
  4. 토큰 교환 — 클라이언트는 받은 인가 코드를 자신의 비밀(client secret)과 함께 인가 서버 뒷단으로 보내 액세스 토큰으로 교환한다. 이 통신은 브라우저를 거치지 않는 서버 간 통신이라 가로채기가 어렵다.
  5. 자원 접근 — 클라이언트는 발급받은 액세스 토큰을 자원 서버에 제시하고, 토큰이 유효하면 사진 데이터를 받는다.

코드와 토큰을 분리한 이유가 여기서 드러난다. 만약 토큰을 브라우저 주소창(리다이렉트)에 직접 실어 보낸다면 로그·기록·확장 프로그램을 통해 노출될 위험이 크다. 인가 코드는 한 번 쓰면 폐기되고, 진짜 토큰은 안전한 뒷단 통신에서만 오간다.

PKCE: 공개 클라이언트를 위한 보안 강화

위 흐름에서 4단계는 client secret을 전제로 한다. 그런데 모바일 앱이나 React 같은 브라우저 SPA(Single Page Application)는 코드가 사용자 기기에 그대로 노출되므로 비밀을 안전하게 숨길 곳이 없다. 이런 ‘공개 클라이언트(public client)’를 위해 등장한 보강 장치가 PKCE(Proof Key for Code Exchange, RFC 7636)다.

PKCE의 원리는 일회용 자물쇠와 열쇠에 비유할 수 있다.

  • 클라이언트는 매 로그인마다 무작위 문자열 code_verifier를 만든다.
  • 이를 SHA-256으로 해시한 code_challenge를 인가 요청에 함께 보낸다.
  • 토큰을 교환할 때 원본 code_verifier를 제출한다. 인가 서버는 이를 해시해 처음 받은 code_challenge와 같은지 검증한다.

해시는 단방향이라 challenge에서 verifier를 역산할 수 없다. 따라서 공격자가 중간에서 인가 코드를 가로채더라도, 원본 verifier가 없으면 토큰으로 바꿀 수 없다. oauth.net은 PKCE를 “CSRF와 인가 코드 주입 공격을 막는 인가 코드 흐름의 확장”으로 정의한다. 다만 oauth.net은 “PKCE는 클라이언트 인증을 대체하지 않는다”는 점도 분명히 한다. 비밀이 있는 서버 앱은 client secret을, 비밀을 숨길 수 없는 공개 앱은 PKCE를 쓰는 식으로 상호 보완적이다.

액세스 토큰·리프레시 토큰·스코프

발급되는 토큰에도 종류가 있다.

  • 액세스 토큰(Access Token) — 자원 서버에 접근할 때 제시하는 열쇠. 보안을 위해 수명이 짧게(보통 수십 분~몇 시간) 설정된다.
  • 리프레시 토큰(Refresh Token) — 액세스 토큰이 만료됐을 때 다시 로그인하지 않고 새 액세스 토큰을 받아 오는 장기 토큰. 더 민감하므로 안전한 저장이 중요하다.
  • 스코프(Scope)photos.read, email처럼 권한의 범위를 정하는 문자열. 동의 화면에 표시되는 “이 앱이 요청하는 권한” 목록이 바로 스코프다.

짧은 액세스 토큰과 긴 리프레시 토큰을 나눈 이유는 보안과 편의의 절충이다. 액세스 토큰이 유출되더라도 금방 만료돼 피해가 제한되고, 사용자는 리프레시 토큰 덕분에 매번 다시 로그인하지 않아도 된다.

OAuth 2.0과 OIDC: 인가와 인증의 차이

여기서 흔한 오해 하나를 짚어야 한다. 앞서 강조했듯 OAuth 2.0은 ‘인가’ 프레임워크다. 즉 “이 토큰으로 무엇을 할 수 있는가”는 말해 주지만, “이 토큰의 주인이 누구인가”는 표준 차원에서 보장하지 않는다.

그래서 ‘구글로 로그인’처럼 신원 확인이 목적인 경우에는 OAuth 2.0 위에 올린 인증 계층인 OIDC(OpenID Connect)를 함께 쓴다. OIDC는 액세스 토큰과 별도로, 사용자의 신원 정보를 담은 ID 토큰(JWT 형식)을 발급한다. 정리하면 OAuth 2.0은 ‘권한 위임’, OIDC는 그 위에 얹은 ‘로그인(신원 확인)’을 담당한다. 우리가 흔히 ‘OAuth 로그인’이라 부르는 것의 상당수는 사실 OIDC가 작동하는 장면이다.

그랜트 타입 정리와 OAuth 2.1

OAuth 2.0이 정의한 흐름은 인가 코드 그랜트만이 아니다. 현재 권장·비권장 구분은 아래와 같다.

그랜트 타입 용도 상태
Authorization Code (+ PKCE) 웹·모바일·SPA 로그인 권장
Client Credentials 사용자 없는 서버 간 통신 권장
Device Code TV·IoT 등 입력 제한 기기 권장
Refresh Token 액세스 토큰 갱신 권장
Implicit 과거 SPA용 폐기(legacy)
Password 아이디·비밀번호 직접 전달 폐기(legacy)

oauth.net은 Implicit과 Password 그랜트를 모두 ‘Legacy’로 분류한다. 두 방식 모두 토큰이나 비밀번호가 쉽게 노출되는 구조적 약점 때문이다. 이런 정리를 표준 차원에서 묶으려는 작업이 OAuth 2.1로, oauth.net의 설명에 따르면 “OAuth 2.0과 여러 확장을 하나로 통합하려는 진행 중인 노력”이다. OAuth 2.1의 가장 큰 방향은 모든 인가 코드 흐름에 PKCE를 사실상 의무화하고 Implicit·Password를 제거하는 것이다.

한계와 실무 고려사항

OAuth 2.0이 만능은 아니다. 가장 자주 지적되는 한계는 복잡성과 구현 오류 위험이다. 흐름이 여러 단계로 나뉘고 redirect URI, state 파라미터, 토큰 저장 위치 등 잘못 구현하면 보안 구멍이 생길 지점이 많다. 실제로 redirect URI 검증을 느슨하게 두거나 state 파라미터를 빠뜨려 CSRF에 노출되는 사례가 적지 않다.

또 하나, OAuth 2.0 자체는 ‘인가’만 정의하므로 신원 확인을 OAuth만으로 처리하려다 보안 사고로 이어지는 경우도 있다. 신원이 필요하면 OIDC를 함께 도입하는 것이 정석이다. 그렇다면 우리 서비스에는 어떤 흐름이 맞을까? 사용자 로그인이 목적이면 인가 코드 그랜트 + PKCE + OIDC 조합이 현재 기준 사실상 표준이고, 서버 간 자동화에는 Client Credentials가 적합하다. 직접 구현이 부담스럽다면 Auth0구글 Identity 플랫폼 같은 검증된 서비스를 활용하는 편이 안전하다.

정리

OAuth 2.0 동작 원리의 핵심은 비밀번호를 넘기지 않고 범위가 제한된 토큰으로 권한을 위임한다는 데 있다. 네 가지 역할이 책임을 나누고, 인가 코드 그랜트가 코드와 토큰을 분리해 노출을 줄이며, PKCE가 공개 클라이언트의 빈틈을 메우고, 짧은 액세스 토큰과 긴 리프레시 토큰이 보안과 편의를 절충한다. 그리고 신원 확인이 필요한 순간에는 OIDC가 그 위에서 작동한다. 비밀번호 자체를 없애는 흐름이 궁금하다면 패스키 로그인 원리도 함께 살펴볼 만하다. 인증과 인가는 닮았지만 다른 문제이며, 두 개념을 구분하는 것이 안전한 로그인 설계의 출발점이다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다