Skip to content

Instantly share code, notes, and snippets.

@dominicmh
Created January 19, 2022 06:44
Show Gist options
  • Save dominicmh/8888cfc2e078bf5196d29b0c6140eae0 to your computer and use it in GitHub Desktop.
Save dominicmh/8888cfc2e078bf5196d29b0c6140eae0 to your computer and use it in GitHub Desktop.
class IosAppVerification implements MfaAppVerification {
final _client = GipClient();
@override
Future<StartMfaPhoneRequestInfo> createMfaRequestInfo(
String phoneNumber) async {
final futureAppVerificationInfo = _interceptAppVerificationNotification();
final apnsToken = await _getApnsToken();
final response = await _client.verifyIosClient(apnsToken);
final notificationTimeLimit = Duration(
seconds: int.tryParse(response.json?['suggestedTimeout']) ?? 5);
final appVerificationInfo = await futureAppVerificationInfo.timeout(
notificationTimeLimit,
onTimeout: () => throw TimeoutException(
"The expected app verification notification wasn't received within time limit. Possibly, due to disabled background refresh in the user's device settings. Would require reCAPTCHA verification for MFA."),
);
return StartMfaPhoneRequestInfo.ios(
phoneNumber: phoneNumber,
receipt: appVerificationInfo.receipt,
secret: appVerificationInfo.secret,
);
}
Future<String> _getApnsToken() async {
final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
if (apnsToken == null) {
throw Exception(
"APNS Token was null. Possibly, it was unintendedly requested on Android.");
} else {
return apnsToken;
}
}
Future<_AppVerificationInfo> _interceptAppVerificationNotification() async =>
await FirebaseMessaging.onMessage
.map((message) => _extractAppVerificationInfo(message))
.where((appVerificationInfo) => appVerificationInfo != null)
.cast<_AppVerificationInfo>() // requires prior null check
.first;
static _AppVerificationInfo? _extractAppVerificationInfo(
RemoteMessage message) {
print("Parsing push notification: ${message.data}");
try {
final verificationInfoData =
json.decode(message.data['com.google.firebase.auth']);
return _AppVerificationInfo(
receipt: verificationInfoData['receipt']!,
secret: verificationInfoData['secret']!,
);
} catch (e) {
print(
"Received notification wasn't intended for app verification and is ignored. ${e.toString()}");
return null;
}
}
}
class _AppVerificationInfo {
final String receipt;
final String secret;
_AppVerificationInfo({required this.receipt, required this.secret});
}
class GipClient {
// ref. https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/verifyIosClient
Future<ApiResponse> verifyIosClient(String apnsToken) {
const method = 'accounts:verifyIosClient';
final body = <String, dynamic>{
'appToken': apnsToken,
'isSandbox': true, // TODO: Handle properly
};
return _post(
method: method,
body: body,
pathPrefix: '/v1',
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment