본문으로 건너뛰기

OAuth 2.0 인증 가이드

카페24 API는 OAuth 2.0 기반의 보안 인증 시스템을 제공합니다. 이 가이드에서는 각 단계별 인증 방식과 토큰 관리 방법을 자세히 설명합니다.

📚 목차


🔑 OAuth 2.0 개요

OAuth 2.0은 사용자의 인증 정보를 직접 공유하지 않고도 제3자 애플리케이션이 API에 안전하게 접근할 수 있는 개방형 표준입니다.

주요 특징

  • 보안성: 사용자 비밀번호를 공개하지 않음
  • 권한 제어: Scope를 통한 세밀한 권한 관리
  • 토큰 기반: 일시적인 Access Token 사용
  • 갱신 가능: Refresh Token으로 자동 갱신

핵심 용어

용어설명
Resource Owner쇼핑몰 관리자 (최종 사용자)
Client카페24 앱스토어에 등록된 애플리케이션
Authorization Server카페24 OAuth 서버
Resource Server카페24 API 서버
Access TokenAPI 접근 권한을 나타내는 임시 토큰
Refresh TokenAccess Token 갱신용 토큰
Scope토큰이 가진 권한 범위
Authorization Code임시 인증 코드 (1분 유효)

🔄 인증 흐름도

인증 흐름도


🎯 Step 1: 인증 코드 요청

개요

사용자가 애플리케이션을 승인하면 Authorization Code를 받습니다. 이 코드는 Access Token을 발급받기 위해 필요합니다.

⚠️ 중요: 코드 요청은 웹 브라우저에서만 진행하세요. cURL이나 프로그래밍 언어로 직접 요청하면 안 됩니다.

필수 파라미터

파라미터설명예시
mall_id쇼핑몰 IDyourmall
response_type응답 타입 (고정)code
client_id앱의 Client IDBrIfqEKoPxeE.....
redirect_uri인증 후 리다이렉트 URLhttps://yourapp.com/callback
scope요청할 권한 범위 (공백으로 구분)mall.read_product mall.read_order
stateCSRF 방지 토큰 (필수)random_string_12345

요청 URL

https://{mall_id}.cafe24api.com/api/v2/oauth/authorize?response_type=code&client_id={client_id}&state={state}&redirect_uri={redirect_uri}&scope={scope}

실제 사용 예

# 웹 브라우저 주소창에 입력하거나 링크로 제공
https://yourmall.cafe24api.com/api/v2/oauth/authorize?response_type=code&client_id=BrIfqEKoPxeE&state=xyz789&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback&scope=mall.read_product+mall.read_order+mall.read_store

성공 응답

사용자가 권한을 승인하면 카페24 서버가 설정된 redirect_uri로 리다이렉트합니다:

HTTP/1.1 302 Found
Location: https://yourapp.com/callback?code={authorization_code}&state=xyz789

응답 파라미터

파라미터설명
codeAuthorization Code (1분 유효, 1회 사용)
state요청 시 보낸 state 값 (위변조 방지 확인용)

구현 예제

Node.js/Express

const express = require('express');
const crypto = require('crypto');
const app = express();

const CLIENT_ID = process.env.CAFE24_CLIENT_ID;
const CLIENT_SECRET = process.env.CAFE24_CLIENT_SECRET;
const MALL_ID = process.env.CAFE24_MALL_ID;
const REDIRECT_URI = process.env.CAFE24_REDIRECT_URI;

// 로그인 버튼 클릭 시
app.get('/login', (req, res) => {
// State 값 생성 (CSRF 방지)
const state = crypto.randomBytes(16).toString('hex');

// 세션에 저장
req.session.oauthState = state;

// Scope 정의
const scopes = ['mall.read_product', 'mall.read_order', 'mall.read_store'].join(' ');

// 카페24 OAuth URL 생성
const authUrl = new URL(`https://${MALL_ID}.cafe24api.com/api/v2/oauth/authorize`);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('client_id', CLIENT_ID);
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('redirect_uri', REDIRECT_URI);
authUrl.searchParams.append('scope', scopes);

// 사용자를 OAuth 페이지로 리다이렉트
res.redirect(authUrl.toString());
});

