OAuth Flow 한 번에 정리하기

OAuth Flow 한 번에 정리하기

OAuth를 공부하다 보면 “Authorization Code Flow를 써라”, “Client Credentials는 machine-to-machine용이다”, “Implicit은 이제 쓰지 마라” 같은 말을 자주 듣게 됩니다. 처음에는 다 같은 OAuth처럼 보이는데, 왜 이렇게 flow가 여러 개인지 헷갈리기 쉽죠.

사실 스펙 관점에서 더 정확한 이름은 flow보다 grant type입니다. 클라이언트가 어떤 근거(grant)를 가지고 access token을 받아 오는지를 구분하는 이름인데요. 다만 실무 문서와 제품 콘솔에서는 OAuth flow라는 표현을 더 자주 쓰므로, 이 글에서도 독자가 익숙한 flow라는 말을 함께 쓰겠습니다.

이번 글은 각 flow를 깊게 구현하는 글이라기보다, 어떤 상황에서 어떤 OAuth flow를 선택해야 하는지 정리하는 지도에 가깝습니다. Authorization Code의 전체 흐름은 OAuth 2.0 쉽게 이해하기에서, 엔드포인트별 파라미터는 OAuth 2.0 엔드포인트 제대로 이해하기에서 이미 다뤘으니 여기서는 선택 기준에 집중해볼게요.

OAuth flow는 왜 여러 개일까요?

OAuth의 목적은 access token을 안전하게 발급하는 것입니다. 그런데 access token을 받아야 하는 상황은 하나가 아닙니다.

사용자가 브라우저에서 로그인하고 “이 앱에 캘린더 읽기 권한을 허용합니다”라고 동의하는 상황이 있습니다. 반대로 사용자 없이 백엔드 서비스가 다른 백엔드 API를 호출해야 하는 상황도 있죠. TV 앱이나 CLI처럼 브라우저 입력이 불편한 장치에서 로그인해야 할 수도 있고, 마이크로서비스 안에서 이미 받은 토큰을 다른 서비스용 토큰으로 바꿔야 할 수도 있습니다.

이 모든 상황을 하나의 flow로 처리하려고 하면 보안과 사용성이 모두 애매해집니다. 그래서 OAuth는 상황별로 다른 grant type을 제공합니다. 핵심 질문은 단순합니다.

사용자가 지금 이 흐름 안에 직접 참여하나요?

사용자가 브라우저에서 로그인하고 동의한다면 Authorization Code + PKCE가 기본입니다. 사용자가 전혀 없고 서비스 자체가 API를 호출한다면 Client Credentials가 맞습니다. 이미 받은 access token을 갱신하는 문제라면 Refresh Token이고, 입력이 제한된 장치라면 Device Authorization Grant를 봐야 합니다.

Authorization Code + PKCE

새로운 OAuth 연동을 만든다면 기본값은 거의 항상 Authorization Code + PKCE입니다. 웹앱, 모바일 앱, SPA, 데스크톱 앱처럼 사용자가 직접 로그인하고 권한을 허용하는 흐름에 씁니다.

큰 흐름은 이렇습니다. 클라이언트가 사용자를 인가 서버(Authorization Server)의 /authorize 엔드포인트로 보냅니다. 사용자는 로그인하고 권한을 허용합니다. 인가 서버는 access token을 바로 주지 않고 짧게 사는 authorization code를 redirect URI로 돌려보냅니다. 그다음 클라이언트가 이 code를 /token 엔드포인트에서 access token으로 교환합니다.

Authorization Code + PKCE 흐름
사용자 -> 클라이언트 -> 인가 서버 로그인 화면
사용자 -> 권한 허용
인가 서버 -> 클라이언트 redirect_uri로 authorization code 전달
클라이언트 -> 인가 서버 /token에서 access token으로 교환
클라이언트 -> 자원 서버 API 호출

여기서 PKCE(Proof Key for Code Exchange)는 authorization code를 가로채도 공격자가 토큰으로 바꾸지 못하게 막는 장치입니다. 클라이언트는 인가 요청 전에 code_verifier라는 무작위 문자열을 만들고, 그 해시값인 code_challenge를 먼저 보냅니다. 나중에 토큰 교환 단계에서 원본 code_verifier를 제출하면 인가 서버가 둘을 비교합니다. 자세한 동작 원리는 PKCE로 Authorization Code 흐름 안전하게 보호하기에서 따로 다루고 있습니다.

