Skip to content

Commit cc394c3

Browse files
committed
feat: state 값이 아닌 exchange로 변경
1 parent dc22658 commit cc394c3

File tree

5 files changed

+109
-31
lines changed

5 files changed

+109
-31
lines changed

ProjectVG.Api/Controllers/OAuthController.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,28 +92,26 @@ public async Task<IActionResult> OAuth2Callback(
9292
return Redirect(result.RedirectUrl!);
9393
}
9494

95-
[HttpGet("oauth2/token")]
96-
public async Task<IActionResult> GetOAuth2Token([FromQuery] string state)
95+
[HttpPost("oauth2/exchange")]
96+
public async Task<IActionResult> ExchangeOAuth2Token([FromBody] ExchangeTokenRequest request)
9797
{
9898
try
9999
{
100-
if (string.IsNullOrEmpty(state))
100+
if (string.IsNullOrEmpty(request.ExchangeToken))
101101
{
102-
return BadRequest(new { success = false, message = "State parameter is required" });
102+
return BadRequest(new { success = false, message = "Exchange token is required" });
103103
}
104104

105-
var tokenData = await _oauth2Service.GetTokenDataAsync(state);
105+
var tokenData = await _oauth2Service.ExchangeTokenAsync(request.ExchangeToken, HttpContext);
106106
if (tokenData == null)
107107
{
108-
return BadRequest(new { success = false, message = "Invalid or expired token request" });
108+
return BadRequest(new { success = false, message = "Invalid or expired exchange token" });
109109
}
110110

111-
await _oauth2Service.DeleteTokenDataAsync(state);
112-
113-
Console.WriteLine($"[OAuth2 Token] User UID: {tokenData.UID}");
114-
Console.WriteLine($"[OAuth2 Token] Access Token: {tokenData.AccessToken[..Math.Min(20, tokenData.AccessToken.Length)]}...");
115-
Console.WriteLine($"[OAuth2 Token] Refresh Token: {tokenData.RefreshToken[..Math.Min(20, tokenData.RefreshToken.Length)]}...");
116-
Console.WriteLine($"[OAuth2 Token] Expires In: {tokenData.ExpiresIn} seconds");
111+
Console.WriteLine($"[OAuth2 Exchange] User UID: {tokenData.UID}");
112+
Console.WriteLine($"[OAuth2 Exchange] Access Token: {tokenData.AccessToken[..Math.Min(20, tokenData.AccessToken.Length)]}...");
113+
Console.WriteLine($"[OAuth2 Exchange] Refresh Token: {tokenData.RefreshToken[..Math.Min(20, tokenData.RefreshToken.Length)]}...");
114+
Console.WriteLine($"[OAuth2 Exchange] Expires In: {tokenData.ExpiresIn} seconds");
117115

118116
Response.Headers.Append("X-Access-Token", tokenData.AccessToken);
119117
Response.Headers.Append("X-Refresh-Token", tokenData.RefreshToken);

ProjectVG.Application/Models/Auth/OAuth2Models.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public class OAuth2TokenData
5353
public int ExpiresIn { get; set; }
5454
public string UID { get; set; } = string.Empty;
5555
public DateTime CreatedAt { get; set; }
56+
public string ClientIP { get; set; } = string.Empty;
57+
public string UserAgent { get; set; } = string.Empty;
58+
public bool IsUsed { get; set; } = false;
5659
}
5760

5861
public class OAuth2CallbackResult
@@ -61,4 +64,10 @@ public class OAuth2CallbackResult
6164
public string? RedirectUrl { get; set; }
6265
public string Message { get; set; } = string.Empty;
6366
}
67+
68+
public class ExchangeTokenRequest
69+
{
70+
public string ExchangeToken { get; set; } = string.Empty;
71+
public string? ClientFingerprint { get; set; }
72+
}
6473
}

