First of all I would like to notice that I am not experienced iOS developer, I am writing app in React Native.
The essence of the problem:
If my app was swiped away and push notification from FCM arrived, I would like to send network request in response to user action in notification, but it's failed. The code doesn't even send request, because logs on server side doesn't exist. In the meantime everything works fine in case when app was rolled up or in the foreground state.
Details:
I test current functionality on iOS 10.3.3.
Here is the code:
AppDelegate.m:
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
@import Firebase;
#import "FirebaseMessagingModule.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
[FIRApp configure];
FirebaseMessagingModule* firebaseMessagingModule = [bridge moduleForClass:FirebaseMessagingModule.class];
[FIRMessaging messaging].delegate = firebaseMessagingModule;
/*
Notifications require some setup
*/
if ([UNUserNotificationCenter class] != nil) {
// iOS 10 or later
// For iOS 10 display notification (sent via APNS)
[UNUserNotificationCenter currentNotificationCenter].delegate = firebaseMessagingModule;
UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert |
UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
[[UNUserNotificationCenter currentNotificationCenter]
requestAuthorizationWithOptions:authOptions
completionHandler:^(BOOL granted, NSError * _Nullable error) {
// ...
}];
} else {
// iOS 10 notifications aren't available; fall back to iOS 8-9 notifications.
UIUserNotificationType allNotificationTypes =
(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge);
UIUserNotificationSettings *settings =
[UIUserNotificationSettings settingsForTypes:allNotificationTypes categories:nil];
[application registerUserNotificationSettings:settings];
}
[application registerForRemoteNotifications];
[firebaseMessagingModule register2FACategory];
return YES;
}
...
@end
FirebaseMessagingModule.h:
//
// FirebaseMessagingModule.h
// bastionpassmobile
//
// Created by Anotn on 25/05/2020.
// Copyright © 2020 Facebook. All rights reserved.
//
#ifndef FirebaseMessagingModule_h
#define FirebaseMessagingModule_h
#endif /* FirebaseMessagingModule_h */
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@import Firebase;
@interface FirebaseMessagingModule : NSObject <RCTBridgeModule, FIRMessagingDelegate, UNUserNotificationCenterDelegate>
@property NSString* token;
- (void)register2FACategory;
@end
FirebaseMessagingModule.m:
//
// FirebaseMessagingModule.m
// bastionpassmobile
//
// Created by Anotn on 25/05/2020.
// Copyright © 2020 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FirebaseMessagingModule.h"
@import Firebase;
#import "constants.h"
#import "MF_Base32Additions.h"
#import <CommonCrypto/CommonCrypto.h>
@implementation FirebaseMessagingModule
RCT_EXPORT_MODULE()
/*
Registers 2FA category for remote notifications.
*/
- (void)register2FACategory
{
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
UNNotificationAction* twofaDeclineAction = [UNNotificationAction actionWithIdentifier:TWOFA_NOTIFICATION_ACTION_DECLINE_ID title:TWOFA_NOTIFICATION_ACTION_DECLINE_LABEL options:UNNotificationActionOptionAuthenticationRequired];
UNNotificationAction* twofaConfirmAction = [UNNotificationAction actionWithIdentifier:TWOFA_NOTIFICATION_ACTION_CONFIRM_ID title:TWOFA_NOTIFICATION_ACTION_CONFIRM_LABEL options:UNNotificationActionOptionAuthenticationRequired];
UNNotificationCategory* twofaCategory = [UNNotificationCategory categoryWithIdentifier:TWOFA_NOTIFICATION_CATEGORY_ID actions:@[twofaDeclineAction, twofaConfirmAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone];
NSSet* categories = [NSSet setWithObject:twofaCategory];
[center setNotificationCategories:categories];
}
- (void)messaging:(FIRMessaging *)messaging
didReceiveRegistrationToken:(NSString *)fcmToken
NS_SWIFT_NAME(messaging(_:didReceiveRegistrationToken:))
{
[self setToken:fcmToken];
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler __API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0), tvos(10.0))
{
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0)) __API_UNAVAILABLE(tvos)
{
BOOL is2FAConfirm = [response.actionIdentifier isEqualToString:TWOFA_NOTIFICATION_ACTION_CONFIRM_ID];
BOOL is2FADecline = [response.actionIdentifier isEqualToString:TWOFA_NOTIFICATION_ACTION_DECLINE_ID];
if (is2FAConfirm || is2FADecline) {
// Retrieve notification data
NSDictionary* userInfo = response.notification.request.content.userInfo;
NSString* sessionId = userInfo[TWOFA_NOTIFICATION_DATA_SESSIONID_KEY];
NSString* username = userInfo[TWOFA_NOTIFICATION_DATA_USERNAME_KEY];
NSString* stage = userInfo[TWOFA_NOTIFICATION_DATA_STAGE_KEY];
// Retrieve storage data
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString* authTokensString = [userDefaults valueForKey:STORAGE_KEY_AUTH_TOKENS];
NSData* authTokensData = [authTokensString dataUsingEncoding:NSUTF8StringEncoding];
NSInputStream* authTokensStream = [NSInputStream inputStreamWithData:authTokensData];
[authTokensStream open];
NSData* authTokens = [NSJSONSerialization JSONObjectWithStream:authTokensStream options:0 error:nil];
[authTokensStream close];
NSDictionary<NSString*, NSString*>* clientIds = [authTokens valueForKey:STORAGE_KEY_CLIENT_IDS];
NSString* clientId = [clientIds valueForKey:username];
NSURLSessionConfiguration* urlSessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"twoFAConfirmation"];
urlSessionConfiguration.networkServiceType = NSURLNetworkServiceTypeBackground;
urlSessionConfiguration.discretionary = NO;
urlSessionConfiguration.HTTPAdditionalHeaders = @{
@"Content-Type": @"application/json",
@"Accept": @"application/json",
@"X-Client-Type": @"mobile"
};
NSURLSession* urlSession = [NSURLSession sessionWithConfiguration:urlSessionConfiguration];
NSURL* url = [NSURL URLWithString:[API_URL stringByAppendingString:is2FAConfirm ? API_ENDPOINT_2FA_APPROVE : API_ENDPOINT_2FA_REJECT]];
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
urlRequest.HTTPMethod = @"POST";
urlRequest.allHTTPHeaderFields = @{
@"Content-Type": @"application/json",
@"Accept": @"application/json",
@"X-Client-Type": @"mobile"
};
if (is2FADecline) {
urlRequest.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{
TWOFA_CONFIRMATION_REQUEST_KEY_SESSIONID: sessionId,
TWOFA_CONFIRMATION_REQUEST_KEY_AUTH_CLIENTID: clientId
} options:0 error:nil];
NSURLSessionUploadTask* rejectConfirmationTask = [urlSession uploadTaskWithStreamedRequest:urlRequest];
[rejectConfirmationTask resume];
} else if (is2FAConfirm) {
NSString* hotpSecretsString = [userDefaults valueForKey:STORAGE_KEY_HOTP_SECRETS];
NSData* hotpSecretsData = [hotpSecretsString dataUsingEncoding:NSUTF8StringEncoding];
NSInputStream* hotpSecretsStream = [NSInputStream inputStreamWithData:hotpSecretsData];
[hotpSecretsStream open];
NSData* hotpSecrets = [NSJSONSerialization JSONObjectWithStream:hotpSecretsStream options:0 error:nil];
[hotpSecretsStream close];
NSString* hotpSecret = [hotpSecrets valueForKey:username];
NSString* totpCode = [self getTotpCode:hotpSecret];
urlRequest.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{
TWOFA_CONFIRMATION_REQUEST_KEY_SESSIONID: sessionId,
TWOFA_CONFIRMATION_REQUEST_KEY_AUTH_CLIENTID: clientId,
TWOFA_CONFIRMATION_REQUEST_KEY_TOKEN: totpCode
} options:0 error:nil];
NSURLSessionUploadTask* approveConfirmationTask = [urlSession uploadTaskWithStreamedRequest:urlRequest];
[approveConfirmationTask resume];
}
} else {
NSLog(@"Undefined type of action");
}
completionHandler();
}
/*
Returns totp code for provided hotp secret.
*/
- (NSString*)getTotpCode:(NSString*)hotpSecret
{
NSString* hotpSecretDecoded = [NSString stringFromBase32String:hotpSecret];
const char *cKey = [hotpSecretDecoded cStringUsingEncoding:NSUTF8StringEncoding];
const uint64_t cData = CFSwapInt64([[NSDate date] timeIntervalSince1970] * 1000.0 / 30000);
unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), &cData, sizeof(cData), cHMAC);
NSData* hash = [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)];
int offset = *(int*)[[hash subdataWithRange:NSMakeRange([hash length] - 1, 1)] bytes] & 0x0f;
const long* dbc1 = [[hash subdataWithRange:NSMakeRange(offset, (offset + 4) - offset)] bytes];
long dbc2 = -(~CFSwapInt32(*dbc1) + 1) & 0x7fffffff;
long totp = dbc2 % (long)pow(10, TWOFA_OTP_LENGTH);
NSString* totpCode = @"";
NSString* totpCodeSource = [[NSNumber numberWithLong:totp] stringValue];
while (([totpCode length] + [totpCodeSource length]) < TWOFA_OTP_LENGTH) {
totpCode = [totpCode stringByAppendingString:@"0"];
}
return [totpCode stringByAppendingString:totpCodeSource];
}
/*
Resolves with FCM registration token or rejected if token was not provided.
*/
RCT_REMAP_METHOD(getToken, getTokenResolver:(RCTPromiseResolveBlock)resolve getTokenRejecter:(RCTPromiseRejectBlock)reject)
{
if (self.token) {
resolve(self.token);
} else {
reject(@"0", @"Failed on get token", [[NSError alloc] init]);
}
}
/*
Resolves with FCM registration token or rejected if attempt to retrieve token will be failed.
*/
RCT_REMAP_METHOD(retrieveToken, retrieveTokenResolver:(RCTPromiseResolveBlock)resolve retrieveTokenRejecter:(RCTPromiseRejectBlock)reject)
{
[[FIRInstanceID instanceID] instanceIDWithHandler:^(FIRInstanceIDResult * _Nullable result,
NSError * _Nullable error) {
if (error != nil) {
reject(@"0", @"Failed to retrieve token", [[NSError alloc] init]);
} else {
resolve(result.token);
}
}];
}
@end
Any help would be appreciated!
[UPDATE]: Tester in our company tested current functionality and on iOS 13.5 it works even if app was killed. Maybe it's somehow affected by device settings or restrictions on older SDKs?
User contributions licensed under CC BY-SA 3.0