Using AFOAuth2Client and AFNetworking on iOS 6 I am able to get an access token, but am unable to access a resource, the server responds with a status code of 401 unauthorized. This is against a custom Rails 3 API backend using doorkeeper as the OAuth provider. The following client ruby code, using the OAuth2 gem, works OK:
client = OAuth2::Client.new(app_id, secret, site: "http://subdomain.example.com/")
access_token = client.password.get_token('username', 'password')
access_token.get('/api/1/products').parsed
The iOS code is as below, in the login button handler I authenticate using the username and password, and store the credentials:
- (IBAction)login:(id)sender {
NSString *username = [usernameField text];
NSString *password = [passwordField text];
NSURL *url = [NSURL URLWithString:kClientBaseURL];
AFOAuth2Client *client = [AFOAuth2Client clientWithBaseURL:url clientID:kClientID secret:kClientSecret];
[client authenticateUsingOAuthWithPath:#"oauth/token"
username:username
password:password
scope:nil
success:^(AFOAuthCredential *credential) {
NSLog(#"Successfully received OAuth credentials %#", credential.accessToken);
[AFOAuthCredential storeCredential:credential
withIdentifier:client.serviceProviderIdentifier];
[self performSegueWithIdentifier:#"LoginSegue" sender:sender];
}
failure:^(NSError *error) {
NSLog(#"Error: %#", error);
[passwordField setText:#""];
}];
}
and I've subclassed AFHTTPClient for my endpoint and in initWithBaseURL it retrieves the credentials and sets the authorization header with the access token:
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (!self) {
return nil;
}
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self setDefaultHeader:#"Accept" value:#"application/json"];
AFOAuthCredential *credential = [AFOAuthCredential retrieveCredentialWithIdentifier:#"subdomain.example.com"];
[self setAuthorizationHeaderWithToken:credential.accessToken];
return self;
}
Is this the correct way to use AFOAuth2Client and AFNetworking? And any idea why this is not working?
Managed to get this working by changing:
AFOAuthCredential *credential = [AFOAuthCredential retrieveCredentialWithIdentifier:#"subdomain.example.com"];
[self setAuthorizationHeaderWithToken:credential.accessToken];
to:
AFOAuthCredential *credential = [AFOAuthCredential retrieveCredentialWithIdentifier:#"subdomain.example.com"];
NSString *authValue = [NSString stringWithFormat:#"Bearer %#", credential.accessToken];
[self setDefaultHeader:#"Authorization" value:authValue];
UPDATE
What I had failed to notice was that AFOAuth2Client is itself a subclass of AFHTTPClient so can be used as the base class of the API class, e.g.:
#interface YFExampleAPIClient : AFOAuth2Client
+ (YFExampleAPIClient *)sharedClient;
/**
*/
- (void)authenticateWithUsernameAndPassword:(NSString *)username
password:(NSString *)password
success:(void (^)(AFOAuthCredential *credential))success
failure:(void (^)(NSError *error))failure;
#end
And the implementation becomes:
#implementation YFExampleAPIClient
+ (YFExampleAPIClient *)sharedClient {
static YFExampleAPIClient *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURL *url = [NSURL URLWithString:kClientBaseURL];
_sharedClient = [YFExampleAPIClient clientWithBaseURL:url clientID:kClientID secret:kClientSecret];
});
return _sharedClient;
}
- (void)authenticateWithUsernameAndPassword:(NSString *)username
password:(NSString *)password
success:(void (^)(AFOAuthCredential *credential))success
failure:(void (^)(NSError *error))failure {
[self authenticateUsingOAuthWithPath:#"oauth/token"
username:username
password:password
scope:nil
success:^(AFOAuthCredential *credential) {
NSLog(#"Successfully received OAuth credentials %#", credential.accessToken);
[self setAuthorizationHeaderWithCredential:credential];
success(credential);
}
failure:^(NSError *error) {
NSLog(#"Error: %#", error);
failure(error);
}];
}
- (id)initWithBaseURL:(NSURL *)url
clientID:(NSString *)clientID
secret:(NSString *)secret {
self = [super initWithBaseURL:url clientID:clientID secret:secret];
if (!self) {
return nil;
}
[self setDefaultHeader:#"Accept" value:#"application/json"];
return self;
}
#end
Note that initWithBaseURL is overridden to set the HTTP accept header.
Full source code is available on GitHub - https://github.com/yellowfeather/rails-saas-ios
Related
I'm sending a request to a server to test a specific situation. The response is a custom 510 http error and the content is the info of the error.
The web service works fine the first time a send the request. The next time I tried to replicate the error the response is nil. But, if I change the request avoiding the error, it works fine and the response is what it is supposed to be.
I'm executing the request with a brand new object each time.
#interface SCBaseConnection()
#property (strong, nonatomic) NSURLSessionDownloadTask *task;
#end
#implementation SCBaseConnection
- (instancetype) initWithUrl:(NSString *)url
path:(NSString *)path
body:(NSString *)body
headers:(NSDictionary *)headers
method:(NSString *)method
requestCode:(NSInteger)requestCode
{
self = [super init];
NSLog(#"%#", headers);
NSURL *uri = [NSURL URLWithString:[NSString stringWithFormat:#"%#%#", url, path]];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:uri];
request.HTTPMethod = method;
if (body) {
request.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];
}
if (headers) {
NSArray *keys = [headers allKeys];
for (NSString *key in keys) {
[request setValue:[headers objectForKey:key] forHTTPHeaderField:key];
}
}
NSURLSession *session = [NSURLSession sessionWithConfiguration: [NSURLSessionConfiguration defaultSessionConfiguration]];
self.task = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
int statusCode = (int)[response getStatusCode];
NSLog(#"%#", #(statusCode));
if (HTTP_UNAUTHORIZED == statusCode) {
[[NSNotificationCenter defaultCenter] postNotificationName:kUnauthorizedHttpRequest object:response];
}
if (error) {
[MCMGeneralUtils logError:error];
NSLog(#"%#", error.userInfo);
NSLog(#"%#", error);
}
NSData *res = [self dataFromFile:location];
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate didConnectionFinished:self
statusCode:statusCode
response:res
requestPath:path
requestCode:requestCode];
});
}];
return self;
}
This is the content of the error.userInfo after the second request.
NSErrorFailingURLKey = "http://192.168.1.201:23111/api/paciente";
NSErrorFailingURLStringKey = "http://192.168.1.201:2311/api/paciente";
NSLocalizedDescription = "The requested URL was not found on this server.";
The first time the request has no errors.
UPDATE
- (IBAction)save:(UIBarButtonItem *)sender
{
MCMPatientNew *patient = [MCMPatientNew new];
patient.name = self.name;
patient.lastname = self.lastname;
patient.fullname = [NSString stringWithFormat:#"%# %#", self.name, self.lastname];
patient.email = self.email;
patient.phones = [self extracPhones];
patient.patientNew = YES;
NSError *error = nil;
if ([patient assertPatient:&error]) {
MCMUser *user = [MCMUser loadUserInManagedContext:self.managedContext];
patient.delegate = self;
[patient storePatientInManagedContext:self.managedContext];
if ([MCMGeneralUtils isInternetRechable]) {
[self presentViewController:self.serverConnectionAlert animated:YES completion:nil];
[patient postPatientWithToken:user.token doctorId:user.userId];
} else {
[self storeInRequestLogWithRequestCode:REQUEST_CODE_PACIENTE_INSERT
appId:patient.appId
ready:YES
inManagedContext:self.managedContext];
[self cancel:nil];
[self postNotificationWithObject:patient];
}
} else {
[self displayErrorMessageWithErrorInfo:error.userInfo];
}
}
UPDATE 2
- (void)postPatientWithToken:(NSString *)accessToken doctorId:(NSNumber *)doctorId
{
NSMutableDictionary *mutable = [NSMutableDictionary dictionaryWithDictionary:[self jsonToPost]];
[mutable setObject:doctorId forKey:#"doctorId"];
NSDictionary *body = #{#"obj" : mutable};
[self connectToServerWithAccessToken:accessToken
body:body
path:PATH_PACIENTE_INSERT
method:HTTP_METHOD_POST
requestCode:REQUEST_CODE_PACIENTE_INSERT
delegate:self];
}
-
- (void)connectToServerWithAccessToken:(NSString *)accessToken
body:(NSDictionary *)body
path:(NSString *)path
method:(NSString *)method
requestCode:(NSInteger)requestCode
delegate:(id<SCBaseConnectionDelegate>)delegate
{
NSString *authenticator = [NSString stringWithFormat:#"Bearer %#", accessToken];
NSDictionary *headers = #{HEADER_CONTENT_TYPE : CONTENT_TYPE_APPLICATION_JSON,
HEADER_AUTHORIZATION : authenticator};
NSString *bodyStr = body ? [SCJson jsonFromDictionary:body] : #"";
SCBaseConnection *connection = [[SCBaseConnection alloc] initWithUrl:API_URL
path:path
body:bodyStr
headers:headers
method:method
requestCode:requestCode];
connection.delegate = delegate;
[connection execute];
}
-
- (BOOL)execute
{
if (self.task) {
[self.task resume];
return YES;
}
return NO;
}
I was trying to implement an AFNetworking client by subclassing AFHTTPClient and setting base path
#define BaseURLString #"http://company.com/api/"
#implementation WineAPIClient
+(id)sharedInstance{
static APIClient *__sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__sharedInstance = [APIClient alloc]initWithBaseURL:[NSURL URLWithString:BaseURLString]];
});
return __sharedInstance;
}
- (id)initWithBaseURL:(NSURL *)url
{
self = [super initWithBaseURL:url];
if(self){
[self setParameterEncoding:AFJSONParameterEncoding];
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
}
return self;
}
#end
This is how i am making request to the server:
[[APIClient sharedInstance] getPath:#"wines"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", responseObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error fetching wines!");
NSLog(#"%#",error);
}];
Now i have a class called LoginViewController which gets token number ones i loged in.
Now my question in how to set the token as an Authorization header in my AFHttpClinet class.
and make rest other request using the Authorization header.
Can any one help me out with this?
You can use methods of AFNetworking
setAuthorizationHeaderWithUsername:password:
+(id)sharedInstance{
static APIClient *__sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__sharedInstance = [APIClient alloc]initWithBaseURL:[NSURL URLWithString:BaseURLString]];
[__sharedInstance setAuthorizationHeaderWithUsername:#"username" password:#"password"];
});
return __sharedInstance;
}
OR
+(id)sharedInstance{
static APIClient *__sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__sharedInstance = [APIClient alloc]initWithBaseURL:[NSURL URLWithString:BaseURLString]];
});
return __sharedInstance;
}
- (void)updateAuthorizationHeaderUsername:(NSString *)username Password:(NSString *)password
{
[self setAuthorizationHeaderWithUsername:username password:password];
}
- (void)updateAuthorizationHeader:(NSString *)token
{
[self setDefaultHeader:#"token" value:token];
}
Try to do like this
[[APIClient sharedInstance] getPath:#"wines"
parameters:#{#"Authorization":TOKEN}
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", responseObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error fetching wines!");
NSLog(#"%#",error);
}];
Where TOKEN is you token
I'm new to iOS and working on a basic app, it's currently working with SSKeychain and AFNetworking to interact with an API. When you log in with the app I retrieve and set the auth_token in my CredentialStore class, I need to send the auth token to the API as a http header to get access. How can I retrieve the token that I'm storing in the CredentialStore class in my HomeViewController.
Here is my CredentialStore:
#import "GFCredentialStore.h"
#import "SSKeychain.h"
#define SERVICE_NAME #"Groupify"
#define AUTH_TOKEN_KEY #"auth_token"
#implementation GFCredentialStore : NSObject
- (BOOL)isLoggedIn {
return [self authToken] != nil;
}
- (void)clearSavedCredentials {
[self setAuthToken:nil];
}
- (NSString *)authToken {
return [self secureValueForKey:AUTH_TOKEN_KEY];
}
- (void)setAuthToken:(NSString *)authToken {
[self setSecureValue:authToken forKey:AUTH_TOKEN_KEY];
[[NSNotificationCenter defaultCenter] postNotificationName:#"token-changed" object:self];
}
- (void)setSecureValue:(NSString *)value forKey:(NSString *)key {
if (value) {
[SSKeychain setPassword:value
forService:SERVICE_NAME
account:key];
} else {
[SSKeychain deletePasswordForService:SERVICE_NAME account:key];
}
}
- (NSString *)secureValueForKey:(NSString *)key {
return [SSKeychain passwordForService:SERVICE_NAME account:key];
}
#end
Here I am trying to retrieve the authToken value and set it to a string so that I can send it as a http header:
#import "GFHomeViewController.h"
#import <AFNetworking.h>
#import "GFCredentialStore.h"
#define kBaseURL "http://localhost:3000/"
#define kHomeURL "newsfeed.json"
#interface GFHomeViewController ()
#end
#implementation GFHomeViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
__weak typeof(self)weakSelf = self;
NSString *urlString = [NSString stringWithFormat:#"%s%s", kBaseURL, kHomeURL];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
[manager.requestSerializer setValue: forHTTPHeaderField:#"auth_token"];
[manager GET:urlString parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"JSON: %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
Assuming SSKeychain is saving and returning values to/from the keychain, I'd think you would simply be able to do something like this
GFCredentialStore *credentialStore = [[CGCredentialStore alloc] init];
if ([credentialStore isLoggedIn]) {
NSString *authToken = [credentialStore authToken];
[manager.requestSerializer setValue:authToken forHTTPHeaderField:#"auth_token"];
} else {
// prompt/display controller for login
}
If I'm missing something in your problem/issue you're having, please clarify your question, and I'll have another go at it.
I'm using AFNetworking, AFOAuth1Client and AFLinkedInOAuth1Client to get the OAuth token from LinkedIn's API. This is all working well.
When I make a call using getPath to v1/people/~ I am receiving a 401, consistently.
If I push all the same values from my code into the LinkedIn console the generated link gives me the basic profile I am after.
What is causing the 401? I have a feeling it is either AFNetworking or my configuration of it.
Also, do you have any suggestions on how to diagnose the underlying issue?
Code below
+ (JJLinkedInClient *)sharedInstance {
DEFINE_SHARED_INSTANCE_USING_BLOCK(^{
return [[self alloc] init];
});
}
- (id)init {
if ( (self = [super init]) ) {
_client = [[AFLinkedInOAuth1Client alloc] initWithBaseURL:[NSURL URLWithString:kJJLinkedInAPIBaseURLString]
key:#"XXXXXXXX"
secret:#"YYYYYYYY"];
// [_client registerHTTPOperationClass:[AFJSONRequestOperation class]];
[_client registerHTTPOperationClass:[AFHTTPRequestOperation class]];
// [_client setDefaultHeader:#"Accept" value:#"application/json"];
}
return self;
}
- (void)authorize:(void(^)())success {
__block JJLinkedInClient *weakSelf = self;
[self.client authorizeUsingOAuthWithRequestTokenPath:#"uas/oauth/requestToken"
userAuthorizationPath:#"uas/oauth/authorize"
callbackURL:[NSURL URLWithString:#"XXXXXXX://linkedin-auth-success"]
accessTokenPath:#"uas/oauth/accessToken"
accessMethod:#"POST"
success:^(AFOAuth1Token *accessToken) {
NSLog(#"Success: %#", accessToken);
[weakSelf getProfile];
success();
} failure:^(NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)getProfile {
[self.client getPath:#"v1/people/~"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", responseObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%#", error);
}];
}
The problem is in the AFPercentEscapedQueryStringPairMemberFromStringWithEncoding function, inside AFOAuth1Client. It needs to not escape the tilde, and it needs to escape the comma.
Since this is a static function though, I don't think I can override it in AFLinkedInOAuth1Client. I'll follow up with #mattt and see what he says. For now you can change it to this, to get it working:
static NSString * AFPercentEscapedQueryStringPairMemberFromStringWithEncoding(NSString *string, NSStringEncoding encoding) {
static NSString * const kAFCharactersToBeEscaped = #":/?&=;+!##$(),";
static NSString * const kAFCharactersToLeaveUnescaped = #"[].~";
// static NSString * const kAFCharactersToBeEscaped = #":/?&=;+!##$()~";
// static NSString * const kAFCharactersToLeaveUnescaped = #"[].";
return (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)string, (__bridge CFStringRef)kAFCharactersToLeaveUnescaped, (__bridge CFStringRef)kAFCharactersToBeEscaped, CFStringConvertNSStringEncodingToEncoding(encoding));
}
I'm using AFHTTPClient from AFNetworking to make a call from my IOS app to my server, which is using Django with TastyPie. It's working great when I turn authentication off on the server side; however, when I require authentication and insert the proper username and password into my code, the I receive the following 401 authentication error:
\2012-09-16 00:24:37.877 RESTtest[76909:f803]
Complex AF: Error Domain=AFNetworkingErrorDomain Code=-1011
"Expected status code in (200-299), got 401"
UserInfo=0x686ba00 {AFNetworkingOperationFailingURLResponseErrorKey=<NSHTTPURLResponse: 0x686f130>,
NSErrorFailingURLKey=http://127.0.0.1:8000/api/v1/shoppinglist,
NSLocalizedDescription=Expected status code in (200-299), got 401,
AFNetworkingOperationFailingURLRequestErrorKey=<NSMutableURLRequest http://127.0.0.1:8000/api/v1/shoppinglist>}
Here is my code:
AFAPIClient.h
#import "AFHTTPClient.h"
#interface AFAPIClient : AFHTTPClient
-(void)setUsername:(NSString *)username andPassword:(NSString *)password;
+ (AFAPIClient *)sharedClient;
#end
AFAPIClient.m:
#import "AFAPIClient.h"
#import "AFJSONRequestOperation.h"
static NSString * const baseURL = #"http://#127.0.0.1:8000/api/v1";
#implementation AFAPIClient
+ (AFAPIClient *)sharedClient {
static AFAPIClient *_sharedClient = nil;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
_sharedClient = [[AFAPIClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];
//[_sharedClient setAuthorizationHeaderWithUsername:#"myusername" password:#"mypassword"]; I tried putting the authorization command here
});
return _sharedClient;
};
#pragma mark - Methods
-(void)setUsername:(NSString *)username andPassword:(NSString *)password;
{
[self clearAuthorizationHeader];
[self setAuthorizationHeaderWithUsername:username password:password];
}
- (id)initWithBaseURL:(NSURL *)url
{
self = [super initWithBaseURL:url];
if (!self) {
return nil;
}
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self setParameterEncoding:AFJSONParameterEncoding];
//[self setAuthorizationHeaderWithUsername:#"myusername" password:#"mypassword"]; I also tried putting the authorization command here
// Accept HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
[self setDefaultHeader:#"Accept" value:#"application/json"];
return self;
}
#end
TQViewController.h:
[...]
- (IBAction)sendAFClientRequest:(id)sender {
//[[AFAPIClient sharedClient] setUsername:#"myusername" andPassword:#"mypassword"];
[[AFAPIClient sharedClient] getPath:#"shoppinglist" parameters:nil success:^(AFHTTPRequestOperation *operation, id response) {
NSLog(#"Complex AF: %#", [response valueForKeyPath:#"objects"]);
} failure:^(AFHTTPRequestOperation *operation, id response) {
NSLog(#"Complex AF: %#", response);
}
];
}
[...]
I know this isn't a problem with my server or my username/password, as I can authenticate just fine by inserting the username/password into the URL:
#"http://myusername:mypassword#127.0.0.1:8000/api/v1/shoppinglist"
Any help on this would be great. It would be wonderful to be able to use AFHTTPClient without inserting the authentication information directly into the static base URL, which seems completely improper. Thanks in advance!
Based on this: https://github.com/AFNetworking/AFNetworking/issues/426
I override the - (void)getPath:(NSString *)path parameters... method in the AFHTTPClient
subclass to look something like this:
- (void)getPath:(NSString *)path parameters:(NSDictionary *)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
NSURLRequest *request = [self requestWithMethod:#"GET" path:path parameters:parameters];
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithRequest:request success:success failure:failure];
[operation setAuthenticationChallengeBlock:^(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge) {
NSURLCredential *newCredential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
[challenge.sender useCredential:newCredential forAuthenticationChallenge:challenge];
}];
[self enqueueHTTPRequestOperation:operation];
}
It only adds the authentication challenge block to the AFHTTPRequestOpertaion, the rest is the same as the original implementation https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFHTTPClient.m