| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- using System.Text.Json;
- using Application.Abstractions.YouTube;
- using Microsoft.Extensions.Logging;
- namespace Infrastructure.Authentication;
- internal sealed class GoogleOAuthService(
- HttpClient httpClient,
- ILogger<GoogleOAuthService> logger
- ) : IGoogleOAuthService
- {
- private const string AuthEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
- private const string TokenEndpoint = "https://oauth2.googleapis.com/token";
- // ClientId/Secret은 호출자가 DB Config에서 가져와 전달하거나,
- // ExchangeCodeAsync 내부에서 Config를 조회하도록 확장 가능
- // 현재는 HttpClient DI 시 BaseAddress 없이 사용
- public string GetAuthorizationUrl(string state, string redirectUri, string[] scopes)
- {
- var scope = string.Join(" ", scopes);
- var parameters = new Dictionary<string, string>
- {
- ["response_type"] = "code",
- ["access_type"] = "offline", // refresh_token 획득을 위해
- ["prompt"] = "consent", // 항상 동의 화면 (refresh_token 보장)
- ["state"] = state,
- ["redirect_uri"] = redirectUri,
- ["scope"] = scope
- };
- var query = string.Join("&", parameters.Select(kv =>
- $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"));
- // client_id는 프론트엔드에서 URL에 추가 (DB Config에서 조회)
- return $"{AuthEndpoint}?{query}";
- }
- public async Task<GoogleOAuthTokens?> ExchangeCodeAsync(string code, string redirectUri, string clientId, string clientSecret, CancellationToken ct)
- {
- try
- {
- var content = new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["code"] = code,
- ["redirect_uri"] = redirectUri,
- ["grant_type"] = "authorization_code",
- ["client_id"] = clientId,
- ["client_secret"] = clientSecret
- });
- var response = await httpClient.PostAsync(TokenEndpoint, content, ct);
- if (!response.IsSuccessStatusCode)
- {
- var errorBody = await response.Content.ReadAsStringAsync(ct);
- logger.LogWarning("Google OAuth code exchange failed: {StatusCode} {Body}", response.StatusCode, errorBody);
- return null;
- }
- var json = await response.Content.ReadAsStringAsync(ct);
- return ParseTokenResponse(json);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Google OAuth code exchange error");
- return null;
- }
- }
- public async Task<GoogleOAuthTokens?> RefreshTokenAsync(string refreshToken, CancellationToken ct)
- {
- try
- {
- var content = new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["refresh_token"] = refreshToken,
- ["grant_type"] = "refresh_token"
- });
- var response = await httpClient.PostAsync(TokenEndpoint, content, ct);
- if (!response.IsSuccessStatusCode)
- {
- var errorBody = await response.Content.ReadAsStringAsync(ct);
- logger.LogWarning("Google OAuth token refresh failed: {StatusCode} {Body}", response.StatusCode, errorBody);
- return null;
- }
- var json = await response.Content.ReadAsStringAsync(ct);
- return ParseTokenResponse(json);
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Google OAuth token refresh error");
- return null;
- }
- }
- public async Task RevokeTokenAsync(string token, CancellationToken ct)
- {
- try
- {
- var content = new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["token"] = token
- });
- var response = await httpClient.PostAsync("https://oauth2.googleapis.com/revoke", content, ct);
- if (!response.IsSuccessStatusCode)
- {
- var errorBody = await response.Content.ReadAsStringAsync(ct);
- logger.LogWarning("Google OAuth revoke failed: {StatusCode} {Body}", response.StatusCode, errorBody);
- }
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Google OAuth revoke error (ignored)");
- }
- }
- private static GoogleOAuthTokens? ParseTokenResponse(string json)
- {
- var doc = JsonSerializer.Deserialize<JsonElement>(json);
- if (!doc.TryGetProperty("access_token", out var accessTokenProp))
- {
- return null;
- }
- var accessToken = accessTokenProp.GetString()!;
- var refreshToken = doc.TryGetProperty("refresh_token", out var rtProp) ? rtProp.GetString() : null;
- var expiresIn = doc.TryGetProperty("expires_in", out var expProp) ? expProp.GetInt32() : 3600;
- var scope = doc.TryGetProperty("scope", out var scopeProp) ? scopeProp.GetString() ?? "" : "";
- return new GoogleOAuthTokens(
- accessToken,
- refreshToken,
- DateTime.UtcNow.AddSeconds(expiresIn - 60), // 60초 여유
- scope.Split(' ', StringSplitOptions.RemoveEmptyEntries)
- );
- }
- }
|