Skip to content

Commit 2dd7aae

Browse files
committed
fix(gotrue): don't throw in verifyOTP when secure email/phone change returns no session
A secure email or phone change verifies in two steps. The server accepts the first OTP without returning a session and only issues one once the second OTP is verified. verifyOTP previously threw an AuthException whenever the session was null, breaking the legitimate intermediate step. Only persist the session and notify subscribers when a session is present, and return the intermediate response otherwise, matching auth-js behaviour. Fixes #981
1 parent 623777b commit 2dd7aae

2 files changed

Lines changed: 23 additions & 18 deletions

File tree

packages/gotrue/lib/src/gotrue_client.dart

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -607,17 +607,20 @@ class GoTrueClient {
607607

608608
final authResponse = AuthResponse.fromJson(response);
609609

610-
if (authResponse.session == null) {
611-
throw AuthException('An error occurred on token verification.');
610+
// A secure email or phone change verifies in two steps: the server accepts
611+
// the first OTP without returning a session and only issues one once the
612+
// second OTP is verified. In that case there is nothing to persist yet, so
613+
// return the intermediate response instead of treating it as an error.
614+
final session = authResponse.session;
615+
if (session != null) {
616+
_saveSession(session);
617+
notifyAllSubscribers(
618+
type == OtpType.recovery
619+
? AuthChangeEvent.passwordRecovery
620+
: AuthChangeEvent.signedIn,
621+
);
612622
}
613623

614-
_saveSession(authResponse.session!);
615-
notifyAllSubscribers(
616-
type == OtpType.recovery
617-
? AuthChangeEvent.passwordRecovery
618-
: AuthChangeEvent.signedIn,
619-
);
620-
621624
return authResponse;
622625
}
623626

packages/gotrue/test/otp_mock_test.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -466,22 +466,24 @@ void main() {
466466
);
467467
});
468468

469-
test('response with null session', () async {
469+
test('response with null session returns the intermediate response',
470+
() async {
470471
final client = GoTrueClient(
471472
url: 'https://example.com',
472473
httpClient: NullSessionClient(testEmail),
473474
asyncStorage: TestAsyncStorage(),
474475
);
475476

476-
await expectLater(
477-
() => client.verifyOTP(
478-
email: testEmail,
479-
token: '123456',
480-
type: OtpType.email,
481-
),
482-
throwsA(isA<AuthException>().having((e) => e.message, 'message',
483-
'An error occurred on token verification.')),
477+
// Verifying the first OTP of a secure email change does not return a
478+
// session. This should not throw, the intermediate response is returned
479+
// so the second OTP can subsequently be verified.
480+
final response = await client.verifyOTP(
481+
email: testEmail,
482+
token: '123456',
483+
type: OtpType.emailChange,
484484
);
485+
486+
expect(response.session, isNull);
485487
});
486488
});
487489

0 commit comments

Comments
 (0)