Files
yandex-music-api-go/internal/client/auth.go
T
2026-10-14 23:04:51 +03:00

199 lines
5.8 KiB
Go

package client
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
)
// DeviceCode represents the device authorization response
type DeviceCode struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURL string `json:"verification_url"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
// OAuthToken represents an OAuth token response
type OAuthToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
// DeviceAuth performs OAuth Device Flow authentication
func (r *Request) DeviceAuth(ctx context.Context, onCode func(*DeviceCode)) (*OAuthToken, error) {
// Step 1: Request device code
deviceCode, err := r.requestDeviceCode(ctx)
if err != nil {
return nil, err
}
// Call callback with device code
if onCode != nil {
onCode(deviceCode)
}
// Step 2: Poll for token
return r.pollForToken(ctx, deviceCode)
}
// requestDeviceCode requests a device code from Yandex OAuth
func (r *Request) requestDeviceCode(ctx context.Context) (*DeviceCode, error) {
// Use Yandex OAuth endpoint for device auth
data := url.Values{}
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
data.Set("scope", "login:info login:email yandex_music")
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/device/code", strings.NewReader(data.Encode()))
if err != nil {
return nil, utils.NewYandexMusicError("failed to create device code request", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", UserAgent)
resp, err := r.client.Do(req)
if err != nil {
return nil, utils.NewYandexMusicError("failed to request device code", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, utils.NewYandexMusicError("failed to read device code response", err)
}
if resp.StatusCode != http.StatusOK {
return nil, utils.NewYandexMusicError(fmt.Sprintf("device code request failed: %s", string(body)), utils.ErrBadRequest)
}
var deviceCode DeviceCode
if err := json.Unmarshal(body, &deviceCode); err != nil {
return nil, utils.NewYandexMusicError("failed to parse device code", err)
}
return &deviceCode, nil
}
// pollForToken polls for the OAuth token
func (r *Request) pollForToken(ctx context.Context, deviceCode *DeviceCode) (*OAuthToken, error) {
interval := deviceCode.Interval
if interval == 0 {
interval = 5
}
deadline := time.Now().Add(time.Duration(deviceCode.ExpiresIn) * time.Second)
ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
data := url.Values{}
data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
data.Set("device_code", deviceCode.DeviceCode)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
if time.Now().After(deadline) {
return nil, utils.NewYandexMusicError("device authorization timed out", utils.ErrTimeout)
}
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/token", strings.NewReader(data.Encode()))
if err != nil {
return nil, utils.NewYandexMusicError("failed to create token request", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", UserAgent)
resp, err := r.client.Do(req)
if err != nil {
continue // Retry on network error
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var token OAuthToken
if err := json.Unmarshal(body, &token); err != nil {
return nil, utils.NewYandexMusicError("failed to parse token", err)
}
return &token, nil
}
// Check for specific errors
var errResp map[string]string
json.Unmarshal(body, &errResp)
if errResp["error"] == "authorization_pending" {
continue // Keep polling
}
if errResp["error"] == "slow_down" {
interval *= 2
ticker.Reset(time.Duration(interval) * time.Second)
continue
}
if errResp["error"] == "expired_token" {
return nil, utils.NewYandexMusicError("device code expired", utils.ErrTimeout)
}
if errResp["error"] == "access_denied" {
return nil, utils.NewYandexMusicError("access denied by user", utils.ErrUnauthorized)
}
}
}
}
// RefreshToken refreshes an access token using a refresh token
func (r *Request) RefreshToken(ctx context.Context, refreshToken string) (*OAuthToken, error) {
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("client_id", "23cabbbdc6cd418abb4b39c32c41195d")
data.Set("refresh_token", refreshToken)
req, err := http.NewRequestWithContext(ctx, "POST", "https://oauth.yandex.ru/token", strings.NewReader(data.Encode()))
if err != nil {
return nil, utils.NewYandexMusicError("failed to create refresh request", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", UserAgent)
resp, err := r.client.Do(req)
if err != nil {
return nil, utils.NewYandexMusicError("failed to refresh token", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, utils.NewYandexMusicError("failed to read refresh response", err)
}
if resp.StatusCode != http.StatusOK {
return nil, utils.NewYandexMusicError(fmt.Sprintf("token refresh failed: %s", string(body)), utils.ErrUnauthorized)
}
var token OAuthToken
if err := json.Unmarshal(body, &token); err != nil {
return nil, utils.NewYandexMusicError("failed to parse refreshed token", err)
}
return &token, nil
}
// Missing imports - need to add at top of file
// "io"
// "net/http"