CLI & Backend: Implement OAuth device flow for CLI authentication
- Add DeviceFlowAuth function in auth package for CLI - Update FieldCheck to use DeviceFlowAuth when token is not set - Modify RequireToken middleware to validate GitHub tokens - Add GitHub user info fetching and email retrieval - Implement device code and token request handlers - Update router to include new device flow endpoints - Enhance error handling and logging throughout the authentication process - Refactor token validation to use GitHub API instead of local config - Add user authorization check against database
- commit
- 5637d74cd2108df56960dda7c65efdc3525b760d
- author
- bamen · · 2024-09-03 13:00 UTC
- committer
- bamen · · 2024-09-03 13:00 UTC
- parent
- 0993a06
- signature
- unsigned
8 files · +327 -24
A internal/cli/auth/device_flow.go +113 -0
| @@ -0,0 +1,113 @@ | |||
| 1 | + | package auth | |
| 2 | + | ||
| 3 | + | import ( | |
| 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 | + | ||
| 14 | + | func 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 | + | ||
| 43 | + | func 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 | + | ||
| 75 | + | func 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 | + | } | |
| @@ -0,0 +1,113 @@ | |||||
| 1 | + | package auth | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 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 | + | ||||
| 14 | + | func 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 | + | ||||
| 43 | + | func 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 | + | ||||
| 75 | + | func 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 | + | } | |||
M internal/cli/cmd/deploy.go +1 -1
| @@ -56,7 +56,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 56 | 56 | log.Error("Deployment failed", | |
| 57 | 57 | "status_code", deployErr.StatusCode, | |
| 58 | 58 | "message", deployErr.Message, | |
| 59 | - | "raw_response", deployErr.RawResponse) | |
| 59 | + | ) | |
| 60 | 60 | } else { | |
| 61 | 61 | log.Error("Deployment failed", "error", err) | |
| 62 | 62 | } | |
| @@ -56,7 +56,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||||
| 56 | log.Error("Deployment failed", | 56 | log.Error("Deployment failed", | ||
| 57 | "status_code", deployErr.StatusCode, | 57 | "status_code", deployErr.StatusCode, | ||
| 58 | "message", deployErr.Message, | 58 | "message", deployErr.Message, | ||
| 59 | - | "raw_response", deployErr.RawResponse) | 59 | + | ) |
| 60 | } else { | 60 | } else { | ||
| 61 | log.Error("Deployment failed", "error", err) | 61 | log.Error("Deployment failed", "error", err) | ||
| 62 | } | 62 | } | ||
M internal/cli/handler/fieldcheck.go +10 -7
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "fmt" | |
| 5 | 5 | ||
| 6 | 6 | "github.com/bnema/gordon/internal/cli" | |
| 7 | + | "github.com/bnema/gordon/internal/cli/auth" | |
| 7 | 8 | "github.com/bnema/gordon/internal/common" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| @@ -31,15 +32,17 @@ func FieldCheck(a *cli.App) error { | |||
| 31 | 32 | return err | |
| 32 | 33 | } | |
| 33 | 34 | ||
| 34 | - | wasTokenEmpty, err := checkAndSetField(&a.Config.General.Token, | |
| 35 | - | "Enter the token (check your backend config.yml):", | |
| 36 | - | "Token is not set in config.yml") | |
| 37 | - | if err != nil { | |
| 38 | - | return err | |
| 35 | + | if a.Config.General.Token == "" { | |
| 36 | + | fmt.Println("Token is not set. Starting OAuth device flow...") | |
| 37 | + | err := auth.DeviceFlowAuth(&a.Config) | |
| 38 | + | if err != nil { | |
| 39 | + | fmt.Printf("Device flow authentication error: %v\n", err) | |
| 40 | + | return fmt.Errorf("failed to complete device flow authentication: %w", err) | |
| 41 | + | } | |
| 39 | 42 | } | |
| 40 | 43 | ||
| 41 | - | // Save config if one of the fields was empty | |
| 42 | - | if wasBackendURLEmpty || wasTokenEmpty { | |
| 44 | + | // Save config if BackendURL was empty or if a new token was set | |
| 45 | + | if wasBackendURLEmpty || a.Config.General.Token != "" { | |
| 43 | 46 | err = a.Config.SaveConfig() | |
| 44 | 47 | if err != nil { | |
| 45 | 48 | return err | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | 5 | ||||
| 6 | "github.com/bnema/gordon/internal/cli" | 6 | "github.com/bnema/gordon/internal/cli" | ||
| 7 | + | "github.com/bnema/gordon/internal/cli/auth" | |||
| 7 | "github.com/bnema/gordon/internal/common" | 8 | "github.com/bnema/gordon/internal/common" | ||
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| @@ -31,15 +32,17 @@ func FieldCheck(a *cli.App) error { | |||||
| 31 | return err | 32 | return err | ||
| 32 | } | 33 | } | ||
| 33 | 34 | ||||
| 34 | - | wasTokenEmpty, err := checkAndSetField(&a.Config.General.Token, | 35 | + | if a.Config.General.Token == "" { |
| 35 | - | "Enter the token (check your backend config.yml):", | 36 | + | fmt.Println("Token is not set. Starting OAuth device flow...") |
| 36 | - | "Token is not set in config.yml") | 37 | + | err := auth.DeviceFlowAuth(&a.Config) |
| 37 | - | if err != nil { | 38 | + | if err != nil { |
| 38 | - | return err | 39 | + | fmt.Printf("Device flow authentication error: %v\n", err) |
| 40 | + | return fmt.Errorf("failed to complete device flow authentication: %w", err) | |||
| 41 | + | } | |||
| 39 | } | 42 | } | ||
| 40 | 43 | ||||
| 41 | - | // Save config if one of the fields was empty | 44 | + | // Save config if BackendURL was empty or if a new token was set |
| 42 | - | if wasBackendURLEmpty || wasTokenEmpty { | 45 | + | if wasBackendURLEmpty || a.Config.General.Token != "" { |
| 43 | err = a.Config.SaveConfig() | 46 | err = a.Config.SaveConfig() | ||
| 44 | if err != nil { | 47 | if err != nil { | ||
| 45 | return err | 48 | return err | ||
M internal/cli/handler/pingrequest.go +2 -1
| @@ -32,7 +32,8 @@ func PerformPingRequest(a *cli.App) (PingResponse, error) { | |||
| 32 | 32 | defer resp.Http.Body.Close() | |
| 33 | 33 | ||
| 34 | 34 | if resp.StatusCode != http.StatusOK { | |
| 35 | - | return PingResponse{}, fmt.Errorf("expected status code 200, got %d", resp.StatusCode) | |
| 35 | + | // Return the error message from the server | |
| 36 | + | return PingResponse{}, fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(resp.Body)) | |
| 36 | 37 | } | |
| 37 | 38 | ||
| 38 | 39 | // Unmarshal the JSON response into the PingResponse struct | |
| @@ -32,7 +32,8 @@ func PerformPingRequest(a *cli.App) (PingResponse, error) { | |||||
| 32 | defer resp.Http.Body.Close() | 32 | defer resp.Http.Body.Close() | ||
| 33 | 33 | ||||
| 34 | if resp.StatusCode != http.StatusOK { | 34 | if resp.StatusCode != http.StatusOK { | ||
| 35 | - | return PingResponse{}, fmt.Errorf("expected status code 200, got %d", resp.StatusCode) | 35 | + | // Return the error message from the server |
| 36 | + | return PingResponse{}, fmt.Errorf("server returned status %d: %s", resp.StatusCode, string(resp.Body)) | |||
| 36 | } | 37 | } | ||
| 37 | 38 | ||||
| 38 | // Unmarshal the JSON response into the PingResponse struct | 39 | // Unmarshal the JSON response into the PingResponse struct | ||
M internal/common/initconfig.go +8 -0
| @@ -194,3 +194,11 @@ func (config *Config) GetVersion() string { | |||
| 194 | 194 | func (config *Config) GetStorageDir() string { | |
| 195 | 195 | return config.General.StorageDir | |
| 196 | 196 | } | |
| 197 | + | ||
| 198 | + | func (c *Config) GetBackendURL() string { | |
| 199 | + | return c.Http.BackendURL | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | func (c *Config) SetToken(token string) { | |
| 203 | + | c.General.Token = token | |
| 204 | + | } | |
| @@ -194,3 +194,11 @@ func (config *Config) GetVersion() string { | |||||
| 194 | func (config *Config) GetStorageDir() string { | 194 | func (config *Config) GetStorageDir() string { | ||
| 195 | return config.General.StorageDir | 195 | return config.General.StorageDir | ||
| 196 | } | 196 | } | ||
| 197 | + | ||||
| 198 | + | func (c *Config) GetBackendURL() string { | |||
| 199 | + | return c.Http.BackendURL | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | func (c *Config) SetToken(token string) { | |||
| 203 | + | c.General.Token = token | |||
| 204 | + | } | |||
M internal/httpserve/handler/oauth.go +48 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "io" | |
| 8 | 8 | "net/http" | |
| 9 | + | "net/url" | |
| 9 | 10 | "time" | |
| 10 | 11 | ||
| 11 | 12 | "github.com/bnema/gordon/internal/db" | |
| @@ -20,6 +21,7 @@ import ( | |||
| 20 | 21 | ||
| 21 | 22 | var urlToken string | |
| 22 | 23 | ||
| 24 | + | // ClLI : | |
| 23 | 25 | // compareGordonToken compares the token from the URL query parameter with the one from the config.yml | |
| 24 | 26 | func CompareGordonToken(c echo.Context, a *server.App) error { | |
| 25 | 27 | configToken, err := a.Config.GetToken() | |
| @@ -328,3 +330,49 @@ func githubGetUserDetails(c echo.Context, a *server.App) (userInfo *queries.Gith | |||
| 328 | 330 | ||
| 329 | 331 | return userInfo, nil | |
| 330 | 332 | } | |
| 333 | + | ||
| 334 | + | func DeviceCodeRequest(c echo.Context, a *server.App) error { | |
| 335 | + | proxyURL := a.Config.Build.ProxyURL | |
| 336 | + | resp, err := http.Post(proxyURL+"/github/device/code", "application/json", nil) | |
| 337 | + | if err != nil { | |
| 338 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) | |
| 339 | + | } | |
| 340 | + | defer resp.Body.Close() | |
| 341 | + | ||
| 342 | + | if resp.StatusCode != http.StatusOK { | |
| 343 | + | body, _ := io.ReadAll(resp.Body) | |
| 344 | + | return c.JSON(resp.StatusCode, map[string]string{"error": string(body)}) | |
| 345 | + | } | |
| 346 | + | ||
| 347 | + | var deviceCode map[string]interface{} | |
| 348 | + | if err := json.NewDecoder(resp.Body).Decode(&deviceCode); err != nil { | |
| 349 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to decode response"}) | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | return c.JSON(http.StatusOK, deviceCode) | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | func DeviceTokenRequest(c echo.Context, a *server.App) error { | |
| 356 | + | proxyURL := a.Config.Build.ProxyURL | |
| 357 | + | deviceCode := c.FormValue("device_code") | |
| 358 | + | ||
| 359 | + | resp, err := http.PostForm(proxyURL+"/github/device/token", url.Values{ | |
| 360 | + | "device_code": {deviceCode}, | |
| 361 | + | }) | |
| 362 | + | if err != nil { | |
| 363 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) | |
| 364 | + | } | |
| 365 | + | defer resp.Body.Close() | |
| 366 | + | ||
| 367 | + | if resp.StatusCode != http.StatusOK { | |
| 368 | + | body, _ := io.ReadAll(resp.Body) | |
| 369 | + | return c.JSON(resp.StatusCode, map[string]string{"error": string(body)}) | |
| 370 | + | } | |
| 371 | + | ||
| 372 | + | var tokenResp map[string]interface{} | |
| 373 | + | if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { | |
| 374 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to decode response"}) | |
| 375 | + | } | |
| 376 | + | ||
| 377 | + | return c.JSON(http.StatusOK, tokenResp) | |
| 378 | + | } | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "io" | 7 | "io" | ||
| 8 | "net/http" | 8 | "net/http" | ||
| 9 | + | "net/url" | |||
| 9 | "time" | 10 | "time" | ||
| 10 | 11 | ||||
| 11 | "github.com/bnema/gordon/internal/db" | 12 | "github.com/bnema/gordon/internal/db" | ||
| @@ -20,6 +21,7 @@ import ( | |||||
| 20 | 21 | ||||
| 21 | var urlToken string | 22 | var urlToken string | ||
| 22 | 23 | ||||
| 24 | + | // ClLI : | |||
| 23 | // compareGordonToken compares the token from the URL query parameter with the one from the config.yml | 25 | // compareGordonToken compares the token from the URL query parameter with the one from the config.yml | ||
| 24 | func CompareGordonToken(c echo.Context, a *server.App) error { | 26 | func CompareGordonToken(c echo.Context, a *server.App) error { | ||
| 25 | configToken, err := a.Config.GetToken() | 27 | configToken, err := a.Config.GetToken() | ||
| @@ -328,3 +330,49 @@ func githubGetUserDetails(c echo.Context, a *server.App) (userInfo *queries.Gith | |||||
| 328 | 330 | ||||
| 329 | return userInfo, nil | 331 | return userInfo, nil | ||
| 330 | } | 332 | } | ||
| 333 | + | ||||
| 334 | + | func DeviceCodeRequest(c echo.Context, a *server.App) error { | |||
| 335 | + | proxyURL := a.Config.Build.ProxyURL | |||
| 336 | + | resp, err := http.Post(proxyURL+"/github/device/code", "application/json", nil) | |||
| 337 | + | if err != nil { | |||
| 338 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) | |||
| 339 | + | } | |||
| 340 | + | defer resp.Body.Close() | |||
| 341 | + | ||||
| 342 | + | if resp.StatusCode != http.StatusOK { | |||
| 343 | + | body, _ := io.ReadAll(resp.Body) | |||
| 344 | + | return c.JSON(resp.StatusCode, map[string]string{"error": string(body)}) | |||
| 345 | + | } | |||
| 346 | + | ||||
| 347 | + | var deviceCode map[string]interface{} | |||
| 348 | + | if err := json.NewDecoder(resp.Body).Decode(&deviceCode); err != nil { | |||
| 349 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to decode response"}) | |||
| 350 | + | } | |||
| 351 | + | ||||
| 352 | + | return c.JSON(http.StatusOK, deviceCode) | |||
| 353 | + | } | |||
| 354 | + | ||||
| 355 | + | func DeviceTokenRequest(c echo.Context, a *server.App) error { | |||
| 356 | + | proxyURL := a.Config.Build.ProxyURL | |||
| 357 | + | deviceCode := c.FormValue("device_code") | |||
| 358 | + | ||||
| 359 | + | resp, err := http.PostForm(proxyURL+"/github/device/token", url.Values{ | |||
| 360 | + | "device_code": {deviceCode}, | |||
| 361 | + | }) | |||
| 362 | + | if err != nil { | |||
| 363 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) | |||
| 364 | + | } | |||
| 365 | + | defer resp.Body.Close() | |||
| 366 | + | ||||
| 367 | + | if resp.StatusCode != http.StatusOK { | |||
| 368 | + | body, _ := io.ReadAll(resp.Body) | |||
| 369 | + | return c.JSON(resp.StatusCode, map[string]string{"error": string(body)}) | |||
| 370 | + | } | |||
| 371 | + | ||||
| 372 | + | var tokenResp map[string]interface{} | |||
| 373 | + | if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { | |||
| 374 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to decode response"}) | |||
| 375 | + | } | |||
| 376 | + | ||||
| 377 | + | return c.JSON(http.StatusOK, tokenResp) | |||
| 378 | + | } | |||
M internal/httpserve/middleware/requiretoken.go +131 -11
| @@ -1,40 +1,160 @@ | |||
| 1 | 1 | package middleware | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "encoding/json" | |
| 4 | 5 | "fmt" | |
| 6 | + | "io" | |
| 7 | + | "net/http" | |
| 5 | 8 | "strings" | |
| 6 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/db/queries" | |
| 7 | 11 | "github.com/bnema/gordon/internal/server" | |
| 12 | + | "github.com/charmbracelet/log" | |
| 8 | 13 | "github.com/labstack/echo/v4" | |
| 9 | 14 | ) | |
| 10 | 15 | ||
| 11 | - | // RequireToken is a middleware that checks for a valid token in the request | |
| 16 | + | // GithubUserInfo is an alias for the type defined in the queries package | |
| 17 | + | type GithubUserInfo = queries.GithubUserInfo | |
| 18 | + | ||
| 19 | + | // RequireToken is a middleware that checks for a valid GitHub token in the request | |
| 12 | 20 | func RequireToken(a *server.App) echo.MiddlewareFunc { | |
| 13 | 21 | return func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 14 | 22 | return func(c echo.Context) error { | |
| 15 | - | if err := validateToken(c, a); err != nil { | |
| 16 | - | return err | |
| 23 | + | log.Debug("Starting RequireToken middleware") | |
| 24 | + | ||
| 25 | + | githubUser, err := validateGitHubToken(c) | |
| 26 | + | if err != nil { | |
| 27 | + | log.Error("Token validation failed", "error", err) | |
| 28 | + | return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid or missing token"}) | |
| 29 | + | } | |
| 30 | + | log.Debug("GitHub token validated successfully", "user", githubUser.Login) | |
| 31 | + | ||
| 32 | + | // Check if the user exists and matches the GitHub user | |
| 33 | + | isValid, err := queries.CheckDBUserIsGood(a, githubUser) | |
| 34 | + | if err != nil { | |
| 35 | + | log.Error("Error checking user authorization", "error", err) | |
| 36 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error checking user authorization"}) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | if !isValid { | |
| 40 | + | log.Warn("User not authorized", "user", githubUser.Login) | |
| 41 | + | return c.JSON(http.StatusForbidden, map[string]string{"error": "User not authorized"}) | |
| 42 | + | } | |
| 43 | + | log.Debug("User authorization check passed", "user", githubUser.Login) | |
| 44 | + | ||
| 45 | + | // Update or create session | |
| 46 | + | err = queries.CreateOrUpdateDBSession(a, c.Request().Header.Get("Authorization"), c.Request().UserAgent()) | |
| 47 | + | if err != nil { | |
| 48 | + | log.Error("Error updating session", "error", err) | |
| 49 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error updating session"}) | |
| 17 | 50 | } | |
| 51 | + | log.Debug("Session created or updated successfully", "user", githubUser.Login) | |
| 52 | + | ||
| 53 | + | // Store the user information in the context for later use | |
| 54 | + | c.Set("user", githubUser) | |
| 55 | + | log.Debug("User information stored in context", "user", githubUser.Login) | |
| 56 | + | ||
| 18 | 57 | return next(c) | |
| 19 | 58 | } | |
| 20 | 59 | } | |
| 21 | 60 | } | |
| 22 | 61 | ||
| 23 | - | func validateToken(c echo.Context, a *server.App) error { | |
| 62 | + | func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) { | |
| 24 | 63 | token := c.Request().Header.Get("Authorization") | |
| 25 | 64 | if token == "" { | |
| 26 | - | return echo.NewHTTPError(401, "Token is missing") | |
| 65 | + | log.Warn("Token is missing in the request") | |
| 66 | + | return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is missing") | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | token = strings.TrimPrefix(token, "Bearer ") | |
| 70 | + | log.Debug("Validating GitHub token") | |
| 71 | + | ||
| 72 | + | // Validate the token against GitHub API | |
| 73 | + | client := &http.Client{} | |
| 74 | + | req, err := http.NewRequest("GET", "https://api.github.com/user", nil) | |
| 75 | + | if err != nil { | |
| 76 | + | log.Error("Failed to create request", "error", err) | |
| 77 | + | return nil, fmt.Errorf("failed to create request: %w", err) | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | req.Header.Set("Authorization", "token "+token) | |
| 81 | + | req.Header.Set("Accept", "application/vnd.github.v3+json") | |
| 82 | + | ||
| 83 | + | resp, err := client.Do(req) | |
| 84 | + | if err != nil { | |
| 85 | + | log.Error("Failed to send request to GitHub API", "error", err) | |
| 86 | + | return nil, fmt.Errorf("failed to send request: %w", err) | |
| 87 | + | } | |
| 88 | + | defer resp.Body.Close() | |
| 89 | + | ||
| 90 | + | if resp.StatusCode != http.StatusOK { | |
| 91 | + | log.Warn("Invalid token", "status", resp.Status) | |
| 92 | + | return nil, echo.NewHTTPError(http.StatusUnauthorized, "Invalid token") | |
| 27 | 93 | } | |
| 28 | 94 | ||
| 29 | - | token = strings.Replace(token, "Bearer ", "", 1) | |
| 30 | - | configToken, err := a.Config.GetToken() | |
| 95 | + | body, err := io.ReadAll(resp.Body) | |
| 31 | 96 | if err != nil { | |
| 32 | - | return fmt.Errorf("failed to get token from config: %w", err) | |
| 97 | + | log.Error("Failed to read response body", "error", err) | |
| 98 | + | return nil, fmt.Errorf("failed to read response body: %w", err) | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | var githubUser GithubUserInfo | |
| 102 | + | if err := json.Unmarshal(body, &githubUser); err != nil { | |
| 103 | + | log.Error("Failed to parse user data", "error", err) | |
| 104 | + | return nil, fmt.Errorf("failed to parse user data: %w", err) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | log.Debug("GitHub user info fetched successfully", "user", githubUser.Login) | |
| 108 | + | ||
| 109 | + | // Fetch user's email | |
| 110 | + | emails, err := fetchGitHubUserEmails(token) | |
| 111 | + | if err != nil { | |
| 112 | + | log.Error("Failed to fetch user emails", "error", err) | |
| 113 | + | return nil, fmt.Errorf("failed to fetch user emails: %w", err) | |
| 114 | + | } | |
| 115 | + | githubUser.Emails = emails | |
| 116 | + | log.Debug("GitHub user emails fetched successfully", "user", githubUser.Login, "emailCount", len(emails)) | |
| 117 | + | ||
| 118 | + | return &githubUser, nil | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func fetchGitHubUserEmails(token string) ([]string, error) { | |
| 122 | + | log.Debug("Fetching GitHub user emails") | |
| 123 | + | client := &http.Client{} | |
| 124 | + | req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil) | |
| 125 | + | if err != nil { | |
| 126 | + | log.Error("Failed to create request for emails", "error", err) | |
| 127 | + | return nil, fmt.Errorf("failed to create request: %w", err) | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | req.Header.Set("Authorization", "token "+token) | |
| 131 | + | req.Header.Set("Accept", "application/vnd.github.v3+json") | |
| 132 | + | ||
| 133 | + | resp, err := client.Do(req) | |
| 134 | + | if err != nil { | |
| 135 | + | log.Error("Failed to send request for emails", "error", err) | |
| 136 | + | return nil, fmt.Errorf("failed to send request: %w", err) | |
| 137 | + | } | |
| 138 | + | defer resp.Body.Close() | |
| 139 | + | ||
| 140 | + | if resp.StatusCode != http.StatusOK { | |
| 141 | + | log.Warn("Failed to fetch user emails", "status", resp.Status) | |
| 142 | + | return nil, fmt.Errorf("failed to fetch user emails: %s", resp.Status) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | var emailsResp []struct { | |
| 146 | + | Email string `json:"email"` | |
| 147 | + | } | |
| 148 | + | if err := json.NewDecoder(resp.Body).Decode(&emailsResp); err != nil { | |
| 149 | + | log.Error("Failed to parse email response", "error", err) | |
| 150 | + | return nil, fmt.Errorf("failed to parse email response: %w", err) | |
| 33 | 151 | } | |
| 34 | 152 | ||
| 35 | - | if token != configToken { | |
| 36 | - | return echo.NewHTTPError(401, "Token does not match") | |
| 153 | + | emails := make([]string, len(emailsResp)) | |
| 154 | + | for i, e := range emailsResp { | |
| 155 | + | emails[i] = e.Email | |
| 37 | 156 | } | |
| 38 | 157 | ||
| 39 | - | return nil | |
| 158 | + | log.Debug("GitHub user emails fetched successfully", "emailCount", len(emails)) | |
| 159 | + | return emails, nil | |
| 40 | 160 | } | |
| @@ -1,40 +1,160 @@ | |||||
| 1 | package middleware | 1 | package middleware | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "encoding/json" | |||
| 4 | "fmt" | 5 | "fmt" | ||
| 6 | + | "io" | |||
| 7 | + | "net/http" | |||
| 5 | "strings" | 8 | "strings" | ||
| 6 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/db/queries" | |||
| 7 | "github.com/bnema/gordon/internal/server" | 11 | "github.com/bnema/gordon/internal/server" | ||
| 12 | + | "github.com/charmbracelet/log" | |||
| 8 | "github.com/labstack/echo/v4" | 13 | "github.com/labstack/echo/v4" | ||
| 9 | ) | 14 | ) | ||
| 10 | 15 | ||||
| 11 | - | // RequireToken is a middleware that checks for a valid token in the request | 16 | + | // GithubUserInfo is an alias for the type defined in the queries package |
| 17 | + | type GithubUserInfo = queries.GithubUserInfo | |||
| 18 | + | ||||
| 19 | + | // RequireToken is a middleware that checks for a valid GitHub token in the request | |||
| 12 | func RequireToken(a *server.App) echo.MiddlewareFunc { | 20 | func RequireToken(a *server.App) echo.MiddlewareFunc { | ||
| 13 | return func(next echo.HandlerFunc) echo.HandlerFunc { | 21 | return func(next echo.HandlerFunc) echo.HandlerFunc { | ||
| 14 | return func(c echo.Context) error { | 22 | return func(c echo.Context) error { | ||
| 15 | - | if err := validateToken(c, a); err != nil { | 23 | + | log.Debug("Starting RequireToken middleware") |
| 16 | - | return err | 24 | + | |
| 25 | + | githubUser, err := validateGitHubToken(c) | |||
| 26 | + | if err != nil { | |||
| 27 | + | log.Error("Token validation failed", "error", err) | |||
| 28 | + | return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid or missing token"}) | |||
| 29 | + | } | |||
| 30 | + | log.Debug("GitHub token validated successfully", "user", githubUser.Login) | |||
| 31 | + | ||||
| 32 | + | // Check if the user exists and matches the GitHub user | |||
| 33 | + | isValid, err := queries.CheckDBUserIsGood(a, githubUser) | |||
| 34 | + | if err != nil { | |||
| 35 | + | log.Error("Error checking user authorization", "error", err) | |||
| 36 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error checking user authorization"}) | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | if !isValid { | |||
| 40 | + | log.Warn("User not authorized", "user", githubUser.Login) | |||
| 41 | + | return c.JSON(http.StatusForbidden, map[string]string{"error": "User not authorized"}) | |||
| 42 | + | } | |||
| 43 | + | log.Debug("User authorization check passed", "user", githubUser.Login) | |||
| 44 | + | ||||
| 45 | + | // Update or create session | |||
| 46 | + | err = queries.CreateOrUpdateDBSession(a, c.Request().Header.Get("Authorization"), c.Request().UserAgent()) | |||
| 47 | + | if err != nil { | |||
| 48 | + | log.Error("Error updating session", "error", err) | |||
| 49 | + | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error updating session"}) | |||
| 17 | } | 50 | } | ||
| 51 | + | log.Debug("Session created or updated successfully", "user", githubUser.Login) | |||
| 52 | + | ||||
| 53 | + | // Store the user information in the context for later use | |||
| 54 | + | c.Set("user", githubUser) | |||
| 55 | + | log.Debug("User information stored in context", "user", githubUser.Login) | |||
| 56 | + | ||||
| 18 | return next(c) | 57 | return next(c) | ||
| 19 | } | 58 | } | ||
| 20 | } | 59 | } | ||
| 21 | } | 60 | } | ||
| 22 | 61 | ||||
| 23 | - | func validateToken(c echo.Context, a *server.App) error { | 62 | + | func validateGitHubToken(c echo.Context) (*GithubUserInfo, error) { |
| 24 | token := c.Request().Header.Get("Authorization") | 63 | token := c.Request().Header.Get("Authorization") | ||
| 25 | if token == "" { | 64 | if token == "" { | ||
| 26 | - | return echo.NewHTTPError(401, "Token is missing") | 65 | + | log.Warn("Token is missing in the request") |
| 66 | + | return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is missing") | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | token = strings.TrimPrefix(token, "Bearer ") | |||
| 70 | + | log.Debug("Validating GitHub token") | |||
| 71 | + | ||||
| 72 | + | // Validate the token against GitHub API | |||
| 73 | + | client := &http.Client{} | |||
| 74 | + | req, err := http.NewRequest("GET", "https://api.github.com/user", nil) | |||
| 75 | + | if err != nil { | |||
| 76 | + | log.Error("Failed to create request", "error", err) | |||
| 77 | + | return nil, fmt.Errorf("failed to create request: %w", err) | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | req.Header.Set("Authorization", "token "+token) | |||
| 81 | + | req.Header.Set("Accept", "application/vnd.github.v3+json") | |||
| 82 | + | ||||
| 83 | + | resp, err := client.Do(req) | |||
| 84 | + | if err != nil { | |||
| 85 | + | log.Error("Failed to send request to GitHub API", "error", err) | |||
| 86 | + | return nil, fmt.Errorf("failed to send request: %w", err) | |||
| 87 | + | } | |||
| 88 | + | defer resp.Body.Close() | |||
| 89 | + | ||||
| 90 | + | if resp.StatusCode != http.StatusOK { | |||
| 91 | + | log.Warn("Invalid token", "status", resp.Status) | |||
| 92 | + | return nil, echo.NewHTTPError(http.StatusUnauthorized, "Invalid token") | |||
| 27 | } | 93 | } | ||
| 28 | 94 | ||||
| 29 | - | token = strings.Replace(token, "Bearer ", "", 1) | 95 | + | body, err := io.ReadAll(resp.Body) |
| 30 | - | configToken, err := a.Config.GetToken() | |||
| 31 | if err != nil { | 96 | if err != nil { | ||
| 32 | - | return fmt.Errorf("failed to get token from config: %w", err) | 97 | + | log.Error("Failed to read response body", "error", err) |
| 98 | + | return nil, fmt.Errorf("failed to read response body: %w", err) | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | var githubUser GithubUserInfo | |||
| 102 | + | if err := json.Unmarshal(body, &githubUser); err != nil { | |||
| 103 | + | log.Error("Failed to parse user data", "error", err) | |||
| 104 | + | return nil, fmt.Errorf("failed to parse user data: %w", err) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | log.Debug("GitHub user info fetched successfully", "user", githubUser.Login) | |||
| 108 | + | ||||
| 109 | + | // Fetch user's email | |||
| 110 | + | emails, err := fetchGitHubUserEmails(token) | |||
| 111 | + | if err != nil { | |||
| 112 | + | log.Error("Failed to fetch user emails", "error", err) | |||
| 113 | + | return nil, fmt.Errorf("failed to fetch user emails: %w", err) | |||
| 114 | + | } | |||
| 115 | + | githubUser.Emails = emails | |||
| 116 | + | log.Debug("GitHub user emails fetched successfully", "user", githubUser.Login, "emailCount", len(emails)) | |||
| 117 | + | ||||
| 118 | + | return &githubUser, nil | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func fetchGitHubUserEmails(token string) ([]string, error) { | |||
| 122 | + | log.Debug("Fetching GitHub user emails") | |||
| 123 | + | client := &http.Client{} | |||
| 124 | + | req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil) | |||
| 125 | + | if err != nil { | |||
| 126 | + | log.Error("Failed to create request for emails", "error", err) | |||
| 127 | + | return nil, fmt.Errorf("failed to create request: %w", err) | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | req.Header.Set("Authorization", "token "+token) | |||
| 131 | + | req.Header.Set("Accept", "application/vnd.github.v3+json") | |||
| 132 | + | ||||
| 133 | + | resp, err := client.Do(req) | |||
| 134 | + | if err != nil { | |||
| 135 | + | log.Error("Failed to send request for emails", "error", err) | |||
| 136 | + | return nil, fmt.Errorf("failed to send request: %w", err) | |||
| 137 | + | } | |||
| 138 | + | defer resp.Body.Close() | |||
| 139 | + | ||||
| 140 | + | if resp.StatusCode != http.StatusOK { | |||
| 141 | + | log.Warn("Failed to fetch user emails", "status", resp.Status) | |||
| 142 | + | return nil, fmt.Errorf("failed to fetch user emails: %s", resp.Status) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | var emailsResp []struct { | |||
| 146 | + | Email string `json:"email"` | |||
| 147 | + | } | |||
| 148 | + | if err := json.NewDecoder(resp.Body).Decode(&emailsResp); err != nil { | |||
| 149 | + | log.Error("Failed to parse email response", "error", err) | |||
| 150 | + | return nil, fmt.Errorf("failed to parse email response: %w", err) | |||
| 33 | } | 151 | } | ||
| 34 | 152 | ||||
| 35 | - | if token != configToken { | 153 | + | emails := make([]string, len(emailsResp)) |
| 36 | - | return echo.NewHTTPError(401, "Token does not match") | 154 | + | for i, e := range emailsResp { |
| 155 | + | emails[i] = e.Email | |||
| 37 | } | 156 | } | ||
| 38 | 157 | ||||
| 39 | - | return nil | 158 | + | log.Debug("GitHub user emails fetched successfully", "emailCount", len(emails)) |
| 159 | + | return emails, nil | |||
| 40 | } | 160 | } | ||
M internal/httpserve/router.go +14 -4
| @@ -50,14 +50,24 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | |||
| 50 | 50 | // bindAPIEndpoints expose /api endpoints and speaks only JSON | |
| 51 | 51 | func bindAPIEndpoints(e *echo.Echo, a *server.App) { | |
| 52 | 52 | apiGroup := e.Group("/api") | |
| 53 | - | apiGroup.Use(middleware.RequireToken(a)) | |
| 54 | - | apiGroup.GET("/ping", func(c echo.Context) error { | |
| 53 | + | ||
| 54 | + | // Device flow endpoints (without RequireToken middleware) | |
| 55 | + | apiGroup.POST("/device/code", func(c echo.Context) error { | |
| 56 | + | return handler.DeviceCodeRequest(c, a) | |
| 57 | + | }) | |
| 58 | + | apiGroup.POST("/device/token", func(c echo.Context) error { | |
| 59 | + | return handler.DeviceTokenRequest(c, a) | |
| 60 | + | }) | |
| 61 | + | ||
| 62 | + | // Other API endpoints that require a token | |
| 63 | + | protectedApiGroup := apiGroup.Group("", middleware.RequireToken(a)) | |
| 64 | + | protectedApiGroup.GET("/ping", func(c echo.Context) error { | |
| 55 | 65 | return handler.GetInfos(c, a) | |
| 56 | 66 | }) | |
| 57 | - | apiGroup.POST("/deploy", func(c echo.Context) error { | |
| 67 | + | protectedApiGroup.POST("/deploy", func(c echo.Context) error { | |
| 58 | 68 | return handler.PostDeploy(c, a) | |
| 59 | 69 | }) | |
| 60 | - | apiGroup.POST("/push", func(c echo.Context) error { | |
| 70 | + | protectedApiGroup.POST("/push", func(c echo.Context) error { | |
| 61 | 71 | return handler.PostPush(c, a) | |
| 62 | 72 | }) | |
| 63 | 73 | } | |
| @@ -50,14 +50,24 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | |||||
| 50 | // bindAPIEndpoints expose /api endpoints and speaks only JSON | 50 | // bindAPIEndpoints expose /api endpoints and speaks only JSON | ||
| 51 | func bindAPIEndpoints(e *echo.Echo, a *server.App) { | 51 | func bindAPIEndpoints(e *echo.Echo, a *server.App) { | ||
| 52 | apiGroup := e.Group("/api") | 52 | apiGroup := e.Group("/api") | ||
| 53 | - | apiGroup.Use(middleware.RequireToken(a)) | 53 | + | |
| 54 | - | apiGroup.GET("/ping", func(c echo.Context) error { | 54 | + | // Device flow endpoints (without RequireToken middleware) |
| 55 | + | apiGroup.POST("/device/code", func(c echo.Context) error { | |||
| 56 | + | return handler.DeviceCodeRequest(c, a) | |||
| 57 | + | }) | |||
| 58 | + | apiGroup.POST("/device/token", func(c echo.Context) error { | |||
| 59 | + | return handler.DeviceTokenRequest(c, a) | |||
| 60 | + | }) | |||
| 61 | + | ||||
| 62 | + | // Other API endpoints that require a token | |||
| 63 | + | protectedApiGroup := apiGroup.Group("", middleware.RequireToken(a)) | |||
| 64 | + | protectedApiGroup.GET("/ping", func(c echo.Context) error { | |||
| 55 | return handler.GetInfos(c, a) | 65 | return handler.GetInfos(c, a) | ||
| 56 | }) | 66 | }) | ||
| 57 | - | apiGroup.POST("/deploy", func(c echo.Context) error { | 67 | + | protectedApiGroup.POST("/deploy", func(c echo.Context) error { |
| 58 | return handler.PostDeploy(c, a) | 68 | return handler.PostDeploy(c, a) | ||
| 59 | }) | 69 | }) | ||
| 60 | - | apiGroup.POST("/push", func(c echo.Context) error { | 70 | + | protectedApiGroup.POST("/push", func(c echo.Context) error { |
| 61 | return handler.PostPush(c, a) | 71 | return handler.PostPush(c, a) | ||
| 62 | }) | 72 | }) | ||
| 63 | } | 73 | } | ||