using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; using ytLive.Models; namespace ytLive.Services; /// /// Handles YouTube OAuth2 authentication flow. /// Uses a local HTTP listener for the redirect callback. /// public class YouTubeAuthService { private const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"; private const string TokenEndpoint = "https://oauth2.googleapis.com/token"; private const string Scope = "https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl"; private readonly string _clientId; private readonly string _clientSecret; private readonly HttpClient _http = new(); public YouTubeChannel? CurrentChannel { get; private set; } public YouTubeAuthService(string clientId, string clientSecret) { _clientId = clientId; _clientSecret = clientSecret; } public string GetAuthorizationUrl(string redirectUri) { var parameters = HttpUtility.ParseQueryString(string.Empty); parameters["client_id"] = _clientId; parameters["redirect_uri"] = redirectUri; parameters["response_type"] = "code"; parameters["scope"] = Scope; parameters["access_type"] = "offline"; parameters["prompt"] = "consent"; return $"{AuthorizationEndpoint}?{parameters}"; } public async Task ExchangeCodeForToken(string code, string redirectUri) { var body = new FormUrlEncodedContent(new Dictionary { ["code"] = code, ["client_id"] = _clientId, ["client_secret"] = _clientSecret, ["redirect_uri"] = redirectUri, ["grant_type"] = "authorization_code" }); var response = await _http.PostAsync(TokenEndpoint, body); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); var tokenData = JsonSerializer.Deserialize(json); var accessToken = tokenData.GetProperty("access_token").GetString()!; var refreshToken = tokenData.GetProperty("refresh_token").GetString()!; var expiresIn = tokenData.GetProperty("expires_in").GetInt32(); // Fetch channel info return await FetchChannelInfo(accessToken, refreshToken, expiresIn); } public async Task RefreshToken() { if (CurrentChannel == null) return false; var body = new FormUrlEncodedContent(new Dictionary { ["refresh_token"] = CurrentChannel.RefreshToken, ["client_id"] = _clientId, ["client_secret"] = _clientSecret, ["grant_type"] = "refresh_token" }); var response = await _http.PostAsync(TokenEndpoint, body); if (!response.IsSuccessStatusCode) return false; var json = await response.Content.ReadAsStringAsync(); var tokenData = JsonSerializer.Deserialize(json); CurrentChannel.AccessToken = tokenData.GetProperty("access_token").GetString()!; CurrentChannel.TokenExpiry = DateTime.UtcNow.AddSeconds(tokenData.GetProperty("expires_in").GetInt32()); return true; } private async Task FetchChannelInfo(string accessToken, string refreshToken, int expiresIn) { _http.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); var response = await _http.GetAsync("https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true"); if (!response.IsSuccessStatusCode) return null; var json = await response.Content.ReadAsStringAsync(); var data = JsonSerializer.Deserialize(json); var items = data.GetProperty("items"); if (items.GetArrayLength() == 0) return null; var snippet = items[0].GetProperty("snippet"); CurrentChannel = new YouTubeChannel { ChannelId = items[0].GetProperty("id").GetString()!, DisplayName = snippet.GetProperty("title").GetString()!, ProfileImageUrl = snippet.GetProperty("thumbnails").GetProperty("default").GetProperty("url").GetString()!, AccessToken = accessToken, RefreshToken = refreshToken, TokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn) }; return CurrentChannel; } }