Skip to content
66 changes: 44 additions & 22 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type LoginOptions struct {
DeviceCode string
}

var pollDeviceToken = larkauth.PollDeviceToken

// NewCmdAuthLogin creates the auth login subcommand.
func NewCmdAuthLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
opts := &LoginOptions{Factory: f}
Expand Down Expand Up @@ -235,6 +237,9 @@ func authLoginRun(opts *LoginOptions) error {

// --no-wait: return immediately with device code and URL
if opts.NoWait {
if err := saveLoginRequestedScope(authResp.DeviceCode, finalScope); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to cache requested scopes: %v\n", err)
}
Comment thread
JackZhao10086 marked this conversation as resolved.
data := map[string]interface{}{
"verification_url": authResp.VerificationUriComplete,
"device_code": authResp.DeviceCode,
Expand All @@ -244,7 +249,7 @@ func authLoginRun(opts *LoginOptions) error {
encoder := json.NewEncoder(f.IOStreams.Out)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(data); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "error: failed to write JSON output: %v\n", err)
return output.Errorf(output.ExitInternal, "internal", "failed to write JSON output: %v", err)
}
return nil
}
Expand All @@ -261,7 +266,7 @@ func authLoginRun(opts *LoginOptions) error {
encoder := json.NewEncoder(f.IOStreams.Out)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(data); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "error: failed to write JSON output: %v\n", err)
return output.Errorf(output.ExitInternal, "internal", "failed to write JSON output: %v", err)
}
} else {
fmt.Fprintf(f.IOStreams.ErrOut, msg.OpenURL)
Expand All @@ -270,20 +275,26 @@ func authLoginRun(opts *LoginOptions) error {

// Step 3: Poll for token
log(msg.WaitingAuth)
result := larkauth.PollDeviceToken(opts.Ctx, httpClient, config.AppID, config.AppSecret, config.Brand,
result := pollDeviceToken(opts.Ctx, httpClient, config.AppID, config.AppSecret, config.Brand,
authResp.DeviceCode, authResp.Interval, authResp.ExpiresIn, f.IOStreams.ErrOut)

if !result.OK {
if opts.JSON {
b, _ := json.Marshal(map[string]interface{}{
encoder := json.NewEncoder(f.IOStreams.Out)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(map[string]interface{}{
"event": "authorization_failed",
"error": result.Message,
})
fmt.Fprintln(f.IOStreams.Out, string(b))
}); err != nil {
return output.Errorf(output.ExitInternal, "internal", "failed to write JSON output: %v", err)
}
return output.ErrBare(output.ExitAuth)
}
return output.ErrAuth("authorization failed: %s", result.Message)
}
if result.Token == nil {
return output.ErrAuth("authorization succeeded but no token returned")
}

// Step 6: Get user info
log(msg.AuthSuccess)
Expand All @@ -296,6 +307,8 @@ func authLoginRun(opts *LoginOptions) error {
return output.ErrAuth("failed to get user info: %v", err)
}

scopeSummary := loadLoginScopeSummary(config.AppID, openId, finalScope, result.Token.Scope)

// Step 7: Store token
now := time.Now().UnixMilli()
storedToken := &larkauth.StoredUAToken{
Expand All @@ -318,21 +331,11 @@ func authLoginRun(opts *LoginOptions) error {
return output.Errorf(output.ExitInternal, "internal", "failed to update login profile: %v", err)
}

if opts.JSON {
b, _ := json.Marshal(map[string]interface{}{
"event": "authorization_complete",
"user_open_id": openId,
"user_name": userName,
"scope": result.Token.Scope,
})
fmt.Fprintln(f.IOStreams.Out, string(b))
} else {
fmt.Fprintln(f.IOStreams.ErrOut)
output.PrintSuccess(f.IOStreams.ErrOut, fmt.Sprintf(msg.LoginSuccess, userName, openId))
if result.Token.Scope != "" {
fmt.Fprintf(f.IOStreams.ErrOut, msg.GrantedScopes, result.Token.Scope)
}
if issue := ensureRequestedScopesGranted(finalScope, result.Token.Scope, msg, scopeSummary); issue != nil {
return handleLoginScopeIssue(opts, msg, f, issue, openId, userName)
}

writeLoginSuccess(opts, msg, f, openId, userName, scopeSummary)
return nil
}

Expand All @@ -345,13 +348,26 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo
if err != nil {
return err
}
requestedScope, err := loadLoginRequestedScope(opts.DeviceCode)
if err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to load cached requested scopes: %v\n", err)
}
cleanupRequestedScope := func() {
if err := removeLoginRequestedScope(opts.DeviceCode); err != nil {
fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to remove cached requested scopes: %v\n", err)
}
}
log(msg.WaitingAuth)
result := larkauth.PollDeviceToken(opts.Ctx, httpClient, config.AppID, config.AppSecret, config.Brand,
result := pollDeviceToken(opts.Ctx, httpClient, config.AppID, config.AppSecret, config.Brand,
opts.DeviceCode, 5, 180, f.IOStreams.ErrOut)

if !result.OK {
if shouldRemoveLoginRequestedScope(result) {
cleanupRequestedScope()
}
return output.ErrAuth("authorization failed: %s", result.Message)
}
defer cleanupRequestedScope()
Comment thread
JackZhao10086 marked this conversation as resolved.
if result.Token == nil {
return output.ErrAuth("authorization succeeded but no token returned")
}
Expand All @@ -367,6 +383,8 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo
return output.ErrAuth("failed to get user info: %v", err)
}

scopeSummary := loadLoginScopeSummary(config.AppID, openId, requestedScope, result.Token.Scope)

// Store token
now := time.Now().UnixMilli()
storedToken := &larkauth.StoredUAToken{
Expand All @@ -389,7 +407,11 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo
return output.Errorf(output.ExitInternal, "internal", "failed to update login profile: %v", err)
}

output.PrintSuccess(f.IOStreams.ErrOut, fmt.Sprintf(msg.LoginSuccess, userName, openId))
if issue := ensureRequestedScopesGranted(requestedScope, result.Token.Scope, msg, scopeSummary); issue != nil {
return handleLoginScopeIssue(opts, msg, f, issue, openId, userName)
}

writeLoginSuccess(opts, msg, f, openId, userName, scopeSummary)
return nil
}

Expand Down
48 changes: 33 additions & 15 deletions cmd/auth/login_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ type loginMsg struct {
ConfirmAuth string

// Non-interactive prompts (login.go)
OpenURL string
WaitingAuth string
AuthSuccess string
LoginSuccess string
GrantedScopes string
OpenURL string
WaitingAuth string
AuthSuccess string
LoginSuccess string
ScopeMismatch string
ScopeHint string
RequestedScopes string
NewlyGrantedScopes string
MissingScopes string
NoScopes string
StatusHint string

// Non-interactive hint (no flags)
HintHeader string
Expand All @@ -50,11 +56,17 @@ var loginMsgZh = &loginMsg{
ErrNoDomain: "请至少选择一个业务域",
ConfirmAuth: "确认授权?",

OpenURL: "在浏览器中打开以下链接进行认证:\n\n",
WaitingAuth: "等待用户授权...",
AuthSuccess: "授权成功,正在获取用户信息...",
LoginSuccess: "登录成功! 用户: %s (%s)",
GrantedScopes: " 已授权 scopes: %s\n",
OpenURL: "在浏览器中打开以下链接进行认证:\n\n",
WaitingAuth: "等待用户授权...",
AuthSuccess: "授权成功,正在获取用户信息...",
LoginSuccess: "登录成功! 用户: %s (%s)",
ScopeMismatch: "授权完成,但以下请求 scopes 未被授予: %s",
ScopeHint: "以上结果是本次授权请求用户最终确认后的结果,请勿持续重试;Scopes 未授予的原因是多样的,如 scope 被禁用;具体原因已通过授权页提示用户。可执行 `lark-cli auth status` 查看账号当前已授予的全部 scopes;",
RequestedScopes: " 本次请求 scopes: %s\n",
NewlyGrantedScopes: " 本次新授予 scopes: %s\n",
MissingScopes: " 本次未授予 scopes: %s\n",
NoScopes: "(空)",
StatusHint: "可执行 `lark-cli auth status` 查看账号当前已授予的全部 scopes;",

HintHeader: "请指定要授权的权限:\n",
HintCommon1: " --recommend 授权推荐权限",
Expand All @@ -79,11 +91,17 @@ var loginMsgEn = &loginMsg{
ErrNoDomain: "please select at least one domain",
ConfirmAuth: "Confirm authorization?",

OpenURL: "Open this URL in your browser to authenticate:\n\n",
WaitingAuth: "Waiting for user authorization...",
AuthSuccess: "Authorization successful, fetching user info...",
LoginSuccess: "Login successful! User: %s (%s)",
GrantedScopes: " Granted scopes: %s\n",
OpenURL: "Open this URL in your browser to authenticate:\n\n",
WaitingAuth: "Waiting for user authorization...",
AuthSuccess: "Authorization successful, fetching user info...",
LoginSuccess: "Login successful! User: %s (%s)",
ScopeMismatch: "authorization completed, but these requested scopes were not granted: %s",
ScopeHint: "The result above is the user's final confirmation for this authorization request. Do not retry continuously. Scopes may be not granted for various reasons, such as a scope being disabled. The specific reason has already been shown to the user on the authorization page. Run `lark-cli auth status` to inspect all scopes currently granted to the account.",
RequestedScopes: " Requested scopes: %s\n",
NewlyGrantedScopes: " Newly granted scopes: %s\n",
MissingScopes: " Not granted scopes: %s\n",
NoScopes: "(none)",
StatusHint: "Run `lark-cli auth status` to inspect all scopes currently granted to the account.",

HintHeader: "Please specify the scopes to authorize:\n",
HintCommon1: " --recommend authorize recommended scopes",
Expand Down
6 changes: 0 additions & 6 deletions cmd/auth/login_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,6 @@ func TestLoginMsg_FormatStrings(t *testing.T) {
t.Errorf("%s LoginSuccess has no format verb", lang)
}

// GrantedScopes should contain %s
got = fmt.Sprintf(msg.GrantedScopes, "scope1 scope2")
if got == msg.GrantedScopes {
t.Errorf("%s GrantedScopes has no format verb", lang)
}

// SummaryDomains should contain %s
got = fmt.Sprintf(msg.SummaryDomains, "calendar, task")
if got == msg.SummaryDomains {
Expand Down
Loading
Loading