mirror of
https://github.com/sstent/go-garth.git
synced 2026-04-05 20:32:47 +00:00
sync
This commit is contained in:
126
auth.go
126
auth.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -74,22 +75,22 @@ func NewAuthenticator(opts ClientOptions) Authenticator {
|
|||||||
|
|
||||||
// Login authenticates with Garmin services
|
// Login authenticates with Garmin services
|
||||||
func (a *GarthAuthenticator) Login(ctx context.Context, username, password, mfaToken string) (*Token, error) {
|
func (a *GarthAuthenticator) Login(ctx context.Context, username, password, mfaToken string) (*Token, error) {
|
||||||
// Fetch and store OAuth1 token to initialize session
|
// Step 1: Get OAuth1 token FIRST
|
||||||
oauth1Token, err := a.fetchOAuth1Token(ctx)
|
oauth1Token, err := a.fetchOAuth1Token(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get OAuth1 token: %w", err)
|
return nil, fmt.Errorf("failed to get OAuth1 token: %w", err)
|
||||||
}
|
}
|
||||||
a.oauth1Token = oauth1Token
|
a.oauth1Token = oauth1Token
|
||||||
|
|
||||||
// Get login parameters including CSRF token
|
// Step 2: Now get login parameters with OAuth1 context
|
||||||
authToken, tokenType, err := a.fetchLoginParams(ctx)
|
authToken, tokenType, err := a.fetchLoginParamsWithOAuth1(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get login params: %w", err)
|
return nil, fmt.Errorf("failed to get login params: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
a.csrfToken = authToken // Store for session
|
a.csrfToken = authToken // Store for session
|
||||||
|
|
||||||
// Call authenticate with the extracted token and its type
|
// Step 3: Authenticate with all tokens
|
||||||
token, err := a.authenticate(ctx, username, password, mfaToken, authToken, tokenType)
|
token, err := a.authenticate(ctx, username, password, mfaToken, authToken, tokenType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -174,34 +175,41 @@ func (a *GarthAuthenticator) GetClient() *http.Client {
|
|||||||
return a.client
|
return a.client
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchLoginParams retrieves required tokens from Garmin login page and returns token + type
|
// fetchLoginParamsWithOAuth1 retrieves login parameters with OAuth1 context
|
||||||
func (a *GarthAuthenticator) fetchLoginParams(ctx context.Context) (token string, tokenType string, err error) {
|
func (a *GarthAuthenticator) fetchLoginParamsWithOAuth1(ctx context.Context) (token string, tokenType string, err error) {
|
||||||
// Step 1: Set cookies by accessing the embed endpoint
|
// Build login URL with OAuth1 context
|
||||||
embedURL := "https://sso.garmin.com/sso/embed?" + url.Values{
|
params := url.Values{}
|
||||||
"id": []string{"gauth-widget"},
|
params.Set("id", "gauth-widget")
|
||||||
"embedWidget": []string{"true"},
|
params.Set("embedWidget", "true")
|
||||||
"gauthHost": []string{"https://sso.garmin.com/sso"},
|
params.Set("gauthHost", "https://sso.garmin.com/sso")
|
||||||
}.Encode()
|
params.Set("service", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
params.Set("source", "https://sso.garmin.com/sso")
|
||||||
|
params.Set("redirectAfterAccountLoginUrl", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
params.Set("redirectAfterAccountCreationUrl", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
params.Set("consumeServiceTicket", "false")
|
||||||
|
params.Set("generateExtraServiceTicket", "true")
|
||||||
|
params.Set("clientId", "GarminConnect")
|
||||||
|
params.Set("locale", "en_US")
|
||||||
|
|
||||||
embedReq, err := http.NewRequestWithContext(ctx, "GET", embedURL, nil)
|
// Add OAuth1 token if we have it
|
||||||
if err != nil {
|
if a.oauth1Token != "" {
|
||||||
return "", "", fmt.Errorf("failed to create embed request: %w", err)
|
params.Set("oauth_token", a.oauth1Token)
|
||||||
}
|
|
||||||
embedReq.Header = a.getEnhancedBrowserHeaders(embedURL)
|
|
||||||
|
|
||||||
_, err = a.client.Do(embedReq)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("embed request failed: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Get login parameters including CSRF token
|
loginURL := "https://sso.garmin.com/sso/signin?" + params.Encode()
|
||||||
loginURL := a.buildLoginURL()
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", loginURL, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", loginURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("failed to create login page request: %w", err)
|
return "", "", fmt.Errorf("failed to create login page request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header = a.getEnhancedBrowserHeaders(loginURL)
|
// Set headers with proper referrer chain
|
||||||
|
req.Header.Set("User-Agent", a.userAgent)
|
||||||
|
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||||
|
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||||
|
req.Header.Set("Referer", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
req.Header.Set("Origin", "https://connect.garmin.com")
|
||||||
|
|
||||||
resp, err := a.client.Do(req)
|
resp, err := a.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -216,14 +224,15 @@ func (a *GarthAuthenticator) fetchLoginParams(ctx context.Context) (token string
|
|||||||
|
|
||||||
bodyStr := string(body)
|
bodyStr := string(body)
|
||||||
|
|
||||||
// Use our robust CSRF token extractor with multiple patterns
|
// Extract CSRF/lt token
|
||||||
token, tokenType, err = getCSRFTokenWithType(bodyStr)
|
token, tokenType, err = getCSRFTokenWithType(bodyStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
filename := fmt.Sprintf("login_page_%d.html", time.Now().Unix())
|
// Save for debugging
|
||||||
|
filename := fmt.Sprintf("login_page_oauth1_%d.html", time.Now().Unix())
|
||||||
if writeErr := os.WriteFile(filename, body, 0644); writeErr == nil {
|
if writeErr := os.WriteFile(filename, body, 0644); writeErr == nil {
|
||||||
return "", "", fmt.Errorf("authentication token not found: %w (HTML saved to %s)", err, filename)
|
return "", "", fmt.Errorf("authentication token not found with OAuth1 context: %w (HTML saved to %s)", err, filename)
|
||||||
}
|
}
|
||||||
return "", "", fmt.Errorf("authentication token not found: %w (failed to save HTML for debugging)", err)
|
return "", "", fmt.Errorf("authentication token not found with OAuth1 context: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return token, tokenType, nil
|
return token, tokenType, nil
|
||||||
@@ -250,6 +259,7 @@ func (a *GarthAuthenticator) buildLoginURL() string {
|
|||||||
|
|
||||||
// fetchOAuth1Token retrieves initial OAuth1 token for session
|
// fetchOAuth1Token retrieves initial OAuth1 token for session
|
||||||
func (a *GarthAuthenticator) fetchOAuth1Token(ctx context.Context) (string, error) {
|
func (a *GarthAuthenticator) fetchOAuth1Token(ctx context.Context) (string, error) {
|
||||||
|
// Step 1: Initial OAuth1 request - this should NOT have parameters initially
|
||||||
oauth1URL := "https://connect.garmin.com/oauthConfirm"
|
oauth1URL := "https://connect.garmin.com/oauthConfirm"
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", oauth1URL, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", oauth1URL, nil)
|
||||||
@@ -257,7 +267,9 @@ func (a *GarthAuthenticator) fetchOAuth1Token(ctx context.Context) (string, erro
|
|||||||
return "", fmt.Errorf("failed to create OAuth1 request: %w", err)
|
return "", fmt.Errorf("failed to create OAuth1 request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set proper headers
|
||||||
req.Header.Set("User-Agent", a.userAgent)
|
req.Header.Set("User-Agent", a.userAgent)
|
||||||
|
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
|
||||||
resp, err := a.client.Do(req)
|
resp, err := a.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -265,24 +277,52 @@ func (a *GarthAuthenticator) fetchOAuth1Token(ctx context.Context) (string, erro
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Extract oauth_token from Location header or response body
|
// Handle redirect case - OAuth1 token often comes from redirect location
|
||||||
if location := resp.Header.Get("Location"); location != "" {
|
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||||
if u, err := url.Parse(location); err == nil {
|
location := resp.Header.Get("Location")
|
||||||
if token := u.Query().Get("oauth_token"); token != "" {
|
if location != "" {
|
||||||
return token, nil
|
if u, err := url.Parse(location); err == nil {
|
||||||
|
if token := u.Query().Get("oauth_token"); token != "" {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Or extract from HTML response
|
// If no redirect, parse response body
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
tokenPattern := regexp.MustCompile(`oauth_token=([^&\s"]+)`)
|
if err != nil {
|
||||||
matches := tokenPattern.FindStringSubmatch(string(body))
|
return "", fmt.Errorf("failed to read OAuth1 response: %w", err)
|
||||||
if len(matches) > 1 {
|
|
||||||
return matches[1], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("OAuth1 token not found")
|
// Look for oauth_token in various formats
|
||||||
|
patterns := []string{
|
||||||
|
`oauth_token=([^&\s"']+)`,
|
||||||
|
`"oauth_token":\s*"([^"]+)"`,
|
||||||
|
`'oauth_token':\s*'([^']+)'`,
|
||||||
|
`oauth_token["']?\s*[:=]\s*["']?([^"'\s&]+)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
if matches := re.FindStringSubmatch(string(body)); len(matches) > 1 {
|
||||||
|
return matches[1], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: save response to project debug directory
|
||||||
|
debugDir := "go-garth/debug"
|
||||||
|
if err := os.MkdirAll(debugDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create debug directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := filepath.Join(debugDir, fmt.Sprintf("oauth1_response_%d.html", time.Now().Unix()))
|
||||||
|
absPath, _ := filepath.Abs(filename)
|
||||||
|
if writeErr := os.WriteFile(filename, body, 0644); writeErr == nil {
|
||||||
|
return "", fmt.Errorf("OAuth1 token not found (response saved to %s)", absPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("OAuth1 token not found in response (failed to save debug file)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate performs the authentication flow
|
// authenticate performs the authentication flow
|
||||||
@@ -292,16 +332,18 @@ func (a *GarthAuthenticator) authenticate(ctx context.Context, username, passwor
|
|||||||
data.Set("password", password)
|
data.Set("password", password)
|
||||||
data.Set("embed", "true")
|
data.Set("embed", "true")
|
||||||
data.Set("rememberme", "on")
|
data.Set("rememberme", "on")
|
||||||
// Use correct token field based on token type
|
|
||||||
|
// Set the correct token field based on type
|
||||||
if tokenType == "lt" {
|
if tokenType == "lt" {
|
||||||
data.Set("lt", authToken)
|
data.Set("lt", authToken)
|
||||||
} else {
|
} else {
|
||||||
data.Set("_csrf", authToken)
|
data.Set("_csrf", authToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Set("_eventId", "submit")
|
data.Set("_eventId", "submit")
|
||||||
data.Set("geolocation", "")
|
data.Set("geolocation", "")
|
||||||
data.Set("clientId", "GarminConnect")
|
data.Set("clientId", "GarminConnect")
|
||||||
data.Set("service", "https://connect.garmin.com")
|
data.Set("service", "https://connect.garmin.com/oauthConfirm") // Updated service URL
|
||||||
data.Set("webhost", "https://connect.garmin.com")
|
data.Set("webhost", "https://connect.garmin.com")
|
||||||
data.Set("fromPage", "oauth")
|
data.Set("fromPage", "oauth")
|
||||||
data.Set("locale", "en_US")
|
data.Set("locale", "en_US")
|
||||||
|
|||||||
224
claude.md
Normal file
224
claude.md
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
// Fixed authentication flow based on Python garth implementation
|
||||||
|
|
||||||
|
// fetchOAuth1Token should be the first step and get the initial OAuth1 token
|
||||||
|
func (a *GarthAuthenticator) fetchOAuth1Token(ctx context.Context) (string, error) {
|
||||||
|
// Step 1: Initial OAuth1 request - this should NOT have parameters initially
|
||||||
|
oauth1URL := "https://connect.garmin.com/oauthConfirm"
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", oauth1URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create OAuth1 request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set proper headers
|
||||||
|
req.Header.Set("User-Agent", a.userAgent)
|
||||||
|
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("OAuth1 request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Handle redirect case - OAuth1 token often comes from redirect location
|
||||||
|
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||||
|
location := resp.Header.Get("Location")
|
||||||
|
if location != "" {
|
||||||
|
if u, err := url.Parse(location); err == nil {
|
||||||
|
if token := u.Query().Get("oauth_token"); token != "" {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no redirect, parse response body
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read OAuth1 response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for oauth_token in various formats
|
||||||
|
patterns := []string{
|
||||||
|
`oauth_token=([^&\s"']+)`,
|
||||||
|
`"oauth_token":\s*"([^"]+)"`,
|
||||||
|
`'oauth_token':\s*'([^']+)'`,
|
||||||
|
`oauth_token["']?\s*[:=]\s*["']?([^"'\s&]+)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
if matches := re.FindStringSubmatch(string(body)); len(matches) > 1 {
|
||||||
|
return matches[1], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: save response for analysis
|
||||||
|
filename := fmt.Sprintf("oauth1_response_%d.html", time.Now().Unix())
|
||||||
|
if writeErr := os.WriteFile(filename, body, 0644); writeErr == nil {
|
||||||
|
return "", fmt.Errorf("OAuth1 token not found (response saved to %s)", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("OAuth1 token not found in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated Login method with correct flow
|
||||||
|
func (a *GarthAuthenticator) Login(ctx context.Context, username, password, mfaToken string) (*Token, error) {
|
||||||
|
// Step 1: Get OAuth1 token FIRST
|
||||||
|
oauth1Token, err := a.fetchOAuth1Token(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get OAuth1 token: %w", err)
|
||||||
|
}
|
||||||
|
a.oauth1Token = oauth1Token
|
||||||
|
|
||||||
|
// Step 2: Now get login parameters with OAuth1 context
|
||||||
|
authToken, tokenType, err := a.fetchLoginParamsWithOAuth1(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get login params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.csrfToken = authToken
|
||||||
|
|
||||||
|
// Step 3: Authenticate with all tokens
|
||||||
|
token, err := a.authenticate(ctx, username, password, mfaToken, authToken, tokenType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save token to storage
|
||||||
|
if err := a.storage.SaveToken(token); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New method to fetch login params with OAuth1 context
|
||||||
|
func (a *GarthAuthenticator) fetchLoginParamsWithOAuth1(ctx context.Context) (token string, tokenType string, err error) {
|
||||||
|
// Build login URL with OAuth1 context
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("id", "gauth-widget")
|
||||||
|
params.Set("embedWidget", "true")
|
||||||
|
params.Set("gauthHost", "https://sso.garmin.com/sso")
|
||||||
|
params.Set("service", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
params.Set("source", "https://sso.garmin.com/sso")
|
||||||
|
params.Set("redirectAfterAccountLoginUrl", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
params.Set("redirectAfterAccountCreationUrl", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
params.Set("consumeServiceTicket", "false")
|
||||||
|
params.Set("generateExtraServiceTicket", "true")
|
||||||
|
params.Set("clientId", "GarminConnect")
|
||||||
|
params.Set("locale", "en_US")
|
||||||
|
|
||||||
|
// Add OAuth1 token if we have it
|
||||||
|
if a.oauth1Token != "" {
|
||||||
|
params.Set("oauth_token", a.oauth1Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
loginURL := "https://sso.garmin.com/sso/signin?" + params.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", loginURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to create login page request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers with proper referrer chain
|
||||||
|
req.Header.Set("User-Agent", a.userAgent)
|
||||||
|
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||||
|
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||||
|
req.Header.Set("Referer", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
req.Header.Set("Origin", "https://connect.garmin.com")
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("login page request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to read login page response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyStr := string(body)
|
||||||
|
|
||||||
|
// Extract CSRF/lt token
|
||||||
|
token, tokenType, err = getCSRFTokenWithType(bodyStr)
|
||||||
|
if err != nil {
|
||||||
|
// Save for debugging
|
||||||
|
filename := fmt.Sprintf("login_page_oauth1_%d.html", time.Now().Unix())
|
||||||
|
if writeErr := os.WriteFile(filename, body, 0644); writeErr == nil {
|
||||||
|
return "", "", fmt.Errorf("authentication token not found with OAuth1 context: %w (HTML saved to %s)", err, filename)
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("authentication token not found with OAuth1 context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, tokenType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced authentication method
|
||||||
|
func (a *GarthAuthenticator) authenticate(ctx context.Context, username, password, mfaToken, authToken, tokenType string) (*Token, error) {
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("username", username)
|
||||||
|
data.Set("password", password)
|
||||||
|
data.Set("embed", "true")
|
||||||
|
data.Set("rememberme", "on")
|
||||||
|
|
||||||
|
// Set the correct token field based on type
|
||||||
|
if tokenType == "lt" {
|
||||||
|
data.Set("lt", authToken)
|
||||||
|
} else {
|
||||||
|
data.Set("_csrf", authToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Set("_eventId", "submit")
|
||||||
|
data.Set("geolocation", "")
|
||||||
|
data.Set("clientId", "GarminConnect")
|
||||||
|
data.Set("service", "https://connect.garmin.com/oauthConfirm") // This should match OAuth1 context
|
||||||
|
data.Set("webhost", "https://connect.garmin.com")
|
||||||
|
data.Set("fromPage", "oauth")
|
||||||
|
data.Set("locale", "en_US")
|
||||||
|
data.Set("id", "gauth-widget")
|
||||||
|
data.Set("redirectAfterAccountLoginUrl", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
data.Set("redirectAfterAccountCreationUrl", "https://connect.garmin.com/oauthConfirm")
|
||||||
|
|
||||||
|
loginURL := "https://sso.garmin.com/sso/signin"
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", loginURL, strings.NewReader(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create SSO request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("User-Agent", a.userAgent)
|
||||||
|
req.Header.Set("Referer", "https://sso.garmin.com/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https://sso.garmin.com/sso")
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SSO request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusPreconditionFailed {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return a.handleMFA(ctx, username, password, mfaToken, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("authentication failed with status: %d, response: %s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var authResponse struct {
|
||||||
|
Ticket string `json:"ticket"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse SSO response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if authResponse.Ticket == "" {
|
||||||
|
return nil, errors.New("empty ticket in SSO response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.exchangeTicketForToken(ctx, authResponse.Ticket)
|
||||||
|
}
|
||||||
2
go-garth/debug/oauth1_response_1756930976.html
Normal file
2
go-garth/debug/oauth1_response_1756930976.html
Normal file
File diff suppressed because one or more lines are too long
2
go-garth/debug/oauth1_response_1756930988.html
Normal file
2
go-garth/debug/oauth1_response_1756930988.html
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user