diff --git a/auth.go b/auth.go index 5473c8b..403b236 100644 --- a/auth.go +++ b/auth.go @@ -174,13 +174,9 @@ func refreshAccessToken( data.Set("grant_type", "refresh_token") data.Set("refresh_token", refreshToken) data.Set("client_id", cfg.ClientID) - if !cfg.IsPublicClient() { - data.Set("client_secret", cfg.ClientSecret) - } + cfg.setClientSecret(data) // Server doesn't persist extra_claims across refresh, so re-send them. - if cfg.ExtraClaims != "" { - data.Set(extraClaimsFormKey, cfg.ExtraClaims) - } + cfg.setExtraClaims(data) tokenResp, err := doTokenExchange(ctx, cfg, cfg.Endpoints.TokenURL, data, func(errResp ErrorResponse, _ []byte) error { diff --git a/browser_flow.go b/browser_flow.go index 44f2b94..659a1b1 100644 --- a/browser_flow.go +++ b/browser_flow.go @@ -40,12 +40,8 @@ func exchangeCode( data.Set("client_id", cfg.ClientID) data.Set("code_verifier", codeVerifier) - if !cfg.IsPublicClient() { - data.Set("client_secret", cfg.ClientSecret) - } - if cfg.ExtraClaims != "" { - data.Set(extraClaimsFormKey, cfg.ExtraClaims) - } + cfg.setClientSecret(data) + cfg.setExtraClaims(data) tokenResp, err := doTokenExchange(ctx, cfg, cfg.Endpoints.TokenURL, data, nil) if err != nil { diff --git a/device_flow.go b/device_flow.go index 8df2035..312657a 100644 --- a/device_flow.go +++ b/device_flow.go @@ -70,7 +70,7 @@ func requestDeviceCode(ctx context.Context, cfg *AppConfig) (*oauth2.DeviceAuthR UserCode: deviceResp.UserCode, VerificationURI: deviceResp.VerificationURI, VerificationURIComplete: deviceResp.VerificationURIComplete, - Expiry: time.Now().Add(time.Duration(deviceResp.ExpiresIn) * time.Second), + Expiry: expiryFromNow(deviceResp.ExpiresIn), Interval: int64(deviceResp.Interval), }, nil } @@ -151,9 +151,7 @@ func exchangeDeviceCode( data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") data.Set("device_code", deviceCode) data.Set("client_id", cID) - if cfg.ExtraClaims != "" { - data.Set(extraClaimsFormKey, cfg.ExtraClaims) - } + cfg.setExtraClaims(data) resp, err := cfg.RetryClient.Post(reqCtx, tokenURL, retry.WithBody("application/x-www-form-urlencoded", strings.NewReader(data.Encode())), @@ -192,7 +190,7 @@ func exchangeDeviceCode( AccessToken: tokenResp.AccessToken, RefreshToken: tokenResp.RefreshToken, TokenType: tokenResp.TokenType, - Expiry: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second), + Expiry: expiryFromNow(tokenResp.ExpiresIn), }, nil } diff --git a/token_cmd.go b/token_cmd.go index cd39457..31049c9 100644 --- a/token_cmd.go +++ b/token_cmd.go @@ -285,9 +285,7 @@ func doRevoke( if tokenTypeHint != "" { data.Set("token_type_hint", tokenTypeHint) } - if !cfg.IsPublicClient() { - data.Set("client_secret", cfg.ClientSecret) - } + cfg.setClientSecret(data) resp, err := cfg.RetryClient.Post(ctx, revokeURL, retry.WithBody( "application/x-www-form-urlencoded", diff --git a/tokens.go b/tokens.go index 060fa70..3a92596 100644 --- a/tokens.go +++ b/tokens.go @@ -125,13 +125,34 @@ func doTokenExchange( return &tokenResp, nil } +// expiryFromNow returns the absolute expiry time for a token that the server +// says expires in expiresIn seconds from now. +func expiryFromNow(expiresIn int) time.Time { + return time.Now().Add(time.Duration(expiresIn) * time.Second) +} + +// setClientSecret adds the client_secret form parameter for confidential +// clients. Public (PKCE) clients omit it. +func (c *AppConfig) setClientSecret(data url.Values) { + if !c.IsPublicClient() { + data.Set("client_secret", c.ClientSecret) + } +} + +// setExtraClaims adds the extra_claims form parameter when configured. +func (c *AppConfig) setExtraClaims(data url.Values) { + if c.ExtraClaims != "" { + data.Set(extraClaimsFormKey, c.ExtraClaims) + } +} + // tokenResponseToCredstore converts a tokenResponse to a credstore.Token. func tokenResponseToCredstore(cfg *AppConfig, tr *tokenResponse) *credstore.Token { return &credstore.Token{ AccessToken: tr.AccessToken, RefreshToken: tr.RefreshToken, TokenType: tr.TokenType, - ExpiresAt: time.Now().Add(time.Duration(tr.ExpiresIn) * time.Second), + ExpiresAt: expiryFromNow(tr.ExpiresIn), ClientID: cfg.ClientID, } } diff --git a/tui/flow_renderer.go b/tui/flow_renderer.go index 33b31f2..dd511b4 100644 --- a/tui/flow_renderer.go +++ b/tui/flow_renderer.go @@ -21,8 +21,7 @@ type FlowRenderer struct { spinnerFrame int spinnerChars []rune contentDirty bool // Flag to indicate if content needs redraw - inProgressStepIdx int // Index of the in-progress step for spinner updates - hasInProgressStep bool // Whether there's a step in progress + inProgressStepIdx int // Index of the in-progress step for spinner updates, or -1 if none deviceUserCode string // Device code to display deviceVerificationURI string // Device verification URL deviceVerificationURIComplete string // Complete URL with user code @@ -41,9 +40,10 @@ func NewFlowRenderer(flowType, clientMode, serverURL string) *FlowRenderer { model.height = height return &FlowRenderer{ - model: model, - spinnerFrame: 0, - spinnerChars: []rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'}, + model: model, + spinnerFrame: 0, + spinnerChars: []rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'}, + inProgressStepIdx: -1, } } @@ -65,10 +65,9 @@ func (r *FlowRenderer) UpdateStep(index int, status FlowStepStatus, message stri // Track if this step is in progress for spinner animation if status == StepInProgress { - r.hasInProgressStep = true r.inProgressStepIdx = index } else if r.inProgressStepIdx == index { - r.hasInProgressStep = false + r.inProgressStepIdx = -1 } } @@ -146,7 +145,7 @@ func (r *FlowRenderer) UpdateDisplay() { resized := r.checkResize() // If only spinner changed (not content), do a minimal update - if !r.contentDirty && r.hasInProgressStep { + if !r.contentDirty && r.inProgressStepIdx >= 0 { r.updateSpinnerOnly() return } @@ -198,7 +197,7 @@ func (r *FlowRenderer) UpdateDisplay() { // updateSpinnerOnly updates only the spinner character without redrawing everything func (r *FlowRenderer) updateSpinnerOnly() { - if !r.hasInProgressStep { + if r.inProgressStepIdx < 0 { return }