Python/Flask

from flask import Flask, session, redirect, url_for, request
from urllib.parse import urlencode
import secrets

app = Flask(__name__)

CLIENT_ID = os.getenv('CAFE24_CLIENT_ID')
CLIENT_SECRET = os.getenv('CAFE24_CLIENT_SECRET')
MALL_ID = os.getenv('CAFE24_MALL_ID')
REDIRECT_URI = os.getenv('CAFE24_REDIRECT_URI')

@app.route('/login')
def login():
# State 값 생성
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
session.modified = True

# Scope 정의
scopes = ['mall.read_product', 'mall.read_order', 'mall.read_store']

# 파라미터 구성
params = {
'response_type': 'code',
'client_id': CLIENT_ID,
'state': state,
'redirect_uri': REDIRECT_URI,
'scope': ' '.join(scopes)
}

# OAuth URL 생성
auth_url = f"https://{MALL_ID}.cafe24api.com/api/v2/oauth/authorize?{urlencode(params)}"

return redirect(auth_url)

🎯 Step 2: Access Token 발급

개요

Authorization Code를 사용하여 실제 API 호출에 필요한 Access Token을 받습니다. 이 요청은 백엔드 서버에서 안전하게 진행해야 합니다.

필수 파라미터

파라미터설명예시
grant_type요청 타입 (고정)authorization_code
codeStep 1에서 받은 Authorization Codeabc123...
redirect_uriStep 1에서 사용한 동일한 Redirect URLhttps://yourapp.com/callback

Authorization Header

Authorization: Basic {base64_encode({client_id}:{client_secret})}

요청

curl -X POST \\
'https://{mall_id}.cafe24api.com/api/v2/oauth/token' \\
-H 'Authorization: Basic {base64_encode({client_id}:{client_secret})}' \\
-H 'Content-Type: application/x-www-form-urlencoded' \\
-d 'grant_type=authorization_code&code={code}&redirect_uri={redirect_uri}'

Base64 인코딩 생성

# macOS/Linux
echo -n "BrIfqEKoPxeE:xYz789" | base64
# 결과: QnJJZnFFSm9QeGVFOnh5Wig0OQ==

성공 응답 (200 OK)

{
"access_token": "0iqR5nM5EJIq..........",
"expires_at": "2021-03-01T14:00:00.000",
"refresh_token": "JeTJ7XpnFC0P..........",
"refresh_token_expires_at": "2021-03-15T12:00:00.000",
"client_id": "BrIfqEKoPxeE..........",
"mall_id": "yourmall",
"user_id": "test",
"scopes": [
"mall.read_order",
"mall.read_product",
"mall.read_store"
],
"issued_at": "2021-03-01T12:00:00.000",
"shop_no": "1",
"token_type": "Bearer"
}

응답 필드 설명

필드설명유효 기간
access_tokenAPI 호출용 토큰2시간
refresh_token토큰 갱신용 토큰2주
expires_atAccess Token 만료 시간-
refresh_token_expires_atRefresh Token 만료 시간-
token_type토큰 타입 (항상 Bearer)-
scopes승인된 권한 목록-
issued_at토큰 발급 시간-

구현 예제

Node.js/Express

const axios = require('axios');
const Base64 = require('js-base64').Base64;

