⚠️ 작성 중인 초안입니다 — 검토·확정 전이며, 각 문서의 "미확정" 항목은 정책이 아닙니다.
Skip to content

03 REST API

엔드포인트별 요청·응답. 예제는 실제 서버 응답을 옮긴 것이다. 라이브러리를 쓰면 대부분 직접 호출할 일이 없다.

상태확정
최종 확인2026-09-23

모든 URL은 호스트 뒤에 붙는다. 호스트는 02 사전 설정 › 환경과 주소. 토큰·userinfo·폐기 엔드포인트는 서버 간 호출 전용이며 브라우저에서 직접 부를 수 없다(CORS 없음).

파라미터 표의 "필수" 열은 클라이언트가 지켜야 하는 것이다. 서버가 누락을 거부하는 것은 client_id·redirect_uri·response_type·scope·PKCE뿐이고, state·nonce는 빠져도 서버가 막지 않는다 — 넣지 않으면 CSRF·재사용 방어가 조용히 사라진다. 리다이렉트 상태 코드는 302가 아니라 303이다.

디스커버리

서버 설정을 JSON으로 알려준다. 라이브러리는 이 주소 하나로 나머지 엔드포인트와 서명키 위치를 알아낸다.

메서드URL인증 방식
GET/.well-known/openid-configuration없음

응답 (발췌)

