Freesound OAuth2 authentication fails - ios

I'm trying to create an iOS app that uses OAuth2 authentication using the native iOS NSURLSession URL loading classes. I gain an access token fine using the directions here:
http://www.freesound.org/docs/api/authentication.html
I subsequently launch the application and run a search query
https://www.freesound.org/apiv2/search/text/?query=snare
The request header fields looks like this (note my access token is not expired and I have confirmed it is the same as I received from performing the steps above)
{
"Authorization: Bearer" = MY_ACCESS_TOKEN;
}
This fails with:
{"detail": "Authentication credentials were not provided."}
The response headers look like this:
{
Allow = "GET, HEAD, OPTIONS";
Connection = "keep-alive";
"Content-Type" = "application/json";
Date = "Sat, 31 Jan 2015 13:56:32 GMT";
Server = "nginx/1.2.1";
"Transfer-Encoding" = Identity;
Vary = "Accept, Cookie";
"Www-Authenticate" = "Bearer realm=\"api\"";
}
The funny thing is that this does not always happen. If I repeat this entire process a number of times, deleting the app in between, it will eventually work. Once it works, it will continue to work while I'm developing. Sometimes then when I come back to it, say the next day, it stops working and I need to repeat this deleting and re-installing routine to get it back working again!
There's an authentication challenge delegate method on NSURLSession that will get called if implemented. It's a 'server trust' challenge. Could this be something to do with it? Would you even expect an authentication challenge of this nature? There's nothing mentioned about it in the docs alluded to above.
Any help would be much appreciated.
EDIT
This is how the search text ("snare") GET call is made.
I basically pass in an NSMutableURLRequest with the URL set to the above (https://www.freesound.org/apiv2/search/text/?query=snare). useAccessToken is set to YES.
- (void)makeRequest:(NSMutableURLRequest *)request useAccessToken:(BOOL)useAccessToken completion:(CompletionBlock)completion {
NSAssert(completion, #"No completion block.");
if (useAccessToken) {
NSString *accessToken = [[ODMFreesoundTokenCache sharedCache] accessToken];
NSAssert(accessToken.length, #"No access token.");
[request addValue:accessToken forHTTPHeaderField:#"Authorization: Bearer"];
}
NSLog(#"Making request: %# \n\nWith access token: %#", request, [[ODMFreesoundTokenCache sharedCache] accessToken]);
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSInteger code = [(NSHTTPURLResponse *)response statusCode];
if (code == 200) {
if (!error) {
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"json: %#", json);
completion(json, error);
}
else {
completion(nil, error);
}
}
else {
NSString *reason = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *error = [NSError errorWithDomain:#"Request Error" code:code userInfo: reason ? #{NSLocalizedDescriptionKey : reason} : nil];
NSLog(#"error: %#", error);
completion(nil, error);
}
}];
[task resume];
}

The 2 flows for authentication described in the doc are not "safe" for a device. Using API keys would require the secret to be stored in the device.
The OAuth2 flow they support (authorization_code) requires a server to server call to exchange a code for the actual token (This step: http://www.freesound.org/docs/api/authentication.html#step-3). This call requires another credential (the client_secret that you probably should not store in the device either.
You need a server in between that negotiates this for you. Or a server that translates the code flow into token one. (Illustrated here: https://auth0.com/docs/protocols#5).

Related

iOS - Refreshing Expired Auth Token In NSURLSession

In my app, I need to add my authentication token in the HTTPHeadField for every NSURLRequest API call that I make to my server. This token is only valid for 2 days. When it becomes invalid, I'll receive a "token_invalid" error response from my server, meaning that I'll need to send an API call to my server to refresh my auth token.
The problem that's hard to wrap my head around is that these NSURLRequests are done concurrently, so when each fails due to an expired token, ALL of them are going to attempt to refresh the token. How do I set this up so that the token is refreshed ONCE, and when that's done, re-attempt all the failed requests?
PROGRESS
What I have so far works, but only to a certain extent that confuses me. When I successfully refresh the auth token, I iterate through all the failed requests, and re-attempt them. However, all of them are being re-attempted in that ONE API call that was responsible for refreshing the auth token.
For example, 3 API calls are being made (Friend Requests, Notifications, and Getting a User's Friends). If the "Get Friend Requests" API call fails first, it's responsible for refreshing the token. The other two API requests are put in the failedRequests array. When the auth token is successfully refreshed, only the "Get Friend Request" API call's success block is being passed through...3 TIMES!
I kinda understand why it's doing that, because I'm re-attempting all the failed API requests in the context of one NSURLRequest's sendTask method. Is there a way for me to re-attempt the failed requests in their given contexts when the auth token is refreshed in the kind of way that Key-Value Observing works?
-(void)sendTask:(NSURLRequest*)request successCallback:(void (^)(NSDictionary*))success errorCallback:(void (^)(NSString*))errorCallback
{
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
MyAPIInterface *__weak weakSelf = self;
[self parseResponse:response data:data fromRequest:request successCallback:success errorCallback:^(NSString *error)
{
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 401) {
if ([error isEqualToString:#"invalid_credentials"]) {
errorCallback(#"Invalid username and/or password");
}
else if ([error isEqualToString:#"token_expired"]) {
// check if request's auth token differs from api's current auth token
NSArray *requestHeaderValueComponents = [[request valueForHTTPHeaderField:#"Authorization"] componentsSeparatedByString:#" "];
NSString *requestAuthToken = requestHeaderValueComponents[1];
// if new auth token hasn't been retrieved yet
if ([requestAuthToken isEqualToString:weakSelf.authToken]) {
NSLog(#"THE AUTH TOKENS ARE EQUAL");
if (!weakSelf.currentlyRefreshingToken.boolValue) {
//lock alreadyRefreshingToken boolean
weakSelf.currentlyRefreshingToken = [NSNumber numberWithBool:YES];
NSLog(#"NOT REFRESHING TOKEN");
// add mutable failed request (to change auth token header later) to failedRequests array
NSMutableArray *mutableFailedRequests = [weakSelf.failedRequests mutableCopy];
NSMutableURLRequest *mutableFailedRequest = [request mutableCopy];
[mutableFailedRequests addObject:mutableFailedRequest];
weakSelf.failedRequests = [mutableFailedRequests copy];
// refresh auth token
[weakSelf refreshAuthenticationTokenWithSuccessCallback:^(NSDictionary *response) {
//store authToken
weakSelf.authToken = response[#"token"];
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:weakSelf.authToken forKey:#"authToken"];
[defaults synchronize];
//change auth token http header of each failed request and re-attempt them
for (NSMutableURLRequest *failedRequest in weakSelf.failedRequests) {
NSString *newAuthHeaderValue = [NSString stringWithFormat:#"Bearer %#", weakSelf.authToken];
[failedRequest setValue:newAuthHeaderValue forHTTPHeaderField:#"Authorization"];
[weakSelf sendTask:failedRequest successCallback:success errorCallback:errorCallback];
}
//clear failedRequests array and unlock alreadyRefreshingToken boolean
[weakSelf clearFailedRequests];
weakSelf.currentlyRefreshingToken = [NSNumber numberWithBool:NO];
NSLog(#"TOKEN REFRESHING SUCCESSFUL");
} errorCallback:^(NSString *error) {
NSLog(#"TOKEN NOT REFRESHABLE! HAVE TO LOG IN MANUALLY");
//clear failedRequests array
[weakSelf clearFailedRequests];
weakSelf.currentlyRefreshingToken = [NSNumber numberWithBool:NO];
errorCallback(#"Your login session has expired");
}];
}
else {
NSLog(#"ALREADY REFRESHING TOKEN. JUST ADD TO FAILED LIST");
// add mutable failed request (to change auth token header later) to failedRequests array
NSMutableArray *mutableFailedRequests = [weakSelf.failedRequests mutableCopy];
NSMutableURLRequest *mutableFailedRequest = [request mutableCopy];
[mutableFailedRequests addObject:mutableFailedRequest];
weakSelf.failedRequests = [mutableFailedRequests copy];
}
}
// if new auth token has been retrieved, simply re-attempt request with new auth token
else {
NSMutableURLRequest *failedRequest = [request mutableCopy];
NSString *newAuthHeaderValue = [NSString stringWithFormat:#"Bearer %#", weakSelf.authToken];
[failedRequest setValue:newAuthHeaderValue forHTTPHeaderField:#"Authorization"];
[weakSelf sendTask:failedRequest successCallback:success errorCallback:errorCallback];
}
}
else {
errorCallback(error);
}
}
else {
errorCallback(error);
}
}];
}];
[task resume];
}
1)I think you should be getting the token from successful login to the account.
2)So when ever the token gets expired. Show login screen to user.
3)If user logged in successfully he get new access token.
4) You can use this for your next request

IIS webdav and Ensembles

I'm trying to use sync through an IIS 8 Webdav backend with Ensembles. The problem i encounter is that the first sync works fine, but when i try to sync a second time or on a second unit (iPad in this case) I get a server error 405 "method not allowed". Has anyone encountered this and got it working, to sync with IIS Webdav?
This is the allheaderfield property of the server response:
" UserInfo={NSLocalizedDescription=HTTP status code was {
Allow = "COPY, PROPFIND, DELETE, MOVE, PROPPATCH, LOCK, UNLOCK";
Connection = "Keep-Alive";
"Content-Length" = 1293;
"Content-Type" = "text/html";
Date = "Mon, 25 Jan 2016 12:02:07 GMT";
"Persistent-Auth" = true;
Server = "Microsoft-IIS/8.5";
"X-UA-Compatible" = "IE=8";
EDIT:
It might be possible that this isn't a configuration problem after all. I added a few logs and the createDirectoryAtPath method gives me HTTP error 405, this is the original code:
- (void)createDirectoryAtPath:(NSString *)path completion:(CDECompletionBlock)completion{
NSMutableURLRequest *request = [self mutableURLRequestForPath:path];
request.HTTPMethod = #"MKCOL";
[request setValue:#"application/xml" forHTTPHeaderField:#"Content-Type"];
[self sendURLRequest:request completion:^(NSError *error, NSInteger statusCode, NSData *responseData) {
if (completion) completion(error);
}];}
And this is the directoryExistsAtPath method:
- (void)directoryExistsAtPath:(NSString *)path completion:(CDEDirectoryExistenceCallback)completion{
[self sendPropertyFindRequestForPath:path depth:0 completion:^(NSError *error, NSInteger statusCode, NSData *responseData) {
if (error && statusCode != 404) {
if (completion) completion(NO, error);
}
else if (statusCode == 404) {
if (completion) completion(NO, nil);
}
else {
CDEWebDavResponseParser *parser = [[CDEWebDavResponseParser alloc] initWithData:responseData];
BOOL succeeded = [parser parse:&error];
if (!succeeded) {
if (completion) completion(NO, error);
return;
}
BOOL isDir = [parser.cloudItems.lastObject isKindOfClass:[CDECloudDirectory class]];
if (completion) completion(isDir, nil);
}
}];}
If i replace the first parameter (currently the isDir-variable) in the completion block at the end to YES, the 405 error does not appear.On logging the parser.clouditems.lastobject, I find that it is often (or always?) empty). So setting the parameter to YES, results in data being uploaded to my webdav, and the folders are in place. However, testing on a second unit (or reinstalling the app on the same unit), download never happens - the downloadFromPath never gets called, a "GET"-request is never sent.
Looking at the calling code in the underlying framework (CDECloudmanager mostly) hasn't led me anywhere so far.
As the directoryExistsAtPath is optional, i tried commenting it out, but i don't think it made a difference.
Another thing I noticed is that I get several baseline files in the baselines folder. According to the Ensembles documentation, there should only be one.
Any clues?
Ok seems I got this working at last. I had to make a change in the CDEWebDavCloudFileSystem class to avoid the 405 error. Doing that however, I encountered a 404 error. That one was solved by configuring the IIS webdav.
So step 1, I changed the request in the sendPropertyFindRequestForPath method:
new code:
static NSString *xml = #"<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:propfind xmlns:D=\"DAV:\"><D:allprop/></D:propfind>";
original code:
static NSString *xml = #"<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:href/><D:resourcetype/><D:creationdate/><D:getlastmodified/><D:getcontentlength/><D:response/></D:prop></D:propfind>";
To remove the 404 error appearing after that I had to add mime type application/xml to the .cdeevent extension.
This link goes into detail of how to configure IIS for that:
http://anandthearchitect.com/2013/08/01/webdav-404file-or-directory-not-found/

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")

How to fix NSURLErrorDomain error -1012 in authentication?

I'm dealing the authenticate issue with Tumblr account using [NSURLConnection sendAsynchronousRequest:queue:completionHandler:] to send the authenticate request, but here I meet a tough problem:
Whenever I send the request at the first time, everything goes perfectly, but when the first authentication is done and then resend the request second time, there comes "NSURLErrorDomain error -1012".
The authenticate page is loaded in a webview so that the authentication should be done in my app without a browser. But it is interesting that if the process runs in a browser there comes no error, errors only happen when using webview.
It was weird that the authentication goes with the same code, but only the first authentication can be done, only if I reinstall the app can I authenticate it again, and after this the problem comes again.
I did everything I can chase to solve the issue, I clean the cache and cookie in webview, step the authentication process to see parameters, set the cachePolicy of the request but nothing helps.
I also found that on ios6 the process goes without any error. But on ios7 I get the -1012.
code -1012 tells me that the user cancelled the authentication, but the process goes automatically and I do not cancel it.
I'm wondering if the problem comes from the NSURLConnection.
- (void)authenticate:(NSString *)URLScheme WithViewController:(UIViewController *)con callback:(TMAuthenticationCallback)callback {
self.threeLeggedOAuthTokenSecret = nil;
self.hostViewController = con;
self.callback = callback;
[self emptyCookieJar];
NSString *tokenRequestURLString = [NSString stringWithFormat:#"http://www.tumblr.com/oauth/request_token?oauth_callback=%#", TMURLEncode([NSString stringWithFormat:#"%#://tumblr-authorize", URLScheme])];
NSLog(#"%#", tokenRequestURLString);
NSMutableURLRequest *request = mutableRequestWithURLString(tokenRequestURLString);
NSLog(#"%#", request);
[[self class] signRequest:request withParameters:nil consumerKey:self.OAuthConsumerKey
consumerSecret:self.OAuthConsumerSecret token:nil tokenSecret:nil];
[self openOAuthViewController];
NSURLConnectionCompletionHandler handler = ^(NSURLResponse *response, NSData *data, NSError *error) {
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
if (error) {
if (callback) {
callback(nil, nil, error);
}
return;
}
NSLog(#"%d", statusCode);
if (statusCode == 200) {
self.threeLeggedOAuthCallback = callback;
NSDictionary *responseParameters = formEncodedDataToDictionary(data);
self.threeLeggedOAuthTokenSecret = responseParameters[#"oauth_token_secret"];
NSURL *authURL = [NSURL URLWithString:
[NSString stringWithFormat:#"http://www.tumblr.com/oauth/authorize?oauth_token=%#",
responseParameters[#"oauth_token"]]];
[self initOAuthViewControllerWithURL:authURL];
} else {
if (callback) {
callback(nil, nil, errorWithStatusCode(statusCode));
}
}
};
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:handler];
}
Code above, everything goes normally before [NSURLConnection sendAsynchronousRequest:queue:completionHandler:],and after this method I got the error in completionHandler.

objective c ios 7 get data from rest api

i want to get all articles from the shopware api(http://wiki.shopware.de/Shopware-API_cat_919.html)
but the i dont get the data into an NSDictionary
url i call: http://myshop.com/api/articles
here is the source i got
NSURL *url = [NSURL URLWithString:weburl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response,
NSData *data, NSError *connectionError) {
if (data.length > 0 && connectionError == nil) {
NSDictionary *rest_data = [NSJSONSerialization JSONObjectWithData:data
options:0
error:NULL];
_newsDataForTable = [NSMutableArray array];
NSDictionary *news;
for (id key in rest_data[#"postalcodes"]) {
news = [rest_data[#"postalcodes"] objectForKey:key];
}
int iterator = 0;
for (id key in news) {
[_newsDataForTable insertObject:key[#"title"] atIndex:iterator];
iterator++;
}
[_newsTable reloadData];
[_newsTable numberOfRowsInSection:[_newsDataForTable count]];
[_newsTable reloadRowsAtIndexPaths:0 withRowAnimation:UITableViewRowAnimationLeft];
}
}];
}
There are a couple of things in your approach that could use improvement.
First, this is performing networking on the main queue. That is a no-no, wether the networking is synchronous or not. Creating a new NSOperationQueue for your connections and passing that instead of [NSOperationQueue mainQueue] is a huge improvement.
Second, the error handling is incorrect. In general the correct error handling pattern for Objective-C is to check wether a call resulted in the expected result before using the error. In this case, it's the NSURLResponse that should be checked, not the data. NSURLConnection may be able to connect to the remove service just fine but get no data back - and for many HTTP requests this is expected, correct behavior. If there is a problem connecting, the NSURLResponse will be nil. Check wether the response is nil, if it is then handle the error.
You're also not checking the HTTP response status code or MIME type. The server could respond with a 500, indicating a server error, or could mistakenly send you HTML (which would give the JSON parser fits).
A verbose example that does the above correctly is here. :
[NSURLConnection sendAsynchronousRequest:request queue:[self connectionQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (response != nil){
if ([[self acceptableStatusCodes] containsIndex:[(NSHTTPURLResponse *)response statusCode] ]){
// The server responded with an HTTP status code that indicates success
if ([[self acceptableMIMETypes] containsObject:[[response MIMEType] lowerCaseString] ]){
// The server responded with a MIME type we can understand.
if ([data length] > 0){
NSError *jsonError = nil;
id jsonObject = nil;
// The server provided data in the response, which means we can attempt to parse it
// Note that we are not specifying NSJSONReadingMutableContainers or NSJSONReadingMutableLeaves, as this would result in
// an object that is not safe to use across threads.
jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
if (jsonObject != nil){
// The JSON parser successfully parsed the data, and returned an object. There is nothing to tell us what kind of object was returned.
// We need to make sure it responds to the selectors we will be using - ideally, we'd pass this object to a method that takes an
// id parameter, not NSDictionary, and inside that method it would check wether the id object responds to the specific selectors
// it is going to use on it.
if ([jsonObject respondsToSelector:#selector(dictionaryWithDictionary:)]){
[self doStuffWithDictionary:jsonObject];
}
} else {
// The JSON parser was unable to understand the data we provided, and the error should indicate why.
[self presentError:jsonError];
}
} else {
// The server responded with data that was zero length. How you deal with this is up to your application's needs.
// You may create your own instance of NSError that describes the problem and pass it to your error handling, etc.
}
} else {
// The server response was a MIME type we could not understand. How you handle this is up to you.
}
} else {
// The server response indicates something went wrong: a 401 Not Found, etc.
// It's up to your application to decide what to do about HTTP statuses that indicate failure.
// You may create your own instance of NSError that describes the problem and pass it to your error handling, etc.
}
} else {
// Only inspect the error parameter if the response is nil.
// The error indicates why the URL loading system could not connect to the server.
// It is only valid to use this error if the server could not connect - which is indicated by a nil response
[self presentError:connectionError];
}
}];
// Returns the HTTP status codes we find acceptable.
- (NSIndexSet *) acceptableStatusCodes {
return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 99)];
}
// Returns the mime types we can accept and understand.
- (NSSet *) acceptableMimeTypes {
NSSet *result = nil;
result = [NSSet setWithObjects:#"application/json", #"application/json; charset=utf-8", nil];
return result;
}
// Generic error handling method.
- (void) presentError:(NSError *)error {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
}];
}
Yup, that's a lot of code, and it should be broken into smaller methods - but it illustrates the logic that should be implemented.
The NSError you are getting now
In your comments you indicate that you are getting an NSError with the domain NSURLErrorDomain and code -1002. If you look at NSURLErrors.h, you will see that NSURL errors map to CFURL errors. If you look at CFNetworkErrors.h, you can see that error code -1002 is kCFURLErrorUnsupportedURL. The URL loading system thinks the URL you are using is not a supported type. This is most likely because the scheme of your URL is incorrect, or how you are attempting to pass credentials as part of the URL is incorrect. Elsewhere in your comments you indicate you are passing credentials as follows:
username:apikey:someurl.com/foo/
Which should be more like:
https://username:apikey#someurl.com/foo/
But only if the service you are accessing is using a supported HTTP authentication type (i.e. Basic authentication). Either way, correctly composing the URL will fix the error you are currently seeing.

Resources