Skip to content

Commit

Permalink
Merge branch 'v4.2.1' into v4.2.1-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
frankmer committed Oct 20, 2023
2 parents 793936e + 5036335 commit a922871
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 151 deletions.
64 changes: 29 additions & 35 deletions lib/state_notifiers/token_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,6 @@ class TokenNotifier extends StateNotifier<TokenState> {
);
state = state.addOrReplaceTokens(failedTokens);
}
for (var newToken in tokens) {
if (newToken is PushToken && !newToken.isRolledOut && !failedTokens.contains(newToken)) rolloutPushToken(newToken);
}
});
}

Expand All @@ -94,15 +91,9 @@ class TokenNotifier extends StateNotifier<TokenState> {
try {
isLoading = Future(() async {
tokens = await _repo.loadTokens();
final pushTokens = tokens.whereType<PushToken>();
if (pushTokens.isNotEmpty) {
checkNotificationPermission();
}

final pushTokensNotRolledOut = pushTokens.where((element) => !element.isRolledOut).toList();
state = TokenState(tokens: tokens);
for (final pushToken in pushTokensNotRolledOut) {
rolloutPushToken(pushToken);
if (state.pushTokens.firstWhereOrNull((element) => element.isRolledOut == true) != null) {
checkNotificationPermission();
}
});
} catch (_) {
Expand Down Expand Up @@ -191,11 +182,7 @@ class TokenNotifier extends StateNotifier<TokenState> {
void addTokenFromOtpAuth({
required String otpAuth,
}) async {
Logger.info(
'Try to handle otpAuth:',
name: 'token_notifier.dart#addTokenFromOtpAuth',
error: otpAuth,
);
Logger.info('Try to handle otpAuth:', name: 'token_notifier.dart#addTokenFromOtpAuth');

try {
Map<String, dynamic> uriMap = _qrParser.parseQRCodeToMap(otpAuth);
Expand Down Expand Up @@ -227,6 +214,9 @@ class TokenNotifier extends StateNotifier<TokenState> {
return;
}
addOrReplaceToken(newToken);
if (newToken is PushToken) {
rolloutPushToken(newToken);
}

return;
} on ArgumentError catch (e, s) {
Expand Down Expand Up @@ -306,18 +296,18 @@ class TokenNotifier extends StateNotifier<TokenState> {
Future<bool> rolloutPushToken(PushToken token) async {
token = (getTokenFromId(token.id)) as PushToken? ?? token;
assert(token.url != null, 'Token url is null. Cannot rollout token without url.');
Logger.info('Rolling out token "${token.id}"', name: 'token_widgets.dart#rolloutPushToken');
Logger.info('Rolling out token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken');
if (token.isRolledOut) return true;
if (token.rolloutState != PushTokenRollOutState.rolloutNotStarted &&
token.rolloutState != PushTokenRollOutState.generatingRSAKeyPairFailed &&
token.rolloutState != PushTokenRollOutState.sendRSAPublicKeyFailed &&
token.rolloutState != PushTokenRollOutState.parsingResponseFailed) {
Logger.info('Ignoring rollout request: Rollout of token "${token.id}" already started. Tokenstate: ${token.rolloutState} ',
name: 'token_widgets.dart#rolloutPushToken');
name: 'token_notifier.dart#rolloutPushToken');
return false;
}
if (token.expirationDate?.isBefore(DateTime.now()) == true) {
Logger.info('Ignoring rollout request: Token "${token.id}" is expired. ', name: 'token_widgets.dart#rolloutPushToken');
Logger.info('Ignoring rollout request: Token "${token.id}" is expired. ', name: 'token_notifier.dart#rolloutPushToken');
if (globalNavigatorKey.currentContext != null) {
showMessage(
message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutTokenExpired(token.label),
Expand All @@ -341,10 +331,9 @@ class TokenNotifier extends StateNotifier<TokenState> {
p0 = p0.withPrivateTokenKey(keyPair.privateKey);
return p0.withPublicTokenKey(keyPair.publicKey);
});
Logger.info('Updated token "${token.id}"', name: 'token_widgets.dart#rolloutPushToken');
checkNotificationPermission();
Logger.info('Updated token "${token.id}"', name: 'token_notifier.dart#rolloutPushToken');
} catch (e, s) {
Logger.error('Error while generating RSA key pair.', name: 'token_widgets.dart#rolloutPushToken', error: e, stackTrace: s);
Logger.error('Error while generating RSA key pair.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s);
updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.generatingRSAKeyPairFailed));
return false;
}
Expand All @@ -371,7 +360,7 @@ class TokenNotifier extends StateNotifier<TokenState> {
token = token.withPublicServerKey(publicServerKey);
} on FormatException catch (e, s) {
showMessage(message: "Couldn't parsing RSA public key: ${e.message}", duration: const Duration(seconds: 3));
Logger.warning('Error while parsing RSA public key.', name: 'token_widgets.dart#rolloutPushToken', error: e, stackTrace: s);
Logger.warning('Error while parsing RSA public key.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s);
updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.parsingResponseFailed));
return false;
}
Expand All @@ -381,33 +370,38 @@ class TokenNotifier extends StateNotifier<TokenState> {
return true;
} else {
Logger.warning('Post request on roll out failed.',
name: 'token_widgets.dart#rolloutPushToken',
name: 'token_notifier.dart#rolloutPushToken',
error: 'Token: ${token.serial}\nStatus code: ${response.statusCode},\nURL:${response.request?.url}\nBody: ${response.body}');

String? message;
try {
message = response.body.isNotEmpty ? (json.decode(response.body)['result']?['error']?['message']) : null;
message = message != null ? '\n$message' : '';
showMessage(
message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutFailed(token.label, response.statusCode) + message,
duration: const Duration(seconds: 3),
);
} on FormatException catch (_) {
message = AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutNoConnectionToServer(token.label);
// Format Exception is thrown if the response body is not a valid json. This happens if the server is not reachable.
showMessage(
message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutNoConnectionToServer(token.label),
duration: const Duration(seconds: 3),
);
}
message = message != null ? '\n$message' : '';
showMessage(
message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutFailed(token.label, response.statusCode) + message,
duration: const Duration(seconds: 3),
);

updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed));
return false;
}
} catch (e, s) {
if (e is PlatformException && e.code == FIREBASE_TOKEN_ERROR_CODE || e is SocketException || e is TimeoutException || e is FirebaseException) {
Logger.warning('Connection error: Roll out push token failed.', name: 'token_widgets.dart#rolloutPushToken', error: e, stackTrace: s);
Logger.warning('Connection error: Roll out push token failed.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s);
showMessage(
message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutNoConnectionToServer(token.label),
duration: const Duration(seconds: 3),
);
updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed));
} else if (e is HandshakeException) {
Logger.warning('SSL error: Roll out push token failed.', name: 'token_widgets.dart#rolloutPushToken', error: e, stackTrace: s);
Logger.warning('SSL error: Roll out push token failed.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s);
showMessage(
message: AppLocalizations.of(globalNavigatorKey.currentContext!)!.errorRollOutSSLHandshakeFailed,
duration: const Duration(seconds: 3),
Expand All @@ -420,20 +414,20 @@ class TokenNotifier extends StateNotifier<TokenState> {
duration: const Duration(seconds: 3),
);
}
Logger.error('Roll out push token failed.', name: 'token_widgets.dart#rolloutPushToken', error: e, stackTrace: s);
Logger.error('Roll out push token failed.', name: 'token_notifier.dart#rolloutPushToken', error: e, stackTrace: s);
updateToken(token, (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed));
}
return false;
}
}

Future<RSAPublicKey> _parseRollOutResponse(Response response) async {
Logger.info('Parsing rollout response, try to extract public_key.', name: 'token_widgets.dart#_parseRollOutResponse');
Logger.info('Parsing rollout response, try to extract public_key.', name: 'token_notifier.dart#_parseRollOutResponse');
try {
String key = json.decode(response.body)['detail']['public_key'];
key = key.replaceAll('\n', '');

Logger.info('Extracting public key was successful.', name: 'token_widgets.dart#_parseRollOutResponse', error: key);
Logger.info('Extracting public key was successful.', name: 'token_notifier.dart#_parseRollOutResponse', error: key);

return _rsaUtils.deserializeRSAPublicKeyPKCS1(key);
} on FormatException catch (e) {
Expand Down
14 changes: 3 additions & 11 deletions lib/utils/firebase_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class FirebaseUtils {
Future<void> initFirebase({
required Future<void> Function(RemoteMessage) foregroundHandler,
required Future<void> Function(RemoteMessage) backgroundHandler,
required void Function(String?) updateFirebaseToken,
required dynamic Function(String?) updateFirebaseToken,
}) async {
if (_initialized) {
return;
Expand All @@ -31,7 +31,7 @@ class FirebaseUtils {
await Firebase.initializeApp();

try {
await FirebaseMessaging.instance.requestPermission();
// await FirebaseMessaging.instance.requestPermission();
} on FirebaseException catch (e, s) {
Logger.warning(
'e.code: ${e.code}, '
Expand All @@ -55,7 +55,7 @@ class FirebaseUtils {
try {
String? firebaseToken = await getFBToken();

if (firebaseToken != await SecureTokenRepository.getCurrentFirebaseToken()) {
if (firebaseToken != await SecureTokenRepository.getCurrentFirebaseToken() && firebaseToken != null) {
updateFirebaseToken(firebaseToken);
}
} on PlatformException catch (error) {
Expand Down Expand Up @@ -117,15 +117,7 @@ class FirebaseUtils {
firebaseToken = await FirebaseMessaging.instance.getToken();
} on FirebaseException catch (e, s) {
String errorMessage = e.message ?? 'no error message';
final SnackBar snackBar = SnackBar(
content: Text(
"Unable to retrieve Firebase token! ($errorMessage: ${e.code})",
overflow: TextOverflow.fade,
softWrap: false,
),
);
Logger.warning('Unable to retrieve Firebase token! ($errorMessage: ${e.code})', name: 'push_provider.dart#getFBToken', error: e, stackTrace: s);
globalSnackbarKey.currentState?.showSnackBar(snackBar);
}

// Fall back to the last known firebase token
Expand Down
13 changes: 8 additions & 5 deletions lib/utils/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,26 @@ class Logger {
/*----------- LOGGING METHODS -----------*/

static void info(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) {
final infoString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.INFO);
String infoString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.INFO);
infoString = _textFilter(infoString);
if (instance._verbose || verbose) {
instance._logToFile(infoString);
}
_print(infoString);
}

static void warning(String message, {dynamic error, dynamic stackTrace, String? name, bool verbose = false}) {
final warningString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.WARNING);
String warningString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.WARNING);
warningString = _textFilter(warningString);
if (instance._verbose || verbose) {
instance._logToFile(warningString);
}
_printWarning(warningString);
}

static void error(String? message, {required dynamic error, required dynamic stackTrace, String? name}) {
final errorString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.ERROR);
String errorString = instance._convertLogToSingleString(message, error: error, stackTrace: stackTrace, name: name, logLevel: LogLevel.ERROR);
errorString = _textFilter(errorString);
if (message != null) {
instance._lastError = message.substring(0, min(message.length, 100));
} else if (error != null) {
Expand Down Expand Up @@ -333,10 +336,10 @@ class Logger {

/*----------- HELPER -----------*/

String _textFilter(String text) {
static String _textFilter(String text) {
for (var key in filterParameterKeys) {
final regex = RegExp(r'(?<=' + key + r':\s).+?(?=[},])');
text = text.replaceAll(regex, '***');
text = text.replaceAll(regex, '******');
}
return text;
}
Expand Down
70 changes: 38 additions & 32 deletions lib/utils/push_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,21 @@ import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:privacyidea_authenticator/l10n/app_localizations.dart';
import 'package:privacyidea_authenticator/model/push_request.dart';
import 'package:privacyidea_authenticator/model/tokens/push_token.dart';
import 'package:privacyidea_authenticator/repo/secure_token_repository.dart';
import 'package:privacyidea_authenticator/state_notifiers/push_request_notifier.dart';
import 'package:privacyidea_authenticator/utils/customizations.dart';
import 'package:privacyidea_authenticator/utils/firebase_utils.dart';
import 'package:privacyidea_authenticator/utils/riverpod_providers.dart';
import 'package:privacyidea_authenticator/utils/rsa_utils.dart';
import 'package:privacyidea_authenticator/utils/utils.dart';
import 'package:privacyidea_authenticator/utils/view_utils.dart';

import '../l10n/app_localizations.dart';
import '../model/push_request.dart';
import '../model/tokens/push_token.dart';
import '../repo/secure_token_repository.dart';
import '../state_notifiers/push_request_notifier.dart';
import 'customizations.dart';
import 'firebase_utils.dart';
import 'logger.dart';
import 'network_utils.dart';
import 'riverpod_providers.dart';
import 'rsa_utils.dart';
import 'utils.dart';
import 'view_utils.dart';

/// This class bundles all logic that is needed to handle incomig PushRequests, e.g.,
/// firebase, polling, notifications.
Expand All @@ -63,7 +62,7 @@ class PushProvider {
await firebaseUtils.initFirebase(
foregroundHandler: _foregroundHandler,
backgroundHandler: _backgroundHandler,
updateFirebaseToken: _updateFirebaseToken,
updateFirebaseToken: updateFirebaseToken,
);
}

Expand Down Expand Up @@ -263,6 +262,7 @@ class PushProvider {
pollForChallenge(p).then((errorMessage) {
if (errorMessage != null && showMessageForEachToken) {
Logger.warning(errorMessage, name: 'push_provider.dart#pollForChallenges');
// TODO: Improve error message
showMessage(message: errorMessage);
}
});
Expand Down Expand Up @@ -327,39 +327,44 @@ class PushProvider {

if (firebaseToken != null && (await SecureTokenRepository.getCurrentFirebaseToken()) != firebaseToken) {
try {
_updateFirebaseToken(firebaseToken);
} catch (error) {
final SnackBar snackBar = SnackBar(
content: Text(
"Unknown error: $error",
overflow: TextOverflow.fade,
softWrap: false,
),
);
globalSnackbarKey.currentState?.showSnackBar(snackBar);
await updateFirebaseToken(firebaseToken);
} catch (error, stackTrace) {
Logger.error('Could not update firebase token.', name: 'push_provider.dart#updateFbTokenIfChanged', error: error, stackTrace: stackTrace);
}
}
}

/// This method attempts to update the fbToken for all PushTokens that can be
/// updated. I.e. all tokens that know the url of their respective privacyIDEA
/// server. If the update fails for one or all tokens, this method does *not*
/// give any feedback!.
/// server.
/// If the fbToken is not provided, it will be fetched from the firebase instance.
/// If the fbToken is not available, this method will return null.
/// Returns a tuple of two lists. The first list contains all tokens that
/// could not be updated. The second list contains all tokens that do not
/// support updating the fbToken.
///
/// This should only be used to attempt to update the fbToken automatically,
/// as this can not be guaranteed to work. There is a manual option available
/// through the settings also.
static void _updateFirebaseToken(String? firebaseToken) async {
static Future<(List<PushToken>, List<PushToken>)?> updateFirebaseToken([String? firebaseToken]) async {
firebaseToken ??= await instance?.firebaseUtils?.getFBToken();
if (firebaseToken == null) {
// Nothing to update here!
return;
Logger.warning('Could not update firebase token because no firebase token is available.', name: 'push_provider.dart#_updateFirebaseToken');
return null;
}

List<PushToken> tokenList = (await const SecureTokenRepository().loadTokens()).whereType<PushToken>().where((t) => t.url != null).toList();

bool allUpdated = true;

final List<PushToken> failedTokens = [];
final List<PushToken> unsuportedTokens = [];

for (PushToken p in tokenList) {
if (p.url == null) {
unsuportedTokens.add(p);
continue;
}
// POST /ttype/push HTTP/1.1
//Host: example.com
//
Expand All @@ -369,13 +374,12 @@ class PushProvider {
//signature=SIGNATURE(<new firebase token>|<tokenserial>|<timestamp>)

String timestamp = DateTime.now().toUtc().toIso8601String();

String message = '$firebaseToken|${p.serial}|$timestamp';
// Because no context is available, trySignWithToken will fail without feedback for the user
// Just like this whole function // TODO improve that?
String? signature = await const RsaUtils().trySignWithToken(p, message);
if (signature == null) {
return;
failedTokens.add(p);
allUpdated = false;
continue;
}
Response response = instance != null
? await instance!._ioClient.doPost(
Expand All @@ -387,12 +391,14 @@ class PushProvider {
Logger.info('Updating firebase token for push token succeeded!', name: 'push_provider.dart#_updateFirebaseToken');
} else {
Logger.warning('Updating firebase token for push token failed!', name: 'push_provider.dart#_updateFirebaseToken');
failedTokens.add(p);
allUpdated = false;
}
}

if (allUpdated) {
SecureTokenRepository.setCurrentFirebaseToken(firebaseToken);
}
return (failedTokens, unsuportedTokens);
}
}
Loading

0 comments on commit a922871

Please sign in to comment.