이 flow는 사용자가 있는 거의 모든 일반 앱에 적합합니다. 서버 렌더링 웹앱은 물론이고, 예전에는 Implicit을 쓰던 SPA와 모바일 앱도 이제는 Authorization Code + PKCE를 쓰는 것이 표준 방향입니다. OAuth 2.1 draft도 Implicit을 제거하고 PKCE를 기본 보안 장치로 삼는 쪽으로 정리하고 있어요.

Client Credentials

Client Credentials는 사용자 없이 클라이언트 자신이 access token을 받는 flow입니다. 그래서 흔히 machine-to-machine OAuth flow라고 부릅니다.

예를 들어 결제 배치 작업이 결제 API를 호출하거나, 백엔드 서비스 A가 내부 서비스 B의 관리 API를 호출하거나, CI/CD 파이프라인이 배포 API를 호출하는 상황을 생각해볼 수 있습니다. 여기에는 “로그인할 사용자”가 없습니다. 권한의 주체는 사람 계정이 아니라 클라이언트 애플리케이션 자체입니다.

요청도 단순합니다. 클라이언트는 토큰 엔드포인트에 grant_type=client_credentials를 보내고, client_idclient_secret 또는 private_key_jwt 같은 클라이언트 인증 방식으로 자기 신원을 증명합니다.

Client Credentials 토큰 요청
POST /token HTTP/1.1
Host: as.example.com
Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&scope=reports:read

이 flow에서 가장 흔한 실수는 사용자 데이터 접근에 Client Credentials를 쓰려는 것입니다. Client Credentials로 받은 토큰은 “사용자 dale의 캘린더”가 아니라 “report-worker 서비스가 읽을 수 있는 리포트 API”처럼 서비스 자체의 권한을 나타냅니다. 사용자 동의가 필요한 데이터라면 Authorization Code + PKCE로 가야 합니다.

또 하나 기억할 점은 refresh token입니다. Client Credentials에서는 보통 refresh token을 발급하지 않습니다. 사용자 세션을 오래 유지하는 문제가 아니기 때문에 access token이 만료되면 같은 client credentials로 새 access token을 받으면 됩니다.

Refresh Token

Refresh Token은 사용자에게 다시 로그인과 동의를 요구하지 않고 새 access token을 발급받는 흐름입니다. 엄밀히 말하면 독립적인 로그인 flow라기보다, Authorization Code + PKCE 뒤에 붙는 토큰 갱신 흐름에 가깝습니다.

access token은 짧게 사는 것이 안전합니다. 유출되더라도 피해 시간을 줄일 수 있기 때문인데요. 하지만 access token이 한 시간마다 만료될 때마다 사용자에게 다시 로그인하라고 하면 서비스 경험이 망가집니다. 그래서 인가 서버는 조건이 맞을 때 refresh token을 함께 발급합니다.

Refresh Token 토큰 요청
POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
&client_id=s6BhdRkqt3

refresh token은 access token보다 오래 살기 때문에 훨씬 민감합니다. 요즘 권고에서는 refresh token rotation을 기본으로 봅니다. 새 access token을 받을 때마다 refresh token도 새로 발급하고, 기존 refresh token은 즉시 폐기하는 방식입니다. 특히 SPA나 모바일 앱처럼 secret을 안전하게 숨기기 어려운 공개 클라이언트에서는 rotation이 사실상 필수라고 보는 편이 좋습니다.

Refresh Token을 선택할지 말지는 “사용자가 없는가?”가 아니라 “사용자가 다시 오지 않아도 세션을 이어가야 하는가?”로 판단해야 합니다. 짧은 로그인 세션이면 필요 없을 수도 있고, 장기간 API 연동이 필요한 서비스라면 반드시 고려해야 합니다.

Device Authorization Grant

Device Authorization Grant는 사용자가 로그인해야 하지만, 지금 쓰는 장치에서 로그인 입력이 불편할 때 쓰는 flow입니다. RFC 8628이 정의한 확장 grant입니다.

