Skip to content

Commit 4d63311

Browse files
authored
login: wrap and propagate errors (#232)
1 parent e59704f commit 4d63311

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)