Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 53 additions & 24 deletions cmd/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package login

import (
"bufio"
"context"
"errors"
"fmt"
"os"
Expand All @@ -13,6 +14,8 @@ import (
"github.com/fosrl/cli/internal/config"
"github.com/fosrl/cli/internal/logger"
"github.com/fosrl/cli/internal/utils"
"github.com/mattn/go-isatty"
"github.com/muesli/cancelreader"
"github.com/pkg/browser"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -66,38 +69,65 @@ func loginWithWeb(hostname string) (string, error) {
// Calculate expiry time from relative seconds
expiresAt := time.Now().Add(time.Duration(startResp.ExpiresInSeconds) * time.Second)

// Build the base login URL (without query parameter) for display
baseLoginURL := fmt.Sprintf("%s/auth/login/device", strings.TrimSuffix(hostname, "/"))
// Build the login URL with code as query parameter for browser
loginURL := fmt.Sprintf("%s?code=%s", baseLoginURL, code)
// Build the device login URL with the one-time code
loginURL := fmt.Sprintf("%s/auth/login/device?code=%s", strings.TrimSuffix(hostname, "/"), code)

// Display code and instructions (similar to GH CLI format)
logger.Info("First copy your one-time code: %s", code)
logger.Info("Press Enter to open %s in your browser...", baseLoginURL)

// Wait for Enter in a goroutine (non-blocking) and open browser when pressed
go func() {
reader := bufio.NewReader(os.Stdin)
_, err := reader.ReadString('\n')
if err == nil {
// User pressed Enter, open browser
if err := browser.OpenURL(loginURL); err != nil {
// Don't fail if browser can't be opened, just warn
logger.Warning("Failed to open browser automatically")
logger.Info("Please manually visit: %s", baseLoginURL)
}
logger.Info("Press Enter to open %s in your browser...", loginURL)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

enterCh := make(chan struct{}, 1)
if isatty.IsTerminal(os.Stdin.Fd()) {
stdin, err := cancelreader.NewReader(os.Stdin)
if err != nil {
logger.Info("Visit %s to authorize this device", loginURL)
} else {
go func() {
<-ctx.Done()
stdin.Cancel()
}()
go func() {
_, err := bufio.NewReader(stdin).ReadString('\n')
if err != nil {
return
}
select {
case enterCh <- struct{}{}:
case <-ctx.Done():
}
}()
}
} else {
logger.Info("Visit %s to authorize this device", loginURL)
}

browserOpened := false
openBrowserOnce := func() {
if browserOpened {
return
}
}()
browserOpened = true
if err := browser.OpenURL(loginURL); err != nil {
logger.Warning("Failed to open browser automatically")
logger.Info("Please manually visit: %s", loginURL)
}
}

// Poll for verification (starts immediately, doesn't wait for Enter)
pollInterval := 1 * time.Second
startTime := time.Now()
maxPollDuration := 5 * time.Minute

var token string

for {
// print
select {
case <-enterCh:
openBrowserOnce()
default:
}

logger.Debug("Polling for device web auth verification...")
// Check if code has expired
if time.Now().After(expiresAt) {
Expand All @@ -122,12 +152,11 @@ func loginWithWeb(hostname string) (string, error) {

// Check verification status
if pollResp.Verified {
token = pollResp.Token
if token == "" {
if pollResp.Token == "" {
logger.Error("Verification succeeded but no token received")
return "", fmt.Errorf("verification succeeded but no token received")
}
return token, nil
return pollResp.Token, nil
}

// Check for expired or not found messages
Expand Down
Loading