How to automatically refresh expired token with AFOAuth2Manager? - ios

I'm writing a small iOS client for a server protected with OAuth2.
I'm wondering if is it possible using AFOAuth2Manager [here] auto-refreshing the expired token.
The idea is that the logic for refreshing the client when the server responds with a 401, or raise an error when the refresh method returns a 401 should be quite common, so probably it is integrated in some library.

I created a subclass of AFOAuth2Manager
In this subclass I override this method:
- (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure {
return [self HTTPRequestOperationWithRequest:request
success:success
failure:failure
checkIfTokenIsExpired:YES];
}
calling a custom method with an additional parameter: checkIfTokenIsExpired. This is required in order to avoid infinite loops.
The implementation of this method is straigth forward: if we don't need to check the token just call the super class.
if (!checkIfTokenIsExpired) {
return [super HTTPRequestOperationWithRequest:request
success:success
failure:failure];
}
otherwise we perform the request with a custom failure block
else {
return [super HTTPRequestOperationWithRequest:request
success:success
failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
if (operation.response.statusCode == ERROR_CODE_UNAUTHORIZED) { //1
[self reauthorizeWithSuccess: ^{ //2
NSURLRequest *req = [self.requestSerializer requestByAddingHeadersToRequest:request]; //3
AFHTTPRequestOperation *moperation = [self HTTPRequestOperationWithRequest:req //4
success:success
failure:failure
checkIfTokenIsExpired:NO];
[self.operationQueue addOperation:moperation]; //5
} failure: ^(NSError *error) {
failure(nil, error);
}];
}
else {
failure(operation, error); //6
}
}];
}
//1: check the http status code, if 401 try to automatically re-authorize.
//2: reauthorize is a private mathod that uses AFOAuthManager to refresh the token.
//3: In this case we are re-authorized with success and we want to resubmit a copy of the previous request. The method requestByAddingHeadersToRequest: just copy all the header fields from the previous request.
//4: Create a copy of the previous request, but this time the last parameter is false because we don't want check again! The successBlock and failureBlock are the same of the previous request.
//5: Add the operation to the queue.
//6: If the reauthorize method fails just call the failure block.

Unfortunately I didn't found any framework for solve this problem so I wrote a short wrapper around AFNetworking (if someone is interested I can publish on github)
The logic is to execute the request, and in case of http response 401, try to refresh the auth-token and when it's done to re-execute the previous request.

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.
Simple, but effective?, trying now, will edit with report...

Swift solution with Alamofire 4.0. Based on RequestAdapter and RequestRetrier protocols: example link

Related

AFHTTPRequestOperation dependency

I have the following scenario in an application that uses AFNetworking to make services calls:
I call a special service that will generate a token for me
I call the service that I want, sending this token as a parameter
I call another special service to destroy the token.
I have to follow these 3 steps every time I make a request to the server. I cannot change the way the server works, so I have to comply to this requirement. I also cannot use the same token for more than one request.
My question is the following - I tried to accomplish this using AFHTTPRequestOperations:
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.manager.requestSerializer requestWithMethod:#"POST" URLString:[[NSURL URLWithString:#"serviceName.json" relativeToURL:self.manager.baseURL] absoluteString] parameters:#{ #"token": token } error:&serializationError];
AFHTTPRequestOperation *myRequestOperation = [self.manager HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) {
// Success login
} failure:^(AFHTTPRequestOperation * _Nonnull operation, NSError * _Nonnull error) {
// Failure logic
}];
[myRequestOperation addDependency:createTokenRequestOperation];
where self.manager is an instance of AFHTTPRequestOperationManager, but there is a problem - I do not have a value for token.
Since myRequestOperation should execute only after point 1 from the list above, I make it dependent on the operation that will get me a token.
Now comes my confusion - how can I create an operation that uses a parameter from a previous operation, when I need to have both of them instantiated in order to make the one depend on the other?
Since I was not able to find a solution that will work for me, I ended up using PromiseKit, which allows me to chain asynchronous calls like this:
[NSURLConnection promise:rq1].then(^(id data1){
return [NSURLConnection promise:rq2];
}).then(^(id data2){
return [NSURLConnection promise:rq3];
}).then(^(id data3){
// Work with the data returned from rq3
});

AFNetworking to REST API returns 401

