1package auth
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "time"
10
11 "github.com/bnema/gordon/internal/common"
12)
13
14func DeviceFlowAuth(config *common.Config) error {
15 fmt.Println("Starting device flow authentication...")
16
17 // Request device code
18 deviceCode, err := requestDeviceCode(config)
19 if err != nil {
20 return fmt.Errorf("error requesting device code: %w", err)
21 }
22
23 fmt.Printf("Please go to %v and enter the code: %v\n", deviceCode["verification_uri"], deviceCode["user_code"])
24
25 // Poll for access token
26 interval := 5 // Default to 5 seconds if not provided
27 if i, ok := deviceCode["interval"].(float64); ok {
28 interval = int(i)
29 }
30
31 token, err := pollForAccessToken(config, deviceCode["device_code"].(string), interval)
32 if err != nil {
33 return fmt.Errorf("error polling for access token: %w", err)
34 }
35
36 // Save the token
37 config.SetToken(token)
38
39 fmt.Println("Authentication successful!")
40 return nil
41}
42
43func requestDeviceCode(config *common.Config) (map[string]interface{}, error) {
44 resp, err := http.PostForm(config.GetBackendURL()+"/api/device/code", url.Values{})
45 if err != nil {
46 return nil, fmt.Errorf("error making request: %w", err)
47 }
48 defer resp.Body.Close()
49
50 body, err := io.ReadAll(resp.Body)
51 if err != nil {
52 return nil, fmt.Errorf("error reading response body: %w", err)
53 }
54
55 if resp.StatusCode != http.StatusOK {
56 return nil, fmt.Errorf("server returned non-200 status code: %d. Body: %s", resp.StatusCode, string(body))
57 }
58
59 var deviceCode map[string]interface{}
60 if err := json.Unmarshal(body, &deviceCode); err != nil {
61 return nil, fmt.Errorf("error parsing JSON response: %w", err)
62 }
63
64 // Check for required fields
65 requiredFields := []string{"verification_uri", "user_code", "device_code", "interval"}
66 for _, field := range requiredFields {
67 if _, ok := deviceCode[field]; !ok {
68 return nil, fmt.Errorf("missing required field in response: %s", field)
69 }
70 }
71
72 return deviceCode, nil
73}
74
75func pollForAccessToken(config *common.Config, deviceCode string, interval int) (string, error) {
76 maxAttempts := 60 // Maximum number of attempts (5 minutes with 5-second intervals)
77 for attempt := 0; attempt < maxAttempts; attempt++ {
78 time.Sleep(time.Duration(interval) * time.Second)
79
80 resp, err := http.PostForm(config.GetBackendURL()+"/api/device/token", url.Values{
81 "device_code": {deviceCode},
82 })
83 if err != nil {
84 continue
85 }
86 defer resp.Body.Close()
87
88 body, err := io.ReadAll(resp.Body)
89 if err != nil {
90 continue
91 }
92
93 var tokenResp map[string]interface{}
94 if err := json.Unmarshal(body, &tokenResp); err != nil {
95 continue
96 }
97
98 if accessToken, ok := tokenResp["access_token"].(string); ok && accessToken != "" {
99 return accessToken, nil
100 }
101
102 if tokenResp["error"] == "authorization_pending" {
103 // Optionally, you can print a dot to show progress without cluttering the output
104 fmt.Print(".")
105 continue
106 }
107
108 // If we get here, there's an unexpected response but we can continue to try
109 // fmt.Printf("Unexpected response from server (attempt %d): %v\n", attempt+1, tokenResp)
110 }
111
112 return "", fmt.Errorf("max attempts reached, failed to obtain access token")
113}