RFC 6749
고친 사람 github-actions[bot]
RFC 6749 는 OAuth 2.0 의 규칙을 정한 표준 문서입니다. 다른 앱이 내 비밀번호를 받지 않고도 내 데이터에 접근하게 해 주는 절차가 이 문서에서 나옵니다. 허락하는 쪽과 열쇠를 내주는 쪽을 나눕니다. 그 열쇠를 주고받는 방법도 정합니다.
쉽고 빠른 이해
RFC 6749 는 앱에 비밀번호 대신 출입증을 건네는 방법을 적어 둔 문서입니다. 「다른 서비스에서 사진 가져오기」 같은 버튼을 누를 때 뒤에서 오가는 요청과 응답의 모양이 여기서 나옵니다. 남의 앱이 내 데이터에 접근해야 할 때 씁니다. 사용자가 누구인지 확인하는 로그인은 맡지 않습니다.
이 규칙이 없으면 남의 앱에 비밀번호를 그대로 줘야 합니다. 그러면 그 앱이 계정 전부를 손에 쥡니다. 그 앱 하나만 골라 접근을 끊을 수도 없습니다.
돌아가는 방식은 이렇습니다.
- 앱이 사용자를 데이터 주인 서비스의 승인 화면으로 보냅니다
- 사용자가 허락하면 앱은 범위와 기한이 정해진 출입증을 받습니다
- 앱은 그 출입증을 내밀어 데이터를 가져옵니다
대가도 있습니다. 문서는 큰 틀만 정합니다. 출입증의 모양 같은 세부는 비워 둡니다. 그래서 같은 규칙을 따르는 서비스끼리도 세부 동작이 다릅니다.
상세
이 문서의 제목과 위치
RFC(Request for Comments)는 IETF(Internet Engineering Task Force, 인터넷 표준을 만드는 단체)가 번호를 붙여 내는 문서입니다. 6749번 문서의 제목은 「The OAuth 2.0 Authorization Framework」입니다. 2012년 10월에 나왔습니다.
이 문서가 다루는 것은 OAuth 2.0 입니다. 사용자의 허락을 받아 다른 앱이 그 사용자의 데이터에 접근하게 하는 규칙입니다. 그래서 사람들은 「OAuth 2.0」과 「RFC 6749」를 같은 것을 가리키는 말로 섞어 씁니다.
문서의 단계는 제안 표준(Proposed Standard)입니다. IETF 가 표준으로 내놓은 문서 가운데 첫 단계라는 뜻입니다. 단계가 첫 칸이어도 업계는 이 문서를 OAuth 2.0 의 정본으로 씁니다.
이 문서는 OAuth 1.0 을 적은 RFC 5849 를 폐기했습니다. 폐기는 앞 문서가 더 이상 정본이 아니라는 뜻입니다. 다만 2.0 은 1.0 과 메시지를 주고받는 방식이 달라서 서로 호환되지 않습니다.
이 문서가 풀려던 문제
OAuth 이전에는 앱이 남의 서비스에 있는 데이터를 쓰려면 사용자의 비밀번호를 받아야 했습니다. 인쇄 서비스가 사진 공유 서비스의 사진을 가져가려면 사용자가 사진 공유 서비스의 비밀번호를 인쇄 서비스에 입력하는 식입니다.
이 방식에는 문제가 여럿 붙습니다. 인쇄 서비스가 비밀번호를 보관해야 하니 그쪽이 털리면 계정이 같이 털립니다. 사진만 필요한데 계정 전부에 손이 닿습니다. 인쇄 서비스 하나만 골라 끊으려면 비밀번호를 바꿔야 합니다. 그러면 다른 앱도 전부 끊깁니다.
RFC 6749 는 비밀번호 대신 출입증을 건네게 해서 이 문제를 풉니다. 출입증에는 쓸 수 있는 범위와 기한이 붙습니다. 앱마다 따로 거둬들일 수도 있습니다. 이 출입증을 액세스 토큰이라고 부릅니다.
네 역할
RFC 6749 는 등장하는 쪽을 넷으로 나눕니다. 이 넷의 이름을 알면 문서의 나머지가 읽힙니다.
| 역할 | 하는 일 | 사진 인쇄 예에서 |
|---|---|---|
| 자원 소유자 | 데이터의 주인. 접근을 허락한다 | 사진을 올린 사용자 |
| 클라이언트 | 남의 데이터를 쓰려는 앱 | 인쇄 서비스 |
| 인가 서버 | 허락을 확인하고 액세스 토큰을 내준다 | 사진 공유 서비스의 로그인·승인 서버 |
| 자원 서버 | 데이터를 들고 있다. 토큰을 보고 응답한다 | 사진을 내주는 사진 공유 서비스의 서버 |
인가 서버와 자원 서버는 한 회사가 함께 운영하는 일이 많습니다. 한 서버가 두 역할을 다 맡아도 됩니다. 둘을 나눈 까닭은 하는 일이 다르기 때문입니다. 한쪽은 열쇠를 내주고 다른 쪽은 열쇠를 확인합니다.
큰 흐름
전체 흐름은 세 마디로 줄어듭니다. 클라이언트가 자원 소유자에게 허락을 받습니다. 그 허락을 인가 서버에 내밀어 액세스 토큰으로 바꿉니다. 그 토큰을 들고 자원 서버에 데이터를 청합니다.
이 가운데 허락을 증명하는 값을 인가 그랜트(authorization grant)라고 부릅니다. 그랜트는 「허락해 줬다는 증거」쯤으로 읽으면 됩니다. 이 증거를 어떤 모양으로 받느냐에 따라 아래 네 방식이 갈립니다.
네 가지 그랜트
앱이 놓인 처지가 다르면 허락을 받는 방법도 달라야 합니다. 브라우저 없이 도는 서버 프로그램과 사용자 앞에 화면을 띄우는 앱은 사정이 다릅니다. 그래서 그랜트도 넷으로 나뉩니다.
| 그랜트 | 어떻게 허락을 받나 | 맞는 경우 |
|---|---|---|
| 인가 코드 그랜트 | 사용자를 인가 서버로 보냈다 받아 온 일회용 코드를 토큰으로 바꾼다 | 서버가 있는 웹 앱 |
| 임플리싯 그랜트 | 코드 없이 토큰을 브라우저 주소에 바로 받는다 | 브라우저 안에서만 도는 앱 |
| 자원 소유자 비밀번호 자격증명 그랜트 | 사용자의 아이디와 비밀번호를 앱이 받아 토큰으로 바꾼다 | 인가 서버와 아주 가까운 자사 앱 |
| 클라이언트 자격증명 그랜트 | 사용자 없이 앱 자신의 비밀 값으로 토큰을 받는다 | 서버끼리 부르는 작업 |
새 그랜트를 더하는 길도 열려 있습니다. 이 길로 더한 그랜트가 확장 그랜트입니다. 뒤의 RFC 들이 이 길로 그랜트를 늘렸습니다.
네 가운데 둘은 지금 권하지 않습니다. 임플리싯 그랜트는 쓰지 않기를 권합니다(SHOULD NOT). 비밀번호 그랜트는 쓰면 안 됩니다(MUST NOT). 임플리싯은 토큰이 브라우저 주소에 드러나 새기 쉽습니다. 비밀번호 그랜트는 앱이 비밀번호를 받으니 이 문서가 풀려던 문제로 되돌아갑니다.
인가 코드 그랜트의 한 왕복
네 그랜트 중 가장 널리 쓰이는 것이 인가 코드 그랜트입니다. 이 소절은 요청, 돌아오는 주소, 토큰 응답 세 장면을 따라가며 메시지가 어떻게 생겼는지 봅니다. 그림에서 자원 소유자인 사용자는 자기 브라우저로 등장합니다.
sequenceDiagram
participant 브라우저 as 사용자(브라우저)
participant 클라이언트
participant 인가서버 as 인가 서버
participant 자원서버 as 자원 서버
클라이언트->>브라우저: 인가 서버로 가라고 돌려보낸다
브라우저->>인가서버: 승인 화면을 연다
Note over 브라우저,인가서버: 사용자가 로그인하고 허락한다
인가서버->>브라우저: 코드를 붙여 클라이언트로 돌려보낸다
브라우저->>클라이언트: 코드를 전한다
클라이언트->>인가서버: 코드를 토큰으로 바꿔 달라
인가서버-->>클라이언트: 액세스 토큰
클라이언트->>자원서버: 토큰을 들고 데이터를 청한다
그림에서 사용자의 브라우저가 두 번 길을 바꿉니다. 이렇게 응답으로 다른 주소로 가라고 시키는 것을 리다이렉션이라고 합니다. 코드는 브라우저를 거쳐 오지만 토큰은 서버끼리 직접 주고받습니다. 토큰을 브라우저에 안 남기려고 이렇게 둘로 나눴습니다.
첫 요청은 클라이언트가 사용자를 인가 서버로 보내는 주소입니다. 각 칸의 뜻은 오른쪽 주석에 있습니다.
GET /authorize
?response_type=code // 코드를 달라
&client_id=s6BhdRkqt3 // 앱 번호
&redirect_uri=... // 돌아올 주소
&scope=photo.read // 원하는 범위
&state=xyz // 위조 방지 값
스코프는 앱이 원하는 접근 범위입니다. 「사진 읽기만」처럼 좁게 청할 수 있어서 계정 전부를 내주지 않아도 됩니다.
state 는 클라이언트가 지어 보낸 값입니다. 돌아올 때 같은 값이 붙어 와야 합니다. 남이 꾸민
응답을 걸러 내려는 장치입니다. 아래 「보안을 위해 정한 것」에서 다시 봅니다.
사용자가 허락하면 인가 서버는 브라우저를 redirect_uri 로 돌려보냅니다. 이때 주소에 코드와
state 가 붙습니다.
https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
인가 코드는 토큰이 아니라 토큰과 바꿀 교환권입니다. 클라이언트는 이 코드를 인가 서버의 토큰 주소로 보내고 아래 응답을 받습니다.
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer", // 토큰 쓰는 법
"expires_in": 3600, // 남은 초
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
응답 본문은 JSON(JavaScript Object Notation) 입니다. access_token 이 출입증입니다. 한 시간이
지나면 못 씁니다. 그때 리프레시 토큰으로 사용자를 다시 부르지 않고 새 액세스 토큰을 받습니다.
token_type 의 Bearer 는 「지닌 사람」이라는 뜻입니다. 가진 쪽이면 누구나 쓸 수 있는
Bearer 토큰이라서 요청에 그대로 실어 보냅니다. 그만큼 새지 않게 지켜야 합니다.
세 개의 엔드포인트
엔드포인트는 요청을 받는 주소 하나를 말합니다. 이 흐름에 필요한 주소는 셋입니다. 표의 마지막 칸이 위 왕복에서 본 주소입니다.
| 엔드포인트 | 누가 갖나 | 누가 부르나 | 위 왕복에서 |
|---|---|---|---|
| 인가 엔드포인트 | 인가 서버 | 사용자의 브라우저. 승인 화면이 여기 뜬다 | /authorize |
| 토큰 엔드포인트 | 인가 서버 | 클라이언트가 직접. 코드를 토큰으로 바꾼다 | 코드를 보낸 토큰 주소 |
| 리다이렉션 엔드포인트 | 클라이언트 | 사용자의 브라우저. 허락이 끝나면 돌아온다 | redirect_uri 에 적은 주소 |
인가 엔드포인트와 토큰 엔드포인트는 TLS(Transport Layer Security)를 써야 합니다(MUST). TLS 는 오가는 내용을 암호로 싸서 중간에서 못 엿보게 하는 규칙입니다. 비밀번호와 토큰이 지나는 길이라 맨 문자로 보내면 그대로 샙니다.
클라이언트의 두 종류
클라이언트는 비밀 값을 지킬 수 있느냐로 나뉩니다. 이 구분이 어떤 그랜트를 쓸지를 가릅니다.
기밀 클라이언트는 자기 서버 안에 비밀 값을 숨겨 둘 수 있는 앱입니다. 서버가 있는 웹 앱이 그렇습니다. 토큰을 받을 때 이 비밀 값으로 자기가 누구인지 증명합니다.
공개 클라이언트는 비밀 값을 숨길 곳이 없는 앱입니다. 사용자 기기에 깔리는 모바일 앱이나 브라우저에서 도는 스크립트가 그렇습니다. 앱 안에 비밀 값을 넣어도 누구나 앱을 뜯어 꺼낼 수 있습니다.
공개 클라이언트는 비밀 값이 없습니다. 그래서 코드만 손에 넣으면 누구나 그 코드를 토큰으로 바꿀 수 있습니다. 이 빈틈을 막으려고 뒤에 PKCE(Proof Key for Code Exchange)가 나왔습니다. 코드를 청한 앱만 그 코드를 토큰으로 바꿀 수 있게 하는 확장입니다.
클라이언트 종류와 그랜트는 아래처럼 이어집니다. 사용자가 끼는지를 먼저 봅니다. 끼면 비밀 값을 숨길 수 있는지를 봅니다.
flowchart TD
A{"사용자가 끼나?"}
A -->|아니오| C["클라이언트 자격증명 그랜트"]
A -->|예| B{"비밀 값을 숨길 수 있나?"}
B -->|"예 (기밀 클라이언트)"| D["인가 코드 그랜트"]
B -->|"아니오 (공개 클라이언트)"| E["인가 코드 그랜트 + PKCE"]
오류를 알리는 값
토큰 엔드포인트가 요청을 거절할 때는 error 칸에 정해진 값을 담아 돌려줍니다. 값이 정해져
있어서 클라이언트가 무엇을 고칠지 가릴 수 있습니다.
| 값 | 뜻 |
|---|---|
invalid_request |
필요한 칸이 빠졌거나 요청 모양이 틀렸다 |
invalid_client |
클라이언트 인증에 실패했다 |
invalid_grant |
코드나 리프레시 토큰이 틀렸거나 기한이 지났다 |
unauthorized_client |
이 클라이언트는 이 그랜트를 쓸 수 없다 |
unsupported_grant_type |
인가 서버가 이 그랜트를 지원하지 않는다 |
invalid_scope |
청한 범위가 틀렸거나 허락된 범위를 넘는다 |
실무에서 가장 자주 만나는 값은 invalid_grant 입니다. 코드를 두 번 쓰거나, 받은 지 오래된
코드를 내밀거나, 기한 지난 리프레시 토큰을 쓰면 이 값이 옵니다.
보안을 위해 정한 것
RFC 6749 는 공격을 막으려고 몇 가지 규칙을 둡니다. 아래는 백엔드를 짤 때 바로 부딪히는 것들입니다.
클라이언트는 인가 코드를 두 번 쓰면 안 됩니다(MUST NOT). 코드의 기한은 길어야 10분으로 두기를 권합니다(RECOMMENDED). 코드가 새더라도 쓸 수 있는 시간을 줄이려는 것입니다.
state 는 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)를 막는 데 씁니다.
여기서 막으려는 것은 공격자가 자기 계정의 코드를 피해자 브라우저에 밀어 넣는 공격입니다.
sequenceDiagram
participant 공격자
participant 피해자 as 피해자 브라우저
participant 클라이언트
participant 인가서버 as 인가 서버
공격자->>인가서버: 자기 계정으로 허락하고 코드를 받는다
Note over 공격자: 코드를 쓰지 않고 남겨 둔다
공격자->>피해자: 코드가 붙은 돌아오는 주소를 열게 만든다
피해자->>클라이언트: 코드를 전한다
Note over 클라이언트: 자기가 보낸 state 가 아니면 여기서 버린다
클라이언트->>인가서버: 코드를 토큰으로 바꿔 달라
인가서버-->>클라이언트: 공격자 계정의 액세스 토큰
막지 못하면 피해자의 인쇄 서비스가 공격자의 사진 계정에 연결됩니다. 인쇄 서비스가 사진을 올리는 기능까지 있다면 피해자가 올린 사진이 공격자 계정에 쌓입니다.
클라이언트가 보낸 state 와 돌아온 state 를 맞춰 보면 이런 응답을 걸러 낼 수 있습니다. 공격자의
코드에는 피해자 쪽 클라이언트가 보낸 state 가 붙어 있지 않기 때문입니다.
인가 서버는 돌려보낼 주소를 미리 등록받아 그것과 견줘야 합니다. 아무 주소로나 돌려보내면 공격자가 자기 주소를 적어 코드를 빼 갑니다. 이런 빈틈을 오픈 리다이렉트라고 부릅니다.
이 문서가 비워 둔 것
제목에 「프레임워크」가 붙은 데는 까닭이 있습니다. 문서는 큰 틀만 정합니다. 여러 대목은 다른 문서나 구현에 넘겼습니다.
| 비워 둔 것 | 어디서 채우나 |
|---|---|
| 액세스 토큰을 요청에 싣는 법 | 함께 나온 RFC 6750 이 Bearer 토큰 사용법을 정했다 |
| 액세스 토큰의 모양 | 정하지 않는다. 무작위 문자열도 되고 JWT(JSON Web Token) 도 된다 |
| 자원 서버가 토큰을 확인하는 법 | 정하지 않는다. 뒤에 토큰 인트로스펙션 같은 문서가 나왔다 |
| 사용자가 누구인지 알려 주는 법 | 정하지 않는다. OpenID Connect 가 이 위에 얹혀 채운다 |
마지막 줄이 자주 헷갈립니다. RFC 6749 는 인가, 곧 무엇을 해도 되는지를 다룹니다. 이 사람이 누구인지를 확인하는 인증은 이 문서의 일이 아닙니다. 「OAuth 로 로그인」이라고 부르는 것은 대개 OpenID Connect 를 함께 쓰는 경우입니다.
비워 둔 대목이 많아서 같은 RFC 6749 를 따르는 서비스끼리도 세부가 갈립니다. 대신 서비스마다 제 사정에 맞는 토큰 모양과 확인 방식을 고를 수 있습니다.
뒤에 고쳐지고 덧붙은 문서
나온 뒤로 RFC 6749 를 고친 RFC 가 셋 있습니다. 그래도 RFC 6749 는 여전히 정본입니다. 뒤의 RFC 는 그 위에 규칙을 얹습니다.
| 문서 | 무엇을 바꿨나 |
|---|---|
| RFC 8252 | 모바일·데스크톱 앱은 앱 안 화면이 아니라 사용자의 브라우저로 승인을 받도록 권했다 |
| RFC 8996 | 낡은 TLS 1.0 과 1.1 을 쓰지 않게 했다 |
| RFC 9700 | 쌓인 공격 사례를 반영해 보안 권고를 새로 썼다. 임플리싯과 비밀번호 그랜트를 물린 것이 이 문서다 |
이렇게 덧붙은 규칙을 한 문서로 모으려는 작업이 OAuth 2.1 입니다. PKCE 를 모든 클라이언트에 요구하고 물린 두 그랜트를 뺍니다. OAuth 2.1 의 RFC 번호는 미확인입니다.
관련 항목
이 문서가 정의하는 역할
자원 소유자 · 클라이언트 · 인가 서버 · 자원 서버 · 기밀 클라이언트 · 공개 클라이언트
이 문서가 정의하는 그랜트
인가 그랜트 · 인가 코드 그랜트 · 임플리싯 그랜트 · 자원 소유자 비밀번호 자격증명 그랜트 · 클라이언트 자격증명 그랜트 · 확장 그랜트
이 문서가 정의하는 값과 엔드포인트
액세스 토큰 · 리프레시 토큰 · 인가 코드 · 스코프 · 인가 엔드포인트 · 토큰 엔드포인트 · 리다이렉트 URI · state 파라미터
이 문서를 고치거나 넓힌 표준 문서
RFC 6750 · RFC 7636 · RFC 8252 · RFC 8996 · RFC 9700 · RFC 7009 · RFC 7662 · RFC 8414 · RFC 8628 · RFC 6819 · OAuth 2.1
이 문서 위에 얹히는 규격
OpenID Connect · ID 토큰 · JWT · Bearer 토큰 · PKCE · 토큰 인트로스펙션 · 디바이스 인가 그랜트
이 문서가 기대는 하부 기술
HTTP · TLS · HTTPS · URI · JSON · 리다이렉션 · 엔드포인트
이 문서가 막으려는 공격
크로스 사이트 요청 위조 · 오픈 리다이렉트 · 인가 코드 가로채기 · 토큰 탈취 · 피싱
이 문서가 다루는 인가와 맞세워지는 개념
인가 · 인증 · 인증과 인가 · 자격증명 · 권한 · 최소 권한
이것이 속하는 상위 분류
다른 이름: The OAuth 2.0 Authorization Framework · OAuth 2.0 인가 프레임워크 · RFC6749