Files
go-garth/internal/auth/oauth/oauth.go
sstent bb07b261bf Refactor: Complete authentication flow changes
This commit includes the remaining files from the authentication flow refactoring.\nThese changes were part of the initial diff between c00ea67f31 and HEAD,\nand complete the transition to the new SSO and OAuth-based authentication mechanism.
2025-09-18 13:32:33 -07:00

162 lines
4.5 KiB
Go

package oauth
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"garmin-connect/internal/types"
"garmin-connect/internal/utils"
)
// GetOAuth1Token retrieves an OAuth1 token using the provided ticket
func GetOAuth1Token(domain, ticket string) (*types.OAuth1Token, error) {
scheme := "https"
if strings.HasPrefix(domain, "127.0.0.1") {
scheme = "http"
}
consumer, err := utils.LoadOAuthConsumer()
if err != nil {
return nil, fmt.Errorf("failed to load OAuth consumer: %w", err)
}
baseURL := fmt.Sprintf("%s://connectapi.%s/oauth-service/oauth/", scheme, domain)
loginURL := fmt.Sprintf("%s://sso.%s/sso/embed", scheme, domain)
tokenURL := fmt.Sprintf("%spreauthorized?ticket=%s&login-url=%s&accepts-mfa-tokens=true",
baseURL, ticket, url.QueryEscape(loginURL))
// Parse URL to extract query parameters for signing
parsedURL, err := url.Parse(tokenURL)
if err != nil {
return nil, err
}
// Extract query parameters
queryParams := make(map[string]string)
for key, values := range parsedURL.Query() {
if len(values) > 0 {
queryParams[key] = values[0]
}
}
// Create OAuth1 signed request
baseURLForSigning := parsedURL.Scheme + "://" + parsedURL.Host + parsedURL.Path
authHeader := utils.CreateOAuth1AuthorizationHeader("GET", baseURLForSigning, queryParams,
consumer.ConsumerKey, consumer.ConsumerSecret, "", "")
req, err := http.NewRequest("GET", tokenURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("User-Agent", "com.garmin.android.apps.connectmobile")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
bodyStr := string(body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("OAuth1 request failed with status %d: %s", resp.StatusCode, bodyStr)
}
// Parse query string response - handle both & and ; separators
bodyStr = strings.ReplaceAll(bodyStr, ";", "&")
values, err := url.ParseQuery(bodyStr)
if err != nil {
return nil, fmt.Errorf("failed to parse OAuth1 response: %w", err)
}
oauthToken := values.Get("oauth_token")
oauthTokenSecret := values.Get("oauth_token_secret")
if oauthToken == "" || oauthTokenSecret == "" {
return nil, fmt.Errorf("missing oauth_token or oauth_token_secret in response")
}
return &types.OAuth1Token{
OAuthToken: oauthToken,
OAuthTokenSecret: oauthTokenSecret,
MFAToken: values.Get("mfa_token"),
Domain: domain,
}, nil
}
// ExchangeToken exchanges an OAuth1 token for an OAuth2 token
func ExchangeToken(oauth1Token *types.OAuth1Token) (*types.OAuth2Token, error) {
scheme := "https"
if strings.HasPrefix(oauth1Token.Domain, "127.0.0.1") {
scheme = "http"
}
consumer, err := utils.LoadOAuthConsumer()
if err != nil {
return nil, fmt.Errorf("failed to load OAuth consumer: %w", err)
}
exchangeURL := fmt.Sprintf("%s://connectapi.%s/oauth-service/oauth/exchange/user/2.0", scheme, oauth1Token.Domain)
// Prepare form data
formData := url.Values{}
if oauth1Token.MFAToken != "" {
formData.Set("mfa_token", oauth1Token.MFAToken)
}
// Convert form data to map for OAuth signing
formParams := make(map[string]string)
for key, values := range formData {
if len(values) > 0 {
formParams[key] = values[0]
}
}
// Create OAuth1 signed request
authHeader := utils.CreateOAuth1AuthorizationHeader("POST", exchangeURL, formParams,
consumer.ConsumerKey, consumer.ConsumerSecret, oauth1Token.OAuthToken, oauth1Token.OAuthTokenSecret)
req, err := http.NewRequest("POST", exchangeURL, strings.NewReader(formData.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", authHeader)
req.Header.Set("User-Agent", "com.garmin.android.apps.connectmobile")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("OAuth2 exchange failed with status %d: %s", resp.StatusCode, string(body))
}
var oauth2Token types.OAuth2Token
if err := json.Unmarshal(body, &oauth2Token); err != nil {
return nil, fmt.Errorf("failed to decode OAuth2 token: %w", err)
}
// Set expiration time
if oauth2Token.ExpiresIn > 0 {
oauth2Token.ExpiresAt = time.Now().Add(time.Duration(oauth2Token.ExpiresIn) * time.Second)
}
return &oauth2Token, nil
}