diff --git a/src/Garmin/ApiClient.cs b/src/Garmin/ApiClient.cs index 7a4d8e5ff..c69684264 100644 --- a/src/Garmin/ApiClient.cs +++ b/src/Garmin/ApiClient.cs @@ -14,7 +14,8 @@ namespace Garmin public interface IGarminApiClient { Task InitSigninFlowAsync(object queryParams, string userAgent, out CookieJar jar); - Task InitCookieJarAsync(object queryParams, string userAgent, out CookieJar jar) + Task InitCookieJarAsync(object queryParams, string userAgent, out CookieJar jar); + Task GetCsrfTokenAsync(GarminApiAuthentication auth, object queryParams, CookieJar jar); Task SendCredentialsAsync(GarminApiAuthentication auth, object queryParams, object loginData, CookieJar jar); Task SendMfaCodeAsync(string userAgent, object queryParams, object mfaData, CookieJar jar); Task SendServiceTicketAsync(string userAgent, string serviceTicket, CookieJar jar); @@ -25,7 +26,7 @@ public class ApiClient : IGarminApiClient { private const string BASE_URL = "https://connect.garmin.com"; private const string SSO_URL = "https://sso.garmin.com"; - private const string SIGNIN_URL = "https://sso.garmin.com/sso/signin"; + private const string SSO_SIGNIN_URL = "https://sso.garmin.com/sso/signin"; private const string SSO_EMBED_URL = "https://sso.garmin.com/sso/embed"; private static string UPLOAD_URL = $"{BASE_URL}/modern/proxy/upload-service/upload"; @@ -36,7 +37,7 @@ public class ApiClient : IGarminApiClient public Task InitSigninFlowAsync(object queryParams, string userAgent, out CookieJar jar) { - return SIGNIN_URL + return SSO_SIGNIN_URL .WithHeader("User-Agent", userAgent) .WithHeader("origin", ORIGIN) .SetQueryParams(queryParams) @@ -57,7 +58,7 @@ public Task InitCookieJarAsync(object queryParams, string userAgent, out CookieJ public async Task SendCredentialsAsync(GarminApiAuthentication auth, object queryParams, object loginData, CookieJar jar) { var result = new SendCredentialsResult(); - result.RawResponseBody = await SIGNIN_URL + result.RawResponseBody = await SSO_SIGNIN_URL .WithHeader("User-Agent", auth.UserAgent) .WithHeader("origin", ORIGIN) .SetQueryParams(queryParams) @@ -70,6 +71,21 @@ public async Task SendCredentialsAsync(GarminApiAuthentic return result; } + public async Task GetCsrfTokenAsync(GarminApiAuthentication auth, object queryParams, CookieJar jar) + { + var result = new GarminResult(); + result.RawResponseBody = await SSO_SIGNIN_URL + .WithHeader("User-Agent", auth.UserAgent) + .WithHeader("origin", ORIGIN) + .SetQueryParams(queryParams) + .WithCookies(jar) + .StripSensitiveDataFromLogging(auth.Email, auth.Password) + .GetAsync() + .ReceiveString(); + + return result; + } + public Task SendMfaCodeAsync(string userAgent, object queryParams, object mfaData, CookieJar jar) { return "https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode" diff --git a/src/Garmin/Auth/GarminAuthContracts.cs b/src/Garmin/Auth/GarminAuthContracts.cs index a0c5b2bc8..d0f312390 100644 --- a/src/Garmin/Auth/GarminAuthContracts.cs +++ b/src/Garmin/Auth/GarminAuthContracts.cs @@ -5,6 +5,12 @@ public interface GarminResultWrapper string RawResponseBody { get; set; } } +public class GarminResult : GarminResultWrapper +{ + public string RawResponseBody { get; set; } +} + + public class SendCredentialsResult : GarminResultWrapper { public bool WasRedirected { get; set; } diff --git a/src/Garmin/Auth/GarminOAuthService.cs b/src/Garmin/Auth/GarminOAuthService.cs index bcd76579e..caa9a8d4b 100644 --- a/src/Garmin/Auth/GarminOAuthService.cs +++ b/src/Garmin/Auth/GarminOAuthService.cs @@ -1,14 +1,11 @@ using Common.Service; using Common.Stateful; -using Flurl; using Flurl.Http; using System; -using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Runtime.Intrinsics.X86; -using System.Text; -using System.Threading.Tasks; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using IdentityModel.Client; namespace Garmin.Auth; @@ -54,18 +51,68 @@ private async Task GetAuthTokenAsync() throw new GarminAuthenticationError("Failed to initialize sign in flow.", e) { Code = Code.FailedPriorToCredentialsUsed }; } - object loginData = new - { - embed = "true", - username = auth.Email, - password = auth.Password, - lt = "e1s1", - _eventId = "submit", - displayNameRequired = "false", + ///////////////////////////////// + // Get CSRF token + //////////////////////////////// + object csrfRequest = new + { + id = "gauth-widget", + embed = "true", + gauthHost = "https://sso.garmin.com/sso", + service = "https://sso.garmin.com/sso/embed", + source = "https://sso.garmin.com/sso/embed", + redirectAfterAccountLoginUrl = "https://sso.garmin.com/sso/embed", + redirectAfterAccountCreationUrl = "https://sso.garmin.com/sso/embed", }; + var tokenResult = await _apiClient.GetCsrfTokenAsync(auth, csrfRequest, jar); + var tokenRegex = new Regex("name=\"_csrf\"\\s+value=\"(.+?)\""); + var match = tokenRegex.Match(tokenResult.RawResponseBody); + if (!match.Success) + throw new Exception("Failed to regex match token"); + + var csrfToken = match.Groups.Values.First(); + ///////////////////////////////// - // Get CSRF token + // Submit login form + //////////////////////////////// + var loginData = new + { + username = "email", + passowrd = "password", + embed = "true", + _csrf = csrfToken + }; + var signInResult = await _apiClient.SendCredentialsAsync(auth, csrfRequest, loginData, jar); + + if (signInResult.WasRedirected && signInResult.RedirectedTo.Contains("https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode")) + { + // todo: handle mfa flow + throw new NotImplementedException("handle mfa"); + } + + var ticketRegex = new Regex("embed\\?ticket=([^\"]+)\""); + var ticketMatch = ticketRegex.Match(signInResult.RawResponseBody); + if (!ticketMatch.Success) + throw new Exception("Filed to find post signin ticket."); + + var ticket = ticketMatch.Groups.Values.First(); + + ///////////////////////////////// + // Get OAuth Tokens //////////////////////////////// + + // TODO: fetch id and secret from garth hosted file + var c = new FlurlClient(); + var result = await c + .WithHeader("User-Agent", auth.UserAgent) + .HttpClient + .RequestTokenAsync(new TokenRequest() + { + Address = $"https://connectapi.garmin.com/oauth-service/oauth/preauthorized?ticket={ticket}&login-url=https://sso.garmin.com/sso/embed&accepts-mfa-tokens=true", + ClientId = "fc3e99d2-118c-44b8-8ae3-03370dde24c0", + ClientSecret = "E08WAR897WEy2knn7aFBrvegVAf0AFdWBBF" + }); + } }