ProjectVG.Application/Services/Auth/IOAuth2Service.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public interface IOAuth2Service
88
Task<OAuth2CallbackResult> HandleOAuth2CallbackAsync(string code, string state);
99
Task<OAuth2TokenData?> GetTokenDataAsync(string state);
1010
Task DeleteTokenDataAsync(string state);
11+
Task<OAuth2TokenData?> ExchangeTokenAsync(string exchangeToken, Microsoft.AspNetCore.Http.HttpContext httpContext);
1112
Task<TokenResponse> ExchangeAuthorizationCodeAsync(string code, string clientId, string redirectUri, string codeVerifier = "");
1213
Task<OAuth2UserInfo> GetUserInfoAsync(string accessToken, string provider);
1314
Task<OAuth2AuthRequest> StoreOAuth2RequestAsync(string state, OAuth2AuthRequest request);

ProjectVG.Application/Services/Auth/OAuth2Service.cs

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class OAuth2Service : IOAuth2Service
1818
private readonly IAuthService _authService;
1919
private readonly Dictionary<string, string> _oauth2Requests = new();
2020
private readonly Dictionary<string, string> _tokenData = new();
21+
private readonly Dictionary<string, string> _exchangeTokens = new();
2122

2223
public OAuth2Service(
2324
IHttpClientFactory httpClientFactory,
@@ -303,14 +304,18 @@ public async Task<OAuth2CallbackResult> HandleOAuth2CallbackAsync(string code, s
303304
RefreshToken = authResult.Tokens.RefreshToken,
304305
ExpiresIn = (int)(authResult.Tokens.AccessTokenExpiresAt - DateTime.UtcNow).TotalSeconds,
305306
UID = authResult.User!.UID,
306-
CreatedAt = DateTime.UtcNow
307+
CreatedAt = DateTime.UtcNow,
308+
ClientIP = string.Empty,
309+
UserAgent = string.Empty,
310+
IsUsed = false
307311
};
308312

309-
await StoreTokenDataAsync(state, tokenData);
313+
var exchangeToken = GenerateExchangeToken();
314+
await StoreExchangeTokenDataAsync(exchangeToken, tokenData);
310315

311-
var clientRedirectUrl = $"{authRequest.ClientRedirectUri}?" +
312-
$"success=true&" +
313-
$"state={Uri.EscapeDataString(state)}";
316+
var clientRedirectUrl = $"{authRequest.ClientRedirectUri}#" +
317+
$"exchange_token={Uri.EscapeDataString(exchangeToken)}&" +
318+
$"expires_in=300";
314319

315320
return new OAuth2CallbackResult
316321
{
@@ -340,5 +345,43 @@ public async Task<OAuth2CallbackResult> HandleOAuth2CallbackAsync(string code, s
340345
return null;
341346
}
342347

348+
private string GenerateExchangeToken()
349+
{
350+
using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
351+
var bytes = new byte[32];
352+
rng.GetBytes(bytes);
353+
return Convert.ToBase64String(bytes).Replace("+", "-").Replace("/", "_").Replace("=", "");
354+
}
355+
356+
public async Task StoreExchangeTokenDataAsync(string exchangeToken, OAuth2TokenData tokenData)
357+
{
358+
_exchangeTokens[exchangeToken] = JsonSerializer.Serialize(tokenData);
359+
await Task.CompletedTask;
360+
}
361+
362+
public async Task<OAuth2TokenData?> ExchangeTokenAsync(string exchangeToken, Microsoft.AspNetCore.Http.HttpContext httpContext)
363+
{
364+
if (!_exchangeTokens.TryGetValue(exchangeToken, out var json))
365+
{
366+
return null;
367+
}
368+
369+
var tokenData = JsonSerializer.Deserialize<OAuth2TokenData>(json);
370+
if (tokenData == null || tokenData.IsUsed)
371+
{
372+
return null;
373+
}
374+
375+
if (DateTime.UtcNow > tokenData.CreatedAt.AddMinutes(5))
376+
{
377+
_exchangeTokens.Remove(exchangeToken);
378+
return null;
379+
}
380+
381+
_exchangeTokens.Remove(exchangeToken);
382+
383+
return tokenData;
384+
}
385+
343386
}
344387
}

