Skip to content

Commit 45b1d62

Browse files
committed
Map LINE login errors and stop leaking raw errors into login copy
Wait() returned plain errors for verification and login failures, so the provisioning API replaced them with a generic 500 M_UNKNOWN "Internal error in login step". Map them onto declared RespErrors instead, reusing the existing parseLoginErrorDetails so LINE's own short reason strings are quoted where it gives one. Also stop formatting the raw Go error into the login form instructions when no reason could be parsed. That put internal detail directly in user-facing copy; it now shows a fixed message. CreateLogin ignored flowID entirely and returned the email login for any value, including a typo. Validate it against the advertised flow ID and return bridgev2.ErrInvalidLoginFlowID otherwise, and give that ID a named constant so the flow list and the check cannot drift apart. Note this does not change the larger contract question: a rejected password is still reported as a fresh user_input step on HTTP 200 rather than an error, so clients cannot distinguish it from a legitimate next step. That is worth deciding deliberately before changing.
1 parent e59704f commit 45b1d62

3 files changed

Lines changed: 81 additions & 9 deletions

File tree

pkg/connector/client_lifecycle_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ func TestLineClientDisconnectBeforeConnectRejectsStartup(t *testing.T) {
7878

7979
func TestCreateLoginSharesFinalizationLock(t *testing.T) {
8080
connector := &LineConnector{}
81-
firstProcess, err := connector.CreateLogin(context.Background(), nil, "")
81+
firstProcess, err := connector.CreateLogin(context.Background(), nil, LoginFlowIDEmail)
8282
if err != nil {
8383
t.Fatalf("first CreateLogin returned error: %v", err)
8484
}
85-
secondProcess, err := connector.CreateLogin(context.Background(), nil, "")
85+
secondProcess, err := connector.CreateLogin(context.Background(), nil, LoginFlowIDEmail)
8686
if err != nil {
8787
t.Fatalf("second CreateLogin returned error: %v", err)
8888
}

pkg/connector/connector.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,15 +145,20 @@ func (lc *LineConnector) LoadUserLogin(ctx context.Context, login *bridgev2.User
145145
return nil
146146
}
147147

148+
const LoginFlowIDEmail = "dev.highest.matrix.line.email_login"
149+
148150
func (lc *LineConnector) GetLoginFlows() []bridgev2.LoginFlow {
149151
return []bridgev2.LoginFlow{{
150152
Name: "Login",
151153
Description: "Login with your LINE Email and Password",
152-
ID: "dev.highest.matrix.line.email_login",
154+
ID: LoginFlowIDEmail,
153155
}}
154156
}
155157

156158
func (lc *LineConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
159+
if flowID != LoginFlowIDEmail {
160+
return nil, bridgev2.ErrInvalidLoginFlowID
161+
}
157162
return &LineEmailLogin{User: user, finalizeMu: &lc.loginFinalizeMu}, nil
158163
}
159164

@@ -233,7 +238,7 @@ func (ll *LineEmailLogin) StartWithOverride(ctx context.Context, override *bridg
233238
ll.logLoginFailure(err, "reconnect")
234239
reason := loginErrorReason(err)
235240
if reason == "" {
236-
reason = fmt.Sprintf("Login failed: %v", err)
241+
reason = genericLoginFailureReason
237242
}
238243
return ll.loginErrorStep(reason), nil
239244
}
@@ -260,7 +265,7 @@ func (ll *LineEmailLogin) SubmitUserInput(ctx context.Context, input map[string]
260265
ll.logLoginFailure(err, "credentials")
261266
reason := loginErrorReason(err)
262267
if reason == "" {
263-
reason = fmt.Sprintf("Login failed: %v", err)
268+
reason = genericLoginFailureReason
264269
}
265270
return ll.loginErrorStep(reason), nil
266271
}
@@ -456,10 +461,10 @@ func (ll *LineEmailLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error)
456461
if res.AuthToken != "" {
457462
return ll.finishLogin(ctx, res)
458463
}
459-
return nil, fmt.Errorf("verification failed: no auth token received")
464+
return nil, ErrLoginVerificationFailed
460465
case err := <-ll.pollErr:
461466
ll.logLoginFailure(err, "verification_poll")
462-
return nil, fmt.Errorf("verification failed: %w", err)
467+
return nil, wrapLineLoginError(err)
463468
case <-ctx.Done():
464469
return nil, ctx.Err()
465470
}
@@ -469,7 +474,7 @@ func (ll *LineEmailLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error)
469474
res, err := loginWithCredentials(ll.Email, ll.Password, ll.Certificate)
470475
if err != nil {
471476
ll.logLoginFailure(err, "pin_continuation")
472-
return nil, fmt.Errorf("login failed: %w", err)
477+
return nil, wrapLineLoginError(err)
473478
}
474479
return ll.handleLoginResponse(ctx, res)
475480
}
@@ -581,7 +586,7 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult
581586
ll.User.Bridge.Log.Info().Int("keys", len(meta.ExportedKeyMap)).Msg("Preserved existing E2EE keys after re-login")
582587
}
583588
if !res.NoE2EE && len(meta.ExportedKeyMap) == 0 {
584-
return nil, fmt.Errorf("LINE login completed without E2EE keychain; please reconnect again and complete the LINE verification prompt")
589+
return nil, ErrLoginNoKeychain
585590
}
586591

