Appearance
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_supported의form_post·fragment: 지정하지 않는다. 기본(query)만 사용한다.claims_supported의sid: 세션 식별자. 백채널 로그아웃을 제공하지 않으므로 쓸 일이 없다.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_id | String | 발급받은 클라이언트 ID | O |
redirect_uri | String | 등록된 값과 정확히 일치. URL 인코딩 | O |
response_type | String | code 고정 | O |
scope | String | 공백 구분. openid 포함 필수. 예: openid email. 미지원 scope는 오류 없이 무시된다 | O |
state | String | CSRF 방지용 임의값(32자 이상 권장). 콜백에서 그대로 돌아온다 | O |
nonce | String | ID 토큰 재사용 방지용 임의값. ID 토큰의 nonce로 돌아온다 | O |
code_challenge | String | BASE64URL(SHA256(code_verifier)). code_verifier는 43~128자 임의 문자열로 세션에 보관 | O |
code_challenge_method | String | S256 고정 | O |
prompt | String | login: 세션이 있어도 재인증 · consent: 동의 화면 강제(offline_access 요청 시 필수 — 이미 동의한 사용자에게도 매번 동의 화면이 뜬다) · none: 화면 없이 실패 처리를 직접 해야 하므로 권장하지 않음(세션 없으면 login_required). create는 아직 지원되지 않는다 — 보내면 invalid_request(unsupported prompt value) 예정 | X |
login_hint | String | 로그인·가입 화면의 이메일 칸을 미리 채운다 | X |
max_age | Integer | 마지막 인증 후 허용 초. 초과 시 재인증. 지정하면 ID 토큰에 auth_time 포함 | X |
응답 — redirect_uri 쿼리 파라미터
| 이름 | 타입 | 설명 | 필수 |
|---|---|---|---|
code | String | 인가 코드. 60초 안에 한 번만 교환 가능. 실패한 교환(잘못된 code_verifier 등)은 코드를 소모하지 않아 60초 안에 다시 시도할 수 있다 — 성공한 교환만 코드를 무효화한다 | 성공 시 O |
state | String | 요청의 state 그대로. 다르면 처리 중단 | O |
iss | String | 발급자. 디스커버리 issuer와 대조(RFC 9207) | O |
error | String | 오류 코드. 05 오류와 점검 | 실패 시 O |
error_description | String | 개발자용 설명. 서버 기술 오류는 영문, 사용자 행위(취소 등)는 한글 — 문구는 바뀔 수 있으니 분기·표시에 쓰지 않고 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/token | client_secret_post (본문에 client_id·client_secret). HTTP Basic 헤더도 현재 통과하지만 지원 대상이 아니며 보장하지 않는다 | 인가 코드 |
요청 — 헤더
| 이름 | 값 | 필수 |
|---|---|---|
Content-Type | application/x-www-form-urlencoded | O |
요청 — 본문
| 이름 | 타입 | 설명 | 필수 |
|---|---|---|---|
grant_type | String | authorization_code 고정 | O |
code | String | 인가 코드 | O |
redirect_uri | String | 인가 요청에 보낸 값과 동일 | O |
client_id | String | 클라이언트 ID | O |
client_secret | String | 클라이언트 시크릿 | O |
code_verifier | String | code_challenge를 만든 원본 | O |
응답 — 본문
| 이름 | 타입 | 설명 | 필수 |
|---|---|---|---|
id_token | String | 사용자 정보를 담은 JWT. 검증 후 사용 | O |
access_token | String | 사용자 정보 조회용 | O |
token_type | String | Bearer | O |
expires_in | Integer | 액세스 토큰 만료(초). 600 | O |
scope | String | 실제로 부여된 scope. 요청에서 무시된 항목은 빠져 있다 | O |
refresh_token | String | offline_access를 prompt=consent와 함께 요청한 경우만 | X |
ID 토큰 페이로드
| 클레임 | 설명 | 항상 있는가 |
|---|---|---|
iss | 발급자. 디스커버리의 issuer와 같아야 한다 | 예 |
aud | 연동 서비스의 client_id와 같아야 한다 | 예 |
sub | 연동 서비스 전용 사용자 식별자 | 예 |
email email_verified | email 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": "…"}
| HTTP | error | error_description | 원인 |
|---|---|---|---|
| 400 | invalid_grant | grant request is invalid | 코드 만료(60초) · 코드 재사용 · code_verifier 불일치 · redirect_uri 불일치 |
| 401 | invalid_client | client authentication failed | client_id·client_secret 오류 |
| 400 | invalid_request | (누락 항목 설명) | 필수 파라미터 누락 |
토큰 갱신
리프레시 토큰으로 새 액세스 토큰·ID 토큰을 받는다. 리프레시 토큰도 새 값으로 교체되며 옛 값은 즉시 무효가 된다 — 응답의 새 값을 반드시 저장한다.
| 메서드 | URL | 인증 방식 | 요구 사항 |
|---|---|---|---|
| POST | /oauth/token | client_secret_post | offline_access scope로 발급된 리프레시 토큰 |
요청 — 본문
| 이름 | 타입 | 설명 | 필수 |
|---|---|---|---|
grant_type | String | refresh_token 고정 | O |
refresh_token | String | 현재 리프레시 토큰 | O |
client_id | String | 클라이언트 ID | O |
client_secret | String | 클라이언트 시크릿 | O |
응답 — 본문: 토큰 요청과 같은 구조(access_token expires_in id_token refresh_token scope token_type). refresh_token에 새 값이 온다.
갱신 응답의 id_token에는 최초 인가 요청의 nonce가 그대로 들어 있다. 갱신 경로에서는 nonce를 검증하지 않는다(표준). 서명·iss·aud·exp와 sub가 최초와 같은지만 확인한다.
오류
| HTTP | error | 원인 | 조치 |
|---|---|---|---|
| 400 | invalid_grant | 토큰 만료(30일) · 이미 교체된 옛 토큰 · 계정 정지·탈퇴 · 연결 해제 예정 | 재시도하지 않는다. 토큰을 버리고 사용자를 다시 로그인시킨다 |
사용자 정보 조회
액세스 토큰으로 사용자 정보를 받는다. ID 토큰에 같은 값이 있으므로 보통 필요하지 않다.
| 메서드 | URL | 인증 방식 |
|---|---|---|
| GET | /oauth/userinfo | 액세스 토큰 (Bearer) |
요청 — 헤더
| 이름 | 값 | 필수 |
|---|---|---|
Authorization | Bearer ${ACCESS_TOKEN} | O |
응답 — 본문
| 이름 | 타입 | 설명 | 필수 |
|---|---|---|---|
sub | String | 연동 서비스 전용 식별자 | O |
email | String | 이메일 | email scope이고 계정에 이메일이 있을 때 |
email_verified | Boolean | 이메일 확인 여부 | 같음 |
예제
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 }오류
| HTTP | error | 원인 |
|---|---|---|
| 400 | invalid_request — no access token provided (+ WWW-Authenticate: Bearer realm="…") | Authorization 헤더 누락 |
| 401 | invalid_token — invalid token provided | 만료·폐기된 액세스 토큰 |
토큰 폐기
액세스 토큰 또는 리프레시 토큰을 즉시 무효화한다. 연동 서비스 로그아웃 시 리프레시 토큰을 보관하고 있었다면 함께 폐기하는 것을 권장한다.
| 메서드 | URL | 인증 방식 |
|---|---|---|
| POST | /oauth/revoke | client_secret_post |
요청 — 본문
| 이름 | 타입 | 설명 | 필수 |
|---|---|---|---|
token | String | 폐기할 토큰 | O |
token_type_hint | String | access_token 또는 refresh_token | X |
client_id client_secret | String | 클라이언트 인증 | O |
응답: 200, 본문 없음. 이미 무효한 토큰도 200. 폐기된 액세스 토큰으로 사용자 정보를 조회하면 401 invalid_token.
로그아웃
멤버십 세션을 끊는다. 사용자 브라우저를 이 URL로 보낸다. 확인 화면이 뜨고, 확인하면 세션이 끊긴다.
| 메서드 | URL | 인증 방식 |
|---|---|---|
| GET | /oauth/logout | 없음 (브라우저 세션) |
요청 — 쿼리 파라미터
| 이름 | 타입 | 설명 | 필수 |
|---|---|---|---|
id_token_hint | String | 로그인 때 받은 ID 토큰. 어느 클라이언트에서 온 요청인지 식별 | 권장 |
post_logout_redirect_uri | String | 로그아웃 후 돌아갈 주소. 등록된 값만 허용 예정 | X |
state | String | 복귀 시 그대로 전달 | X |
현재 동작
확인 화면은 서버 기본 화면(영문, "Do you want to sign-out from …?")이며 post_logout_redirect_uri는 등록 경로가 없어 지정할 수 없다. 한글 화면과 복귀 주소 등록은 다음 배포에 반영된다 예정 — 07 변경 이력.