diff --git a/browser_flow.go b/browser_flow.go index f8efb21..e5cdb11 100644 --- a/browser_flow.go +++ b/browser_flow.go @@ -14,65 +14,6 @@ import ( "github.com/go-authgate/cli/tui" ) -// performBrowserFlow runs the Authorization Code Flow with PKCE. -// -// Returns: -// - (storage, true, nil) on success -// - (nil, false, nil) when openBrowser() fails — caller should fall back to Device Code Flow -// - (nil, false, err) on a hard error (CSRF mismatch, token exchange failure, etc.) -func performBrowserFlow(ctx context.Context) (*TokenStorage, bool, error) { - state, err := generateState() - if err != nil { - return nil, false, fmt.Errorf("failed to generate state: %w", err) - } - - pkce, err := GeneratePKCE() - if err != nil { - return nil, false, fmt.Errorf("failed to generate PKCE: %w", err) - } - - authURL := buildAuthURL(state, pkce) - - fmt.Println("Step 1: Opening browser for authorization...") - fmt.Printf("\n %s\n\n", authURL) - - if err := openBrowser(ctx, authURL); err != nil { - // Browser failed to open — signal the caller to fall back immediately. - fmt.Printf("Could not open browser: %v\n", err) - return nil, false, nil - } - - fmt.Println("Browser opened. Please complete authorization in your browser.") - fmt.Printf("Step 2: Waiting for callback on http://localhost:%d/callback ...\n", callbackPort) - - storage, err := startCallbackServer(ctx, callbackPort, state, - func(callbackCtx context.Context, code string) (*TokenStorage, error) { - fmt.Println("Step 3: Exchanging authorization code for tokens...") - return exchangeCode(callbackCtx, code, pkce.Verifier) - }) - if err != nil { - if errors.Is(err, ErrCallbackTimeout) { - // User opened the browser but didn't complete authorization in time. - // Fall back to Device Code Flow so they can still authenticate. - fmt.Printf( - "Browser authorization timed out after %s, falling back to Device Code Flow...\n", - callbackTimeout, - ) - return nil, false, nil - } - return nil, false, fmt.Errorf("authentication failed: %w", err) - } - storage.Flow = "browser" - - if err := saveTokens(storage); err != nil { - fmt.Printf("Warning: Failed to save tokens: %v\n", err) - } else { - fmt.Printf("Tokens saved to %s\n", tokenFile) - } - - return storage, true, nil -} - // buildAuthURL constructs the /oauth/authorize URL with all required parameters. func buildAuthURL(state string, pkce *PKCEParams) string { params := url.Values{} diff --git a/device_flow.go b/device_flow.go index 3f08d5b..e3d6b28 100644 --- a/device_flow.go +++ b/device_flow.go @@ -15,56 +15,6 @@ import ( "golang.org/x/oauth2" ) -// performDeviceFlow runs the OAuth 2.0 Device Authorization Grant (RFC 8628) -// and returns tokens on success. -func performDeviceFlow(ctx context.Context) (*TokenStorage, error) { - config := &oauth2.Config{ - ClientID: clientID, - Endpoint: oauth2.Endpoint{ - DeviceAuthURL: serverURL + "/oauth/device/code", - TokenURL: serverURL + "/oauth/token", - }, - Scopes: strings.Fields(scope), - } - - fmt.Println("Step 1: Requesting device code...") - deviceAuth, err := requestDeviceCode(ctx) - if err != nil { - return nil, fmt.Errorf("device code request failed: %w", err) - } - - fmt.Printf("\n----------------------------------------\n") - fmt.Printf("Please open this link to authorize:\n%s\n", deviceAuth.VerificationURIComplete) - fmt.Printf("\nOr visit : %s\n", deviceAuth.VerificationURI) - fmt.Printf("And enter: %s\n", deviceAuth.UserCode) - fmt.Printf("----------------------------------------\n\n") - - fmt.Println("Step 2: Waiting for authorization...") - token, err := pollForTokenWithProgress(ctx, config, deviceAuth) - if err != nil { - return nil, fmt.Errorf("token poll failed: %w", err) - } - - fmt.Println("\nAuthorization successful!") - - storage := &TokenStorage{ - AccessToken: token.AccessToken, - RefreshToken: token.RefreshToken, - TokenType: token.Type(), - ExpiresAt: token.Expiry, - ClientID: clientID, - Flow: "device", - } - - if err := saveTokens(storage); err != nil { - fmt.Printf("Warning: Failed to save tokens: %v\n", err) - } else { - fmt.Printf("Tokens saved to %s\n", tokenFile) - } - - return storage, nil -} - // requestDeviceCode requests a device code from the OAuth server. func requestDeviceCode(ctx context.Context) (*oauth2.DeviceAuthResponse, error) { reqCtx, cancel := context.WithTimeout(ctx, deviceCodeRequestTimeout) diff --git a/tui/browser_model.go b/tui/browser_model.go index 4821a8e..510ec30 100644 --- a/tui/browser_model.go +++ b/tui/browser_model.go @@ -74,6 +74,7 @@ func (m *BrowserModel) Init() tea.Cmd { func (m *BrowserModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: + //nolint:exhaustive // We only handle Ctrl+C, other keys are ignored switch msg.Type { case tea.KeyCtrlC: // Cancel the OAuth flow @@ -85,6 +86,8 @@ func (m *BrowserModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Give a brief moment for the cancellation to propagate time.Sleep(100 * time.Millisecond) return m, tea.Quit + default: + // Ignore other keys } case tea.WindowSizeMsg: @@ -174,6 +177,9 @@ func (m *BrowserModel) handleFlowUpdate(update FlowUpdate) tea.Cmd { case CallbackReceived: m.status = "Callback received, exchanging tokens..." + + case DeviceCodeReceived, PollingUpdate, BackoffChanged: + // These are device flow specific, ignore in browser flow } return waitForUpdate(m.updatesCh) diff --git a/tui/browser_view.go b/tui/browser_view.go index 0b66254..6e876ad 100644 --- a/tui/browser_view.go +++ b/tui/browser_view.go @@ -68,19 +68,20 @@ func renderBrowserView(m *BrowserModel) string { func renderBrowserComplete(m *BrowserModel) string { var b strings.Builder - if m.err != nil { + switch { + case m.err != nil: // Error state b.WriteString("\n") b.WriteString(RenderError(fmt.Sprintf("Authentication failed: %v", m.err))) b.WriteString("\n\n") - } else if !m.ok { + case !m.ok: // Fallback to device flow b.WriteString("\n") b.WriteString( RenderWarning("Browser flow unavailable, falling back to Device Code Flow..."), ) b.WriteString("\n\n") - } else { + default: // Success state b.WriteString("\n") successBox := lipgloss.NewStyle(). diff --git a/tui/components/step_indicator.go b/tui/components/step_indicator.go index 01e009b..fb638c8 100644 --- a/tui/components/step_indicator.go +++ b/tui/components/step_indicator.go @@ -54,15 +54,16 @@ func (s *StepIndicator) View() string { label = fmt.Sprintf("Step %d", i) } - if i < s.CurrentStep { + switch { + case i < s.CurrentStep: // Completed step symbol = "●" style = completedStyle - } else if i == s.CurrentStep { + case i == s.CurrentStep: // Current step symbol = "●" style = currentStyle - } else { + default: // Pending step symbol = "○" style = pendingStyle @@ -89,11 +90,12 @@ func (s *StepIndicator) ViewCompact() string { var symbols []string for i := 1; i <= s.TotalSteps; i++ { - if i < s.CurrentStep { + switch { + case i < s.CurrentStep: symbols = append(symbols, completedStyle.Render("●")) - } else if i == s.CurrentStep { + case i == s.CurrentStep: symbols = append(symbols, currentStyle.Render("●")) - } else { + default: symbols = append(symbols, pendingStyle.Render("○")) } } diff --git a/tui/device_model.go b/tui/device_model.go index 9ba6395..60a39c3 100644 --- a/tui/device_model.go +++ b/tui/device_model.go @@ -77,6 +77,7 @@ func (m *DeviceModel) Init() tea.Cmd { func (m *DeviceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: + //nolint:exhaustive // We only handle Ctrl+C, other keys are ignored switch msg.Type { case tea.KeyCtrlC: // Cancel the OAuth flow @@ -88,6 +89,8 @@ func (m *DeviceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Give a brief moment for the cancellation to propagate time.Sleep(100 * time.Millisecond) return m, tea.Quit + default: + // Ignore other keys } case tea.WindowSizeMsg: @@ -180,6 +183,9 @@ func (m *DeviceModel) handleFlowUpdate(update FlowUpdate) tea.Cmd { case TimerTick: m.elapsed = update.GetDuration("elapsed") m.timer.Update(m.elapsed) + + case BrowserOpened, CallbackReceived: + // These are browser flow specific, ignore in device flow } return waitForUpdate(m.updatesCh) diff --git a/tui/device_view.go b/tui/device_view.go index c0ce5a8..f087234 100644 --- a/tui/device_view.go +++ b/tui/device_view.go @@ -137,7 +137,7 @@ func formatInterval(d time.Duration) string { return fmt.Sprintf("%ds", seconds) } minutes := seconds / 60 - seconds = seconds % 60 + seconds %= 60 if seconds == 0 { return fmt.Sprintf("%dm", minutes) } diff --git a/tui/simple_manager.go b/tui/simple_manager.go index 76b3beb..350d5f6 100644 --- a/tui/simple_manager.go +++ b/tui/simple_manager.go @@ -73,19 +73,20 @@ func (m *SimpleManager) RunBrowserFlow( func (m *SimpleManager) handleBrowserUpdate(update FlowUpdate) { switch update.Type { case StepStart: - if update.Step == 1 { + switch update.Step { + case 1: fmt.Println("Step 1: Opening browser for authorization...") if url := update.GetString("url"); url != "" { fmt.Printf("\n %s\n\n", url) } - } else if update.Step == 2 { + case 2: fmt.Println("Browser opened. Please complete authorization in your browser.") port := update.GetInt("port") if port == 0 { port = 8888 // default } fmt.Printf("Step 2: Waiting for callback on http://localhost:%d/callback ...\n", port) - } else if update.Step == 3 { + case 3: fmt.Println("Step 3: Exchanging authorization code for tokens...") } @@ -101,6 +102,9 @@ func (m *SimpleManager) handleBrowserUpdate(update FlowUpdate) { case CallbackReceived: // Token exchange message printed in StepStart for step 3 return + + case StepProgress, StepComplete, TimerTick, DeviceCodeReceived, PollingUpdate, BackoffChanged: + // Not displayed in simple mode } } @@ -146,14 +150,18 @@ func (m *SimpleManager) RunDeviceFlow( } func (m *SimpleManager) handleDeviceUpdate(update FlowUpdate) { - switch update.Type { - case StepStart: - if update.Step == 1 { + switch update.Step { + case 1: + if update.Type == StepStart { fmt.Println("Step 1: Requesting device code...") - } else if update.Step == 2 { + } + case 2: + if update.Type == StepStart { fmt.Println("Step 2: Waiting for authorization...") } + } + switch update.Type { case DeviceCodeReceived: userCode := update.GetString("user_code") verificationURI := update.GetString("verification_uri") @@ -183,6 +191,9 @@ func (m *SimpleManager) handleDeviceUpdate(update FlowUpdate) { fmt.Println() fmt.Println(update.Message) } + + case StepStart, StepProgress, TimerTick, BrowserOpened, CallbackReceived: + // Already handled or not displayed in simple mode } } @@ -197,8 +208,7 @@ func (m *SimpleManager) ShowTokenInfo(storage *TokenStorage) { fmt.Printf("Token Type : %s\n", storage.TokenType) // Handle ExpiresAt being either time.Time or interface{} - switch expiresAt := storage.ExpiresAt.(type) { - case time.Time: + if expiresAt, ok := storage.ExpiresAt.(time.Time); ok { fmt.Printf("Expires In : %s\n", time.Until(expiresAt).Round(time.Second)) }