대표적인 예는 TV 앱, 게임 콘솔, 프린터, CLI입니다. 이런 장치에서 긴 비밀번호나 다중 인증(Multi-Factor Authentication, MFA) 코드를 직접 입력하는 것은 불편하죠. 대신 장치는 사용자에게 짧은 코드와 인증 URL을 보여줍니다. 사용자는 휴대폰이나 노트북 브라우저로 그 URL에 들어가 코드를 입력하고 로그인합니다.

Device Authorization Grant 흐름
장치 -> 인가 서버: device_code와 user_code 요청
장치 -> 사용자: 인증 URL과 user_code 표시
사용자 -> 다른 브라우저: URL 접속 후 코드 입력과 로그인
장치 -> 인가 서버: 토큰 발급 여부를 주기적으로 확인
인가 서버 -> 장치: 사용자가 승인하면 access token 발급

장치 쪽에서는 device_code를 들고 토큰 엔드포인트를 주기적으로 polling합니다. 아직 사용자가 승인을 끝내지 않았다면 인가 서버는 기다리라는 에러를 돌려주고, 승인이 끝나면 access token을 반환합니다.

이 flow는 사용자 경험이 좋지만 아무 데나 쓰는 것은 아닙니다. 일반 웹앱이나 모바일 앱처럼 브라우저 리다이렉트가 자연스러운 환경에서는 Authorization Code + PKCE가 더 단순하고 안전합니다. Device Authorization Grant는 “사용자는 있지만, 현재 장치에서 브라우저 로그인과 redirect URI 처리가 불편하다”는 조건이 있을 때 선택하면 됩니다.

Token Exchange

Token Exchange는 이미 가진 토큰을 다른 토큰으로 교환하는 flow입니다. RFC 8693이 정의하며, grant type 값은 urn:ietf:params:oauth:grant-type:token-exchange입니다.

일반적인 OAuth 입문 글에서는 자주 나오지 않지만, 마이크로서비스나 서비스 메시, MCP 같은 환경에서는 중요해집니다. 예를 들어 API 게이트웨이가 사용자 access token을 받았는데, 내부 결제 서비스에는 그 토큰을 그대로 넘기고 싶지 않을 수 있습니다. 이때 인가 서버에 “이 subject token을 결제 서비스용 audience를 가진 짧은 토큰으로 바꿔 달라”고 요청할 수 있습니다.

Token Exchange 요청
POST /token HTTP/1.1
Host: as.example.com
Authorization: Basic cnMwODpsb25nLXNlY3VyZS1yYW5kb20tc2VjcmV0
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&resource=https%3A%2F%2Fpayments.example.com%2Fapi
&subject_token=eyJhbGciOi...
&subject_token_type=urn:ietf:params:oauth:token-type:access_token

Token Exchange가 필요한 이유는 토큰의 audience를 좁히기 위해서입니다. 한 서비스에서 받은 토큰을 모든 하위 서비스에 그대로 넘기면, 토큰 하나가 너무 넓은 권한을 갖게 됩니다. 각 서비스에 맞는 audience와 scope를 가진 토큰으로 바꿔 쓰면 피해 범위를 줄일 수 있어요.

다만 Token Exchange는 기본 로그인 flow가 아닙니다. Authorization Code나 Client Credentials로 시작한 뒤, 여러 보안 도메인이나 백엔드 서비스 사이를 지나가야 할 때 등장하는 고급 흐름으로 보는 편이 자연스럽습니다. 토큰 audience와 resource 파라미터의 관계는 OAuth 2.0 메타데이터와 엔드포인트 동적 발견에서 다룬 Resource Indicators와도 맞물립니다.

이제 쓰지 않는 흐름들

OAuth 2.0 원본인 RFC 6749는 Authorization Code, Implicit, Resource Owner Password Credentials, Client Credentials 네 가지 grant type을 정의했습니다. 하지만 시간이 지나면서 일부 흐름은 보안상 권장되지 않는 쪽으로 정리되었습니다.

