OAuth 인가 흐름
인가 코드 요청, 토큰 교환, Bearer 호출과 토큰 갱신을 요청·응답 예제로 설명합니다.
목차
잼쿠봇은 OAuth 2.0 인가 코드 방식으로 스트리머의 동의를 받습니다. 인가 요청부터 토큰 교환, API 호출, 토큰 갱신까지의 요청·응답과 단계별 오류를 설명합니다.
흐름 한눈에 보기
| 단계 | 하는 곳 | 요청 | 결과 |
|---|---|---|---|
| 1. 인가 요청 | 스트리머 브라우저 | GET /developers/oauth/authorize |
로그인·동의 화면 |
| 2. 동의 | 스트리머 | 동의 화면의 허용하기 | redirect_uri 로 인가 코드 전달 |
| 3. 토큰 교환 | 앱 서버 | POST /developers/oauth/token (grant_type=authorization_code) |
액세스 토큰과 리프레시 토큰 |
| 4. API 호출 | 앱 서버 | Authorization: Bearer ACCESS_TOKEN 헤더 |
JSON 응답 |
| 5. 토큰 갱신 | 앱 서버 | POST /developers/oauth/token (grant_type=refresh_token) |
새 토큰 쌍 |
| 6. 토큰 폐기 (선택) | 앱 서버 | POST /developers/oauth/revoke |
그 채널의 토큰 쌍 삭제 |
- 지원하는 방식은 인가 코드(
response_type=code) 하나입니다. 토큰 교환과 갱신에는 항상client_secret이 필요하므로 앱 서버에서 요청합니다. PKCE(code_challenge)는 지원하지 않으며 보내도 무시합니다. - 토큰 한 쌍은 동의한 스트리머 한 채널에 묶입니다. 여러 채널을 다루는 앱은 채널마다 동의를 받고 토큰을 따로 보관합니다.
- 예제의 경로 앞에는 잼쿠봇 사이트의 기본 주소를 붙입니다.
YOUR_CLIENT_ID,YOUR_CLIENT_SECRET,RECEIVED_CODE같은 대문자 값은 발급받은 값으로 바꿉니다.
1. 인가 요청
스트리머의 브라우저에서 아래 주소를 엽니다. 여러 줄은 읽기 쉽게 나눈 것으로 실제로는 하나의 URL 이며, 쿼리 값은 URL 인코딩합니다.
GET /developers/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fmyapp.example.com%2Fcallback
&scope=commands:read%20commands:write%20events:subscribe
&state=RANDOM_STATE
| 이름 | 필수 | 설명 |
|---|---|---|
response_type |
선택 | code 입니다. 생략하면 code 로 처리하고, 다른 값이면 unsupported_response_type 오류입니다. |
client_id |
필수 | 앱의 클라이언트 ID 입니다. |
redirect_uri |
필수 | 앱에 등록한 리디렉션 URL 중 하나와 글자 그대로 같아야 합니다. |
scope |
필수 | 요청할 권한입니다. 공백으로 구분하며 앱이 선언한 범위 안이어야 합니다. 요청 규칙을 참고하세요. |
state |
권장 | 앱이 만든 임의 값입니다. 콜백에 그대로 돌려주며, 비어 있으면 콜백에 붙지 않습니다. |
스트리머가 보는 화면
- 로그인하지 않았다면 바로 치지직 로그인 화면으로 이동합니다. 로그인이 끝나면 동의 화면으로 돌아옵니다.
- 동의 화면에는 로그인한 채널, 앱 이름과 설명, 요청한 권한, 인가 후 이동할
redirect_uri가 나옵니다. - 스트리머가 허용하기 또는 거부를 누릅니다. 권한을 골라 일부만 허용할 수는 없습니다.
이미 동의한 앱이 동의한 범위 안의 권한을 다시 요청하면 동의 화면 없이 바로 콜백으로 코드가 돌아갑니다(치지직과 같은 동작). 동의한 범위를 넘는 권한을 요청할 때만 동의 화면이 다시 나옵니다. 그때 다시 허용하면 그 채널의 동의 범위는 이전 동의에 더해지지 않고 이번 요청의 scope 로 바뀝니다. 스트리머 쪽 화면 설명은 연결된 앱 관리에 있습니다.
콜백
스트리머가 허용하면 redirect_uri 로 인가 코드가 돌아옵니다.
https://myapp.example.com/callback?code=RECEIVED_CODE&state=RANDOM_STATE
거부하거나 요청에 문제가 있으면 code 대신 error 가 붙습니다. error_description 은 붙지 않습니다.
https://myapp.example.com/callback?error=access_denied&state=RANDOM_STATE
- 등록한 리디렉션 URL 에 이미 쿼리가 있으면 그 뒤에 이어 붙입니다.
- 앱은 먼저
state가 요청할 때 만든 값과 같은지 확인하고, 같을 때만 코드를 교환합니다.state는 요청과 콜백을 짝지어 CSRF 를 막는 값입니다. client_id나redirect_uri가 잘못된 요청은 앱으로 돌려보내지 않고 오류 화면에서 멈춥니다. 인가·토큰 오류를 참고하세요.
2. 코드를 토큰으로 교환
콜백으로 받은 인가 코드를 앱 서버에서 토큰으로 바꿉니다. 본문은 폼 형식(application/x-www-form-urlencoded)이 기본이고, Content-Type: application/json 으로 보내면 같은 이름의 JSON 본문도 받습니다. 앱 인증 방법은 아래 클라이언트 인증을 참고하세요.
요청
| 이름 | 필수 | 설명 |
|---|---|---|
grant_type |
필수 | authorization_code 입니다. |
code |
필수 | 콜백으로 받은 인가 코드입니다. |
redirect_uri |
필수 | 인가 요청에 보낸 값과 같은 값입니다. |
client_id |
필수 | 앱의 클라이언트 ID 입니다. Basic 헤더로 인증하면 생략할 수 있습니다. |
client_secret |
필수 | 앱의 시크릿입니다. 본문에 넣거나 Basic 헤더로 보냅니다. |
POST /developers/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=RECEIVED_CODE&redirect_uri=https%3A%2F%2Fmyapp.example.com%2Fcallback&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
클라이언트 인증
토큰 교환·갱신·폐기는 앱 인증이 필요합니다. 두 방법 중 하나만 씁니다.
| 방법 | 보내는 법 |
|---|---|
| 본문 | client_id, client_secret 을 폼 또는 JSON 본문에 넣습니다. |
| HTTP Basic | Authorization: Basic BASE64(client_id:client_secret) 헤더를 보냅니다. 두 값은 묶기 전에 폼 형식으로 URL 인코딩합니다(RFC 6749 §2.3.1). |
POST /developers/oauth/token
Authorization: Basic BASE64_OF_CLIENT_ID_COLON_SECRET
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=REFRESH_TOKEN
- Basic 헤더와 본문
client_secret을 함께 보내거나, 본문client_id가 헤더의 값과 다르면 400invalid_request입니다. - Basic 인증이 실패하면 401
invalid_client와 함께WWW-Authenticate: Basic realm="jamkubot"헤더가 옵니다.
응답
| 필드 | 타입 | 설명 |
|---|---|---|
access_token |
string | API 호출에 쓰는 토큰입니다. 2시간 동안 유효합니다. |
refresh_token |
string | 토큰 갱신에 쓰는 토큰입니다. |
token_type |
string | 항상 Bearer 입니다. |
expires_in |
number | 액세스 토큰의 유효 시간(초)으로 7200 입니다. |
scope |
string | 이 토큰에 담긴 권한입니다. 공백으로 구분합니다. |
{
"access_token": "ACCESS_TOKEN",
"refresh_token": "REFRESH_TOKEN",
"token_type": "Bearer",
"expires_in": 7200,
"scope": "commands:read commands:write events:subscribe"
}
- 인가 코드는 발급 후 5분 안에 한 번만 쓸 수 있습니다. 이미 쓴 코드나 만료된 코드, 인가 요청과 다른
redirect_uri로 교환하면invalid_grant입니다. - 토큰은 (앱, 채널)마다 한 쌍만 유효합니다. 같은 채널에서 새 인가 코드를 교환하면 그 채널의 이전 토큰 쌍은 바로 무효가 됩니다.
- 토큰 응답에는 어느 채널의 토큰인지 알려 주는 값이 없습니다. 받은 액세스 토큰으로 토큰 정보 (me)를 호출하면
streamer_uid와 채널 이름을 알 수 있습니다(권한 불필요). - 토큰 응답과 토큰 오류 응답에는
Cache-Control: no-store,Pragma: no-cache가 붙습니다. 프록시나 HTTP 클라이언트 캐시에 남기지 마세요.
3. API 호출
발급받은 액세스 토큰을 Authorization 헤더에 넣어 호출합니다.
GET /api/v1/developer/commands
Authorization: Bearer ACCESS_TOKEN
- REST API 는
Authorization: Bearer헤더로만 토큰을 받습니다.?access_token=쿼리는 WebSocket 연결에서만 씁니다. - 주소에 채널 UID 를 넣지 않습니다. 토큰이 연결된 채널의 데이터만 다룹니다.
- 요청마다 토큰, 스트리머의 현재 동의, 앱이 선언한 권한을 다시 확인합니다. 연결 해제나 권한 축소는 다음 요청부터 바로 반영됩니다.
- 토큰이 없거나 만료·해제됐으면 401, 그 API 에 필요한 권한이 없으면 403 입니다. 둘 다
WWW-Authenticate헤더로 이유를 함께 알립니다(RFC 6750). - 앱 전체 호출 한도를 넘으면 429 입니다. 응답 헤더
X-RateLimit-*로 남은 횟수를 볼 수 있습니다(제한).
401 {"detail": "유효하지 않거나 만료된 액세스 토큰입니다"}
WWW-Authenticate: Bearer realm="jamkubot", error="invalid_token"
403 {"detail": "이 작업에는 'commands:write' 권한(scope)이 필요합니다"}
WWW-Authenticate: Bearer realm="jamkubot", error="insufficient_scope", scope="commands:write"
API 마다 필요한 권한은 Scope 별 API에, 인증 헤더와 오류 규칙은 공통 사항에 있습니다.
4. 토큰 갱신
액세스 토큰은 2시간 뒤 만료됩니다. expires_in 을 기준으로 만료 전에 리프레시 토큰으로 새 토큰 쌍을 받습니다. 액세스 토큰이 이미 만료된 뒤에도 리프레시 토큰이 살아 있으면 갱신할 수 있습니다. 리프레시 토큰은 30일 유효하고, 갱신할 때마다 새 리프레시 토큰이 다시 30일을 받습니다. 30일 동안 한 번도 갱신하지 않으면 스트리머에게 다시 인가를 받아야 합니다.
요청
| 이름 | 필수 | 설명 |
|---|---|---|
grant_type |
필수 | refresh_token 입니다. |
refresh_token |
필수 | 가장 최근에 받은 리프레시 토큰입니다. |
client_id |
필수 | 앱의 클라이언트 ID 입니다. Basic 헤더로 인증하면 생략할 수 있습니다. |
client_secret |
필수 | 앱의 시크릿입니다. 본문에 넣거나 Basic 헤더로 보냅니다. |
scope 는 보내도 무시합니다. 갱신으로 권한을 줄이거나 늘릴 수 없습니다.
POST /developers/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
응답
토큰 교환과 같은 형식입니다.
{
"access_token": "NEW_ACCESS_TOKEN",
"refresh_token": "NEW_REFRESH_TOKEN",
"token_type": "Bearer",
"expires_in": 7200,
"scope": "commands:read commands:write events:subscribe"
}
갱신은 회전 방식입니다. 새 access_token 과 refresh_token 을 받는 순간 이전 쌍은 액세스 토큰이 아직 만료 전이어도 바로 무효가 됩니다.
- 새 토큰의 권한은 이전 토큰의 권한과 스트리머의 현재 동의가 겹치는 부분입니다. 권한을 늘리려면 인가를 다시 요청합니다.
- 같은 리프레시 토큰으로 동시에 여러 번 갱신하면 한 건만 새 토큰을 받고 나머지는
invalid_grant입니다. 갱신은 한 곳에서 하고 새 토큰을 공유합니다. - 이미 갱신에 쓴(회전된) 리프레시 토큰이 30초가 지난 뒤 다시 오면 토큰이 유출된 것으로 보고 그 채널의 토큰 쌍을 폐기합니다. 정상 앱도 새 토큰을 쓸 수 없게 되니 스트리머에게 다시 인가를 받습니다(동의는 남아 있어 동의 화면 없이 바로 코드가 돌아옵니다). 30초 안의 재시도는
invalid_grant만 받고 토큰은 그대로입니다. invalid_grant가 오면 이미 쓴 리프레시 토큰인지, 30일이 지나 만료됐는지, 스트리머가 연결을 해제했는지, 앱 권한을 줄여 토큰이 지워졌는지, 같은 채널에서 새 코드를 교환했는지 확인합니다. 다시 인가가 필요하면 스트리머에게 동의를 요청합니다.
5. 토큰 폐기
앱 사용자가 연동을 끊거나 토큰이 유출됐을 때 앱이 스스로 토큰을 무효로 만듭니다(RFC 7009, 치지직 /auth/v1/token/revoke 와 같은 역할). 그 채널의 토큰 쌍만 지우고 스트리머의 동의는 남깁니다. 동의까지 끊는 것은 스트리머가 연결된 앱에서 합니다.
요청
| 이름 | 필수 | 설명 |
|---|---|---|
token |
필수 | 폐기할 액세스 토큰 또는 리프레시 토큰입니다. 어느 쪽을 보내도 같은 쌍이 함께 지워집니다. |
token_type_hint |
선택 | access_token 또는 refresh_token. 찾는 순서만 바꾸며, 틀리거나 다른 값이면 무시하고 둘 다 찾습니다. |
client_id, client_secret |
필수 | 클라이언트 인증과 같습니다. 다른 앱의 토큰은 지울 수 없습니다. |
POST /developers/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=REFRESH_TOKEN&token_type_hint=refresh_token&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
응답
성공하면 본문 없이 200 입니다. 이미 지워졌거나 모르는 토큰도 200 으로 답합니다(토큰이 있었는지 알려 주지 않습니다). 응답에는 Cache-Control: no-store 가 붙습니다. 열려 있던 WebSocket 은 늦어도 30초 안에 4401 로 닫힙니다.
오류
| 상태 | error |
원인 |
|---|---|---|
| 400 | invalid_request |
token 이 없거나, 클라이언트 인증을 두 방법으로 함께 보냈습니다. |
| 401 | invalid_client |
앱 인증에 실패했습니다. |
| 429 | slow_down |
같은 IP·같은 앱의 요청이 분당 한도를 넘었습니다. Retry-After 초만큼 기다립니다. |
토큰 유효기간
| 항목 | 기준 |
|---|---|
| 액세스 토큰 | 2시간 (expires_in: 7200) |
| 리프레시 토큰 | 30일. 갱신에 한 번 쓰면 새 토큰으로 바뀌고 새 토큰이 다시 30일을 받습니다. |
| 인가 코드 | 5분, 1회용. 발급받은 앱이 인가 요청과 같은 redirect_uri 로만 교환합니다. |
| 유효한 토큰 수 | (앱, 채널)마다 한 쌍. 새 코드를 교환하거나 갱신하면 이전 쌍은 바로 무효입니다. |
| 권한 | 토큰 ∩ 스트리머의 현재 동의 ∩ 앱 선언. 요청마다 계산하며, 겹치는 권한이 없으면 토큰을 쓸 수 없습니다(401). |
| 연결 해제·앱 삭제·토큰 폐기 | 해당 토큰은 바로 무효입니다. 열린 WebSocket 은 늦어도 30초 안에 4401 로 닫힙니다. |
| 시크릿 재발급 | 이미 발급된 토큰은 그대로 유효합니다. 다음 교환·갱신부터 새 시크릿이 필요합니다. |
| 리디렉션 URL | 앱당 최대 5개. 인가 요청 값은 등록한 값과 정확히 같아야 합니다. |
| 토큰 요청 한도 | 같은 IP·같은 앱의 토큰·폐기 요청 분당 15회, 앱 인증을 통과한 발급·갱신은 앱 하나당 분당 15회(기본값). 운영 설정에 따라 다를 수 있습니다. |
같은 IP 에서 같은 앱으로 보낸 요청은 인증 성공·실패를 가리지 않고 세므로, 다른 사람이 client_id 만 알고 보내는 요청은 그 사람의 IP 몫만 씁니다. 앱 인증을 통과한 발급·갱신은 IP 와 상관없이 앱 몫으로 따로 셉니다. 한도를 넘으면 429 slow_down 과 Retry-After 헤더를 받으니 그만큼 기다린 뒤 다시 요청합니다. 교환·갱신을 반복해서 부르지 말고 유효한 토큰을 보관해 쓰세요. 자세한 수치는 요청 횟수에 있습니다.
인가·토큰 오류
인가 화면 오류
아래 경우에는 앱으로 돌려보내지 않고 인가할 수 없습니다 화면(HTTP 400)에서 멈춥니다. redirect_uri 를 확인하기 전이거나 동의 화면을 제출하는 중에 생긴 문제라 콜백으로 알릴 수 없습니다.
| 화면 문구 | 원인 |
|---|---|
등록되지 않은 client_id 입니다. |
client_id 가 없거나 등록되지 않은 값입니다. |
등록되지 않은 redirect_uri 입니다. 앱 설정의 redirect URI 와 정확히 일치해야 합니다. |
redirect_uri 가 없거나 등록한 값과 다릅니다. |
요청 검증(CSRF)에 실패했습니다. 처음부터 다시 시도해 주세요. |
동의 화면을 연 뒤 같은 브라우저에서 다른 동의 화면을 열었거나, 이미 허용·거부한 화면을 다시 제출했습니다. |
잘못된 요청입니다. |
동의 화면을 연 뒤 앱이 삭제됐거나 리디렉션 URL 이 등록에서 빠졌습니다. |
잘못된 scope 입니다. |
동의 화면을 연 뒤 앱이 선언한 권한에서 요청 권한이 빠졌습니다. |
콜백 오류
콜백 주소에 error=… 와 state 가 붙어 돌아옵니다. 단 invalid_scope·unsupported_response_type 은 스트리머가 이미 이 앱에 동의한 적이 있을 때만 콜백으로 보내고, 그 밖에는 잼쿠봇의 인가할 수 없습니다 화면에서 멈춥니다(아무 앱이나 등록해 잼쿠봇 주소를 외부 사이트로 가는 리디렉터로 쓰지 못하게 하려는 것입니다, RFC 9700 §4.11.2). access_denied 는 스트리머가 화면에서 고른 결과라 항상 콜백으로 갑니다.
error |
원인 | 확인할 것 |
|---|---|---|
invalid_scope |
scope 가 비었거나, 없는 권한이거나, 앱이 선언하지 않은 권한입니다. |
앱 상세의 API Scopes 와 요청 scope |
unsupported_response_type |
response_type 이 code 가 아닙니다. |
response_type=code |
access_denied |
스트리머가 거부를 눌렀습니다. | 다시 동의를 요청할지 안내 |
토큰 요청 오류
토큰 엔드포인트는 OAuth2 형식으로 오류를 돌려줍니다. 프로그램은 error 로 분기하고 error_description 은 사람이 읽는 설명으로만 씁니다.
{
"error": "invalid_grant",
"error_description": "코드가 유효하지 않거나 만료되었습니다"
}
| 상태 | error |
error_description |
원인 |
|---|---|---|---|
| 401 | invalid_client |
client_id/client_secret 가 필요합니다 |
본문과 Basic 헤더 어디에도 client_id 나 client_secret 이 없습니다. JSON 본문이 깨졌을 때도 같습니다. |
| 401 | invalid_client |
client 인증 정보가 올바르지 않습니다 |
Basic 헤더를 BASE64 로 풀 수 없거나 : 가 없습니다. |
| 401 | invalid_client |
client 인증 실패 |
시크릿이 틀렸거나(재발급 전 값 포함) 없는 앱입니다. |
| 400 | invalid_request |
클라이언트 인증은 Basic 헤더와 본문 중 한 가지로만 보내 주세요 |
Basic 헤더와 본문 client_secret 을 함께 보냈거나, 본문 client_id 가 헤더의 값과 다릅니다. |
| 400 | invalid_request |
code/redirect_uri 가 필요합니다 |
코드 교환에 code 나 redirect_uri 가 없습니다. |
| 400 | invalid_request |
refresh_token 이 필요합니다 |
갱신에 refresh_token 이 없습니다. |
| 400 | invalid_grant |
코드가 유효하지 않거나 만료되었습니다 |
만료됐거나 이미 쓴 코드, 다른 앱의 코드, 인가 요청과 다른 redirect_uri 입니다. 연결 해제나 앱 권한 축소로 지워진 코드도 같습니다. |
| 400 | invalid_grant |
refresh_token 이 유효하지 않습니다 |
이미 쓴 리프레시 토큰, 30일이 지난 토큰, 연결 해제·앱 권한 축소·새 코드 교환·토큰 폐기로 무효가 된 토큰입니다. 회전된 토큰을 30초 뒤에 다시 쓰면 그 채널의 토큰 쌍도 함께 폐기됩니다. |
| 400 | unsupported_grant_type |
grant_type 은 authorization_code 또는 refresh_token |
grant_type 이 없거나 다른 값입니다. |
| 429 | slow_down |
요청이 너무 잦습니다. …초 후 다시 시도해 주세요. |
같은 IP 에서 같은 앱으로 보낸 요청이 분당 한도를 넘었습니다. Retry-After 초만큼 기다립니다. |
| 429 | slow_down |
앱의 토큰 발급 한도를 초과했습니다 |
앱 인증을 통과한 발급·갱신이 앱 하나당 분당 한도를 넘었습니다. Retry-After 초만큼 기다립니다. |
검사는 요청 한도 → 앱 인증(client_id, client_secret) → 앱 발급 한도 → grant_type → 필수 값 → 코드·토큰 순서입니다. 예를 들어 시크릿이 틀리면 grant_type 이 잘못됐어도 invalid_client 가 먼저 옵니다.
REST API(/api/v1/developer/*)의 오류는 공통 오류를 참고하세요.
이 가이드로 해결이 안 됐나요?
디스코드로 문의하기