I'm a web developer new to iOS development and I'm trying to tie over the client/server Twitter OAuth process over to an iPhone app.
I want use a passport Twitter strategy on a node.js server for my authentication. So in a web client setting I make a request to a twitter authorization URL I've set up on the server, and the server responds with a redirect that takes me to the Twitter Login page w/ the access token provided. This redirect URL is automatically handled by the passport Twitter strategy.
How do I create the same web redirect effect within the context of an app using AFNetworking. I know how to make a basic POST and GET requests, but is there some setting where AFNetworking can handle the server response by opening safari and redirecting to the link provided?
The code I provided below doesn't throw any errors, but I doesn't redirect...
iOS Code:
- (IBAction)twitterLogin:(id)sender {
NSLog(#"Twitter Login");
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
[manager GET:#"http://54.148.118.153/auth/twitterX/" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"JSON: %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
Node.js server code:
var TwitterStrategy = require('passport-twitter').Strategy;
// twitter authentication and login
app.get('/auth/twitterX', passport.authenticate('twitter'));
// handle callback after twitter has authenticated user
app.get('/auth/twitterX/callback',
passport.authenticate('twitter',{
successRedirect: '/',
failureRedirect: '/'
}
));
// used to serialize user
passport.serializeUser(function(user,done){
done(null, user.twitter_id);
});
// used to deserialize the user
passport.deserializeUser(function(twitter_id,done){
connection.query('SELECT * from User_Twitter where id = '+twitter_id, function(err,rows){
done(err,rows[0]);
});
});
// passport Twitter protocol
passport.use(new TwitterStrategy({
consumerKey:'G17pGaKQUYPPsy5o5H7siZvWj',
consumerSecret:'qiTVnzHsIhWDqlkPd4vF2Xao7L9wvAh08YGpwOpXb5CowesqIb',
callbackURL:'http://54.148.118.153/auth/twitterX/callback'
},
function(token, tokenSecret, profile,done){
//Make code asynchronous - MySQL query won't fire until we have all data back from twitter
process.nextTick(function(){
//Do stuff with Twitter data
})
}
Let me introduce you to AFOAuth2Client, maintained by the same folks who maintain AFNetworking: https://github.com/AFNetworking/AFOAuth2Client
I am going to paste their example code documentation here:
NSURL *url = [NSURL URLWithString:#"http://example.com/"];
AFOAuth2Client *oauthClient = [AFOAuth2Client clientWithBaseURL:url clientID:kClientID secret:kClientSecret];
[oauthClient authenticateUsingOAuthWithPath:#"/oauth/token"
username:#"username"
password:#"password"
scope:#"email"
success:^(AFOAuthCredential *credential) {
NSLog(#"I have a token! %#", credential.accessToken);
[AFOAuthCredential storeCredential:credential withIdentifier:oauthClient.serviceProviderIdentifier];
}
failure:^(NSError *error) {
NSLog(#"Error: %#", error);
}];
Check out that NSLog statement in that success block: once you get that token, you can replace that NSLog statement with whatever storage solution you want to keep the token on the client.
Also, if you have not checked it out yet, I would highly recommend you use CocoaPods as your go-to dependency manager. That way, with one line you can bring in a dependency like AFOAuth2Client: http://cocoapods.org
Related
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
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).
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")
I know for method
-[[RKObjectManager sharedManager].HTTPClient setAuthorizationHeaderWithToken:]
and I suppose that this method accepts accessToken. How to handle OAuth2 refreshToken?
Ok, I resolve my problem.
First of all, you have to download AFOAuth2Client from http://goo.gl/zdTdlJ
This is AFHTTPClient extension witch handles OAuth2 authentication and here's snippet how to initialize it and set it into RestKit as HTTPClient.
AFOAuthCredential* credential = [AFOAuthCredential credentialWithOAuthToken:accessToken tokenType:kAFOAuthCodeGrantType];
[credential setRefreshToken:refreshToken expiration:expirationDate];
AFOAuth2Client* oAuth2Client = [AFOAuth2Client clientWithBaseURL:[NSURL URLWithString:baseUrl]
clientID:clientID
secret:secret];
[oAuth2Client setAuthorizationHeaderWithToken:accessToken];
// This line is for environment where you don't have valid certificate.
// For example for development environment
[oAuth2Client setAllowsInvalidSSLCertificate:YES];
[[RKObjectManager sharedManager] setHTTPClient:oAuth2Client];
[[RKObjectManager sharedManager] getObjectsAtPath:path parameters:nil
success:^(
RKObjectRequestOperation* operation, RKMappingResult* mappingResult) {
// Handle success response
} failure:^(RKObjectRequestOperation* operation, NSError* error) {
// Handle failure response
}];
When logging a user into my application I need to pull a user object down from the server using only the username. This returns the userId (among other things) that I need in order to make other API calls. From that point I'll make a couple other HTTP calls using the userId. How can I make a synchronous call to completely pull down the user object before sending the other calls?
I've setup my object mapping in my app delegate class, which works perfectly, and am using this code to pull the user object down from the server:
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:[#"/api/users/" stringByAppendingString:[_userNameField text]] delegate:self];
This is what I've tried... as suggested here: Making synchronous calls with RestKit
RKObjectLoader* loader = [[RKObjectManager sharedManager] objectLoaderForObject:currentUser method:RKRequestMethodPUT delegate:nil];
RKResponse* response = [loader sendSynchronously];
However this code (1) uses the deprecated method objectLoaderForObject and (2) crashes saying 'Unable to find a routable path for object of type '(null)' for HTTP Method 'POST''.
Putting aside the question of whether this is the ideal design for an iPhone application, I was able to accomplish what I was hoping using blocks.
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:[#"/api/users/" stringByAppendingString:[_userNameField text]] usingBlock:^(RKObjectLoader* loader) {
loader.onDidLoadResponse = ^(RKResponse *response) {
NSLog(#"Response: \n%#", [response bodyAsString]);
};
loader.onDidLoadObjects = ^(NSArray *objects) {
APIUser *apiUser = [objects objectAtIndex:0];
NSLog(#"user_id is %i", apiUser.user_id);
};
loader.onDidFailWithError = ^(NSError *error) {
UIAlertView *badLoginAlert = [[UIAlertView alloc]initWithTitle:NSLocalizedString(#"LOGIN_FAILED", nil)
message:NSLocalizedString(#"BAD PASSWORD OR USERNAME", nil)
delegate:self
cancelButtonTitle:NSLocalizedString(#"OK", nil)
otherButtonTitles:nil];
[badLoginAlert show];
};
}];
Hope this helps someone.