Mall StoryDevelopers
인증소유자 토큰

소유자 동의

쇼핑몰 소유자를 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 · scope

PKCE 준비

요청마다 임의의 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 로 바꿈(바이트를 바로 바꿔야 합니다).

인가 요청

GEThttps://api.mallstory.kr/oauth/authorize
이름형식설명
response_type필수stringcode 고정
client_id필수string앱 등록 때 받은 Client ID
redirect_uri필수string등록한 Redirect URI 중 하나. 정확히 같아야 합니다
scope필수string공백으로 구분한 권한 목록. 예) shop:read orders:read. 앱 등록 때 정한 상한을 넘을 수 없습니다
state필수stringCSRF 방지용 임의 값. 돌아올 때 그대로 붙어 옵니다. 없으면 400
code_challenge필수stringPKCE challenge (base64url)
code_challenge_method필수stringS256 고정
promptstringconsent 면 이미 승인했어도 승인 화면을 띄웁니다. 없으면 앞서 승인한 범위는 다시 묻지 않습니다
  • 두 화면입니다 — ① 소유자 통합 계정 아이디 · 비밀번호로 로그인 ② 로그인한 계정 · 앱 이름 · 쇼핑몰 · 돌아갈 주소 · 요청한 권한을 보고 [승인]. 쓰기 권한은 붉게 표시되어 판매자가 한 번 더 봅니다.
  • 한 번 승인한 범위는 다시 묻지 않습니다(구글과 같습니다) — 로그인만 하면 바로 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=8f3c2a91
state 를 꼭 비교하세요
돌아온 state 가 세션에 둔 값과 다르면 그 요청을 버립니다. code 는 10분 안에 한 번만 쓸 수 있습니다.