587592
detectedLineID := networkid.UserLoginID(mid)

pkg/connector/loginerrors.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package connector
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"maunium.net/go/mautrix/bridgev2"
8+
)
9+
10+
// genericLoginFailureReason is shown in the login form when LINE rejects the sign-in but
11+
// gives no reason we can quote. It replaces formatting the raw Go error into the
12+
// instructions, which leaked internal detail into user-facing copy.
13+
const genericLoginFailureReason = "LINE rejected the sign-in. Please check your email and password and try again."
14+
15+
var (
16+
ErrLoginVerificationFailed = bridgev2.RespError{
17+
ErrCode: "DEV.HIGHEST.LINE.VERIFICATION_FAILED",
18+
Err: "LINE didn't confirm the verification. Please start the login again.",
19+
StatusCode: http.StatusBadRequest,
20+
}
21+
ErrLoginNoKeychain = bridgev2.RespError{
22+
ErrCode: "DEV.HIGHEST.LINE.NO_KEYCHAIN",
23+
Err: "LINE finished signing in without sending the encryption keychain. Please reconnect and complete the verification prompt in the LINE app.",
24+
StatusCode: http.StatusBadRequest,
25+
}
26+
ErrLoginTooManyAttempts = bridgev2.RespError{
27+
ErrCode: "DEV.HIGHEST.LINE.TOO_MANY_ATTEMPTS",
28+
Err: loginTooManyAttemptsReason,
29+
StatusCode: http.StatusTooManyRequests,
30+
}
31+
ErrLoginRejected = bridgev2.RespError{
32+
ErrCode: "DEV.HIGHEST.LINE.LOGIN_REJECTED",
33+
Err: genericLoginFailureReason,
34+
StatusCode: http.StatusUnauthorized,
35+
}
36+
ErrLoginUnknown = bridgev2.RespError{
37+
ErrCode: "M_UNKNOWN",
38+
Err: "Internal error logging in to LINE",
39+
StatusCode: http.StatusInternalServerError,
40+
}
41+
)
42+
43+
// wrapLineLoginError translates a LINE error into one the client can act on, keeping the
44+
// original in the chain with %w so logs are unaffected.
45+
func wrapLineLoginError(err error) error {
46+
if err == nil {
47+
return nil
48+
}
49+
mapped := ErrLoginUnknown
50+
details := parseLoginErrorDetails(err)
51+
reason := details.ErrorReason
52+
if reason == "" {
53+
reason = details.ErrorMessage
54+
}
55+
switch {
56+
case isBlockedUserLoginError(reason):
57+
mapped = ErrLoginTooManyAttempts
58+
case details.HTTPStatus == http.StatusTooManyRequests:
59+
mapped = ErrLoginTooManyAttempts
60+
case details.HTTPStatus == http.StatusUnauthorized, details.HTTPStatus == http.StatusForbidden:
61+
mapped = ErrLoginRejected
62+
case reason != "":
63+
// LINE's own reason strings are short and user-facing, so quote them.
64+
mapped = ErrLoginRejected.WithMessage("%s", reason)
65+
}
66+
return fmt.Errorf("%w: %w", mapped, err)
67+
}

0 commit comments

Comments
 (0)