GoogleOAuthService.cs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. using System.Text.Json;
  2. using Application.Abstractions.YouTube;
  3. using Microsoft.Extensions.Logging;
  4. namespace Infrastructure.Authentication;
  5. internal sealed class GoogleOAuthService(
  6. HttpClient httpClient,
  7. ILogger<GoogleOAuthService> logger
  8. ) : IGoogleOAuthService
  9. {
  10. private const string AuthEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
  11. private const string TokenEndpoint = "https://oauth2.googleapis.com/token";
  12. // ClientId/Secret은 호출자가 DB Config에서 가져와 전달하거나,
  13. // ExchangeCodeAsync 내부에서 Config를 조회하도록 확장 가능
  14. // 현재는 HttpClient DI 시 BaseAddress 없이 사용
  15. public string GetAuthorizationUrl(string state, string redirectUri, string[] scopes)
  16. {
  17. var scope = string.Join(" ", scopes);
  18. var parameters = new Dictionary<string, string>
  19. {
  20. ["response_type"] = "code",
  21. ["access_type"] = "offline", // refresh_token 획득을 위해
  22. ["prompt"] = "consent", // 항상 동의 화면 (refresh_token 보장)
  23. ["state"] = state,
  24. ["redirect_uri"] = redirectUri,
  25. ["scope"] = scope
  26. };
  27. var query = string.Join("&", parameters.Select(kv =>
  28. $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"));
  29. // client_id는 프론트엔드에서 URL에 추가 (DB Config에서 조회)
  30. return $"{AuthEndpoint}?{query}";
  31. }
  32. public async Task<GoogleOAuthTokens?> ExchangeCodeAsync(string code, string redirectUri, CancellationToken ct)
  33. {
  34. try
  35. {
  36. var content = new FormUrlEncodedContent(new Dictionary<string, string>
  37. {
  38. ["code"] = code,
  39. ["redirect_uri"] = redirectUri,
  40. ["grant_type"] = "authorization_code"
  41. });
  42. var response = await httpClient.PostAsync(TokenEndpoint, content, ct);
  43. if (!response.IsSuccessStatusCode)
  44. {
  45. var errorBody = await response.Content.ReadAsStringAsync(ct);
  46. logger.LogWarning("Google OAuth code exchange failed: {StatusCode} {Body}", response.StatusCode, errorBody);
  47. return null;
  48. }
  49. var json = await response.Content.ReadAsStringAsync(ct);
  50. return ParseTokenResponse(json);
  51. }
  52. catch (Exception ex)
  53. {
  54. logger.LogError(ex, "Google OAuth code exchange error");
  55. return null;
  56. }
  57. }
  58. public async Task<GoogleOAuthTokens?> RefreshTokenAsync(string refreshToken, CancellationToken ct)
  59. {
  60. try
  61. {
  62. var content = new FormUrlEncodedContent(new Dictionary<string, string>
  63. {
  64. ["refresh_token"] = refreshToken,
  65. ["grant_type"] = "refresh_token"
  66. });
  67. var response = await httpClient.PostAsync(TokenEndpoint, content, ct);
  68. if (!response.IsSuccessStatusCode)
  69. {
  70. var errorBody = await response.Content.ReadAsStringAsync(ct);
  71. logger.LogWarning("Google OAuth token refresh failed: {StatusCode} {Body}", response.StatusCode, errorBody);
  72. return null;
  73. }
  74. var json = await response.Content.ReadAsStringAsync(ct);
  75. return ParseTokenResponse(json);
  76. }
  77. catch (Exception ex)
  78. {
  79. logger.LogError(ex, "Google OAuth token refresh error");
  80. return null;
  81. }
  82. }
  83. private static GoogleOAuthTokens? ParseTokenResponse(string json)
  84. {
  85. var doc = JsonSerializer.Deserialize<JsonElement>(json);
  86. if (!doc.TryGetProperty("access_token", out var accessTokenProp))
  87. {
  88. return null;
  89. }
  90. var accessToken = accessTokenProp.GetString()!;
  91. var refreshToken = doc.TryGetProperty("refresh_token", out var rtProp) ? rtProp.GetString() : null;
  92. var expiresIn = doc.TryGetProperty("expires_in", out var expProp) ? expProp.GetInt32() : 3600;
  93. var scope = doc.TryGetProperty("scope", out var scopeProp) ? scopeProp.GetString() ?? "" : "";
  94. return new GoogleOAuthTokens(
  95. accessToken,
  96. refreshToken,
  97. DateTime.UtcNow.AddSeconds(expiresIn - 60), // 60초 여유
  98. scope.Split(' ', StringSplitOptions.RemoveEmptyEntries)
  99. );
  100. }
  101. }