OAuth 2 bearer Authorization header - ios

With an update to the client's API the HTTPBasicAuthication method has been replace with a OAuth2 Bearer Authorization header.
With the old API I would do the following:
NSURLCredential *credential = [NSURLCredential credentialWithUser:self.account.username
password:self.account.token
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *space = [[NSURLProtectionSpace alloc] initWithHost:kAPIHost
port:443
protocol:NSURLProtectionSpaceHTTPS
realm:#"my-api"
authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
But this will not work with the Bearer header.
Now normally I would just add the header my self by adding it like so:
NSString *authorization = [NSString stringWithFormat:#"Bearer %#",self.account.token];
[urlRequest setValue:authorization forHTTPHeaderField:#"Authorization"];
But the problem with this solutions is that the API redirect most of the calls to other URLs, this has to do with security.
After the NSURLRequest gets redirected the Authorization header is removed from the request and since I'm unable to add the Bearer method to the NSURLCredentialStorage it can't authenticate any more after being redirected.
What would be a good solutions? I can only think to catch the redirect and modify the NSURLRequest so it does include the Bearer header. But how?

Well after much research I found out that I will just have to replace the NSURLRequest when a call is redirected.
Not as nice as I would like it to be, but is does work.
I used AFNetworking and added the redirect block, then check wether the Authorization header is still set if not I create a new NSMutableURLRequest and set all the properties to match the old request (I know I could have just created a mutable copy):
[requestOperation setRedirectResponseBlock:^NSURLRequest *(NSURLConnection *connection, NSURLRequest *request, NSURLResponse *redirectResponse) {
if ([request.allHTTPHeaderFields objectForKey:#"Authorization"] != nil) {
return request;
}
NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:request.URL cachePolicy:request.cachePolicy timeoutInterval:request.timeoutInterval];
NSString *authValue = [NSString stringWithFormat:#"Bearer %#", self.account.token];
[urlRequest setValue:authValue forHTTPHeaderField:#"Authorization"];
return urlRequest;
}];

I'm using AFNetworking Library
Find AFHttpClient.m and you have a method
- (void)setAuthorizationHeaderWithToken:(NSString *)token {
[self setDefaultHeader:#"Authorization" value:[NSString stringWithFormat:#"Token token=\"%#\"", token]];
}
replace this method with the following or if you need it for back compatibility keep it an add with a different name and use that name
- (void)setAuthorizationHeaderWithToken:(NSString *)token {
[self setDefaultHeader:#"Authorization" value:[NSString stringWithFormat:#"Bearer %#", token]];
}
then make the request with oauth access token. (Following is a GET method service)
NSURL *url = [EFServiceUrlProvider getServiceUrlForMethod:methodName];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
[httpClient setAuthorizationHeaderWithToken:#"add your access token here"];
[httpClient getPath:#"" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSString *response = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//
}];
Updated
Use Oauth2 Client on AFNetworking written by matt
https://github.com/AFNetworking/AFOAuth2Client

If you happen to be having this issue with Django rest framework and the routers the problem might be related to the trailing slash being clipped by the NSUrlRequest. if the trailing slash is clipped then django will have to redirect your request, to avoid this you can use Trailing_slash = True like this
router = routers.DefaultRouter(trailing_slash=False)
That way not your authorization header nor your parameters will get lost.
Hope this saves somebody some time.

Related

How to implement custom HTTP header RESTKit

I'm using RESTKit to implement a GET request and with that request I want to have a custom http header. In order for the GET request to get the required data, I need to send up a token (as a variable) which I have in the header. However, when I look in the console for a response, it gives me a 401 status code, meaning that the website is not getting the custom http header. What exactly am I doing wrong that causes the custom header to not work.
Here is my code:
NSIndexSet *statusCodeSet = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful);
NSString *urlString = [NSString stringWithFormat:#"https://foo.com/foo/:foo_number/providers/find?name=%#&location=%#", nameIDTextField.text, locationTextField.text];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//Here is my custom header code
RKObjectManager *objectManager = [RKObjectManager managerWithBaseURL:url];
[objectManager.HTTPClient setDefaultHeader:#"Auth-Token" value:[[self userAuthTokenMethod] userAuthToken]];
//End of custom header code
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
NSLog(#"Stuff Here ==> %#", connection);
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:[MappingProvider tokenMapping]
pathPattern:#"/v2/styles"
keyPath:#"data"
statusCodes:statusCodeSet];
NSURLRequest *doctorRequest = [NSURLRequest requestWithURL:url];
RKObjectRequestOperation *operation = [[RKObjectRequestOperation alloc] initWithRequest:doctorRequest
responseDescriptors:#[responseDescriptor]];
[operation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult)
{
NSLog(#"Mapping Results ==> %#", mappingResult.array);
}
failure:^(RKObjectRequestOperation *operation, NSError *error)
{
NSLog(#"ERROR: %#", error);
NSLog(#"Response: %#", operation.HTTPRequestOperation.responseString);
}];
[operation start];
Edit:
Here is the part of the error log:
2013-08-01 22:42:58.547 Empyrean[78461:5803] E restkit.network:RKObjectRequestOperation.m:576 Object request failed: Underlying HTTP request
operation failed with error: Error Domain=org.restkit.RestKit.ErrorDomain Code=-1011 "Expected status code in (200-299), got 401"
If you find any other problems with the code, please feel free to point them out.
You can probably fix your 401-problem most easily by using a NSMutableURLRequest like so:
NSMutableURLRequest *doctorRequest = [NSMutableURLRequest requestWithURL:url];
[doctorRequest setValue:[[self userAuthTokenMethod] userAuthToken] forHTTPHeaderField:#"Auth-Token"];
Apart from that: The request, objectManager and connection objects are not really used in that part of the code. And it probably would be best to set the header field by default for all requests as you tried with the objectManager. But then you need to get the HTTPClient of the objectManager to create your request for you.
The problem is that you are setting the header on the object managers HTTP client and then not using the object manager, because you're creating your URL request explicitly and then an operation to run it. Choose 1 of the 2 options and stick with it. If you create the request yourself then you also need to add the headers to it.

How to add an Auth Token in every request using AFIncrementalStore?

I have an iOS + Rails 3.1 app, and I'm using AFIncrementalStore for the client-server communication.
I have implemented Token Authentication on my Rails server according to this tutorial: http://matteomelani.wordpress.com/2011/10/17/authentication-for-mobile-devices/
I now want to include the &auth_token=XXXXXXXX in every request from client to server, including POST requests. How would I do that? I haven't found the solution in this related post: Using AFIncrementalStore with an Auth token
UPDATE: this is my first code attempt, but doesn't seem to send the auth_token:
(inside my AFIncrementalStoreHTTPClient sub-class)
- (NSMutableURLRequest *)requestForFetchRequest:(NSFetchRequest *)fetchRequest withContext:(NSManagedObjectContext *)context {
NSMutableURLRequest *request = [[super requestForFetchRequest:fetchRequest withContext:context] mutableCopy];
NSMutableString *requestBody = [[NSMutableString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding];
[requestBody appendFormat:#"&%#=%#", #"auth_token", #"xkT2eqqdoNp5y4vQy7xA"];
[request setHTTPBody:[requestBody dataUsingEncoding:NSUTF8StringEncoding]];
return request;
}
UPDATE: I skimmed your question (sorry!), and my sample code below works for a regular AFHTTPClient, but not AFIncrementalStore. The same basic approach will work, though, and there's sample code at this answer that should point you in the right direction.
You can't just append &auth_token=whatever to the end of your HTTP body in all cases.
You probably want to override your getPath... and postPath... methods with something like:
- (void)getPath:(NSString *)path
parameters:(NSDictionary *)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
if (parameters) {
// Make a mutable copy and add the "token" parameter to the dictionary
NSMutableDictionary *mutableParams = [parameters mutableCopy];
[mutableParams setObject:#"whatever" forKey:#"token"];
parameters = [NSDictionary dictionaryWithDictionary:mutableParams];
} else {
parameters = #{#"token" : #"whatever"};
}
[super getPath:path parameters:parameters success:success failure:failure];
}
This approach will allow AFNetworking to appropriately encode your parameters depending on your specific request and encoding settings.
If you are rolling your own AFHTTPRequestOperation objects instead of using the convenience methods (you probably aren't), just make sure you include the token in parameters before you create your NSURLRequest like so:
NSURLRequest *request = [self requestWithMethod:#"GET" path:path parameters:parameters];

NSURLConnection only authenticates on second attempt

I am trying to load a secure website in a UIWebView my basic approach is to create a NSURL, the n a NSURLRequest, then a NSURLConnection, then to load the NSURLRequest in the UIWebView. When the website is loaded I receive
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
I respond to the challenge sender with
- (void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
But after that I get nothing... it just hangs. I put in break points so I know that
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
is being called. If I wait till I am sure that the NSURLConnection is not going to complete then reload the view no authentication challenge is sent but the view will load. I do not have any control over the server. I am open to using AFNetworking, but only if necessary.
The full listing of source code is provided below:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:
(NSURLAuthenticationChallenge *)challenge
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if ([challenge previousFailureCount] == 0)
{
NSString *username = #"username";
NSString *password = #"passsword";
NSURLCredential * cred = [NSURLCredential credentialWithUser:username
password:password
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:cred forAuthenticationChallenge:challenge];
}
else
{
}
}
-(void)updateCard
{
NSURL * url = [NSURL URLWithString:#"https://ssl.letu.edu/applications/chapelattendance/attendance.html"];
NSURLRequest * request = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:50.0];
self.webView =[[UIWebView alloc] initWithFrame:self.bounds];
self.webView.delegate = self;
[self.webView loadRequest:request];
self.connection = [[ NSURLConnection alloc]initWithRequest:request delegate:self];
[self.connection start];
}
Where did I go wrong?
You need to first retrieve the "authentication method" the server is requesting for:
[[challenge protectionSpace] authenticationMethod]
These are the authentication methods (which are string constants) which the expression above returns:
NSURLAuthenticationMethodDefault
NSURLAuthenticationMethodHTTPBasic
NSURLAuthenticationMethodHTTPDigest
NSURLAuthenticationMethodHTMLForm
NSURLAuthenticationMethodNegotiate
NSURLAuthenticationMethodNTLM
NSURLAuthenticationMethodClientCertificate
NSURLAuthenticationMethodServerTrust
Then, you have these options:
If you want to provide the credentials for the given authentication method, you invoke
useCredential:forAuthenticationChallenge:
If you don't want to handle that authentication method yourself and want the system try
to authenticate, you may invoke performDefaultHandlingForAuthenticationChallenge:
which may then fail or not, depending whether the system is capable to handle that type
of authentication and whether it can find credentials in well known storages.
If you cant handle that authentication method -- say authentication method
NSURLAuthenticationMethodNTLM for example -- you can skip this protection
space and try another protection space if another one
exists in this authentication challenge. Then you may possibly get an
authentication method NSURLAuthenticationMethodHTTPBasic which you
are capable to handle.
In order to reject the current protection space you send method
rejectProtectionSpaceAndContinueWithChallenge: to the
authentication challenge sender. Then, NSURLConnection will send
once again willSendRequestForAuthenticationChallenge: to your
delegate with another protection space if any further exists.
You may try to continue without providing credentials at all.
Likely, the authentication will fail. You can try it through
sending message continueWithoutCredentialForAuthenticationChallenge:
to the authentication challenge sender.
And finally, you can cancel the request through canceling the
authentication challenge: send cancelAuthenticationChallenge: to
the authentication challenge sender.
Note: NSURLAuthenticationMethodHTTPBasic and NSURLAuthenticationMethodHTTPDigest authentication methods can be handled with the same NSURLCredential object created with +credentialWithUser:password:persistence:
If anyone comes along and has the same problem be sure I want to share the solution I found. Use AFNetworking.
Here is the revised code:
-(void)updateCard
{
if(!self.webView)
{
self.webView =[[UIWebView alloc] initWithFrame:self.bounds];
self.webView.delegate = self;
}
NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
NSString *username = #"username";
NSString *password = #"password";
NSURL *url = [NSURL URLWithString:#"https://ssl.letu.edu/"];
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL: url];
[client setAuthorizationHeaderWithUsername:username password:password];
NSMutableURLRequest *request = [client requestWithMethod:#"GET" path:#"applications/chapelattendance/attendance.html"
parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
[self.webView loadRequest:request];
}
failure: ^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"Could not load chapel attendance");
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
}
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:urlRequest];
operation.securityPolicy.allowInvalidCertificates = YES;
You need to send the username and password combination with the http header to authenticate the request while sending the same.
NSData *authData = [#"username:password" dataUsingEncoding:NSASCIIStringEncoding];
NSString *authorization = [NSString stringWithFormat:#"Basic %#", [authData base64Encoding]];
[mutableRequest addValue:authorization forHTTPHeaderField:#"Authorization"];

POST Request returning unwanted cached results (AFNetworking)

I'm using AFNetworking to make POST requests from a shared "authenticator" class that passes in a user's username and password. Here is the POST request that I make:
NSURL *url = [NSURL URLWithString:#"https://www..."];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
// params
NSDictionary* dict = #{#"loginName": username,
#"password": password,
#"serviceName": #"...",
#"serviceURL": #"...",
#"action": #"..."};
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
request = [httpClient requestWithMethod:#"POST" path:#"..." parameters:dict];
request.cachePolicy = NSURLRequestReloadIgnoringCacheData;
request.timeoutInterval = 30.0;
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", operation.responseString);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"login failed");
}];
[operation start];
It works very well for the first login and everything returns as expected. When I attempt to login with a different username/password, I see that the output of the operation.responseString is the exact same output as the first login.
Does anyone know why it is returning the output from the first login? I feel that the response is a cached response and I had added the following to try to prevent the return of cached information:
request.cachePolicy = NSURLRequestReloadIgnoringCacheData;
I have set breakpoints to see that the username and password in the NSDictionary for the parameters are the new username/password combination.
The string literals are not manipulated in anyway as well and are the same in every POST request. The elipses are for privacy and are placeholders for strings with semantic meaning.
Try instead
request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
because the NSURLRequestReloadIgnoringCacheData only ignores local cache data and not caches out on the network.
Edit: As Steve Madsen points out below, this was not the real problem, and, in general, responses to POST requests are not cached in any case. The actual problem was that the program didn't log out between two logins, by mistake. But we did fix it in the end!
I have same problem, and fixed finally. Using the method:
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0
diskCapacity:0
diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
from this blog post :How Does Caching Work in AFNetworking? : AFImageCache & NSUrlCache Explained
Try it
[request setHTTPShouldHandleCookies:NO];

Posting a serialized object with AFNetworking failing

I have a a data object, called DataElement. It contains a string of Base64 converted image bytes, along with a couple of other fields.
I am trying to post this to my wcf service and am getting an error 'Expected status code in (200-299), got 400.
The goal is to post data + an image to the WCF (rest) service, and get a modified image back- an end to end test of what I am working on.
In my post method, if I leave the encoded string empty on the object everything works just fine- but if that string is anything other than empty I get this error.
My WCF service isn't even being hit, it just bombs right to the error. Here is my post method... what am I doing wrong?
- (void)postDataToServer:(NSString*)server dataElement:(DataElement*)dataElement asJson:(BOOL)useJson
{
NSString *urlString = [[NSString alloc] init];
NSData *encodedData;
urlString = [[server copy] stringByAppendingString:#"EchoXml"];
encodedData = [self encodeDataElementAsXml:dataElement];
NSURL *url = [NSURL URLWithString:urlString];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:urlString parameters:nil];
[request setHTTPBody:encodedData];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
[_responseTextView setText:[NSString stringWithFormat:#"Successfully uploaded file to %#", urlString]];
NSObject *httpResponseObject;
httpResponseObject = [self parseResponseAsXml:responseObject];
if ([httpResponseObject isKindOfClass:[DataElement class]])
{
DataElement *dataElement = (DataElement *)httpResponseObject;
_responseTextView.text = dataElement.DataText;
if (dataElement.DataImageBase64 != nil)
{
UIImage *dataImage = [self getImageFromString:dataElement.DataImageBase64];
self.responseImageView.image = dataImage;
}
}
NSLog(#"Successfully uploaded file to %#", urlString);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// It goes here immediately
[_responseTextView setText:[NSString stringWithFormat:#"Error: %#", error]];
NSLog(#"Error: %#", error);
}];
[operation start];
}
Edit: Sorry the formatting got wonky when I pasted it in...
The important parts of your code are:
NSString* urlString = [server stringByAppendingString:#"EchoXml"];
NSURL *url = [NSURL URLWithString:urlString];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSMutableURLRequest *request = [httpClient
requestWithMethod:#"POST" path:urlString parameters:nil];
The actual URL that AFNetorking requests is the AFHTTPClient's base URL, with the specified path appended to it.
Your mistake is that you are specifying the same urlString again.
So, if urlString is http://your.server.com/EchoXml, then the effective URL that you're requesting is http://your.server.com/EchoXmlhttp://your.server.com/EchoXml. As you see, that doesn't work.
Fix your base URL and path to be something more appropriate. Since you didn't say what URL you are trying to access, it's hard to give much more detail. Maybe server should be the base URL, and EchoXml the path?
I know its bad form to answer my own question- but I found and fixed the problem. Bottom line, the code I was using above is fine- maybe not optimal (as Kurt pointed out) but it does what it is supposed to do.
The problem was on on my WCF service- REST service requests by default have a 65k upload limit. I reconfigured the service to allow large file uploads and everything is good.

Resources