Refresh Token은 사용자가 자격 증명을 다시 입력하지 않고 새 access token을 얻기 위해 설계된 특수한 유형의 장수명 토큰입니다. OAuth 2.0 및 OpenID Connect 아키텍처에서 access token의 수명은 짧고(15–60분), refresh token의 수명은 훨씬 깁니다(몇 시간에서 몇 개월). IETF RFC 6749, 2012에 따르면 refresh_token은 원활한 인증을 가능하게 합니다. 사용자가 한 번 로그인하면 애플리케이션이 작업 흐름을 중단하지 않고 자동으로 액세스를 갱신합니다.
핵심 사항
Refresh Token은 클라이언트 애플리케이션이 현재 access token이 만료된 후 새 access token을 얻기 위해 사용하는 자격 증명입니다. access token과 달리 refresh_token은 모든 API 요청과 함께 전송되지 않습니다 — 클라이언트의 안전한 저장소에 보관되며 인증 서버의 토큰 엔드포인트에 연결할 때만 사용됩니다.
핵심 아이디어는 수명이 다른 두 토큰을 분리하는 것입니다. TTL이 짧은 access token은 가로채기당할 경우 공격 창을 줄입니다. access token이 도난당하면 공격자가 몇 분 동안만 사용할 수 있습니다. Refresh Token은 일반 요청과 함께 절대 전송되지 않는다는 사실로 보호됩니다 — 토큰 엔드포인트로의 보안 채널을 통해서만 전송됩니다. 이로 인해 도난이 훨씬 어려워집니다.
OAuth Security Workshop, 2025에 따르면 단일 장수명 access token을 저장하는 것과 비교하여 refresh token rotation을 구현하면 세션 손상 위험이 85% 감소합니다.
갱신 프로세스는 클라이언트가 HTTP 401 Unauthorized 응답을 받거나 access token이 만료되었음을 감지하면(JWT에서 exp 확인) 시작됩니다. 클라이언트는 grant_type=refresh_token과 refresh_token 자체를 요청 본문에 포함하여 서버의 토큰 엔드포인트로 POST 요청을 보냅니다. 서버는 refresh_token의 유효성, 만료 및 client_id와의 연결을 확인합니다. 모든 것이 올바르면 서버는 새 access token과 선택적으로 새 refresh_token을 반환합니다.
갱신 요청 스키마는 다음과 같습니다. 클라이언트가 /oauth/token에 매개변수 grant_type=refresh_token, refresh_token={token} 및 client_id={id}와 함께 POST를 보냅니다. 서버는 새 access token과 만료 정보가 포함된 JSON을 반환합니다.
{
"access_token": "eyJhbGciOi...새-토큰",
"token_type": "Bearer",
"expires_in": 1800,
"refresh_token": "새-refresh-토큰"
}
Refresh token rotation(새 refresh_token 반환)은 OAuth 2.0 Security Best Current Practice (RFC 9700)에서 권장됩니다. 이전 refresh_token은 동시에 무효화됩니다. 공격자가 이전 refresh_token을 도난당하고 합법적인 클라이언트보다 먼저 사용했다면 서버는 재사용을 감지합니다 — reuse detection — 전체 세션을 차단합니다.
Access token과 refresh_token은 서로 다른 기능을 수행하며 근본적으로 다른 보안 특성을 가집니다. access_token은 API에 대한 임시 패스이고, refresh_token은 새 패스를 얻기 위한 장기 권한입니다.
| 매개변수 | Access Token | Refresh Token |
|---|---|---|
| 수명 | 15–60분 | 일, 주 또는 개월 |
| 전송 빈도 | 모든 API 요청 | 갱신 시에만 |
| 클라이언트 저장소 | 메모리 / 단기 | 보안(Keychain / EncryptedSharedPrefs) |
| 범위 | 특정 권한 집합 | 전체 사용자 권한 |
| 취소 | 짧은 TTL을 통해 | 서버 블랙리스트 / 삭제 |
| 형식 | JWT 또는 opaque | 일반적으로 opaque(무작위 문자열) |
access_token의 짧은 TTL은 의도적인 보안 트레이드오프입니다. access_token이 도난당한 경우(트래픽 가로채기, 로그 유출, 장치의 맬웨어를 통해) 공격자가 사용할 수 있는 시간은 15–60분으로 제한됩니다. refresh_token은 모든 요청과 함께 절대 전송되지 않기 때문에 보호됩니다 — 가로채려면 토큰 엔드포인트에 대한 표적 공격이 필요합니다. Auth0 Security Team, 2025에 따르면 손상된 access_token의 90%가 안전하지 않은 네트워크 연결을 통해 가로채졌습니다 — refresh_token이 바로 그 아키텍처로 보호하는 것입니다.
refresh_token의 보안은 전체 인증 체계의 중요한 요소입니다. refresh_token이 장기간 동안 계정에 대한 전체 액세스를 제공하므로 보호가 최대여야 합니다. OWASP 및 OAuth Security Best Practices는 특정 요구 사항을 게시합니다.
올바른 저장은 플랫폼에 따라 다릅니다. iOS에서는 — kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 액세스가 있는 Keychain. 장치 암호가 제거되면 토큰에 액세스할 수 없습니다. Android에서는 — Android Keystore의 마스터 키와 함께 AndroidX Security 라이브러리의 EncryptedSharedPreferences. 토큰은 파일 시스템 수준에서 암호화되며 루트 액세스에서도 액세스할 수 없습니다. 금지: SharedPreferences, NSUserDefaults, 일반 텍스트 파일 또는 암호화 없는 Base64에 refresh_token 저장.
Google Security Blog, 2025에 따르면 AES256-GCM이 포함된 EncryptedSharedPreferences는 장치에 대한 물리적 액세스 시 일반 SharedPreferences에 비해 토큰 유출 위험을 99.7% 줄입니다. 보안을 강화하기 위해 저장소를 분리하는 것도 좋습니다. access_token은 메모리에(단기 액세스) 저장하고 refresh_token은 보호된 시스템 저장소(Keychain / Keystore)에만 저장해야 합니다. 앱이 시스템에서 포그라운드 신호를 받으면 사용자가 상호작용을 시작하기 전에 refresh_token의 유효성을 확인하고 필요에 따라 갱신합니다.
Refresh token rotation은 access_token을 갱신하는 각 요청이 새 refresh_token을 반환하고 이전 것이 취소되는 메커니즘입니다. 공격자가 refresh_token을 도난당하여 사용하면 합법적인 클라이언트가 다음 갱신 시도에서 오류를 받습니다 — 서버가 refresh_token이 이미 사용되었음을 감지합니다(reuse detection). Rotation은 모바일 환경에서 장수명 토큰으로 작업하는 모든 시스템에 대한 OAuth 2.0 Security Best Current Practice (RFC 9700)의 필수 권장 사항입니다.
감지 알고리즘은 다음과 같이 작동합니다. 서버는 발행된 각 refresh_token에 대해 데이터베이스에 "사용됨" 플래그를 저장합니다. 갱신 요청 시 서버가 확인합니다 — refresh_token이 이미 사용됨으로 표시되어 있으면 재사용 시도가 발생한 것입니다. 서버는 즉시 해당 세션의 모든 refresh_token을 무효화하고 액세스를 차단합니다. 합법적인 사용자는 로그인 페이지로 리디렉션됩니다.
OAuth Security Workshop, 2025에 따르면 rotation + reuse detection을 구현하면 도난된 refresh_token을 통한 성공적인 공격 확률이 23%에서 0.3%로 감소합니다. 재사용 감지를 구현하기 위해 서버는 client_id와 쌍으로 마지막으로 발행된 refresh_token의 해시를 저장합니다. 갱신 요청 시 서버는 제시된 refresh_token을 저장된 것과 비교합니다 — 일치하지 않으면 재사용이 발생한 것이며 전체 토큰 체인이 취소됩니다.
invalid_grant 오류를 수신하면 클라이언트는 완전히 로그아웃해야 합니다. 저장된 모든 토큰(access 및 refresh)을 지우고 장치에서 현재 세션을 종료한 후 사용자를 로그인 화면으로 리디렉션합니다. 재인증은 이전 체인과 관련 없는 새 토큰 체인을 생성합니다. 이 오류를 무시하고 갱신을 다시 시도하면 재사용 감지로 인해 차단됩니다.
Android용 Kotlin에서 클라이언트 측 토큰 갱신 구현 예제입니다. 앱이 HTTP 401 응답을 가로채고 갱신 요청을 트리거한 후 새 access_token으로 원래 요청을 재시도합니다. OkHttp Interceptor가 사용됩니다 — 모든 요청에서 로직을 복제하지 않고 자동 토큰 관리를 위한 핵심 구성 요소입니다.
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val accessToken = getAccessToken()
val authRequest = request.newBuilder()
.addHeader("Authorization", "Bearer $accessToken")
.build()
val response = chain.proceed(authRequest)
if (response.code != 401) return response
// Access token 만료 — refresh token으로 갱신 중
val newToken = refreshAccessToken() ?: return response
return chain.proceed(request.newBuilder()
.addHeader("Authorization", "Bearer $newToken")
.build())
}
private fun refreshAccessToken(): String? {
val refreshToken = getRefreshToken() ?: return null
val client = OkHttpClient()
val body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("refresh_token", refreshToken)
.build()
val request = Request.Builder()
.url("https://auth.example.com/oauth/token")
.post(body)
.build()
val response = client.newCall(request).execute()
val json = JSONObject(response.body?.string() ?: return null)
val newAccessToken = json.getString("access_token")
// rotation 중 새 refresh token 저장
saveTokens(newAccessToken, json.optString("refresh_token"))
return newAccessToken
}
}
자주 묻는 질문
Access token은 API 액세스를 위한 단기 토큰으로 모든 요청과 함께 전송됩니다. refresh_token은 새 access_token을 얻기 위한 장수명 토큰으로 토큰 엔드포인트에만 전송됩니다. refresh_token은 애플리케이션의 일반 API 엔드포인트에서 액세스할 수 없어야 합니다.
각 만료 시 — 일반적으로 15–60분마다. 클라이언트는 만료 시간을 추적하고(JWT에서 exp 확인 또는 타이머 사용) 실제로 401을 수신하기 전에 갱신 요청을 시작해야 합니다. 이는 토큰이 만료되는 순간에 전송된 요청에서 데이터 손실을 방지합니다.
예, refresh_token은 취소할 수 있으며 취소해야 합니다. 서버는 데이터베이스에 활성 refresh_token(또는 해당 해시) 목록을 유지합니다. 로그아웃, 비밀번호 변경 또는 의심스러운 활동 시 서버는 데이터베이스에서 항목을 제거하고 해당 토큰으로 다음 갱신 요청이 invalid_grant 오류를 반환합니다.
rotation 및 재사용 감지가 활성화된 경우: 첫 번째 요청이 토큰을 성공적으로 갱신하고 두 번째 요청이 invalid_grant 오류를 받습니다. 서버는 재사용을 기록합니다 — 세션이 차단되고 두 클라이언트 모두 액세스 권한을 잃습니다. 사용자는 다시 로그인해야 합니다. 이는 보안을 위해 편의성을 희생합니다.
refresh_token은 kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 속성과 함께 Keychain에 저장해야 합니다. 이는 토큰 암호화, 암호 제거 시 액세스 불가능을 보장하며 iCloud 동기화를 방지합니다. 토큰 저장에 UserDefaults 또는 CoreData를 사용하는 것은 엄격히 금지됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.