diff --git a/packages/apptive_grid_core/CHANGELOG.md b/packages/apptive_grid_core/CHANGELOG.md index a7c7caae..490efb9f 100644 --- a/packages/apptive_grid_core/CHANGELOG.md +++ b/packages/apptive_grid_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.1 + + - **FIX**: A saved token whose refresh fails for want of a connection no longer falls through to `authenticate()`. Only the auth server rejecting the credential (`OpenIdException`) leads to the login; any other failure keeps the credential and propagates from `checkAuthentication`, so an app that is merely offline can answer from its cache instead of sending the user to a login page they cannot load. `performSetup` completes either way, so `isAuthenticated` never hangs on a failed restore. + ## 2.3.0 - **FEAT**: Add `Address` DataType, `Address` model and `AddressDataEntity`. diff --git a/packages/apptive_grid_core/lib/src/network/authentication/apptive_grid_authenticator.dart b/packages/apptive_grid_core/lib/src/network/authentication/apptive_grid_authenticator.dart index a92f8390..a04cf71d 100644 --- a/packages/apptive_grid_core/lib/src/network/authentication/apptive_grid_authenticator.dart +++ b/packages/apptive_grid_core/lib/src/network/authentication/apptive_grid_authenticator.dart @@ -55,8 +55,16 @@ class ApptiveGridAuthenticator { if (client.options.authenticationOptions.persistCredentials) { _authenticationStorage ??= const FlutterSecureStorageCredentialStorage(); - await checkAuthentication(requestNewToken: false) - .then((_) => _setupCompleter.complete()); + try { + await checkAuthentication(requestNewToken: false); + } catch (error) { + // Restoring the session failed for want of a connection — a + // rejected credential is handled inside and never thrown. Start + // without a token; the next authenticated request tries again. + debugPrint('Could not restore the session: $error'); + } finally { + _setupCompleter.complete(); + } } else { _setupCompleter.complete(); } @@ -342,11 +350,17 @@ class ApptiveGridAuthenticator { setToken(token); return; } on OpenIdException catch (openIdError) { + // The auth server turned the credential down: the session is + // over, and a login is the only way on. setCredential(null); debugPrint('Could not refresh saved token: $openIdError'); - } catch (error) { - debugPrint('Error refreshing token: $error'); } + // Anything else — no connection, a timeout, a server having a + // bad day — says nothing about the session. The credential stays + // for the next attempt and the failure propagates, instead of + // falling through to a login: sending someone who is merely + // offline to a login page they cannot load helps nobody, and the + // caller may well have a cached answer to fall back on. } else { setCredential(null); } diff --git a/packages/apptive_grid_core/pubspec.yaml b/packages/apptive_grid_core/pubspec.yaml index 06b9b1a1..e4ba0900 100644 --- a/packages/apptive_grid_core/pubspec.yaml +++ b/packages/apptive_grid_core/pubspec.yaml @@ -1,6 +1,6 @@ name: apptive_grid_core description: Core Library for ApptiveGrid used to provide general ApptiveGrid functionality to other Packages or Apps -version: 2.3.0 +version: 2.3.1 homepage: https://www.apptivegrid.de repository: https://github.com/ApptiveGrid/apptive_grid_flutter/tree/main/packages/apptive_grid_core diff --git a/packages/apptive_grid_core/test/authenticator_test.dart b/packages/apptive_grid_core/test/authenticator_test.dart index 33c181ee..e6539baf 100644 --- a/packages/apptive_grid_core/test/authenticator_test.dart +++ b/packages/apptive_grid_core/test/authenticator_test.dart @@ -1060,8 +1060,12 @@ void main() { test( 'Storage Provided ' - 'Error ' - 'Calls authenticate', () async { + 'Connection error while refreshing ' + 'Keeps the credential, throws, does not log in', () async { + // No connection is not "no session". Sending the user to a login page + // they cannot load — over an app that could answer from its cache — + // was the old behaviour. Now the failure surfaces and the credential + // stays for the next attempt. final tokenTime = DateTime.now(); final httpClient = MockHttpClient(); final authClient = MockAuthClient(); @@ -1134,10 +1138,77 @@ void main() { when(() => client.issuer).thenReturn(zweidenkerIssuer); when(() => client.clientId).thenReturn('clientId'); - await authenticator.checkAuthentication(); + await expectLater(authenticator.checkAuthentication(), throwsException); - verify(testAuthenticator.authorize).called(1); - expect(await authenticator.isAuthenticated, equals(true)); + verifyNever(testAuthenticator.authorize); + verifyNever(() => storage.saveCredential(null)); + expect(await authenticator.isAuthenticatedWithToken, isFalse); + }); + + test( + 'Storage Provided ' + 'Connection error while restoring at setup ' + 'Completes setup without a token', () async { + // The constructor restores the session silently. When that fails for + // want of a connection the authenticator must still finish setting + // up, or isAuthenticated would wait forever. + final tokenTime = DateTime.now(); + final httpClient = MockHttpClient(); + final authClient = MockAuthClient(); + when(() => authClient.issuer).thenReturn(zweidenkerIssuer); + when(() => authClient.clientSecret).thenReturn(''); + when(() => authClient.clientId).thenReturn('test'); + when(() => authClient.httpClient).thenReturn(httpClient); + + final tokenResponse = TokenResponse.fromJson({ + 'token_type': 'Bearer', + 'access_token': '12345', + 'expires_at': tokenTime.millisecondsSinceEpoch, + 'expires_in': tokenTime.microsecondsSinceEpoch, + }); + final credential = Credential.fromJson({ + 'issuer': authClient.issuer.metadata.toJson(), + 'client_id': authClient.clientId, + 'client_secret': authClient.clientSecret, + 'token': tokenResponse.toJson(), + 'nonce': null, + }); + when( + () => httpClient.post( + any(), + body: any(named: 'body'), + headers: any(named: 'headers'), + encoding: any(named: 'encoding'), + ), + ).thenAnswer( + (invocation) => Future.error(Exception('Network is unreachable')), + ); + + final storage = MockAuthenticationStorage(); + when(() => storage.credential) + .thenAnswer((invocation) => jsonEncode(credential.toJson())); + final agClient = MockApptiveGridClient(); + when(() => agClient.options).thenReturn( + const ApptiveGridOptions( + authenticationOptions: ApptiveGridAuthenticationOptions( + autoAuthenticate: true, + persistCredentials: true, + ), + ), + ); + final testAuthenticator = MockAuthenticator(); + + authenticator = ApptiveGridAuthenticator( + client: agClient, + authenticationStorage: storage, + httpClient: httpClient, + ); + authenticator.testAuthenticator = testAuthenticator; + authenticator.setAuthClient(authClient); + + expect(await authenticator.isAuthenticatedWithToken, isFalse); + verifyNever(testAuthenticator.authorize); + verifyNever(() => storage.saveCredential(null)); }); test(