test-clients/oauth2-test-client.html

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -289,21 +289,35 @@ <h1>🔐 OAuth2 Test Client</h1>
289289
document.getElementById('oauthBtn').disabled = true;
290290
}
291291

292-
// 페이지 로드 시 URL 파라미터 확인 (OAuth2 callback 처리)
292+
// 페이지 로드 시 URL Fragment 확인 (OAuth2 callback 처리)
293293
window.onload = async function() {
294294
const urlParams = new URLSearchParams(window.location.search);
295+
const hash = window.location.hash.substring(1);
296+
const hashParams = new URLSearchParams(hash);
295297

296-
if (urlParams.has('success') && urlParams.has('state')) {
297-
// OAuth2 성공 - state로 토큰 요청
298-
const success = urlParams.get('success');
299-
const state = urlParams.get('state');
298+
if (hashParams.has('exchange_token')) {
299+
// OAuth2 성공 - exchange token으로 실제 토큰 요청
300+
const exchangeToken = hashParams.get('exchange_token');
300301

301-
if (success === 'true' && state) {
302+
if (exchangeToken) {
302303
try {
303-
showResult('🔄 토큰 요청 중...', 'info');
304+
showResult('🔄 토큰 교환 중...', 'info');
305+
306+
// Fragment 즉시 정리 (보안)
307+
window.location.hash = '';
304308

305309
const serverUrl = document.getElementById('serverUrl').value;
306-
const tokenResponse = await fetch(`${serverUrl}/auth/oauth2/token?state=${encodeURIComponent(state)}`);
310+
const tokenResponse = await fetch(`${serverUrl}/auth/oauth2/exchange`, {
311+
method: 'POST',
312+
headers: {
313+
'Content-Type': 'application/json'
314+
},
315+
body: JSON.stringify({
316+
exchangeToken: exchangeToken,
317+
clientFingerprint: generateBrowserFingerprint()
318+
})
319+
});
320+
307321
const tokenData = await tokenResponse.json();
308322

309323
if (tokenData.success) {
@@ -319,20 +333,33 @@ <h1>🔐 OAuth2 Test Client</h1>
319333
`Refresh Token: ${refreshToken}\n` +
320334
`Expires In: ${expiresIn}초`, 'success');
321335
} else {
322-
showResult(`❌ 토큰 요청 실패: ${tokenData.message}`, 'error');
336+
showResult(`❌ 토큰 교환 실패: ${tokenData.message}`, 'error');
323337
}
324338
} catch (error) {
325-
showResult(`❌ 토큰 요청 중 오류: ${error.message}`, 'error');
339+
showResult(`❌ 토큰 교환 중 오류: ${error.message}`, 'error');
326340
}
327-
} else {
328-
const error = urlParams.get('error');
329-
showResult(`❌ OAuth2 로그인 실패: ${error}`, 'error');
330341
}
331-
342+
}
343+
344+
// URL 파라미터에서 에러 확인
345+
if (urlParams.has('error')) {
346+
const error = urlParams.get('error');
347+
showResult(`❌ OAuth2 로그인 실패: ${error}`, 'error');
348+
332349
// URL에서 파라미터 제거
333350
window.history.replaceState({}, document.title, window.location.pathname);
334351
}
335352
};
353+
354+
// 간단한 브라우저 핑거프린트 생성
355+
function generateBrowserFingerprint() {
356+
const screen = `${window.screen.width}x${window.screen.height}`;
357+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
358+
const language = navigator.language;
359+
const userAgent = navigator.userAgent.substring(0, 100);
360+
361+
return btoa(`${screen}-${timezone}-${language}-${userAgent}`).substring(0, 32);
362+
}
336363
</script>
337364
</body>
338365
</html>

0 commit comments

Comments
 (0)