Skip to content

Commit 0593aab

Browse files
Handle invalid token after password reset (#1238)
* Handle MSAL authentication after password reset * Adjust unit tests * [BOT] Applying version. * [BOT] Applying format. * Add AcquireTokenWithCacheReset stub * Simplify getToken * Limit startup viewmodel acquireToken to 3 tries * Simplify getToken function * [BOT] Applying format. * Display error message toast if startup acquire token fails * [BOT] Applying format. * Display toast message after token fail * Remove unused import * Rename tranlations * [BOT] Applying format. * Change translation initialization * Remove unused import * [BOT] Applying format. * Fix failing test --------- Co-authored-by: clubapplets-server <1958869+clubapplets-server@users.noreply.github.com>
1 parent c02fd3f commit 0593aab

File tree

10 files changed

+93
-22
lines changed

10 files changed

+93
-22
lines changed

lib/data/services/auth_service.dart

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import 'package:notredame/locator.dart';
99
class AuthService {
1010
String? _token;
1111
final int _maxRetry = 3;
12-
int _retries = 0;
1312

1413
final _scopes = ['api://etsmobileapi/access_as_user'];
1514

@@ -18,21 +17,21 @@ class AuthService {
1817
final Logger _logger = locator<Logger>();
1918

2019
Future<String> getToken() async {
21-
if (_token == null) {
20+
int attempt = 0;
21+
22+
while (attempt <= _maxRetry) {
23+
if (_token != null) return _token!;
24+
2225
final result = await acquireTokenSilent();
2326
if (result.$1 != null) {
24-
_token = result.$1?.accessToken;
25-
} else {
26-
_retries++;
27-
if (_retries > _maxRetry) {
28-
_retries = 0;
29-
throw Exception('Max retries reached');
30-
}
31-
getToken();
27+
_token = result.$1!.accessToken;
28+
return _token!;
3229
}
30+
31+
attempt++;
3332
}
34-
_retries = 0;
35-
return _token!;
33+
34+
throw Exception('Max retries reached');
3635
}
3736

3837
Future<(bool, MsalException?)> createPublicClientApplication({
@@ -65,7 +64,11 @@ class AuthService {
6564

6665
Future<(AuthenticationResult?, MsalException?)> acquireToken({String? loginHint}) async {
6766
try {
68-
final result = await singleAccountPca?.acquireToken(scopes: _scopes, loginHint: loginHint, prompt: Prompt.login);
67+
final result = await singleAccountPca?.acquireToken(
68+
scopes: _scopes,
69+
loginHint: loginHint,
70+
prompt: Prompt.selectAccount,
71+
);
6972
_token = result?.accessToken;
7073
_logger.d('Acquire token => ${result?.toJson()}');
7174
return (result, null);
@@ -84,14 +87,30 @@ class AuthService {
8487
} on MsalException catch (e) {
8588
_logger.e('Acquire token silent failed => $e');
8689

87-
// If it is a UI required exception, try to acquire token interactively.
8890
if (e is MsalUiRequiredException) {
89-
return acquireToken();
91+
return await acquireTokenWithCacheReset();
9092
}
9193
return (null, e);
9294
}
9395
}
9496

97+
Future<(AuthenticationResult?, MsalException?)> acquireTokenWithCacheReset() async {
98+
try {
99+
_token = null;
100+
101+
await signOut();
102+
103+
final result = await singleAccountPca?.acquireToken(scopes: _scopes, prompt: Prompt.login);
104+
105+
_token = result?.accessToken;
106+
_logger.d('Token acquired with cache reset => ${result?.toJson()}');
107+
return (result, null);
108+
} on MsalException catch (e) {
109+
_logger.e('Token acquisition with cache reset failed => $e');
110+
return (null, e);
111+
}
112+
}
113+
95114
Future<(bool, MsalException?)> signOut() async {
96115
try {
97116
_token = null;

lib/data/services/signets-api/request_builder_service.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ mixin RequestBuilderService {
3232
String resultTag, {
3333
Map<String, String>? queryParameters,
3434
}) async {
35-
// Send the envelope
3635
final uri = Uri.https(Urls.signetsAPI, endpoint, queryParameters);
3736
final response = await client.get(uri, headers: _buildHeaders(token));
3837

@@ -42,8 +41,15 @@ mixin RequestBuilderService {
4241
retries = 0;
4342
throw ApiException(prefix: tagError, message: "Token invalide. Veuillez vous déconnecter et vous reconnecter.");
4443
}
44+
4545
final authService = locator<AuthService>();
46-
await authService.acquireTokenSilent();
46+
final tokenResult = await authService.acquireTokenSilent();
47+
48+
if (tokenResult.$1 == null) {
49+
retries = 0;
50+
throw ApiException(prefix: tagError, message: "Session expirée. Veuillez vous reconnecter.");
51+
}
52+
4753
return await sendRequest(
4854
client,
4955
endpoint,

lib/l10n/intl_en.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646

4747
"close_button_text": "Close",
4848

49+
"startup_viewmodel_acquire_token_fail": "Connection failed after multiple attempts. Please try again.",
50+
4951
"title_schedule": "Schedule",
5052
"title_student": "Student",
5153
"title_dashboard": "Dashboard",

lib/l10n/intl_fr.arb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545

4646
"close_button_text": "Fermer",
4747

48+
"startup_viewmodel_acquire_token_fail": "Échec de la connexion après plusieurs tentatives. Veuillez réessayer.",
49+
4850
"title_schedule": "Horaire",
4951
"title_student": "Étudiant",
5052
"title_dashboard": "Accueil",

lib/ui/startup/view_model/startup_viewmodel.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Package imports:
2+
import 'package:fluttertoast/fluttertoast.dart';
23
import 'package:msal_auth/msal_auth.dart';
34
import 'package:stacked/stacked.dart';
45

@@ -10,6 +11,7 @@ import 'package:notredame/data/services/navigation_service.dart';
1011
import 'package:notredame/data/services/networking_service.dart';
1112
import 'package:notredame/domain/constants/preferences_flags.dart';
1213
import 'package:notredame/domain/constants/router_paths.dart';
14+
import 'package:notredame/l10n/app_localizations.dart';
1315
import 'package:notredame/locator.dart';
1416

1517
class StartUpViewModel extends BaseViewModel {
@@ -20,6 +22,9 @@ class StartUpViewModel extends BaseViewModel {
2022
final NavigationService _navigationService = locator<NavigationService>();
2123
final AnalyticsService _analyticsService = locator<AnalyticsService>();
2224

25+
final AppIntl intl;
26+
StartUpViewModel({required this.intl});
27+
2328
/// Try to silent authenticate the user then redirect to [LoginView] or [DashboardView]
2429
Future handleStartUp() async {
2530
if (await handleConnectivityIssues()) return;
@@ -47,9 +52,19 @@ class StartUpViewModel extends BaseViewModel {
4752
_navigationService.pushNamedAndRemoveUntil(RouterPaths.dashboard);
4853
} else {
4954
AuthenticationResult? token;
50-
while (token == null) {
55+
int attempts = 0;
56+
const maxAttempts = 3;
57+
58+
while (token == null && attempts < maxAttempts) {
59+
attempts++;
5160
token = (await _authService.acquireToken()).$1;
61+
if (token == null && attempts >= maxAttempts) {
62+
Fluttertoast.showToast(msg: intl.startup_viewmodel_acquire_token_fail, toastLength: Toast.LENGTH_LONG);
63+
await _analyticsService.logError('StartupViewmodel', 'Failed to acquire token after $maxAttempts attempts');
64+
return;
65+
}
5266
}
67+
5368
_settingsManager.setBool(PreferencesFlag.isLoggedIn, true);
5469
_navigationService.pushNamedAndRemoveUntil(RouterPaths.dashboard);
5570
}

lib/ui/startup/widgets/startup_view.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart';
66
import 'package:stacked/stacked.dart';
77

88
// Project imports:
9+
import 'package:notredame/l10n/app_localizations.dart';
910
import 'package:notredame/ui/core/themes/app_palette.dart';
1011
import 'package:notredame/ui/core/themes/app_theme.dart';
1112
import 'package:notredame/ui/startup/view_model/startup_viewmodel.dart';
@@ -15,7 +16,7 @@ class StartUpView extends StatelessWidget {
1516

1617
@override
1718
Widget build(BuildContext context) => ViewModelBuilder<StartUpViewModel>.nonReactive(
18-
viewModelBuilder: () => StartUpViewModel(),
19+
viewModelBuilder: () => StartUpViewModel(intl: AppIntl.of(context)!),
1920
onViewModelReady: (StartUpViewModel model) {
2021
model.handleStartUp();
2122
},

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco
55
# pub.dev using `pub publish`. This is preferred for private packages.
66
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
77

8-
version: 4.59.7
8+
version: 4.59.8
99

1010
environment:
1111
sdk: '>=3.8.0 <4.0.0'

test/data/mocks/services/auth_service_mock.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ class AuthServiceMock extends MockAuthService {
5757
when(mock.acquireToken()).thenAnswer((_) async => (result, exception));
5858
}
5959

60+
static void stubAcquireTokenWithCacheReset(AuthServiceMock mock, {bool success = true}) {
61+
AuthenticationResult? result;
62+
MsalException? exception;
63+
64+
if (success) {
65+
result = AuthenticationResult(
66+
accessToken: '',
67+
authenticationScheme: '',
68+
expiresOn: DateTime.now(),
69+
idToken: '',
70+
authority: '',
71+
tenantId: '',
72+
scopes: [''],
73+
correlationId: '',
74+
account: Account(id: '', username: '', name: ''),
75+
);
76+
} else {
77+
exception = MsalException(message: 'Error');
78+
}
79+
80+
when(mock.acquireTokenWithCacheReset()).thenAnswer((_) async => (result, exception));
81+
}
82+
6083
static void stubSignOut(AuthServiceMock mock) {
6184
when(mock.signOut()).thenAnswer((_) async => (true, null));
6285
}

test/data/services/signets_api/signets_api_client_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ void main() {
237237
);
238238
} catch (e) {
239239
expect(e, isA<ApiException>());
240-
verify(authServiceMock.acquireTokenSilent()).called(3);
240+
verify(authServiceMock.acquireTokenSilent()).called(1);
241241
}
242242
});
243243
});

test/ui/startup/view_model/startup_viewmodel_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:notredame/data/services/navigation_service.dart';
1111
import 'package:notredame/data/services/networking_service.dart';
1212
import 'package:notredame/domain/constants/preferences_flags.dart';
1313
import 'package:notredame/domain/constants/router_paths.dart';
14+
import 'package:notredame/l10n/app_localizations.dart';
1415
import 'package:notredame/ui/startup/view_model/startup_viewmodel.dart';
1516
import '../../../data/mocks/repositories/settings_repository_mock.dart';
1617
import '../../../data/mocks/services/auth_service_mock.dart';
@@ -25,6 +26,7 @@ void main() {
2526
late AuthServiceMock authServiceMock;
2627

2728
late StartUpViewModel viewModel;
29+
late AppIntl appIntl;
2830

2931
group('StartupViewModel - ', () {
3032
setUp(() async {
@@ -34,7 +36,8 @@ void main() {
3436
networkingServiceMock = setupNetworkingServiceMock();
3537
authServiceMock = setupAuthServiceMock();
3638

37-
viewModel = StartUpViewModel();
39+
appIntl = await setupAppIntl();
40+
viewModel = StartUpViewModel(intl: appIntl);
3841
});
3942

4043
tearDown(() {

0 commit comments

Comments
 (0)