소유자 동의
쇼핑몰 소유자를 API 주소의 동의 화면으로 보내 관리 권한을 승인받고 authorization code 를 받습니다.
업데이트됨 10.13 — 동의 화면 제목에 앱 이름을 싣습니다흐름
Mall Story 는 OAuth 2.0 Authorization Code + PKCE 만 지원합니다. 이 쪽은 쇼핑몰 소유자가 관리 권한을 승인하는 흐름입니다(소유자 토큰).
회원 본인 권한은 회원 동의로
회원이 자기 정보에 동의하는 흐름(
me:*)은 쇼핑몰 주소에서 받습니다. → 회원 동의text
외부 앱 → GET /oauth/authorize ?client_id &redirect_uri &scope &state &code_challenge
판매자 → 동의 화면: 앱 이름 · 쇼핑몰 · 요청 권한 확인 → 소유자 아이디 · 비밀번호 → [승인]
Mall Story → 302 redirect_uri?code=…&state=…
외부 앱 서버 → POST /api/oauth/token (client_id · client_secret · code · code_verifier)
← access_token · refresh_token · expires_in · shop_unique · scopePKCE 준비
요청마다 임의의 code_verifier(43~128자)를 만들고, 그 SHA-256 해시를 base64url(패딩 없음)로 바꾼 값을 code_challenge 로 보냅니다. CSRF 를 막는 state 도 함께 만듭니다. 둘 다 토큰을 받을 때까지 서버 세션에 둡니다.
import crypto from "node:crypto";
// 1) code_verifier (43자) · code_challenge (S256) · state
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
const state = crypto.randomBytes(16).toString("hex");
// 2) codeVerifier · state 를 서버 세션에 저장한다 (예: req.session.pkce = {codeVerifier, state})
// 3) 브라우저를 인가 주소로 보낸다
const url = new URL("https://api.mallstory.kr/oauth/authorize");
url.search = new URLSearchParams({
response_type: "code",
client_id: process.env.MALLSTORY_CLIENT_ID,
redirect_uri: "https://app.example.com/oauth/callback",
scope: "shop:read",
state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
}).toString();
// res.redirect(url.toString());내 구현이 맞는지 확인하기 (RFC 7636 시험값)
verifier 에
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk 를 넣으면 challenge 가 E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM 이어야 합니다. 위 예시는 모두 이 값으로 확인했습니다.자주 틀리는 곳 — base64 끝의 = 를 지우지 않음 · + / 를 - _ 로 바꾸지 않음 · 해시를 16진수 문자열로 만든 뒤 다시 base64 로 바꿈(바이트를 바로 바꿔야 합니다).인가 요청
GET
https://api.mallstory.kr/oauth/authorize| 이름 | 형식 | 설명 |
|---|---|---|
response_type필수 | string | code 고정 |
client_id필수 | string | 앱 등록 때 받은 Client ID |
redirect_uri필수 | string | 등록한 Redirect URI 중 하나. 정확히 같아야 합니다 |
scope필수 | string | 공백으로 구분한 권한 목록. 예) shop:read orders:read. 앱 등록 때 정한 상한을 넘을 수 없습니다 |
state필수 | string | CSRF 방지용 임의 값. 돌아올 때 그대로 붙어 옵니다. 없으면 400 |
code_challenge필수 | string | PKCE challenge (base64url) |
code_challenge_method필수 | string | S256 고정 |
prompt | string | consent 면 이미 승인했어도 승인 화면을 띄웁니다. 없으면 앞서 승인한 범위는 다시 묻지 않습니다 |
동의 화면에서 일어나는 일
- 두 화면입니다 — ① 소유자 통합 계정 아이디 · 비밀번호로 로그인 ② 로그인한 계정 · 앱 이름 · 쇼핑몰 · 돌아갈 주소 · 요청한 권한을 보고 [승인]. 쓰기 권한은 붉게 표시되어 판매자가 한 번 더 봅니다.
- 한 번 승인한 범위는 다시 묻지 않습니다(구글과 같습니다) — 로그인만 하면 바로 code 가 돌아옵니다. 새 권한을 요청하면 승인 화면이 다시 뜨고 새로 요청한 권한에 표시가 붙습니다. 판매자가 연결을 해지하면 처음과 같아집니다. 늘 묻게 하려면
prompt=consent. - 앱은 등록한 쇼핑몰 하나에 묶여 있어 쇼핑몰을 고르지 않습니다. 토큰도 그 쇼핑몰에만 묶입니다.
- 연속으로 비밀번호를 틀리면 30분 동안 잠깁니다.
- [거절] 하면 Redirect URI 로 error=access_denied 가 돌아갑니다.
돌아오는 응답
승인
http
HTTP/1.1 302 Found
Location: https://app.example.com/oauth/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=8f3c2a91거절 · 오류
http
HTTP/1.1 302 Found
Location: https://app.example.com/oauth/callback?error=access_denied&state=8f3c2a91state 를 꼭 비교하세요
돌아온
state 가 세션에 둔 값과 다르면 그 요청을 버립니다. code 는 10분 안에 한 번만 쓸 수 있습니다.