토큰 갱신
엔드포인트
POST /v1/oauth/token주의: 이 엔드포인트는 토큰 교환(/v1/oauth/token)과 동일한 통합 엔드포인트입니다.
grant_type 파라미터로 구분됩니다.
설명
Refresh Token을 사용하여 새로운 Access Token과 Refresh Token을 발급받습니다. Access Token이 만료되었을 때(1시간 후) 사용자가 다시 로그인하지 않고도 새로운 Access Token을 받을 수 있습니다.
Refresh Token은 30일의 유효기간을 가지며, 갱신 시 새로운 Refresh Token도 함께 발급됩니다.
client_secret 선택사항
client_id는 필수이며,
client_secret는 선택사항입니다.
client_secret을 제공하면 서버가 시크릿까지 함께 검증합니다.
요청 파라미터
모든 파라미터는 JSON 형식으로 요청 본문(body)에 포함되어야 합니다.
| 파라미터 | 타입 | 필수 여부 | 설명 | 예시 |
|---|---|---|---|---|
grant_type | String | 필수 | 요청 타입 (refresh_token 고정) | refresh_token |
refresh_token | String | 필수 | 이전에 발급받은 Refresh Token | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
client_id | String | 필수 | DataGSM에서 발급받은 클라이언트 ID | your-client-id |
client_secret | String | 선택 | DataGSM에서 발급받은 클라이언트 시크릿 | your-client-secret |
응답
성공 응답 (200 OK)
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "self:read"
}| 필드 | 타입 | 설명 |
|---|---|---|
access_token | String | 새로 발급된 JWT 형식의 Access Token (1시간 유효) |
token_type | String | 토큰 타입 (Bearer 고정) |
expires_in | Int | Access Token의 만료 시간 (초 단위, 3600 = 1시간) |
refresh_token | String | 새로 발급된 Refresh Token (30일 유효) |
scope | String | 발급된 권한 범위 (공백으로 구분) |
오류 응답
| 상태 코드 | 설명 | 원인 |
|---|---|---|
400 Bad Request | 잘못된 요청 | 필수 파라미터 누락 또는 잘못된 형식, Refresh Token이 유효하지 않거나 만료됨 |
401 Unauthorized | 인증 실패 | Client ID가 존재하지 않거나 Client Secret이 올바르지 않음 |
요청 예시
cURL
curl -X POST "https://oauth.data.hellogsm.kr/v1/oauth/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"client_id": "your-client-id"
}'응답 예시
성공
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"scope": "self:read"
}실패 (400 Bad Request)
{
"error": "invalid_grant",
"error_description": "Refresh Token이 유효하지 않거나 만료되었습니다."
}실패 (401 Unauthorized)
{
"error": "invalid_client",
"error_description": "클라이언트 인증에 실패했습니다."
}사용 예제
다음은 여러 언어에서 토큰을 갱신하는 예제입니다.
async function refreshAccessToken(refreshToken, clientId) {
try {
const response = await fetch('https://oauth.data.hellogsm.kr/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error_description || 'Failed to refresh token');
}
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
};
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}
// 사용 예시
const oldRefreshToken = localStorage.getItem('refreshToken');
const tokens = await refreshAccessToken(oldRefreshToken, 'your-client-id');
// 새 토큰 저장
sessionStorage.setItem('accessToken', tokens.accessToken);
localStorage.setItem('refreshToken', tokens.refreshToken);
console.log('Access Token refreshed successfully');import requests
def refresh_access_token(refresh_token, client_id):
"""
Refresh Token으로 새로운 Access Token을 발급받습니다.
Args:
refresh_token: 이전에 발급받은 Refresh Token
client_id: 클라이언트 ID
Returns:
dict: access_token, refresh_token, expires_in, scope 포함
"""
try:
response = requests.post(
'https://oauth.data.hellogsm.kr/v1/oauth/token',
headers={'Content-Type': 'application/json'},
json={
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
'client_id': client_id,
}
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f'Error: {e}')
raise
# 사용 예시
old_refresh_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
tokens = refresh_access_token(old_refresh_token, 'your-client-id')
print(f'New Access Token: {tokens["access_token"]}')
print(f'New Refresh Token: {tokens["refresh_token"]}')
print(f'Expires In: {tokens["expires_in"]} seconds')import java.net.http.*;
import java.net.URI;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
public class OAuthRefreshService {
private static final HttpClient client = HttpClient.newHttpClient();
private static final ObjectMapper mapper = new ObjectMapper();
public static TokenResponse refreshAccessToken(String refreshToken, String clientId) throws Exception {
// 요청 본문 생성
Map<String, String> requestBody = Map.of(
"grant_type", "refresh_token",
"refresh_token", refreshToken,
"client_id", clientId
);
String requestBodyJson = mapper.writeValueAsString(requestBody);
// HTTP 요청 생성
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://oauth.data.hellogsm.kr/v1/oauth/token"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBodyJson))
.build();
// 요청 전송
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
// 응답 처리
if (response.statusCode() != 200) {
throw new RuntimeException("Failed to refresh token: " + response.body());
}
JsonNode jsonNode = mapper.readTree(response.body());
return new TokenResponse(
jsonNode.get("access_token").asText(),
jsonNode.get("refresh_token").asText(),
jsonNode.get("expires_in").asInt()
);
}
public static class TokenResponse {
public final String accessToken;
public final String refreshToken;
public final int expiresIn;
public TokenResponse(String accessToken, String refreshToken, int expiresIn) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expiresIn = expiresIn;
}
}
public static void main(String[] args) throws Exception {
String oldRefreshToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
TokenResponse tokens = refreshAccessToken(oldRefreshToken, "your-client-id");
System.out.println("New Access Token: " + tokens.accessToken);
System.out.println("New Refresh Token: " + tokens.refreshToken);
System.out.println("Expires In: " + tokens.expiresIn + " seconds");
}
}import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.URI
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
data class RefreshRequest(
val grant_type: String = "refresh_token",
val refresh_token: String,
val client_id: String
)
data class TokenResponse(
@JsonProperty("access_token")
val accessToken: String,
@JsonProperty("refresh_token")
val refreshToken: String,
@JsonProperty("expires_in")
val expiresIn: Int
)
class OAuthRefreshService {
private val client = HttpClient.newHttpClient()
private val mapper = jacksonObjectMapper()
fun refreshAccessToken(refreshToken: String, clientId: String): TokenResponse {
val requestBody = RefreshRequest(
refresh_token = refreshToken,
client_id = clientId
)
val requestBodyJson = mapper.writeValueAsString(requestBody)
val request = HttpRequest.newBuilder()
.uri(URI.create("https://oauth.data.hellogsm.kr/v1/oauth/token"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBodyJson))
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() != 200) {
throw RuntimeException("Failed to refresh token: ${response.body()}")
}
return mapper.readValue(response.body())
}
}
// 사용 예시
fun main() {
val service = OAuthRefreshService()
val oldRefreshToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
val tokens = service.refreshAccessToken(oldRefreshToken, "your-client-id")
println("New Access Token: ${tokens.accessToken}")
println("New Refresh Token: ${tokens.refreshToken}")
println("Expires In: ${tokens.expiresIn} seconds")
}자동 토큰 갱신 구현
Access Token이 만료되기 전에 자동으로 갱신하는 로직을 구현하는 것이 좋습니다.
JavaScript 예제 (타이머 기반)
class TokenManager {
constructor() {
this.accessToken = null;
this.refreshToken = null;
this.refreshTimer = null;
}
setTokens(accessToken, refreshToken, expiresIn) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
// Access Token 만료 5분 전에 자동 갱신
const refreshTime = (expiresIn - 300) * 1000; // 5분 = 300초
this.scheduleRefresh(refreshTime);
}
scheduleRefresh(delay) {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
this.refreshTimer = setTimeout(async () => {
try {
const tokens = await refreshAccessToken(this.refreshToken, 'your-client-id');
this.setTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn);
console.log('Access Token refreshed automatically');
} catch (error) {
console.error('Failed to refresh token:', error);
// 갱신 실패 시 재로그인 유도
window.location.href = '/login';
}
}, delay);
}
getAccessToken() {
return this.accessToken;
}
}
// 사용 예시
const tokenManager = new TokenManager();
tokenManager.setTokens(accessToken, refreshToken, 3600);보안 주의사항
Refresh Token 보호
- Refresh Token은 30일 동안 유효하므로 안전하게 저장해야 합니다.
- 권장: HttpOnly 쿠키 또는 서버 세션에 저장
- 주의: localStorage는 XSS 공격에 취약
토큰 로테이션
- 토큰 갱신 시 새로운 Refresh Token도 함께 발급됩니다.
- 이전 Refresh Token은 무효화되므로, 항상 새로 발급된 Refresh Token을 사용하세요.
만료 처리
- Refresh Token이 만료되면 (
400 Bad Request) 사용자는 다시 로그인해야 합니다. - 만료 30일 전에 사용자에게 재로그인을 안내하는 것을 권장합니다.
다음 단계
토큰 갱신에 성공했다면, 새로운 Access Token으로 API를 호출할 수 있습니다.
- 사용자 데이터 조회 - Access Token으로 사용자 데이터 획득
- 토큰 교환 - 처음부터 OAuth 플로우 이해하기
실전 구현 예제가 필요하면 Examples 기술 문서를 참고하세요.