app.get('/callback', async (req, res) => {
const { code, state } = req.query;

// State 검증
if (state !== req.session.oauthState) {
return res.status(401).json({ error: 'Invalid state' });
}

try {
// Base64 인코딩
const auth = Base64.encode(`${CLIENT_ID}:${CLIENT_SECRET}`);

// Token 요청
const response = await axios.post(
`https://${MALL_ID}.cafe24api.com/api/v2/oauth/token`,
`grant_type=authorization_code&code=${code}&redirect_uri=${REDIRECT_URI}`,
{
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);

const tokenData = response.data;

// 토큰을 DB에 저장하거나 세션에 저장
req.session.tokens = {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
expires_at: new Date(tokenData.expires_at),
refresh_token_expires_at: new Date(tokenData.refresh_token_expires_at)
};

// 사용자 정보 저장
req.session.user = {
mall_id: tokenData.mall_id,
user_id: tokenData.user_id,
shop_no: tokenData.shop_no,
scopes: tokenData.scopes
};

res.redirect('/dashboard');
} catch (error) {
console.error('Token exchange error:', error.response?.data);
res.status(400).json({ error: 'Failed to get access token' });
}
});

Python/Flask

import requests
import base64
from datetime import datetime

@app.route('/callback')
def callback():
code = request.args.get('code')
state = request.args.get('state')

# State 검증
if state != session.get('oauth_state'):
return jsonify({'error': 'Invalid state'}), 401

try:
# Base64 인코딩
auth_string = f"{CLIENT_ID}:{CLIENT_SECRET}"
auth_bytes = auth_string.encode('utf-8')
auth_b64 = base64.b64encode(auth_bytes).decode('utf-8')

# Token 요청
response = requests.post(
f"https://{MALL_ID}.cafe24api.com/api/v2/oauth/token",
data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': REDIRECT_URI
},
headers={
'Authorization': f'Basic {auth_b64}',
'Content-Type': 'application/x-www-form-urlencoded'
}
)

token_data = response.json()

# 토큰 저장
session['tokens'] = {
'access_token': token_data['access_token'],
'refresh_token': token_data['refresh_token'],
'expires_at': token_data['expires_at'],
'refresh_token_expires_at': token_data['refresh_token_expires_at']
}

session['user'] = {
'mall_id': token_data['mall_id'],
'user_id': token_data['user_id'],
'shop_no': token_data['shop_no'],
'scopes': token_data['scopes']
}

session.modified = True

return redirect('/dashboard')
except Exception as e:
print(f"Token exchange error: {e}")
return jsonify({'error': 'Failed to get access token'}), 400

🎯 Step 3: API 호출

개요

발급받은 Access Token을 사용하여 카페24 API를 호출합니다.

Authorization 헤더

모든 API 요청에 Access Token을 Bearer 토큰 형식으로 포함하세요:

Authorization: Bearer {access_token}

요청 예제

상품 조회

curl -X GET \\
'https://yourmall.cafe24api.com/api/v2/admin/products' \\
-H 'Authorization: Bearer 0iqR5nM5EJIq..........' \\
-H 'Content-Type: application/json'

주문 생성

curl -X POST \\
'https://yourmall.cafe24api.com/api/v2/admin/orders' \\
-H 'Authorization: Bearer 0iqR5nM5EJIq..........' \\
-H 'Content-Type: application/json' \\
-d '{
"customer_name": "John Doe",
"customer_email": "john@example.com",
"items": [
{
"product_no": 128,
"quantity": 2
}
]
}'

구현 예제

Node.js/Express

async function makeApiRequest(endpoint, method = 'GET', data = null) {
const { access_token } = req.session.tokens;

const config = {
method,
url: `https://${MALL_ID}.cafe24api.com/api/v2/admin${endpoint}`,
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json'
}
};

if (data) {
config.data = data;
}

try {
const response = await axios(config);
return response.data;
} catch (error) {
if (error.response?.status === 401) {
// Token 만료, 갱신 필요
throw new Error('Token expired');
}
throw error;
}
}

