mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
fix(auth): handle missing scopes and device flow improvements (#752)
* fix(auth): handle missing scopes and device flow improvements * fix: remove redundant error return in login scope handler * test(auth): rename test for zero interval default case * fix: increase device code polling timeout from 180 to 600 seconds
This commit is contained in:
@@ -348,7 +348,7 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo
|
||||
}
|
||||
log(msg.WaitingAuth)
|
||||
result := pollDeviceToken(opts.Ctx, httpClient, config.AppID, config.AppSecret, config.Brand,
|
||||
opts.DeviceCode, 5, 180, f.IOStreams.ErrOut)
|
||||
opts.DeviceCode, 5, 600, f.IOStreams.ErrOut)
|
||||
|
||||
if !result.OK {
|
||||
if shouldRemoveLoginRequestedScope(result) {
|
||||
|
||||
@@ -169,7 +169,7 @@ func handleLoginScopeIssue(opts *LoginOptions, msg *loginMsg, f *cmdutil.Factory
|
||||
if loginSucceeded {
|
||||
b, _ := json.Marshal(authorizationCompletePayload(openId, userName, issue.Summary, issue))
|
||||
fmt.Fprintln(f.IOStreams.Out, string(b))
|
||||
return nil
|
||||
return output.ErrBare(output.ExitAuth)
|
||||
}
|
||||
detail := map[string]interface{}{
|
||||
"requested": issue.Summary.Requested,
|
||||
@@ -200,9 +200,6 @@ func handleLoginScopeIssue(opts *LoginOptions, msg *loginMsg, f *cmdutil.Factory
|
||||
if issue.Hint != "" {
|
||||
fmt.Fprintln(f.IOStreams.ErrOut, issue.Hint)
|
||||
}
|
||||
if loginSucceeded {
|
||||
return nil
|
||||
}
|
||||
return output.ErrBare(output.ExitAuth)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
"github.com/zalando/go-keyring"
|
||||
@@ -371,8 +372,12 @@ func TestHandleLoginScopeIssue_NonJSONAlignsWithLoginSuccess(t *testing.T) {
|
||||
Granted: []string{"base:app:copy"},
|
||||
},
|
||||
}, "ou_user", "tester")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected ExitError, got %v", err)
|
||||
}
|
||||
if exitErr.Code != output.ExitAuth {
|
||||
t.Fatalf("exit code = %d, want %d", exitErr.Code, output.ExitAuth)
|
||||
}
|
||||
got := stderr.String()
|
||||
for _, want := range []string{
|
||||
@@ -410,8 +415,12 @@ func TestHandleLoginScopeIssue_JSONAlignsWithLoginSuccess(t *testing.T) {
|
||||
Granted: []string{"base:app:copy"},
|
||||
},
|
||||
}, "ou_user", "tester")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected ExitError, got %v", err)
|
||||
}
|
||||
if exitErr.Code != output.ExitAuth {
|
||||
t.Fatalf("exit code = %d, want %d", exitErr.Code, output.ExitAuth)
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
@@ -616,8 +625,12 @@ func TestAuthLoginRun_MissingRequestedScopeAlignsWithLoginSuccess(t *testing.T)
|
||||
Ctx: context.Background(),
|
||||
Scope: "im:message:send",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected ExitError, got %v", err)
|
||||
}
|
||||
if exitErr.Code != output.ExitAuth {
|
||||
t.Fatalf("exit code = %d, want %d", exitErr.Code, output.ExitAuth)
|
||||
}
|
||||
got := stderr.String()
|
||||
for _, want := range []string{
|
||||
|
||||
@@ -142,8 +142,12 @@ func PollDeviceToken(ctx context.Context, httpClient *http.Client, appId, appSec
|
||||
errOut = io.Discard
|
||||
}
|
||||
|
||||
if interval < 1 {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
const maxPollInterval = 60
|
||||
const maxPollAttempts = 200
|
||||
const maxPollAttempts = 600
|
||||
|
||||
endpoints := ResolveOAuthEndpoints(brand)
|
||||
deadline := time.Now().Add(time.Duration(expiresIn) * time.Second)
|
||||
|
||||
@@ -5,10 +5,12 @@ package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -17,6 +19,12 @@ import (
|
||||
"github.com/larksuite/cli/internal/keychain"
|
||||
)
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return fn(req)
|
||||
}
|
||||
|
||||
// TestResolveOAuthEndpoints_Feishu validates endpoints for the Feishu brand.
|
||||
func TestResolveOAuthEndpoints_Feishu(t *testing.T) {
|
||||
ep := ResolveOAuthEndpoints(core.BrandFeishu)
|
||||
@@ -172,3 +180,33 @@ func TestLogAuthError_RecordsStructuredEntry(t *testing.T) {
|
||||
t.Fatalf("expected truncated cmdline in log, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollDeviceToken_DefaultsZeroIntervalToFiveSeconds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var requests atomic.Int32
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
requests.Add(1)
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: http.NoBody,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result := PollDeviceToken(ctx, client, "cli_a", "secret_b", core.BrandFeishu, "device-code", 0, 10, nil)
|
||||
if result == nil {
|
||||
t.Fatal("PollDeviceToken() returned nil result")
|
||||
}
|
||||
if result.Message != "Polling was cancelled" {
|
||||
t.Fatalf("PollDeviceToken() message = %q, want polling cancellation", result.Message)
|
||||
}
|
||||
if got := requests.Load(); got != 0 {
|
||||
t.Fatalf("PollDeviceToken() sent %d requests before context cancellation, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const (
|
||||
ExitOK = 0 // 成功
|
||||
ExitAPI = 1 // API / 通用错误(含 permission、not_found、conflict、rate_limit)
|
||||
ExitValidation = 2 // 参数校验失败
|
||||
ExitAuth = 3 // 认证失败(token 无效 / 过期)
|
||||
ExitAuth = 3 // 认证失败(token 无效 / 过期),或登录成功但请求 scopes 未全部授予
|
||||
ExitNetwork = 4 // 网络错误(连接超时、DNS 解析失败等)
|
||||
ExitInternal = 5 // 内部错误(不应发生)
|
||||
ExitContentSafety = 6 // content safety violation (block mode)
|
||||
|
||||
Reference in New Issue
Block a user