Allow Connection For Invalid SSL Pinning on AFNetwroking - ios

I'm trying to implement ssl pinning in our iOS app.
The definition as declared by our security architect was to implement it in 2 phases:
- 1st phase is to send an analytics event each time we recognise a bad pinning happening (if the pinning fails, I should send event, but allow the request to continue)
- 2nd phase is to actually block each call that doesn't successfully pass pinning.
We have 2 network layers in the app, one of them uses Alamofire, the other one is using AFNetworking. I'm successfully implemented phase 1 in the Alamofire based network layer. My issue is with the AFNetworking:
AFNetworking is blocking any call that fails the pinning, once a policy is set. Unfortunatly, I would like it to allow these calls.
I tried setting the policy's allowInvalidCertificates property to YES, which let all those failing pinning to continue as needed, but I'm not sure how can I be notified that a specific pinning was failed (in order to send the event)
Any help would be much appreciated.
UPDATE
So I figure out I can set a block to be called whenever a challenge is raised using:
- (void)setSessionDidReceiveAuthenticationChallengeBlock:(NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential))block {
and/or
- (void)setTaskDidReceiveAuthenticationChallengeBlock:(NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential))block {
This allow me to make the validations on the challenge, and for the 1st phase I could send the event and return NSURLSessionAuthChallengePerformDefaultHandling which is exactly what I need.
For some reason, the task block is not being invoked no matter what I do, while the session block is.

So I figure out I can set a block to be called whenever a challenge is raised using:
- (void)setSessionDidReceiveAuthenticationChallengeBlock:(NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential))block {
and/or
- (void)setTaskDidReceiveAuthenticationChallengeBlock:(NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential))block {
This way I can get do whatever side effect needed (in my case, send event with relevant data) and decide if to block the request if needed or let it go on as no policy was configured...

Related

NSURLSessionTask Never Calls Back When WiFi Off

When I turn off my WiFi connection and run the following code on the iPhone 6s 10.2 simulator, the callback is never executed. I expected the callback to fire fairly quickly with an error like "No Internet connection".
NSLog(#"request-start");
NSURLRequest* request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:#"https://www.google.com"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:0];
task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(#"request-end");
}];
[task resume];
My Problem
I'm fetching data when the app first loads. If wifi is turned off, I need to show an error. If I set a timeout, it is obeyed - but it would need to be 10+ seconds and I'd rather not make them wait. I've also tried to detect the network status with reachability, but the network status is often unknown when the app is first loaded.
timeoutIntervalForResource
This property determines the resource timeout interval for all tasks
within sessions based on this configuration. The resource timeout
interval controls how long (in seconds) to wait for an entire resource
to transfer before giving up. The resource timer starts when the
request is initiated and counts until either the request completes or
this timeout interval is reached, whichever comes first.
The default value is 7 days.
and
timeoutIntervalForRequest
Important
Any upload or download tasks created by a background session
are automatically retried if the original request fails due to a
timeout. To configure how long an upload or download task should be
allowed to be retried or transferred, use the
timeoutIntervalForResource property.
The default value is 60.
So, without your timeout set, your connection will run for 7 days.
In general an NSURLSession background session does not fail a task if
something goes wrong on the wire. Rather, it continues looking for a
good time to run the request and retries at that time. This continues
until the resource timeout expires (that is, the value in the
timeoutIntervalForResource property in the NSURLSessionConfiguration
object you use to create the session). The current default for that
value is one week!
Source

NSURLSession: Synchronous task hangs

I have recently replaced NSURLConnection to NSURLSession in my code.
As I am using many synchronous url-requests and NSURLSession doesn't support one, I used semaphores to make NSURLSessionDataTask synchronous.
I referred this link: https://forums.developer.apple.com/thread/11519
I have a singleton 'Network Manger' with NSURLSession as a member variable. NSURLSession is instantiated only once and tasks are added to it.
But now synchronous request cause performance issues in my app. There are lags and app hangs when synchronous request is sent.
Her is the call to synchronous request:
NSURLResponse *urlResponse;
NSError *error;
id serverResponse=[[MyNetworkManager sharedInstance] synchronousDataTaskWithRequest:request
returningResponse:&urlResponse
error:&error];
Everything works perfectly if I do not make NetworkManager as singleton and instantiate it everytime.
MyNetworkManager *manger=[[MyNetworkManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] responseSerializer:nil];
NSURLResponse *urlResponse;
NSError *error;
id *serverResponse=[manger synchronousDataTaskWithRequest:request returningResponse:&urlResponse error:&error];
Notes:
1.Exact Steps: There are total 3 (2 async and 1 sync ) NSURLSessionDataTasks. Second async task is called in completion handler of first task. Third synchronous task is called from completion handler of second. This third task is blocked forever and code doesn't proceed.
2.Everything is happening on background thread.
Why does synchronous data task work only if it is added to a new NSURLSession? Why it doesn't work if added to NSURLSession which has already executed two data tasks?
I was facing the similar issue when I used to synchronus implementation using Semaphore and here is the cause of that issue :
I think that your an HTTP request is causing a redirection, and in the willPerformHTTPRedirection method, you haven't called the completionHandler for the new request. Try implementing the method as :
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler
{
NSLog(#"willPerformRedirection");
completionHandler(request);
}

CKModifyRecordsOperation modifyRecordsCompletionBlock not being called

I'm using CKModifyRecordsOperation to save a set of records and if I have internet connection all works well and completion block is being called. But when I don't have connection the completion block is not being called and I don't get any information that my operations failed.
I'm using the following code in completion block
modifyOperations.modifyRecordsCompletionBlock = ^(NSArray *savedRecords, NSArray *deletedRecordIDs, NSError *error)
{
if(error){
NSLog(#"Error: %#", error.localizedDescription);
}
item.creatorRecordId = record.recordID;
};
and then I'm performing operation using
[self.publicDB addOperation:modifyOperations];
Any ideas how can I get an information if the operation failed for example in the case where there is no internet connection?
CloudKit operations have their qualityOfService property set to NSQualityOfServiceUtility by default.
Operations that use NSQualityOfServiceUtility or NSQualityOfServiceBackground may be marked as using discretionary network requests. The system can hold discretionary network requests if network connectivity is poor, so you might not get a response from the server until conditions improve and the system sends the request.
If you'd like your request to be sent immediately in all cases, set CKOperation.qualityOfService to NSQualityOfServiceUserInitiated or NSQualityOfServiceUserInteractive.

Best solution to refresh token automatically with AFNetworking?

Once your user is logged in, you get a token (digest or oauth) which you set into your HTTP Authorization header and which gives you the authorization to access your web service.
If you store your user's name, password and this token somewhere on the phone (in user defaults, or preferably in the keychain), then your the user is automatically logged in each time the application restarts.
But what if your token expires? Then you "simply" need to ask for a new token and if the user did not change his password, then he should be logged in once again automatically.
One way to implement this token refreshing operation is to subclass AFHTTPRequestOperation and take care of 401 Unauthorized HTTP Status code in order to ask a new token. When the new token is issued, you can call once again the failed operation which should now succeeds.
Then you must register this class so that each AFNetworking request (getPath, postPath, ...) now uses this class.
[httpClient registerHTTPOperationClass:[RetryRequestOperation class]]
Here is an exemple of such a class:
static NSInteger const kHTTPStatusCodeUnauthorized = 401;
#interface RetryRequestOperation ()
#property (nonatomic, assign) BOOL isRetrying;
#end
#implementation RetryRequestOperation
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *, id))success
failure:(void (^)(AFHTTPRequestOperation *, NSError *))failure
{
__unsafe_unretained RetryRequestOperation *weakSelf = self;
[super setCompletionBlockWithSuccess:success failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// In case of a 401 error, an authentification with email/password is tried just once to renew the token.
// If it succeeds, then the opration is sent again.
// If it fails, then the failure operation block is called.
if(([operation.response statusCode] == kHTTPStatusCodeUnauthorized)
&& ![weakSelf isAuthenticateURL:operation.request.URL]
&& !weakSelf.isRetrying)
{
NSString *email;
NSString *password;
email = [SessionManager currentUserEmail];
password = [SessionManager currentUserPassword];
// Trying to authenticate again before relaunching unauthorized request.
[ServiceManager authenticateWithEmail:email password:password completion:^(NSError *logError) {
if (logError == nil) {
RetryRequestOperation *retryOperation;
// We are now authenticated again, the same request can be launched again.
retryOperation = [operation copy];
// Tell this is a retry. This ensures not to retry indefinitely if there is still an unauthorized error.
retryOperation.isRetrying = YES;
[retryOperation setCompletionBlockWithSuccess:success failure:failure];
// Enqueue the operation.
[ServiceManager enqueueObjectRequestOperation:retryOperation];
}
else
{
failure(operation, logError);
if([self httpCodeFromError:logError] == kHTTPStatusCodeUnauthorized)
{
// The authentication returns also an unauthorized error, user really seems not to be authorized anymore.
// Maybe his password has changed?
// Then user is definitely logged out to be redirected to the login view.
[SessionManager logout];
}
}
}];
}
else
{
failure(operation, error);
}
}];
}
- (BOOL)isAuthenticateURL:(NSURL *)url
{
// The path depends on your implementation, can be "auth", "oauth/token", ...
return [url.path hasSuffix:kAuthenticatePath];
}
- (NSInteger)httpCodeFromError:(NSError *)error
{
// How you get the HTTP status code depends on your implementation.
return error.userInfo[kHTTPStatusCodeKey];
}
Please, be aware that this code does not work as is, as it relies on external code that depends on your web API, the kind of authorization (digest, oath, ...) and also which kind of framework you use over AFNetworking (RestKit for example).
This is quite efficient and has proved to work well with both digest and oauth authorization using RestKit tied to CoreData (in that case RetryRequestOperation is a subclass of RKManagedObjectRequestOperation).
My question now is: is this the best way to refresh a token?
I am actually wondering if NSURLAuthenticationChallenge could be used to solve this situation in a more elegant manner.
Your current solution works and you have the code for it, there might be a reasonable amount of code to achieve it but the approach has merits.
Using an NSURLAuthenticationChallenge based approach means subclassing at a different level and augmenting each created operation with setWillSendRequestForAuthenticationChallengeBlock:. In general this would be a better approach as a single operation would be used to perform the whole operation rather than having to copy it and update details, and the operation auth support would be doing the auth task instead of the operation completion handler. This should be less code to maintain, but that code will likely be understood by less people (or take longer to understand by most) so the maintenance side of things probably balances out over all.
If you're looking for elegance, then consider changing, but given that you already have a working solution there is little gain otherwise.
I was searching an answer for this problem and "Matt", the creator of AFNetworking, suggest this:
the best solution I've found for dealing with this is to use dependent
NSOperations to check for a valid, un-expired token before any
outgoing request is allowed to go through. At that point, it's up to
the developer to determine the best course of action for refreshing
the token, or acquiring a new one in the first place.

Can an ASIHTTPRequest be retried?

Is there a way to retry an ASIHTTPRequest? I'm using custom authentication in a REST service instead of basic HTTP authentication and I would like to retry a request if the session has expired. Calling startAsynchronous a second time on a request causes an exception. I would like to do something like this:
[request setCompletionBlock:^{
if ([request responseStatusCode] == 500)
{
// try to login again in case token expired
[server loginAndRetryRequest:request];
} else {
// parse response
}
}];
loginAndRetryRequest: will do another ASIHTTPRequest to login and when it is complete it will start the original request again from it's CompletionBlock (assuming this is possible somehow)?
It should be possible to make a copy of the request and then execute -startAsynchronous again on the copy.
Support for NSCopying protocol was added in release 1.5, which also includes automatic retry in case of timeout (selector -setNumberOfTimesToRetryOnTimeout:.
Another option could be checking their source code to see how the automatic retry is done in case of timeout, but copying and re-issuing the request should work (that was the reason to add support for NSCopying in the first place).

Resources