Skip to main content
Glama
login.go13.4 kB
package cmd import ( "bufio" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strconv" "strings" "time" "github.com/babelcloud/gbox/packages/cli/config" clilog "github.com/babelcloud/gbox/packages/cli/internal" "github.com/babelcloud/gbox/packages/cli/internal/cloud" "github.com/babelcloud/gbox/packages/cli/internal/profile" "os/exec" "runtime" "github.com/adrg/xdg" "github.com/pkg/browser" "github.com/spf13/cobra" "golang.org/x/oauth2" "golang.org/x/oauth2/github" ) const ( configDirName = ".gbox" credentialsFile = "credentials.json" ) var ( configDir = filepath.Join(xdg.Home, configDirName) credentialsPath = filepath.Join(configDir, credentialsFile) // Updated OAuth configuration with new parameters oauth2Config = &oauth2.Config{ ClientID: "Ov23liVORYhMLpBvAtMs", ClientSecret: config.GetGithubClientSecret(), RedirectURL: "http://localhost:18088/login/github", Scopes: []string{"user:email"}, Endpoint: github.Endpoint, } ) type TokenResponse struct { Token string `json:"token"` } // checkBrowserEnvironment checks if the user has a browser environment (GUI and browser available) func checkBrowserEnvironment() bool { // On Windows or Mac, assume browser is available if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { return true } // If running in SSH session and no DISPLAY/WAYLAND, assume no GUI if os.Getenv("SSH_CONNECTION") != "" || os.Getenv("SSH_TTY") != "" { if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" { return false } } // If no GUI session variables, assume no GUI if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" && os.Getenv("XDG_SESSION_TYPE") == "" { return false } // If BROWSER env is set, assume browser is available if os.Getenv("BROWSER") != "" { return true } // If xdg-open is available, assume browser can be launched if _, err := exec.LookPath("xdg-open"); err == nil { return true } // Check for common browser executables in PATH browsers := []string{"firefox", "google-chrome", "chromium", "brave", "opera", "konqueror"} for _, b := range browsers { if _, err := exec.LookPath(b); err == nil { return true } } // No browser environment detected return false } // startLocalServer starts a local server to handle OAuth callback func startLocalServer(codeChan chan string, errorChan chan error) { server := &http.Server{ Addr: ":18088", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/login/github" { code := r.URL.Query().Get("code") if code != "" { html := `<html><body><h1>Authentication successful!</h1><p>You can close this window and return to the terminal. This window will automatically close in <span id="countdown">5</span> seconds...</p><script>setTimeout(function(){window.close()},5000);</script></body></html>` w.Header().Set("Content-Type", "text/html") w.Write([]byte(html)) codeChan <- code } else { errorChan <- fmt.Errorf("no authorization code received") } } else { http.NotFound(w, r) } }), } go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { errorChan <- err } }() // Give server time to start time.Sleep(1 * time.Second) } var loginCmd = &cobra.Command{ Use: "login", Short: "Login using GitHub OAuth", Long: `Authenticate using GitHub OAuth. This will detect your environment and use the appropriate authentication method.`, RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() logger := clilog.New() // Check if GitHub client secret is available hasClientSecret := config.GetGithubClientSecret() != "" // Check if user has browser environment hasBrowser := checkBrowserEnvironment() var accessToken string var err error // Force Device Flow if no client secret is available (for reproducible builds) if !hasClientSecret { logger.Debug("GitHub client secret not available. Using device authorization flow for reproducible builds...") accessToken, err = authenticateWithDevice(ctx) } else if hasBrowser { logger.Debug("Browser environment detected. Using OAuth authorization code flow...") accessToken, err = authenticateWithBrowser(ctx) } else { logger.Debug("No browser detected. Using device authorization flow...") accessToken, err = authenticateWithDevice(ctx) } if err != nil { return fmt.Errorf("authentication failed: %v", err) } _, err = getLocalToken(accessToken) if err != nil { return fmt.Errorf("failed to get local token: %v", err) } return nil }, } // authenticateWithBrowser uses OAuth authorization code flow func authenticateWithBrowser(ctx context.Context) (string, error) { // Start local server to handle callback codeChan := make(chan string, 1) errorChan := make(chan error, 1) startLocalServer(codeChan, errorChan) // Generate authorization URL authURL := oauth2Config.AuthCodeURL("state", oauth2.AccessTypeOffline) fmt.Println("Opening browser for authentication...") debugMode := os.Getenv("DEBUG") == "true" if debugMode { fmt.Printf("If the browser does not open automatically, please visit the following URL manually:\n%s\n", authURL) } if err := browser.OpenURL(authURL); err != nil { fmt.Println("Failed to open browser automatically. Please copy and paste the URL above into your browser.") } // Wait for authorization code select { case code := <-codeChan: // Exchange code for token token, err := oauth2Config.Exchange(ctx, code) if err != nil { return "", fmt.Errorf("failed to exchange code for token: %v", err) } return token.AccessToken, nil case err := <-errorChan: return "", fmt.Errorf("authentication error: %v", err) case <-time.After(5 * time.Minute): return "", fmt.Errorf("authentication timeout") } } // authenticateWithDevice uses OAuth device authorization flow func authenticateWithDevice(ctx context.Context) (string, error) { deviceAuth, err := oauth2Config.DeviceAuth(ctx) if err != nil { return "", fmt.Errorf("failed to get device code: %v", err) } // Highlight device code with ANSI: bold + yellow fmt.Printf("Device code: \x1b[1;33m%s\x1b[0m\n", deviceAuth.UserCode) fmt.Printf("To authenticate, please visit: %s\n", deviceAuth.VerificationURI) fmt.Println("Attempting to open browser...") if err := browser.OpenURL(deviceAuth.VerificationURI); err != nil { fmt.Println("Failed to open browser automatically. Please copy and paste the URL above into your browser.") } fmt.Println("Waiting for authentication to complete...") token, err := oauth2Config.DeviceAccessToken(ctx, deviceAuth) if err != nil { return "", fmt.Errorf("failed to get access token: %v", err) } return token.AccessToken, nil } func getLocalToken(githubToken string) (string, error) { reqBody := map[string]string{ "token": githubToken, } jsonBody, err := json.Marshal(reqBody) if err != nil { return "", err } // Get base URL with proper priority handling baseURL := os.Getenv("GBOX_BASE_URL") if baseURL == "" { baseURL = config.DefaultBaseURL } else if strings.HasSuffix(baseURL, "/") { baseURL = strings.TrimSuffix(baseURL, "/") } baseEndpoint := strings.TrimSuffix(baseURL, "/api/v1") apiURL := baseEndpoint + "/api/public/v1/auth/github/callback/token" resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonBody)) if err != nil { return "", err } defer resp.Body.Close() // Attempt to obtain the token either from the response body (JSON) or the 'token' cookie. body, err := io.ReadAll(resp.Body) if err != nil { return "", err } var tokenResp TokenResponse // 1. Try to parse token from JSON body (backward compatibility). if len(body) > 0 { _ = json.Unmarshal(body, &tokenResp) // ignore error; we'll fall back to cookie if needed } // 2. Fallback to 'token' cookie if JSON body didn't contain it. if tokenResp.Token == "" { for _, c := range resp.Cookies() { if c.Name == "token" { tokenResp.Token = c.Value break } } } if tokenResp.Token == "" { return "", fmt.Errorf("failed to obtain token from response, body: %s", string(body)) } // Get organization list and let user select selectedOrg, err := selectOrganization(tokenResp.Token) if err != nil { return "", fmt.Errorf("failed to select organization: %v", err) } // Create API key var apiKeyInfo *cloud.CreateAPIKeyResponse if selectedOrg != nil { client, err := cloud.NewClient(tokenResp.Token) if err != nil { return "", fmt.Errorf("failed to create cloud client: %v", err) } // Generate API key name apiKeyName := fmt.Sprintf("gbox-cli-%s", selectedOrg.Name) apiKeyInfo, err = client.CreateAPIKey(apiKeyName, selectedOrg.ID) if err != nil { return "", fmt.Errorf("failed to create API key: %v", err) } fmt.Printf("Created API key: %s.\n", apiKeyInfo.KeyName) fmt.Println("Login process successfully.") } if err := os.MkdirAll(configDir, 0o755); err != nil { return "", fmt.Errorf("failed to create config directory: %v", err) } credentials := map[string]string{ "token": tokenResp.Token, } if selectedOrg != nil { credentials["organization_id"] = selectedOrg.ID credentials["organization_name"] = selectedOrg.Name } credentialsData, err := json.MarshalIndent(credentials, "", " ") if err != nil { return "", fmt.Errorf("failed to serialize credentials: %v", err) } if err := os.WriteFile(credentialsPath, credentialsData, 0o600); err != nil { return "", fmt.Errorf("failed to save credentials: %v", err) } // Save API key related data to profile.json if apiKeyInfo != nil && selectedOrg != nil { pm := profile.NewProfileManager() if err := pm.Load(); err != nil { return "", fmt.Errorf("failed to load profile manager: %v", err) } // Use the organization name from API response orgName := selectedOrg.Name if err := pm.Add("", orgName, apiKeyInfo.APIKey, ""); err != nil { return "", fmt.Errorf("failed to add profile: %v", err) } // Print success message with current profile info currentProfile := pm.GetCurrent() if currentProfile != nil { fmt.Printf("Login successful! Switched to profile: \033[32m%s\033[0m\n", pm.GetCurrentProfileID()) } } return tokenResp.Token, nil } func selectOrganization(token string) (*cloud.Organization, error) { client, err := cloud.NewClient(token) if err != nil { return nil, fmt.Errorf("failed to create cloud client: %v", err) } organizations, err := client.GetMyOrganizationList() if err != nil { return nil, fmt.Errorf("failed to get organization list: %v", err) } if len(organizations) == 0 { fmt.Println("No organizations found. Creating a default organization...") // Fetch current user info to build org name me, err := client.GetCurrentUserInfo() if err != nil { return nil, fmt.Errorf("failed to get current user info: %v", err) } userName := "User" if me != nil && me.Name != nil && *me.Name != "" { userName = *me.Name } else if me != nil && me.Email != nil && *me.Email != "" { userName = *me.Email } // name and slug must be provided; slug should be unique and url-friendly // simple slugify: lowercase, replace spaces with '-', remove apostrophes base := strings.ToLower(userName + "s-team") base = strings.ReplaceAll(base, "'", "") base = strings.ReplaceAll(base, " ", "-") // ensure uniqueness by appending timestamp suffix timestamp := time.Now().Unix() defaultName := fmt.Sprintf("%s's Team", userName) defaultSlug := fmt.Sprintf("%s-%d", base, timestamp) created, err := client.CreateOrganization(cloud.CreateOrganizationRequest{ Name: defaultName, Slug: defaultSlug, }) if err != nil { return nil, fmt.Errorf("failed to create default organization: %v", err) } fmt.Printf("Created default organization: %s (%s)\n", created.Name, created.ID) // init API Key for this organization keyName := fmt.Sprintf("gbox-cli-%s", created.Name) apiKeyInfo, err := client.CreateAPIKey(keyName, created.ID) if err != nil { return nil, fmt.Errorf("failed to create API key for default organization: %v", err) } fmt.Printf("Created API key for default organization: %s\n", apiKeyInfo.KeyName) return created, nil } if len(organizations) == 1 { org := organizations[0] fmt.Printf("Automatically selected organization: %s (%s)\n", org.Name, org.ID) return &org, nil } fmt.Println("Available organizations:") for i, org := range organizations { fmt.Printf("%d. %s (%s)\n", i+1, org.Name, org.ID) } reader := bufio.NewReader(os.Stdin) for { fmt.Print("Select an organization by entering the number: ") input, err := reader.ReadString('\n') if err != nil { return nil, fmt.Errorf("failed to read input: %v", err) } input = strings.TrimSpace(input) choice, err := strconv.Atoi(input) if err != nil { fmt.Println("Invalid input. Please enter a valid number.") continue } if choice < 1 || choice > len(organizations) { fmt.Printf("Please enter a number between 1 and %d.\n", len(organizations)) continue } selectedOrg := organizations[choice-1] fmt.Printf("Selected organization: %s (%s)\n", selectedOrg.Name, selectedOrg.ID) return &selectedOrg, nil } } func init() { rootCmd.AddCommand(loginCmd) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babelcloud/gru-sandbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server