// 사용 예
app.get('/api/products', async (req, res) => {
try {
const products = await makeApiRequest('/products');
res.json(products);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

🔄 토큰 갱신

개요

Access Token은 2시간 후 만료됩니다. Refresh Token을 사용하면 새로운 Access Token을 발급받을 수 있습니다.

중요 사항

  • Refresh Token 유효 기간: 2주
  • 자동 갱신: Refresh Token 만료 전에 요청하면 새로운 Refresh Token도 함께 반환됨
  • 이전 토큰 폐기: 새 토큰 발급 시 이전 Refresh Token은 자동으로 폐기됨

요청

curl -X POST \\
'https://{mall_id}.cafe24api.com/api/v2/oauth/token' \\
-H 'Authorization: Basic {base64_encode({client_id}:{client_secret})}' \\
-H 'Content-Type: application/x-www-form-urlencoded' \\
-d 'grant_type=refresh_token&refresh_token={refresh_token}'

성공 응답 (200 OK)

{
"access_token": "21EZes0dGSfN..........",
"expires_at": "2021-03-01T15:50:00.000",
"refresh_token": "xLlhWztQHBik............",
"refresh_token_expires_at": "2021-03-15T13:50:00.000",
"client_id": "BrIfqEKoPxeE..........",
"mall_id": "yourmall",
"user_id": "test",
"scopes": ["mall.read_order", "mall.read_product", "mall.read_store"],
"issued_at": "2021-03-01T13:50:00.000",
"shop_no": "1",
"token_type": "Bearer"
}

구현 예제 - 자동 갱신 로직

Node.js/Express - Middleware

const Base64 = require('js-base64').Base64;

// Token 갱신 함수
async function refreshAccessToken(refreshToken) {
try {
const auth = Base64.encode(`${CLIENT_ID}:${CLIENT_SECRET}`);

const response = await axios.post(
`https://${MALL_ID}.cafe24api.com/api/v2/oauth/token`,
`grant_type=refresh_token&refresh_token=${refreshToken}`,
{
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);

return response.data;
} catch (error) {
console.error('Token refresh error:', error.response?.data);
throw error;
}
}

// Middleware - Token 자동 갱신
app.use(async (req, res, next) => {
// 토큰이 없으면 넘어감
if (!req.session.tokens) {
return next();
}

const { access_token, refresh_token, expires_at } = req.session.tokens;
const now = new Date();
const expiresDate = new Date(expires_at);

// Token 만료 5분 전에 갱신
const refreshThreshold = new Date(expiresDate.getTime() - 5 * 60 * 1000);

if (now > refreshThreshold) {
try {
const newTokens = await refreshAccessToken(refresh_token);
req.session.tokens = {
access_token: newTokens.access_token,
refresh_token: newTokens.refresh_token,
expires_at: new Date(newTokens.expires_at),
refresh_token_expires_at: new Date(newTokens.refresh_token_expires_at)
};
console.log('Token refreshed successfully');
} catch (error) {
console.error('Token refresh failed, logging out user');
req.session.destroy();
return res.redirect('/login');
}
}

next();
});

Python/Flask

def refresh_access_token(refresh_token):
"""Refresh Token으로 새로운 Access Token 발급"""
try:
auth_string = f"{CLIENT_ID}:{CLIENT_SECRET}"
auth_bytes = auth_string.encode('utf-8')
auth_b64 = base64.b64encode(auth_bytes).decode('utf-8')

response = requests.post(
f"https://{MALL_ID}.cafe24api.com/api/v2/oauth/token",
data={
'grant_type': 'refresh_token',
'refresh_token': refresh_token
},
headers={
'Authorization': f'Basic {auth_b64}',
'Content-Type': 'application/x-www-form-urlencoded'
}
)

return response.json()
except Exception as e:
print(f"Token refresh error: {e}")
raise

# Flask before_request - Token 자동 갱신
@app.before_request
def check_and_refresh_token():
if 'tokens' not in session:
return

try:
expires_at = datetime.fromisoformat(session['tokens']['expires_at'].replace('Z', '+00:00'))
refresh_threshold = expires_at - timedelta(minutes=5)

if datetime.now(timezone.utc) > refresh_threshold or datetime.now() > expires_at.replace(tzinfo=None):
new_tokens = refresh_access_token(session['tokens']['refresh_token'])
session['tokens'] = {
'access_token': new_tokens['access_token'],
'refresh_token': new_tokens['refresh_token'],
'expires_at': new_tokens['expires_at'],
'refresh_token_expires_at': new_tokens['refresh_token_expires_at']
}
session.modified = True
print('Token refreshed successfully')
except Exception as e:
print(f'Token refresh failed: {e}')
session.clear()
return redirect('/login')

🛑 토큰 폐기

개요

사용자가 로그아웃하거나 더 이상 API 접근이 필요 없을 때 토큰을 명시적으로 폐기할 수 있습니다.

중요 사항

  • Access Token을 폐기하면 해당 Refresh Token도 함께 폐기됨
  • 폐기된 토큰으로 API 호출 시 401 Unauthorized 에러 발생

요청

curl -X POST \\
'https://{mall_id}.cafe24api.com/api/v2/oauth/revoke' \\
-H 'Authorization: Basic {base64_encode({client_id}:{client_secret})}' \\
-H 'Content-Type: application/x-www-form-urlencoded' \\
-d 'token={token}&token_hint={token_hint}'

파라미터

파라미터설명예시
token폐기할 토큰 (Access 또는 Refresh)0iqR5nM5EJIq..
token_hint토큰 타입access_token 또는 refresh_token

구현 예제

Node.js/Express

async function revokeToken(token, tokenHint = 'access_token') {
try {
const auth = Base64.encode(`${CLIENT_ID}:${CLIENT_SECRET}`);

await axios.post(
`https://${MALL_ID}.cafe24api.com/api/v2/oauth/revoke`,
`token=${token}&token_hint=${tokenHint}`,
{
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);

console.log(`${tokenHint} revoked successfully`);
} catch (error) {
console.error('Token revoke error:', error.response?.data);
throw error;
}
}

// 로그아웃 엔드포인트
app.get('/logout', async (req, res) => {
try {
if (req.session.tokens) {
// Access Token 폐기
await revokeToken(req.session.tokens.access_token, 'access_token');
}

req.session.destroy();
res.redirect('/login');
} catch (error) {
console.error('Logout error:', error);
// 토큰 폐기 실패해도 세션 제거
req.session.destroy();
res.redirect('/login');
}
});

📡 API별 인증 방식

Admin API (OAuth 2.0)

인증 방식: Bearer Token

curl -X GET \\
'https://yourmall.cafe24api.com/api/v2/admin/products' \\
-H 'Authorization: Bearer {access_token}' \\
-H 'Content-Type: application/json'

사용 시 조건:

  • OAuth 2.0 인증 필수
  • Access Token 필요

Front API (Client ID 기반)

인증 방식: Client ID 헤더

curl -X GET \\
'https://yourmall.cafe24api.com/api/v2/products' \\
-H 'X-Cafe24-Client-Id: {client_id}' \\
-H 'Content-Type: application/json'

사용 시 조건:

  • OAuth 2.0 인증 불필요
  • Client ID만 필요

Cafe24 Analytics API (OAuth 2.0)

인증 방식: Bearer Token

curl -X GET \\
'https://ca-api.cafe24data.com/visitors/pageview' \\
-H 'Authorization: Bearer {access_token}' \\
-H 'Content-Type: application/json'

🛡️ Security Best Practices

1. Client Secret 보호

// ✅ 환경 변수 사용
const CLIENT_SECRET = process.env.CAFE24_CLIENT_SECRET;

// .env 파일
CAFE24_CLIENT_ID=BrIfqEKoPxeE
CAFE24_CLIENT_SECRET=xYz789

// .gitignore에 추가
echo ".env" >> .gitignore

2. HTTPS 필수

// ✅ 항상 HTTPS 사용
const redirectUri = 'https://yourapp.com/callback';

3. State 값으로 CSRF 방지

const crypto = require('crypto');
const state = crypto.randomBytes(32).toString('hex');
session.oauthState = state;

// Callback에서 검증
if (req.query.state !== session.oauthState) {
throw new Error('CSRF attack detected');
}

4. Token 저장 방법

서버 세션 (권장)

// 토큰을 서버 세션에만 저장
req.session.tokens = {
access_token: token.access_token,
refresh_token: token.refresh_token,
expires_at: token.expires_at
};

데이터베이스 저장 (암호화)

CREATE TABLE user_tokens (
id INT PRIMARY KEY,
user_id VARCHAR(255),
access_token VARCHAR(255) ENCRYPTED,
refresh_token VARCHAR(255) ENCRYPTED,
expires_at DATETIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

5. Token 만료 처리

// 토큰 만료 시간 확인
const isTokenExpired = (expiresAt) => {
return new Date() > new Date(expiresAt);
};

6. 권한 검증 (Scope 확인)

const requiredScopes = ['mall.read_product', 'mall.write_order'];
const grantedScopes = tokens.scopes;

const hasAllScopes = requiredScopes.every(scope =>
grantedScopes.includes(scope)
);

if (!hasAllScopes) {
throw new Error('Insufficient permissions');
}

🐛 에러 처리 및 트러블슈팅

401 Unauthorized

증상: "error": "invalid_grant" 또는 "error": "invalid_token"

원인:

  • Authorization Code 만료 (1분 이내 사용 필수)
  • Authorization Code 재사용 시도
  • 잘못된 Client ID/Secret
  • Token 만료

해결책:

// 토큰 갱신 시도
if (error.response?.status === 401) {
try {
const newTokens = await refreshAccessToken(refresh_token);
// 새 토큰으로 재시도
} catch (refreshError) {
// 갱신 실패, 재로그인 필요
res.redirect('/login');
}
}

400 Bad Request

증상: "error": "invalid_request"

원인:

  • 필수 파라미터 누락
  • 잘못된 redirect_uri
  • Content-Type 오류

해결책:

// 모든 필수 파라미터 확인
const requiredParams = ['grant_type', 'code', 'redirect_uri'];
const hasAllParams = requiredParams.every(param => data[param]);

if (!hasAllParams) {
throw new Error('Missing required parameters');
}

403 Forbidden

증상: Scope 권한 부족

원인:

  • 요청한 Scope가 승인되지 않음
  • 사용자가 권한 동의 시 특정 권한 거부

해결책:

// 요청한 Scope 확인
const requiredScopes = ['mall.read_product', 'mall.write_order'];
const missingScopes = requiredScopes.filter(
scope => !tokens.scopes.includes(scope)
);

if (missingScopes.length > 0) {
console.log(`Missing scopes: ${missingScopes.join(', ')}`);
// 재인증 필요
res.redirect('/login');
}

✅ 프로덕션 체크리스트

개발 단계

  • 로컬 개발 환경에서 OAuth 흐름 테스트 완료
  • Redirect URI를 localhost에서 테스트
  • State 값 검증 구현
  • Token 저장 메커니즘 구현
  • 에러 처리 로직 구현

보안 검토

  • Client Secret이 환경 변수에만 저장됨
  • .env 파일이 .gitignore에 포함됨
  • HTTPS 적용됨
  • Token이 서버 세션에만 저장됨
  • CSRF 토큰(State) 검증 구현됨
  • Token 만료 처리 구현됨

API 테스트

  • Authorization Code 발급 테스트
  • Access Token 발급 테스트
  • API 호출 테스트 (모든 Scope)
  • Token 갱신 테스트
  • Token 폐기 테스트
  • Scope 권한 검증 테스트

배포 전

  • Redirect URI를 프로덕션 URL로 변경
  • 개발자센터에서 Redirect URI 등록 확인
  • 환경 변수 설정 확인
  • HTTPS 인증서 확인
  • Error logging 설정
  • Token 갱신 로직 테스트

모니터링

  • Token 만료 이슈 모니터링
  • API 호출 실패 로깅
  • Unauthorized 에러 추적
  • Token 갱신 성공/실패율 모니터링

📚 추가 리소스


🤝 지원

문제가 발생하면:

  1. 이 가이드의 트러블슈팅 섹션 확인
  2. API 문서