json
{
  "issuer": "https://membership.m-box.com",
  "authorization_endpoint": "https://membership.m-box.com/oauth/authorize",
  "token_endpoint": "https://membership.m-box.com/oauth/token",
  "userinfo_endpoint": "https://membership.m-box.com/oauth/userinfo",
  "jwks_uri": "https://membership.m-box.com/oauth/jwks",
  "end_session_endpoint": "https://membership.m-box.com/oauth/logout",
  "revocation_endpoint": "https://membership.m-box.com/oauth/revoke",
  "pushed_authorization_request_endpoint": "https://membership.m-box.com/request",
  "scopes_supported": ["openid", "email", "offline_access"],
  "claims_supported": ["sub", "email", "email_verified", "sid", "auth_time", "iss"],
  "response_types_supported": ["code"],
  "response_modes_supported": ["form_post", "fragment", "query"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "subject_types_supported": ["pairwise"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "authorization_response_iss_parameter_supported": true,
  "claims_parameter_supported": false,
  "request_parameter_supported": false,
  "request_uri_parameter_supported": false
}

디스커버리에 보이지만 쓰지 않는 것

  • pushed_authorization_request_endpoint(PAR): 광고되지만 열려 있지 않다(404). 라이브러리가 PAR을 자동 사용하는 설정이면 꺼야 한다. 다음 배포에서 광고를 제거한다 예정.
  • response_modes_supportedform_post·fragment: 지정하지 않는다. 기본(query)만 사용한다.
  • claims_supportedsid: 세션 식별자. 백채널 로그아웃을 제공하지 않으므로 쓸 일이 없다. aud·exp·iat·nonce·at_hash는 목록에 없지만 ID 토큰에 항상 들어온다(표준 클레임은 광고하지 않는다).

JWKS

ID 토큰 서명 검증용 공개키 목록. 키는 kid로 구분한다. 캐시하되, 토큰 헤더의 kid가 목록에 없으면 다시 받는다(키 회전).

메서드URL인증 방식
GET/oauth/jwks없음

응답

json
{ "keys": [ { "kty": "RSA", "kid": "k-2026-09-23-db08ab", "alg": "RS256", "use": "sig", "n": "…", "e": "AQAB" } ] }

인가 코드 요청

사용자에게 로그인 화면(과 첫 연결이면 동의 화면)을 보이고, 완료되면 redirect_uri로 인가 코드를 전달한다. 사용자 브라우저를 이 URL로 보낸다(302). 팝업·iframe이 아니라 최상위 페이지 이동이어야 한다(iframe은 서버가 거부).

메서드URL인증 방식요구 사항
GET/oauth/authorize없음클라이언트 등록, redirect_uri 등록

요청 — 쿼리 파라미터

이름타입설명필수
client_idString발급받은 클라이언트 IDO
redirect_uriString등록된 값과 정확히 일치. URL 인코딩O
response_typeStringcode 고정O
scopeString공백 구분. openid 포함 필수. 예: openid email. 미지원 scope는 오류 없이 무시된다O
stateStringCSRF 방지용 임의값(32자 이상 권장). 콜백에서 그대로 돌아온다O
nonceStringID 토큰 재사용 방지용 임의값. ID 토큰의 nonce로 돌아온다O
code_challengeStringBASE64URL(SHA256(code_verifier)). code_verifier는 43~128자 임의 문자열로 세션에 보관O
code_challenge_methodStringS256 고정O
promptStringlogin: 세션이 있어도 재인증 · consent: 동의 화면 강제(offline_access 요청 시 필수 — 이미 동의한 사용자에게도 매번 동의 화면이 뜬다) · none: 화면 없이 실패 처리를 직접 해야 하므로 권장하지 않음(세션 없으면 login_required). create는 아직 지원되지 않는다 — 보내면 invalid_request(unsupported prompt value) 예정X
login_hintString로그인·가입 화면의 이메일 칸을 미리 채운다X
max_ageInteger마지막 인증 후 허용 초. 초과 시 재인증. 지정하면 ID 토큰에 auth_time 포함X

응답 — redirect_uri 쿼리 파라미터

이름타입설명필수
codeString인가 코드. 60초 안에 한 번만 교환 가능. 실패한 교환(잘못된 code_verifier 등)은 코드를 소모하지 않아 60초 안에 다시 시도할 수 있다 — 성공한 교환만 코드를 무효화한다성공 시 O
stateString요청의 state 그대로. 다르면 처리 중단O
issString발급자. 디스커버리 issuer와 대조(RFC 9207)O
errorString오류 코드. 05 오류와 점검실패 시 O
error_descriptionString개발자용 설명. 서버 기술 오류는 영문, 사용자 행위(취소 등)는 한글 — 문구는 바뀔 수 있으니 분기·표시에 쓰지 않고 error 코드로 판단한다실패 시 O

response_type 오류는 쿼리가 아니라 fragment(#error=unsupported_response_type…)로 온다. redirect_uri·client_id 자체가 잘못되어 돌려보낼 수 없는 오류는 멤버십 도메인의 한글 오류 화면으로 표시된다.

예제

GET /oauth/authorize
  ?client_id=remit-web
  &redirect_uri=https%3A%2F%2Fwww.remit.example%2Fcb
  &response_type=code
  &scope=openid%20email
  &state=Sau634G8avjmwmDjVcNRAKQO5dQ3UTKR
  &nonce=7hAWqHGxvf7iTYF8GLA-Ue5PTHFnLao0
  &code_challenge=tXEQEahZ3p-BDun7VtJuzJ9K7kcvpUMhjhilB-azuio
  &code_challenge_method=S256
성공: https://www.remit.example/cb?code=vgmyQYwAVSz4hZc7lleeZ9ecfNI2iI_Ha-JKpO2ZzzQ&state=Sau634G8…&iss=https%3A%2F%2Fmembership.m-box.com
취소: https://www.remit.example/cb?error=access_denied&error_description=%EC%82%AC%EC%9A%A9%EC%9E%90%EA%B0%80+%EC%B7%A8%EC%86%8C%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4&state=Sau634G8…&iss=…   (「사용자가 취소했습니다」)
PKCE 누락: https://www.remit.example/cb?error=invalid_request&error_description=Authorization+Server+policy+requires+PKCE+to+be+used+for+this+request&state=…&iss=…

토큰 요청

인가 코드를 ID 토큰·액세스 토큰으로 바꾼다. 연동 서비스 서버에서 호출한다(시크릿 필요).

메서드URL인증 방식요구 사항
POST/oauth/tokenclient_secret_post (본문에 client_id·client_secret). HTTP Basic 헤더도 현재 통과하지만 지원 대상이 아니며 보장하지 않는다인가 코드

요청 — 헤더

이름필수
Content-Typeapplication/x-www-form-urlencodedO

요청 — 본문

이름타입설명필수
grant_typeStringauthorization_code 고정O
codeString인가 코드O
redirect_uriString인가 요청에 보낸 값과 동일O
client_idString클라이언트 IDO
client_secretString클라이언트 시크릿O
code_verifierStringcode_challenge를 만든 원본O

응답 — 본문

이름타입설명필수
id_tokenString사용자 정보를 담은 JWT. 검증 후 사용O
access_tokenString사용자 정보 조회용O
token_typeStringBearerO
expires_inInteger액세스 토큰 만료(초). 600O
scopeString실제로 부여된 scope. 요청에서 무시된 항목은 빠져 있다O
refresh_tokenStringoffline_accessprompt=consent와 함께 요청한 경우만X

ID 토큰 페이로드

클레임설명항상 있는가
iss발급자. 디스커버리의 issuer와 같아야 한다
aud연동 서비스의 client_id와 같아야 한다
sub연동 서비스 전용 사용자 식별자
email email_verifiedemail scope 요청 시. 계정에 이메일이 없으면 빠짐아니오
nonce인가 요청의 nonce와 같아야 한다
iat exp발급·만료(Unix 초). exp가 지났으면 거부
at_hash함께 발급된 액세스 토큰의 해시. 라이브러리가 검증
auth_time사용자가 실제로 인증한 시각. prompt=login 또는 max_age 요청 시에만 포함아니오

검증 순서: 헤더 kid로 JWKS에서 키 선택 → RS256 서명 확인 → iss·aud·exp·nonce 확인. 라이브러리의 ID 토큰 검증 기능이 이 전부를 수행한다. aud는 단일 문자열이며 azp는 없다. 시계 차이 허용: exp는 현재 시각 + 60초까지, iat는 현재 시각 − 60초까지 유효로 본다. 직접 구현하는 절차는 04 활용 › 직접 구현.

예제

bash
curl -X POST "https://membership.m-box.com/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=vgmyQYwAVSz4hZc7lleeZ9ecfNI2iI_Ha-JKpO2ZzzQ" \
  -d "redirect_uri=https://www.remit.example/cb" \
  -d "client_id=remit-web" \
  -d "client_secret=${CLIENT_SECRET}" \
  -d "code_verifier=${CODE_VERIFIER}"
json
{
  "access_token": "fHh6IXkn_hauA14JsYGzs4eQmS0y3lnwe9wfMhxQZzL",
  "expires_in": 600,
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImstMjAyNi0wOS0yMy1kYjA4YWIifQ.eyJzdWIiOi…",
  "scope": "openid email",
  "token_type": "Bearer"
}

ID 토큰 헤더·페이로드(디코드):

json
{ "alg": "RS256", "typ": "JWT", "kid": "k-2026-09-23-db08ab" }
json
{
  "sub": "rm_7996cf354b131e55b31f262540e4c50f",
  "email": "hong@example.com",
  "email_verified": true,
  "nonce": "7hAWqHGxvf7iTYF8GLA-Ue5PTHFnLao0",
  "at_hash": "LxK3KvwgAKqAHoh0a5a0sg",
  "aud": "remit-web",
  "exp": 1790149873,
  "iat": 1790149273,
  "iss": "https://membership.m-box.com"
}

오류 — HTTP 400/401, 본문 {"error": "…", "error_description": "…"}

HTTPerrorerror_description원인
400invalid_grantgrant request is invalid코드 만료(60초) · 코드 재사용 · code_verifier 불일치 · redirect_uri 불일치
401invalid_clientclient authentication failedclient_id·client_secret 오류
400invalid_request(누락 항목 설명)필수 파라미터 누락

토큰 갱신

리프레시 토큰으로 새 액세스 토큰·ID 토큰을 받는다. 리프레시 토큰도 새 값으로 교체되며 옛 값은 즉시 무효가 된다 — 응답의 새 값을 반드시 저장한다.

메서드URL인증 방식요구 사항
POST/oauth/tokenclient_secret_postoffline_access scope로 발급된 리프레시 토큰

요청 — 본문

이름타입설명필수
grant_typeStringrefresh_token 고정O
refresh_tokenString현재 리프레시 토큰O
client_idString클라이언트 IDO
client_secretString클라이언트 시크릿O

응답 — 본문: 토큰 요청과 같은 구조(access_token expires_in id_token refresh_token scope token_type). refresh_token에 새 값이 온다.

갱신 응답의 id_token에는 최초 인가 요청의 nonce가 그대로 들어 있다. 갱신 경로에서는 nonce를 검증하지 않는다(표준). 서명·iss·aud·expsub가 최초와 같은지만 확인한다.

오류

HTTPerror원인조치
400invalid_grant토큰 만료(30일) · 이미 교체된 옛 토큰 · 계정 정지·탈퇴 · 연결 해제 예정재시도하지 않는다. 토큰을 버리고 사용자를 다시 로그인시킨다

사용자 정보 조회

액세스 토큰으로 사용자 정보를 받는다. ID 토큰에 같은 값이 있으므로 보통 필요하지 않다.

메서드URL인증 방식
GET/oauth/userinfo액세스 토큰 (Bearer)

요청 — 헤더

이름필수
AuthorizationBearer ${ACCESS_TOKEN}O

응답 — 본문

이름타입설명필수
subString연동 서비스 전용 식별자O
emailString이메일email scope이고 계정에 이메일이 있을 때
email_verifiedBoolean이메일 확인 여부같음

예제

bash
curl "https://membership.m-box.com/oauth/userinfo" -H "Authorization: Bearer ${ACCESS_TOKEN}"
json
{ "sub": "rm_7996cf354b131e55b31f262540e4c50f", "email": "hong@example.com", "email_verified": true }

오류

HTTPerror원인
400invalid_requestno access token provided (+ WWW-Authenticate: Bearer realm="…")Authorization 헤더 누락
401invalid_tokeninvalid token provided만료·폐기된 액세스 토큰

토큰 폐기

액세스 토큰 또는 리프레시 토큰을 즉시 무효화한다. 연동 서비스 로그아웃 시 리프레시 토큰을 보관하고 있었다면 함께 폐기하는 것을 권장한다.

메서드URL인증 방식
POST/oauth/revokeclient_secret_post

요청 — 본문

이름타입설명필수
tokenString폐기할 토큰O
token_type_hintStringaccess_token 또는 refresh_tokenX
client_id client_secretString클라이언트 인증O

응답: 200, 본문 없음. 이미 무효한 토큰도 200. 폐기된 액세스 토큰으로 사용자 정보를 조회하면 401 invalid_token.

로그아웃

멤버십 세션을 끊는다. 사용자 브라우저를 이 URL로 보낸다. 확인 화면이 뜨고, 확인하면 세션이 끊긴다.

메서드URL인증 방식
GET/oauth/logout없음 (브라우저 세션)

요청 — 쿼리 파라미터

이름타입설명필수
id_token_hintString로그인 때 받은 ID 토큰. 어느 클라이언트에서 온 요청인지 식별권장
post_logout_redirect_uriString로그아웃 후 돌아갈 주소. 등록된 값만 허용 예정X
stateString복귀 시 그대로 전달X

현재 동작

확인 화면은 서버 기본 화면(영문, "Do you want to sign-out from …?")이며 post_logout_redirect_uri는 등록 경로가 없어 지정할 수 없다. 한글 화면과 복귀 주소 등록은 다음 배포에 반영된다 예정07 변경 이력.