I'm trying to use a REST API in my iOS app. I know it works because I can make the login request once. Every subsequent request fails with a 401 error. Even if I delete the app from the simulator it still can't be called again until I change the simulator type to one that I haven't used before (i.e. iPad 2, iPhone6, etc.). I can also use a service like https://www.hurl.it to make the same request with the same parameters as many times as I'd like. I'm using AFNetworking and AFHTTPRequestOperationManager. What am I doing wrong?
self.manager = [[AFHTTPRequestOperationManager alloc]initWithBaseURL:[NSURL URLWithString:#"https://api.mydomain.com/"]];
[self.manager POST:#"services/json/user/login" parameters:#{#"username":#"USERNAME", #"password":#"PASSWORD"} success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (![responseObject isKindOfClass:[NSDictionary class]]) { return; }
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSDictionary* json = responseObject;
self.sessionName = json[#"session_name"];
self.sessionId = json[#"sessionid"];
[defaults setObject:self.sessionName forKey:#"SessionNameKey"];
[defaults setObject:self.sessionId forKey:#"SessionIDKey"];
if (completion) { completion(); }
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Session request failed: %#", error.userInfo);
}];
If first login called success, you should get some access_token which you use to send along with any subsequent calls.
If API has basic authentication, then you need to pass credentials in HTTP header.
To set credentials in header you can use following methods:
[self.manager.requestSerializer setAuthorizationHeaderFieldWithUsername:#"username" password:#"password"];
(Unauthorised) 401 means you don't have access to the service. Make sure you are trying with correct credentials. Apparently there's nothing wrong with the code.
Update for iOS9 with Swift 2.0 using Bearer authorization
I had the same problem, actually the sessionManager.session.configuration.HTTPAdditionalHeaders get overwritten by the requestSerializer on iOS9, which cause the 401 Access denied error.
Code that was working on iOS8, but not on iOS9:
sm.session.configuration.HTTPAdditionalHeaders = ["Authorization" : "Bearer \(token!)"]
New code for iOS9
sm.requestSerializer.setValue("Bearer \(token!)", forHTTPHeaderField: "Authorization")

iOS AFNetworking automatically retry request when the internet connection is back

Does AFNetworking for iOS offer a solution to cache failed requests (e.g due to no internet connection) and automatically retry the request when the internet connection is back.
Thanks,
Dorin
See the Network Reachability Manager section of the AFNetworking site. By using "Reachability", your handler will be called whenever the network availability changes. Just set the setReachabilityStatusChangeBlock of the AFHTTPRequestOperationManager (AFNetworking 2) or AFHTTPSessionManager (AFNetworking 3):
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];
[manager.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
switch (status) {
case AFNetworkReachabilityStatusReachableViaWWAN:
case AFNetworkReachabilityStatusReachableViaWiFi:
// do whatever you want when network is available
break;
case AFNetworkReachabilityStatusNotReachable:
default:
// do whatever you want when network is not available
break;
}
}];
[manager.reachabilityManager startMonitoring];
As matt said in AFNetworking issue #393,AFNetworking doesn't have retry mechanism:
This is something that a few people have requested, but each each use case had surprisingly different requirements in what the behavior should be, which leads me to believe that a general solution that's useful for all relevant cases is intractable.
I'm of the opinion that request retrying is an application concern (or perhaps even something for the user to initiate); it's not all that difficult to implement yourself:
- (void)downloadFileRetryingNumberOfTimes:(NSUInteger)ntimes
success:(void (^)(id responseObject))success
failure:(void (^)(NSError *error))failure
{
if (ntimes <= 0) {
if (failure) {
NSError *error = ...;
failure(error);
}
} else {
[self getPath:#"/path/to/file" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (success) {
success(...);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self downloadFileRetryingNumberOfTimes:ntimes - 1 success:success failure:failure];
}];
}
}
Nope, but you can detect in the failure block if the request has timeout. And you can resend it in this case (With like a retry counter of something like this).

How to wait for asyn operation in iOS unit test using NSConditionLock

I have a unit test in which I need to wait for an async task to finish. I am trying to use NSConditionLock as it seems to be a pretty clean solution but I cannot get it to work.
Some test code:
- (void)testSuccess
{
loginLock = [[NSConditionLock alloc] init];
Login login = [[Login alloc] init];
login.delegate = self;
// The login method will make an async call.
// I have setup myself as the delegate.
// I would like to wait to the delegate method to get called
// before my test finishes
[login login];
// try to lock to wait for delegate to get called
[loginLock lockWhenCondition:1];
// At this point I can do some verification
NSLog(#"Done running login test");
}
// delegate method that gets called after login success
- (void) loginSuccess {
NSLog(#"login success");
// Cool the delegate was called this should let the test continue
[loginLock unlockWithCondition:1];
}
I was trying to follow the solution here:
How to unit test asynchronous APIs?
My delegate never gets called if I lock. If I take out the lock code and put in a simple timer it works fine.
Am I locking the entire thread and not letting the login code run and actually make the async call?
I also tried this to put the login call on a different thread so it does not get locked.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[login login];
});
What am I doing wrong?
EDIT adding login code. Trimmed do the code for readability sake. Basically just use AFNetworking to execute a POST. When done will call delegate methods.
Login make a http request:
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (_delegate) {
[_delegate loginSuccess];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (_delegate) {
[_delegate loginFailure];
}
}];
The answer can be found in https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFHTTPRequestOperation.m.
Since you are not setting the completionQueue property of the implicitly created AFHTTPRequestOperation, it is scheduling the callbacks on the main queue, which you are blocking.
Unfortunately, many answers (not all) in the given SO thread ("How to unit test asynchronous APIs?") are bogus and contain subtle issues. Most authors don't care about thread-safity, the need for memory-barriers when accessing shared variables, and how run loops do work actually. In effect, this leads to unreliable and ineffective code.
In your example, the culprit is likely, that your delegate methods are dispatched on the main thread. Since you are waiting on the condition lock on the main thread as well, this leads to a dead lock. One thing, the most accepted answer that suggests this solution does not mention at all.
A possible solution:
First, change your login method so that it has a proper completion handler parameter, which a call-site can set in order to figure that the login process is complete:
typedef void (^void)(completion_t)(id result, NSError* error);
- (void) loginWithCompletion:(completion_t)completion;
After your Edit:
You could implement your login method as follows:
- (void) loginWithCompletion:(completion_t)completion
{
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (completion) {
completion(responseObject, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (completion) {
completion(nil, error);
}
}];
Possible usage:
[self loginWithCompletion:^(id result, NSError* error){
if (error) {
[_delegate loginFailure:error];
}
else {
// Login succeeded with "result"
[_delegate loginSuccess];
}
}];
Now, you have an actual method which you can test. Not actually sure WHAT you are trying to test, but for example:
-(void) testLoginController {
// setup Network MOCK and/or loginController so that it fails:
...
[loginController loginWithCompletion:^(id result, NSError*error){
XCTAssertNotNil(error, #"");
XCTAssert(...);
<signal completion>
}];
<wait on the run loop until completion>
// Test possible side effects:
XCTAssert(loginController.isLoggedIn == NO, #""):
}
For any other further steps, this may help:
If you don't mind to utilize a third party framework, you can then implement the <signal completion> and <wait on the run loop until completion> tasks and other things as described here in this answer: Unit testing Parse framework iOS

avoid duplicate HTTP requests with AFNetworking

I would like to find out if it's possible to avoid duplicate HTTP requests with AFNetworking. Specifically, my app may generate multiple HTTP requests which all have the same url. I would like to prevent AFNetworking from processing duplicates of the same url.
Im not sure if this can be done in AFNetworking or the underlying iOS sdk. I understand that i could manually keep trac of pending url request and avoid duplicates that way, but was wondering if there is a lower level functionality already available to take care of this.
Thanks.
Your best bet is to subclass AFHTTPRequestOperationManager's HTTP request operations and keep track of them there if you want to track requests the same way for each request, otherwise the logic will need to be elsewhere.
AFNetworking doesn't support this because there is probably some logic relevant to when you should and when you should not execute a duplicate request, which would be highly customizable (not generic enough for the framework)
I made a category that checks for in-progress GET requests before making new ones.
https://github.com/NSElvis/AFHTTPSessionManager-AFUniqueGET
It does this by using the method getTasksWithCompletionHandler of the session.
I had the same problem. I have a chat-application and I need to show user avatar for each message. So I made few same requests and I've resolved this issue.
First, I add NSDictionary with NSString avatar URLs keys and completion blocks objects:
#property (strong, nonatomic) NSMutableDictionary* successBlocksDictForGetAvatar;
And here's my method to get user avatar image:
- (void)getAvatarForUser:(ETBUser*)user
completion:(void(^)())completionBlock
{
if (user.avatarURL)
{
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:user.avatarURL]];
if (self.successBlocksDictForGetAvatar[user.avatarURL])
[self.successBlocksDictForGetAvatar[user.avatarURL] addObject:completionBlock];
else
{
NSMutableSet* set = [[NSMutableSet alloc] initWithObjects:completionBlock, nil];
[self.successBlocksDictForGetAvatar setObject:set forKey:user.avatarURL];
AFHTTPRequestOperation* operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
UIImage* avatarImage = [UIImage imageWithData:responseObject];
if (avatarImage)
{
user.avatar = avatarImage;
[[DataManager shared] saveAvatar];
[((NSSet*)self.successBlocksDictForGetAvatar[user.avatarURL]) enumerateObjectsUsingBlock:^(void(^successBlock)(), BOOL *stop) {
successBlock();
}];
[self.successBlocksDictForGetAvatar removeObjectForKey:user.avatarURL];
}
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self.successBlocksDictForGetAvatar removeObjectForKey:user.avatarURL];
}];
[self.manager.operationQueue addOperation:operation];
}
}
}
Here I check if my dictionary contains request. If YES, I add completion block for user in dictionary. Otherwise I setObject:forKey: and make AFNetworking request. In success and fail blocks I clean my dictionary.
P.S. Here's my manager getter:
- (AFHTTPRequestOperationManager*)manager
{
if (!_manager)
{
_manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:kBaseURL];
[_manager.requestSerializer setValue:NetworkConstantsHeaderAcceptValue forHTTPHeaderField:NetworkConstantsHeaderAcceptKey];
[_manager.operationQueue setMaxConcurrentOperationCount:1];
}
return _manager;
}

Resources