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.
Related
Hello I would like to know how it's possible to have responseString and responseObject with the new version of AFNetworking.
When I made GET operation I have success response with NSURLSessionDataTask and id responseData.
And I would like to have responseString and responseObject.
Thanks for your help.
there is my code not the full code but it's like that
void(^wsFailure)(NSURLSessionDataTask *, NSError *) = ^(NSURLSessionDataTask *failedOperation, NSError *error) {
NSLog(#"failed %#",failedOperation);
[self failedWithOperation:failedOperation error:error];
};
void (^wsSuccess)(NSURLSessionDataTask *, id) = ^(NSURLSessionDataTask * _Nonnull succeedOperation, id _Nullable responseObject) {
NSLog(#"responseData: %#", responseObject);
NSString *str = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(#"responseData: %#", str);
}}
AFHTTPResponseSerializer *responseSerializer = [self responseSerializerFromResponseType];
AFHTTPRequestSerializer *requestSerializer = [self requestSerializerFromRequestType];
operationManager.requestSerializer = requestSerializer;
operationManager.responseSerializer = responseSerializer;
- (AFHTTPResponseSerializer *)responseSerializerFromResponseType{
if ([self.request.parameters[#"responseType"] isEqualToString:#"xml"]) {
return [AFXMLParserResponseSerializer serializer];
}
else if ([self.request.parameters[#"responseType"] isEqualToString:#"html"]) {
return [AFHTTPResponseSerializer serializer];
}}
Quickly done, I implemented my own ResponseSerializer, which is just a way to encapsulate a AFNetworkingSerializer (~AFHTTPResponseSerializer which is the superclass of the other ones, and respects the AFURLResponseSerialization protocol) which will return a custom serialized object, which will have the 2 properties you want in addition to the NSDictionary/NSArray serialized object: a NSData and a NSString.
.h
#interface CustomResponseSerializer : NSObject <AFURLResponseSerialization>
-(id)initWithResponseSerializer:(id<AFURLResponseSerialization>)serializer;
#end
.m
#interface CustomResponseSerializer()
#property (nonatomic, strong) id<AFURLResponseSerialization> serializer;
#end
#implementation CustomResponseSerializer
-(id)initWithResponseSerializer:(id<AFURLResponseSerialization>)serializer {
self = [super init];
if (self)
{
_serializer = serializer;
}
return self;
}
- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response data:(nullable NSData *)data error:(NSError * _Nullable __autoreleasing * _Nullable)error {
id serialized = nil;
if ([_serializer respondsToSelector:#selector(responseObjectForResponse:data:error:)]) {
NSError *serializationError = nil;
serialized = [_serializer responseObjectForResponse:response data:data error:&serializationError];
}
//You could put NSError *serializationError = nil; before, and set it into the `CustomSerializedObject` `error` property, I didn't check more about AFNetworking and how they handle a parsing error
return [[CustomSerializedObject alloc] initWithData:data
string:[[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding]
object:serialized];
}
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
[coder encodeObject:self.serializer forKey:NSStringFromSelector(#selector(serializer))];
}
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
self = [self init];
if (!self) {
return nil;
}
self.serializer = [coder decodeObjectForKey:NSStringFromSelector(#selector(serializer))];
return self;
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
CustomResponseSerializer *serializer = [[CustomResponseSerializer allocWithZone:zone] init];
serializer.serializer = [self.serializer copyWithZone:zone];
return serializer;
}
#end
And the object:
#interface CustomSerializedObject: NSObject
#property (nonatomic, strong) NSData *rawData;
#property (nonatomic, strong) NSString *string;
#property (nonatomic, strong) id object;
#property (nonatomic, strong) NSError *error; //If needed
-(id)initWithData:(NSData *)data string:(NSString *)string object:(id)object;
#end
#implementation CustomSerializedObject
-(id)initWithData:(NSData *)data string:(NSString *)string object:(id)object {
self = [super init];
if (self)
{
_rawData = data;
_string = string;
_object = object;
}
return self;
}
#end
How to use:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
NSURL *URL = [NSURL URLWithString:#"https://httpbin.org/get"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
CustomResponseSerializer *responseSerializer = [[CustomResponseSerializer alloc] initWithResponseSerializer:[AFJSONResponseSerializer serializer]];
[manager setResponseSerializer: responseSerializer];
NSURLSessionDataTask *task = [manager dataTaskWithRequest:request
uploadProgress:nil
downloadProgress:nil
completionHandler:^(NSURLResponse * _Nonnull response, CustomSerializedObject * _Nullable responseObject, NSError * _Nullable error) {
NSLog(#"Response: %#", response);
NSLog(#"ResponseObject data: %#", responseObject.rawData); //If you want hex string ouptut see https://stackoverflow.com/questions/1305225/best-way-to-serialize-an-nsdata-into-a-hexadeximal-string
NSLog(#"ResponseObject str: %#", responseObject.string);
NSLog(#"ResponseObject object: %#", responseObject.object);
NSLog(#"error: %#", error);
}];
[task resume];
I am struggling to set up a simple stub of a network POST request. I have modeled as much as I can from the OHHTTPStubs docs and other resources online, but I think I must be missing something. I would like to see the stub called based on the logging by the onStubActivation method. My test looks like:
#import "Cedar.h"
#import "OHHTTPStubs.h"
#import "Client.h"
SPEC_BEGIN(Spec)
describe(#"Client", ^{
__block Client *subject;
__block __weak id<OHHTTPStubsDescriptor> stub;
beforeEach(^{
subject = [[Client alloc] init];
stub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
return YES;
} withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) {
return [OHHTTPStubsResponse
responseWithJSONObject:#{}
statusCode:200
headers:#{ #"Access-Token": #"new-token"}];
}];
stub.name = #"success-stub";
[OHHTTPStubs onStubActivation:
^(NSURLRequest *request, id<OHHTTPStubsDescriptor> stub) {
NSLog(#"%# stubbed by %#.", request.URL, stub.name);
}];
});
describe(#"-signInWithUsername:Password:SuccessBlock:FailureBlock:", ^{
subjectAction(^{
[subject signInWithUsername:#"email#domain.com"
Password:#"password"
SuccessBlock:^void(){NSLog(#"GREAT-SUCCESS");}
FailureBlock:^void(){NSLog(#"GREAT-FAILURE");}];
});
context(#"when the user/password is valid", ^{
it(#"should update the auth token", ^{
subject.token should equal(#"new-token");
});
});
});
});
SPEC_END
Client looks like:
#import "Client.h"
#import "AFNetworking.h"
#interface Client ()
#property (nonatomic) NSString *token;
#property (nonatomic) AFHTTPRequestOperationManager *manager;
#end
#implementation Client
- (instancetype)init
{
self = [super init];
self.manager = [[AFHTTPRequestOperationManager alloc] init]];
return self;
}
- (void)signInWithUsername:(NSString *)username
Password:(NSString *)password
SuccessBlock:(void (^)())successBlock
FailureBlock:(void (^)())failureBlock;
{
[self.manager POST:#"http://localhost:3000/auth/sign_in"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"JSON: %#", responseObject);
successBlock();
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
failureBlock();
}];
}
#end
I am having problems in saving a response coming from a POST request.
Based on AFnetworking documentation and NSScreencast tutorial I created my own subclass of AFHTTPRequestOperationManager, but I am not sure why the response is not saved.
How do I know, the response is not saved?
Because there is an error:(null) message in console and the my method does not perform segue.
I know that I am getting the values, because of the breakpoint that I put NSURLSessionDataTask
But I do not know why the values are not saved and I am getting an error message. I appreciate any help.
The APIClient/Manager
AuthAPIManager.h
#import "AFHTTPSessionManager.h"
#interface AuthAPIManager : AFHTTPSessionManager
+(AuthAPIManager *)sharedManager;
-(NSURLSessionDataTask *)initializeLogin:(NSString *)username completion:(void(^)(NSDictionary *results, NSError *error))completion;
//for login
#property (nonatomic,readonly,retain)NSString *StoreIdentifierForVendor;
#property(nonatomic,copy)NSString *devicetype;
#end
AuthAPIManager.m
#import "AuthAPIManager.h"
#import "LoginInfo.h"
#import "CredentialStore.h"
static AuthAPIManager *sharedManager =nil;
static dispatch_once_t onceToken;
#implementation AuthAPIManager
+(AuthAPIManager *)sharedManager
{
dispatch_once(&onceToken, ^{
sharedManager = [[AuthAPIManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]];
sharedManager.responseSerializer=[AFJSONResponseSerializer serializer];
sharedManager.requestSerializer = [AFJSONRequestSerializer serializer];
});
return sharedManager;
}
-(id)initWithBaseURL:(NSURL *)url
{
self = [super initWithBaseURL:url];
if (self) {
}
return self;
}
-(NSURLSessionDataTask *)initializeLogin:(NSString *)username completion:(void (^)(NSDictionary *, NSError *))completion
{
_devicetype = #"ios";
_StoreIdentifierForVendor = [[[UIDevice currentDevice]identifierForVendor]UUIDString];
id loginParameters =#{#"AccountId":username,
#"DeviceType":_devicetype
};
NSURLSessionDataTask *task =[self POST:#"/Accn" parameters:loginParameters
success:^(NSURLSessionDataTask *task, id responseObject)
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
if (httpResponse.statusCode == 200) {
LoginInfo *loginInfo =[[LoginInfo alloc]initWithDictionary:responseObject];
CredentialStore *credStore =[CredentialStore sharedStore];
credStore.loginInfo =loginInfo;
completion(responseObject,nil);
loginInfo = responseObject;
} else {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, nil);
});
NSLog(#"Received: %#", responseObject);
NSLog(#"Received HTTP %d", httpResponse.statusCode);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
}];
return task;
}
#end
And this is how I am calling my method in my view controller
- (IBAction)login:(id)sender
{
[_usernameTextField resignFirstResponder];
[SVProgressHUD show];
NSURLSessionDataTask *task = [[AuthAPIManager sharedManager] initializeLogin:self.usernameTextField.text completion:^(NSDictionary *results, NSError *error)
{
if (results) {
LoginInfo *loginInfo = [[LoginInfo alloc]initWithDictionary:results];
CredentialStore *credStore =[ CredentialStore sharedStore];
credStore.loginInfo =loginInfo;
[self performSegueWithIdentifier:#"welcomeViewSegue" sender:self];
}
else
{
NSLog(#"there is an error:%#",error);
}
}];
[SVProgressHUD dismiss];
}
Your success block is being called, but your status code is not 200.
You are calling your completion block like this:
completion(nil, nil);
So this code:
if (results) {
[snip]
}
else
{
NSLog(#"there is an error:%#",error);
}
is passed a nil results object.
Set a breakpoint on if (httpResponse.statusCode == 200) and inspect httpResponse in your debugger to see why it's not what you expect. (You may get a different success code, such as 204.)
Call your completion block with results instead of nil.
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));
}
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