우선 Implicit은 SPA에서 access token을 redirect URI로 바로 받기 위해 널리 쓰였습니다. 문제는 access token이 브라우저 주소창, 히스토리, 로그, Referer 헤더 같은 곳에 노출되기 쉽다는 점입니다. OAuth 2.1 draft는 Implicit을 제거하고, SPA도 Authorization Code + PKCE를 쓰는 방향으로 정리합니다.

Resource Owner Password Credentials, 흔히 Password Grant라고 부르는 흐름도 신규 구현에서는 피해야 합니다. 사용자가 클라이언트 앱에 자기 아이디와 비밀번호를 직접 입력하고, 클라이언트가 그 비밀번호로 토큰을 받아 오는 방식인데요. OAuth가 애초에 “사용자 비밀번호를 클라이언트에게 주지 않기 위해” 만들어졌다는 점을 생각하면 방향이 맞지 않습니다. 자사 1st-party 앱이라는 이유로 예외적으로 쓰던 시대가 있었지만, 지금은 Authorization Code + PKCE로 옮기는 것이 맞습니다.

정리하면 새 프로젝트에서 Implicit과 Password Grant를 선택할 이유는 거의 없습니다. 레거시 시스템을 유지보수할 때만 “왜 이렇게 되어 있는지”를 이해하는 용도로 남겨두면 충분합니다.

어떤 flow를 선택해야 할까요?

OAuth flow를 고를 때는 이름부터 외우기보다 상황을 먼저 나누는 게 빠릅니다. 다음 표를 기준으로 시작하면 대부분의 선택이 정리됩니다.

상황추천 flow
웹앱에서 사용자가 로그인하고 동의함Authorization Code + PKCE
SPA나 모바일 앱에서 사용자가 로그인함Authorization Code + PKCE
백엔드 서비스끼리 API를 호출함Client Credentials
access token을 사용자 재로그인 없이 갱신함Refresh Token
TV, CLI, 콘솔처럼 입력이 제한된 장치에서 로그인함Device Authorization Grant
받은 토큰을 다른 audience용 토큰으로 바꿈Token Exchange
예전 SPA에서 토큰을 URL로 바로 받음Implicit, 신규 사용 비권장
사용자 비밀번호를 앱에 직접 입력함Password Grant, 신규 사용 비권장

개인적으로는 세 가지 질문으로 좁혀서 판단합니다.

첫째, 사용자가 지금 브라우저에서 로그인하고 동의할 수 있나요? 그렇다면 Authorization Code + PKCE입니다.

둘째, 사용자가 전혀 없고 서비스 자체의 권한으로 호출하나요? 그렇다면 Client Credentials입니다.

셋째, 이미 받은 토큰을 계속 쓰거나 다른 서비스용으로 바꿔야 하나요? 세션 연장이라면 Refresh Token, audience 변경이라면 Token Exchange입니다.

이 세 질문에 걸리지 않고 “사용자는 있는데 이 장치에서 로그인하기 어렵다”면 Device Authorization Grant를 검토하면 됩니다.

마치며

OAuth flow는 종류가 많아 보이지만, 결국 “누가 어떤 근거로 access token을 받는가”를 구분하는 장치입니다. 사용자가 직접 참여하면 Authorization Code + PKCE, 사용자가 없으면 Client Credentials, 토큰을 갱신하면 Refresh Token, 입력이 어려운 장치라면 Device Authorization Grant, 보안 도메인 사이에서 토큰을 바꾸면 Token Exchange로 보면 됩니다.

실제 구현 단계에서는 flow 이름만으로 충분하지 않습니다. 각 flow가 어떤 엔드포인트와 파라미터를 쓰는지 확인해야 하므로 OAuth 2.0 엔드포인트 제대로 이해하기를 함께 보면 디버깅이 훨씬 쉬워집니다. 인가 서버의 엔드포인트를 런타임에 발견해야 하는 환경이라면 OAuth 2.0 메타데이터와 엔드포인트 동적 발견도 이어서 읽어보세요.

더 자세한 내용은 RFC 6749 - The OAuth 2.0 Authorization Framework, RFC 8628 - OAuth 2.0 Device Authorization Grant, RFC 8693 - OAuth 2.0 Token Exchange를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord