Override UIWebView user agent - but not for Facebook SDK - ios

I'm setting a custom UserAgent for the webviews in my app as per the instructions in this other question. Specifically, I'm setting
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:#"MyApp/MyLongVersionInfoString", #"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
at app launch. (Just setting the appropriate headers for the NSMutableURLRequest to be used by the UIWebView - UserAgent, User-Agent, User_Agent - does not work.)
This causes my embedded webviews to use the correct user agent. However, it also breaks the embedded webviews used by the Facebook SDK for dialogs - after I post to my wall, for instance, the contents of the FB dialog webview are replaced with text similar to window.location.href="fbconnect:\/\/success?post_id=100002469633196_43677789308119... and the webview does not close as it normally would (the user has to manually close it). This only occurs when I have my custom user agent set.
I thought I could circumvent the problem by unsetting the user agent before Facebook calls and resetting it afterwards, but it seems I can't unset the default defaults; I tried calling [[NSUserDefaults standardUserDefaults] removeObjectForKey:#"UserAgent"] and [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:NSRegistrationDomain] before each Facebook call and setting them again in the call's result handler, but I still see the same misbehavior.
I tried switching my initial settings to [[NSUserDefaults standardUserDefaults] setObject:newUA forKey:#"UserAgent"];, but then my webviews don't pick up the user agent.
Surely, surely, someone has used the Facebook SDK before in an app with non-Facebook embedded webviews. What am I missing? I've gone a number of rounds on this, each of which seems to almost-but-not-quite fix everything.

I ended up having to do this by implementing an NSURLProtocol subclass. While my implementation was messy, I'm including a scrubbed version below because I was surprised not to be able to find an example already on StackOverflow.
#import <Foundation/Foundation.h>
#interface MyProtocol : NSURLProtocol {
NSURLConnection *connection;
}
#property (nonatomic, retain) NSURLConnection *connection;
#end
#implementation MyProtocol
#synthesize connection;
#pragma mark -
#pragma mark custom methods
+ (NSString *)myUA {
return [[NSUserDefaults standardUserDefaults] stringForKey:#"MyCustomUserAgent"];
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *scheme = [request.URL.scheme stringByAppendingString:#"://"];
NSString *baseRequestString = [[[request.URL absoluteString] stringByReplacingOccurrencesOfString:request.URL.path withString:#""]
stringByReplacingOccurrencesOfString:scheme withString:#""];
if (request.URL.query != nil) {
NSString *query = [#"?" stringByAppendingString:request.URL.query];
baseRequestString = [baseRequestString stringByReplacingOccurrencesOfString:query withString:#""];
}
BOOL shouldIntercept = [baseRequestString isEqualToString:myWebHost];
BOOL alreadyIntercepted = [[NSURLProtocol propertyForKey:#"UserAgent" inRequest:request] isEqualToString:[self myUA]];
return shouldIntercept && !alreadyIntercepted;
}
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
{
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:request.URL
cachePolicy:request.cachePolicy
timeoutInterval:request.timeoutInterval];
[mutableRequest setAllHTTPHeaderFields:[request allHTTPHeaderFields]];
[mutableRequest setHTTPMethod:[request HTTPMethod]];
if ([request HTTPBody] != nil)
[mutableRequest setHTTPBody:[request HTTPBody]];
if ([request HTTPBodyStream] != nil)
[mutableRequest setHTTPBodyStream:[request HTTPBodyStream]];
[NSURLProtocol setProperty:[[self class] myUA] forKey:#"UserAgent" inRequest:mutableRequest];
[mutableRequest setValue:[[self class] myUA] forHTTPHeaderField:#"User-Agent"];
[mutableRequest setValue:[[self class] myUA] forHTTPHeaderField:#"User_Agent"];
self = [super initWithRequest:mutableRequest cachedResponse:cachedResponse client:client];
return self;
}
- (void) dealloc
{
[connection release];
}
#pragma mark -
#pragma mark boilerplate NSURLProtocol subclass requirements
- (void) startLoading
{
self.connection = [[NSURLConnection connectionWithRequest:[self request] delegate:self] retain];
}
- (void)stopLoading
{
[self.connection cancel];
}
- (void)connection:(NSURLConnection*)conn didReceiveResponse:(NSURLResponse*)response
{
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:[[self request] cachePolicy]];
}
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
[[self client] URLProtocol:self didLoadData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection*)conn
{
[[self client] URLProtocolDidFinishLoading:self];
}
- (NSURLRequest*)connection:(NSURLConnection*)connection willSendRequest:(NSURLRequest*)theRequest redirectResponse:(NSURLResponse*)redirectResponse
{
return theRequest;
}
- (void)connection:(NSURLConnection*)conn didFailWithError:(NSError*)error
{
[[self client] URLProtocol:self didFailWithError:error];
}
#end
Note that the protocol must be registered with [NSURLProtocol registerClass:[MyProtocol class]]; before you make any web requests to which you want it to apply - for instance, in applicationDidFinishLaunchingWithOptions:.

Related

Azure AD B2C getting oauthConnection Error: Bad Request

While trying to integrate Azure AD B2C, I am stuck with an error "oauthConnection Error: Bad Request". Following their given sample app it all works fine. But after integrating the same copy paste code from the working sample app, and trying to log in with Facebook or Google Plus, it throws an error! I am pretty sure that every credential that I used in the sample app is the same for my app. Any idea about this will be highly appreciated. Here is my code, AppDelegate.m
#import "AppData.h"
#import "NXOAuth2.h"
#import "AppDelegate.h"
#interface AppDelegate ()
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self setupOAuth2AccountStore];
// Override point for customization after application launch.
return YES;
}
- (void)setupOAuth2AccountStore {
AppData *data = [AppData getInstance]; // The singleton we use to get the settings
NSDictionary *customHeaders =
[NSDictionary dictionaryWithObject:#"application/x-www-form-urlencoded"
forKey:#"Content-Type"];
// Azure B2C needs
// kNXOAuth2AccountStoreConfigurationAdditionalAuthenticationParameters for
// sending policy to the server,
// therefore we use -setConfiguration:forAccountType:
NSDictionary *B2cConfigDict = #{
kNXOAuth2AccountStoreConfigurationClientID : data.clientID,
kNXOAuth2AccountStoreConfigurationSecret : data.clientSecret,
kNXOAuth2AccountStoreConfigurationScope :
[NSSet setWithObjects:#"openid", data.clientID, nil],
kNXOAuth2AccountStoreConfigurationAuthorizeURL :
[NSURL URLWithString:data.authURL],
kNXOAuth2AccountStoreConfigurationTokenURL :
[NSURL URLWithString:data.tokenURL],
kNXOAuth2AccountStoreConfigurationRedirectURL :
[NSURL URLWithString:data.bhh],
kNXOAuth2AccountStoreConfigurationCustomHeaderFields : customHeaders,
// kNXOAuth2AccountStoreConfigurationAdditionalAuthenticationParameters:customAuthenticationParameters
};
[[NXOAuth2AccountStore sharedStore] setConfiguration:B2cConfigDict
forAccountType:data.accountIdentifier];
}
LoginViewController.m
#import "AppData.h"
#import "LoginViewController.h"
#import "NXOAuth2.h"
#interface LoginViewController ()
#end
#implementation LoginViewController {
NSURL *myLoadedUrl;
bool isRequestBusy;
}
// Put variables here
- (void)viewDidLoad {
[super viewDidLoad];
// OAuth2 Code
self.loginView.delegate = self;
[self requestOAuth2Access];
[self setupOAuth2AccountStore];
NSURLCache *URLCache =
[[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
diskCapacity:20 * 1024 * 1024
diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
}
- (void)resolveUsingUIWebView:(NSURL *)URL {
// We get the auth token from a redirect so we need to handle that in the
// webview.
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(resolveUsingUIWebView:)
withObject:URL
waitUntilDone:YES];
return;
}
NSURLRequest *hostnameURLRequest =
[NSURLRequest requestWithURL:URL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:10.0f];
isRequestBusy = YES;
[self.loginView loadRequest:hostnameURLRequest];
NSLog(#"resolveUsingUIWebView ready (status: UNKNOWN, URL: %#)",
self.loginView.request.URL);
}
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
AppData *data = [AppData getInstance];
NSLog(#"webView:shouldStartLoadWithRequest: %# (%li)", request.URL,
(long)navigationType);
// The webview is where all the communication happens. Slightly complicated.
myLoadedUrl = [webView.request mainDocumentURL];
NSLog(#"***Loaded url: %#", myLoadedUrl);
// if the UIWebView is showing our authorization URL or consent URL, show the
// UIWebView control
if ([request.URL.absoluteString rangeOfString:data.authURL
options:NSCaseInsensitiveSearch]
.location != NSNotFound) {
self.loginView.hidden = NO;
} else if ([request.URL.absoluteString rangeOfString:data.loginURL
options:NSCaseInsensitiveSearch]
.location != NSNotFound) {
// otherwise hide the UIWebView, we've left the authorization flow
self.loginView.hidden = NO;
} else if ([request.URL.absoluteString rangeOfString:data.bhh
options:NSCaseInsensitiveSearch]
.location != NSNotFound) {
// otherwise hide the UIWebView, we've left the authorization flow
self.loginView.hidden = YES;
[[NXOAuth2AccountStore sharedStore] handleRedirectURL:request.URL];
} else {
self.loginView.hidden = NO;
}
return YES;
}
#pragma mark - UIWebViewDelegate methods
- (void)webViewDidFinishLoad:(UIWebView *)webView {
// The webview is where all the communication happens. Slightly complicated.
}
- (void)handleOAuth2AccessResult:(NSURL *)accessResult {
// parse the response for success or failure
if (accessResult)
// if success, complete the OAuth2 flow by handling the redirect URL and
// obtaining a token
{
[[NXOAuth2AccountStore sharedStore] handleRedirectURL:accessResult];
} else {
// start over
[self requestOAuth2Access];
}
}
- (void)setupOAuth2AccountStore {
[[NSNotificationCenter defaultCenter]
addObserverForName:NXOAuth2AccountStoreAccountsDidChangeNotification
object:[NXOAuth2AccountStore sharedStore]
queue:nil
usingBlock:^(NSNotification *aNotification) {
if (aNotification.userInfo) {
// account added, we have access
// we can now request protected data
NSLog(#"Success!! We have an access token.");
} else {
// account removed, we lost access
}
}];
[[NSNotificationCenter defaultCenter]
addObserverForName:NXOAuth2AccountStoreDidFailToRequestAccessNotification
object:[NXOAuth2AccountStore sharedStore]
queue:nil
usingBlock:^(NSNotification *aNotification) {
NSError *error = [aNotification.userInfo
objectForKey:NXOAuth2AccountStoreErrorKey];
// Always got stuck here while trying to login with any credentials
NSLog(#"Error!! %#", error.localizedDescription);
}];
}
- (void)requestOAuth2Access {
AppData *data = [AppData getInstance];
[[NXOAuth2AccountStore sharedStore]
requestAccessToAccountWithType:data.accountIdentifier
withPreparedAuthorizationURLHandler:^(NSURL *preparedURL) {
NSURLRequest *r = [NSURLRequest requestWithURL:preparedURL];
[self.loginView loadRequest:r];
}];
}
ViewController.m
#import "ViewController.h"
#import "AppData.h"
#import "LoginViewController.h"
#import "NXOAuth2.h"
// Login Action
- (IBAction)login:(id)sender {
LoginViewController *userSelectController =
[self.storyboard instantiateViewControllerWithIdentifier:#"login"];
[self.navigationController pushViewController:userSelectController
animated:YES];
}
In case if anybody stumbles in this, Here is the solution
Go to pod, NXOAuth2Client.m and replace the method
- (void)requestTokenWithAuthGrant:(NSString *)authGrant redirectURL:(NSURL *)redirectURL; with the below code
- (void)requestTokenWithAuthGrant:(NSString *)authGrant redirectURL:(NSURL *)redirectURL;
{
NSAssert1(!authConnection, #"authConnection already running with: %#", authConnection);
NSMutableURLRequest *tokenRequest = [NSMutableURLRequest requestWithURL:tokenURL];
[tokenRequest setHTTPMethod:self.tokenRequestHTTPMethod];
[authConnection cancel]; // just to be sure
self.authenticating = YES;
NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithObjectsAndKeys:
#"authorization_code", #"grant_type",
clientId, #"client_id",
// clientSecret, #"client_secret",
[redirectURL absoluteString], #"redirect_uri",
authGrant, #"code",
nil];
if (self.desiredScope) {
[parameters setObject:[[self.desiredScope allObjects] componentsJoinedByString:#" "] forKey:#"scope"];
}
if (self.customHeaderFields) {
[self.customHeaderFields enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
[tokenRequest addValue:obj forHTTPHeaderField:key];
}];
}
if (self.additionalAuthenticationParameters) {
[parameters addEntriesFromDictionary:self.additionalAuthenticationParameters];
}
authConnection = [[NXOAuth2Connection alloc] initWithRequest:tokenRequest
requestParameters:parameters
oauthClient:self
delegate:self];
authConnection.context = NXOAuth2ClientConnectionContextTokenRequest;
}
Commenting clientSecret solved the issue

iOS WebService doesn't seem to be starting. Anyway I can find out if its doing anything?

I have built a WebService to retrieve user specific reports using a PHP API. I am fairly new to this so I followed some tutorials and help pages to build the web service. It doesn't seem to run at all so I am thinking I stuffed up or just incorrectly put code in that didn't belong as I was following multiple tutorials etc to get the desired result.
here is the code for .m file of the web service:
#import "reportsTestViewController.h"
#import "ReportsDataObject.h"
#interface reportsTestViewController ()
#end
#implementation reportsTestViewController
#synthesize label;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.tesg.com.au/allCustBuild.php"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if (connection) {
//connect
label.text = #"connecting...";
} else {
//error
}
}
-(void)setupReportsFromJSONArray:(NSData*)dataFromReportsArray{
NSError *error;
NSMutableArray *reportsArray = [[NSMutableArray alloc] init];
NSArray *arrayFromServer = [NSJSONSerialization JSONObjectWithData:dataFromReportsArray options:0 error:&error];
if(error){
NSLog(#"error parsing the json data from server with error description - %#", [error localizedDescription]);
}
else {
reportsArray = [[NSMutableArray alloc] init];
for(NSDictionary *eachReport in arrayFromServer)
{
ReportsDataObject *report = [[ReportsDataObject alloc] initWithJSONData:eachReport];
[reportsArray addObject:report];
}
//Now you have your reportsArray filled up with all your data objects
}
}
-(void)connectionWasASuccess:(NSData *)data{
[self setupReportsFromJSONArray:data];
}
-(void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
//We check against table to make sure we are displaying the right number of cells
// for the appropriate table. This is so that things will work even if one day you
//decide that you want to have two tables instead of one.
if(tableView == reportsTable){
return([theReportsArray count]);
}
return 0;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if(cell)
{
//set your configuration of your cell
}
//The beauty of this is that you have all your data in one object and grab WHATEVER you like
//This way in the future you can add another field without doing much.
if([theReportsArray count] == 0){
cell.textLabel.text = #"no reports to show";
}
else{
ReportsDataObject *currentReport = [theReportsArray objectAtIndex:indexPath.row];
cell.textLabel.text = [currentReport buildingName];
// in the future you can grab whatever data you need like this
//[currentPlace placeName], or [currentPlace placeDay];
}
return(cell);
}
#end
and the code for the .h:
#import <UIKit/UIKit.h>
#interface reportsTestViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>{
IBOutlet UITableView *reportsTable;
IBOutlet UILabel *Label;
NSArray *theReportsArray;
}
#property (nonatomic, retain) IBOutlet UILabel *label;
#end
I'm sure it's something really ignorant that i've done but after going back through help pages and tuts, i can't find what I did wrong.
It looks like you are establishing your connection ok, but you are never handling any data that gets sent back. Once you create a connection you need to add the delegate methods to handle the data sent back by the connection.
As mentioned in some of the comments you also need to verify that your php page on the server is indeed giving you the information you expect. By logging out the data string as below in the -connectionDidFinishLoading you will be able to see any data sent back from the server for debugging.
//create an NSMutableData property in your interface
#property (nonatomic, strong) NSMutableData *myDataIvar;
//initialize it when you create your connection
if (connection){
self.myDataIvar = [[NSMutableData alloc] init];
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
[self.myDataIvar setLength:0];
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[self.myDataIvar appendData:data];
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(#"Connection Failed: %#", error.userInfo);
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
//this is where you would parse the data received back from the server
NSString *responseString = [[NSString alloc] initWithData:self.myDataIvar encoding:NSUTF8StringEncoding];
NSLog(#"Received Data: %#",responseString);
[self setupReportsFromJSONArray:self.myDataIvar];
}
This way at least you will be able to see what data you are receiving back from the server.
EDIT:
Also your test page does not seem to be putting out any json data. When I navigate to it all I get is "how".
I put up a quick php page that you can use to test your obj-c code. It will echo back an array with 10 test results to fill your tableview if your code is correct.
//Create your request pointing to the test page
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.codeyouniversity.com/json_test.php"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];
If you want to put the php on your own page for testing this is what I used.
<?php
$jsonArray = array('result1','result2','result3','result4','result5','result6','result7','result8','result9','result10');
echo json_encode($jsonArray);
?>
Here's a link to Apples documentation for the URL loading system as well
https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html#//apple_ref/doc/uid/10000165i
try this one if you get result, your url is wrong.I think now itself your url is wrong
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://api.openweathermap.org/data/2.5/weather?q=London,uk"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if (connection) {
//connect
label.text = #"connecting...";
} else {
//error
}
}

Authentication login issue RESTful iOS

Hey Folks i have a problem with authentication for the login credentials of my app. I currently have a rails web service running from my localhost, that features a main login screen for a admin user with password credentials to display functions.
The problem is that i am creating a RESTful iOS application running alongside my rails app, that just takes these JSON requests and bypasses them.
All i want is to create a way for me to enter admin and password without having to use auth_token? it seems to be the only way to do it, to simply authenticate the admin user first in the keychain. Im using the framework AFNetowrking for authentication.SSKeychain a wrapper for accounts, and SVProgressHUD for lightweight huds
.I also have the JSON and XML requests being logged in the i terminal but failing due to not being able to connect to the server with this error
error Domain=NSURLErrorDomain Code=-1004 "Could not connect to the server." UserInfo=0x7556d50 {NSErrorFailingURLStringKey=http://localhost:3000.json/, NSErrorFailingURLKey=http://localhost:3000.json/, NSLocalizedDescription=Could not connect to the server., NSUnderlyingError=0xeb805c0 "Could not connect to the server."}
this is how im storing the credentials for the authClient, all want to do is specify the same info used to log-into the web service client which is.
username:admin and password:taliesin
but unsure how to do this?
these are what i have so for the AuthAPIClient, CredentialsStore and LoginViewController
update if anybody knows a easier way to do this please can you let me know, will be much appreciated.
AuthAPIClient.m
#import "AuthAPIClient.h"
#import "CredentialStore.h"
#define BASE_URL #"http://admin:taliesin#localhost:3000"
#implementation AuthAPIClient
+ (id)sharedClient {
static AuthAPIClient *__instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURL *baseUrl = [NSURL URLWithString:BASE_URL];
__instance = [[AuthAPIClient alloc] initWithBaseURL:baseUrl];
});
return __instance;
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (self) {
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self setAuthTokenHeader];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(tokenChanged:)
name:#"token-changed"
object:nil];
}
return self;
}
- (void)setAuthTokenHeader {
CredentialStore *store = [[CredentialStore alloc] init];
NSString *authToken = [store authToken];
[self setDefaultHeader:#"auth_token" value:authToken];
}
- (void)tokenChanged:(NSNotification *)notification {
[self setAuthTokenHeader];
}
#end
CredentialStore.m
#import "CredentialStore.h"
#import "SSKeychain.h"
#define SERVICE_NAME #"http://admin:taliesin#localhost:3000"
#define AUTH_TOKEN_KEY #"auth_token"
#implementation CredentialStore
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
NSString *user = [NSString stringWithFormat:#"%c%s%#", 'a', "a", #"a"];
NSString *password = [NSString stringWithFormat:#"%c%s%#", 'a', "a", #"a"];
NSURLCredential *credential = [NSURLCredential credentialWithUser:user
password:password
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
}
- (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:#"taliesin"
forService:SERVICE_NAME
account:key];
} else {
[SSKeychain deletePasswordForService:SERVICE_NAME account:key];
}
}
- (NSString *)secureValueForKey:(NSString *)key {
return [SSKeychain passwordForService:SERVICE_NAME account:key];
}
#end
LoginViewApi.m
#import "LoginViewController.h"
#import "AuthAPIClient.h"
#import "CredentialStore.h"
#import "SVProgressHUD.h"
#interface UIViewController ()
#property (nonatomic, strong) IBOutlet UITextField *userTextField;
#property (nonatomic, strong) IBOutlet UITextField *passwordTextField;
#property (nonatomic, strong) CredentialStore *credentialStore;
#end
#implementation LoginViewController
+ (void)presentModallyFromViewController:(UIViewController *)viewController {
LoginViewController *loginViewController = [[LoginViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc]
initWithRootViewController:loginViewController];
[viewController presentViewController:navController
animated:YES
completion:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.credentialStore = [[CredentialStore alloc] init];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self
action:#selector(cancel:)];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:#"Login"
style:UIBarButtonItemStyleDone
target:self
action:#selector(login:)];
[self.userTextField becomeFirstResponder];
}
- (void)login:(id)sender {
[SVProgressHUD show];
id params = #{
#"admin": self.userTextField.text,
#"taliesin": self.passwordTextField.text
};
[[AuthAPIClient sharedClient] postPath:#"/auth/login.json"
parameters:params
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSString *authToken = [responseObject objectForKey:#"auth_token"];
[self.credentialStore setAuthToken:authToken];
[SVProgressHUD dismiss];
[self dismissViewControllerAnimated:YES completion:nil];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (operation.response.statusCode == 500) {
[SVProgressHUD showErrorWithStatus:#"Something went wrong!"];
} else {
NSData *jsonData = [operation.responseString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:nil];
NSString *errorMessage = [json objectForKey:#"error"];
[SVProgressHUD showErrorWithStatus:errorMessage];
}
}];
}
- (void)cancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
#end
Any help or for more questions please let me know cheers :)
Endpoint Addressing
I don't know how AFNetworking handles remote addresses but I think you are making a simple mistake when calling localhost from iOS to contact your web-service. The thing is that iOS is an operating system and it has its own localhost. Thus, when you think you are calling a remote web-service you are actually asking iOS to look within itself for this service. Therefore, wether you are running your rails web-service locally or on an actual remote server, find out the IP address for the server where the service is running and use that as an endpoint rather than localhost.
So update everything that points to your service to use an IP. Don't change it for 127.0.0.1, that would have the same effect.
For example:
#define BASE_URL #"http://admin:taliesin#192.168.0.1:3000"
Authentication: iOS
Assuming your web-service supports, that is, you have configured basic http authentication; then authenticating from iOS should not be a huge problem.
If you were to use RESTKit, it would be as simple as:
RKObjectManager *objectManager = [RKObjectManager sharedManager];
objectManager.client.authenticationType = RKRequestAuthenticationTypeHTTPBasic;
objectManager.client.username = username;
objectManager.client.password = password;
I use Apple's KeychainItemWrapper for storing/retrieving credentials on iOS.
//Store Account on Keychain (disk) for persistence.
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
KeychainItemWrapper *accountItem = appDelegate.accountItem;
[accountItem setObject:password forKey:(__bridge id)kSecValueData];
[accountItem setObject:username forKey:(__bridge id)kSecAttrAccount];
username = password = nil; //keep account information only in the keychain.
I don't know if your code is correct but it should not be that complicated to handle basic http auth. Perhaps AFNetworking does not abstracts it as much as RESTKit does? Or perhaps you are not using the right methods from AFNetworking to do basic http auth? Basic auth does not require an access token and thus I suggest you read a little more about basic auth before implementing it.
Authentication: Rails
As for implementing authentication on Rails, it should not be too complicated either. All you need to do is to configure your controller to present an authentication challenge to the incoming request with basic http auth. One way to do this is by using before_filter: authenticate. Here is an example.
I think Railscasts is superb and they happen to have a tutorial on basic http auth and one specifically for rails 3.1. authentication.
Cheers.

How to make sure I have only 1 asynchronous NSURLConnection at a time?

I'm creating an app which communicates alot with a server. I want to make sure I have only 1 connection at a time - only 1 request pending. I want so that if I try to send another request, It will wait until the current one is finished before sending the next.
How can I implement This?
Tnx!
I don't know of any automatic mechanism to do this. So you have to write a new class yourself (let's call it ConnectionQueue):
Basically, instead of a creating the NSURLConnection directly, you call a method of your ConnectionQueue class (which should have exactly one instance) taking the NSURLRequest and the delegate as a parameter. Both are added to a queue, i.e. a separate NSArray for the requests and the delegates.
If the arrays contains just one element, then no request is outstanding and you can create a NSURLConnection with the specified request. However, instead of the delegate passed to the method, you pass your ConnectionQueue instance as the delegate. As a result, the connection queue will be informed about all actions of the connection. In most cases, you just forward the callback to the original delegate (you'll find it in the first element of the delegate array).
However, if the outstanding connection completes (connection:didFailWithError: or connectionDidFinishLoading: is called), you first call the original delegate and then remove the connection from the two arrays. Finally, if the arrays aren't empty, you start the next connection.
Update:
Here's some code. It compiles but hasn't been tested otherwise. Furthermore, the implementation of the NSURLConnectionDelegate protocol is incomplete. If you're expecting more than implemented callbacks, you'll have to add them.
Header File:
#import <Foundation/Foundation.h>
#interface ConnectionQueue : NSObject {
NSMutableArray *requestQueue;
NSMutableArray *delegateQueue;
NSURLConnection *currentConnection;
}
// Singleton instance
+ (ConnectionQueue *)sharedInstance;
// Cleanup and release queue
+ (void)releaseShared;
// Queue a new connection
- (void)queueRequest:(NSURLRequest *)request delegate:(id)delegate;
#end
Implementation:
#import "ConnectionQueue.h"
#implementation ConnectionQueue
static ConnectionQueue *sharedInstance = nil;
+ (ConnectionQueue*)sharedInstance
{
if (sharedInstance == nil)
sharedInstance = [[ConnectionQueue alloc] init];
return sharedInstance;
}
+ (void)releaseShared
{
[sharedInstance release];
sharedInstance = nil;
}
- (id)init
{
if ((self = [super init])) {
requestQueue = [NSMutableArray arrayWithCapacity:8];
delegateQueue = [NSMutableArray arrayWithCapacity:8];
}
return self;
}
- (void)dealloc
{
[requestQueue release];
[delegateQueue release];
[currentConnection cancel];
[currentConnection release];
[super dealloc];
}
- (void)startNextConnection
{
NSURLRequest *request = [requestQueue objectAtIndex:0];
currentConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)queueRequest:(NSURLRequest *)request delegate:(id)delegate
{
[requestQueue addObject:request];
[delegateQueue addObject:delegate];
if ([requestQueue count] == 1)
[self startNextConnection];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
id delegate = [delegateQueue objectAtIndex:0];
[delegate connection: connection didReceiveResponse: response];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
id delegate = [delegateQueue objectAtIndex:0];
[delegate connection: connection didReceiveData: data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
id delegate = [delegateQueue objectAtIndex:0];
[delegate connection: connection didFailWithError:error];
[currentConnection release];
currentConnection = nil;
[requestQueue removeObjectAtIndex:0];
[delegateQueue removeObjectAtIndex:0];
if ([requestQueue count] >= 1)
[self startNextConnection];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
id delegate = [delegateQueue objectAtIndex:0];
[delegate connectionDidFinishLoading: connection];
[currentConnection release];
currentConnection = nil;
[requestQueue removeObjectAtIndex:0];
[delegateQueue removeObjectAtIndex:0];
if ([requestQueue count] >= 1)
[self startNextConnection];
}
#end
To use the connection queue, create an NSURLRequest instance and then call:
[[ConnectionQueue sharedInstance] queueRequest:request delegate:self];
There's no need to create the singleton instance of ConnectionQueue explicitly. It will be created automatically. However, to properly clean up, you should call [ConnectionQueue releaseShared] when the application quits, e.g. from applicationWillTerminate: of your application delegate.

How to retrieve Facebook response using Facebook iOS SDK

I am using the Facebook iOS SDK for iPhone. I initialize the Facebook instance
facebook = [[Facebook alloc] initWithAppId:kAppId];
And then I do login:
[facebook authorize:permissions delegate:self];
After I logged in to Facebook I am doing the following to get the user profile information:
[facebook requestWithGraphPath:#"me" andDelegate:self];
NSMutableData *response = [fbRequest responseText];
unsigned char *firstBuffer = [response mutableBytes];
NSLog(#"Got Facebook Profile: : \"%s\"\n", (char *)firstBuffer);
But I get the following on my console:
Got Facebook Profile: "(null)"
What am I doing wrong, also I believe that Facebook response is a json string and I am looking to get a hold of that json string.
I thought may be I should make it a wiki and tell the people how I am doing it. because lot of people are facing similar problem.
The first thing that I did was.
In Facebook.m class I added the following statement in the following method
(void)authorizeWithFBAppAuth:(BOOL)tryFBAppAuth
safariAuth:(BOOL)trySafariAuth
trySafariAuth = NO;
This prevents a safari page to get open for the facebook login, but it pops up a screen in app itself. Then i created a helper class for Facebook, the header file code is here.
#import <UIKit/UIKit.h>
#import "FBConnect.h"
#interface FaceBookHelper : UIViewController
<FBRequestDelegate,
FBDialogDelegate,
FBSessionDelegate>{
Facebook *facebook;
NSArray *permissions;
}
#property(readonly) Facebook *facebook;
- (void)login;
-(void)getUserInfo:(id)sender;
- (void)getUserFriendList:(id)sender;
-(void)postToFriendsWall;
The .m file.
static NSString* kAppId = #"xxx";
#define ACCESS_TOKEN_KEY #"fb_access_token"
#define EXPIRATION_DATE_KEY #"fb_expiration_date"
#implementation FaceBookHelper
#synthesize facebook;
//////////////////////////////////////////////////////////////////////////////////////////////////
// UIViewController
/**
* initialization
*/
- (id)init {
if (self = [super init]) {
facebook = [[Facebook alloc] initWithAppId:kAppId];
facebook.sessionDelegate = self;
permissions = [[NSArray arrayWithObjects:
#"email", #"read_stream", #"user_birthday",
#"user_about_me", #"publish_stream", #"offline_access", nil] retain];
[self login];
}
return self;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// NSObject
- (void)dealloc {
[facebook release];
[permissions release];
[super dealloc];
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// private
/**
* Login.
*/
- (void)login {
// only authorize if the access token isn't valid
// if it *is* valid, no need to authenticate. just move on
if (![facebook isSessionValid]) {
[facebook authorize:permissions delegate:self];
}
}
/**
* This is the place only where you will get the hold on the accessToken
*
**/
- (void)fbDidLogin {
NSLog(#"Did Log In");
NSLog(#"Access Token is %#", facebook.accessToken );
NSLog(#"Expiration Date is %#", facebook.expirationDate );
// Store the value in the NSUserDefaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:facebook.accessToken forKey:ACCESS_TOKEN_KEY];
[defaults setObject:facebook.expirationDate forKey:EXPIRATION_DATE_KEY];
[defaults synchronize];
// This is the best place to login because here we know that user has already logged in
[self getUserInfo:self];
//[self getUserFriendList:self];
//[self postToFriendsWall];
}
- (void)fbDidNotLogin:(BOOL)cancelled {
NSLog(#"Failed to log in");
}
- (void)getUserInfo:(id)sender {
[facebook requestWithGraphPath:#"me" andDelegate:self];
}
- (void)getUserFriendList:(id)sender {
[facebook requestWithGraphPath:#"me/friends" andDelegate:self];
}
////////////////////////////////////////////////////////////////////////////////
// FBRequestDelegate
/**
* Called when the Facebook API request has returned a response. This callback
* gives you access to the raw response. It's called before
* (void)request:(FBRequest *)request didLoad:(id)result,
* which is passed the parsed response object.
*/
- (void)request:(FBRequest *)request didReceiveResponse:(NSURLResponse *)response {
NSLog(#"Inside didReceiveResponse: received response");
//NSLog(#"Status Code #", [response statusCode]);
NSLog(#"URL #", [response URL]);
}
/**
* Called when a request returns and its response has been parsed into
* an object. The resulting object may be a dictionary, an array, a string,
* or a number, depending on the format of the API response. If you need access
* to the raw response, use:
*
* (void)request:(FBRequest *)request
* didReceiveResponse:(NSURLResponse *)response
*/
- (void)request:(FBRequest *)request didLoad:(id)result {
NSLog(#"Inside didLoad");
if ([result isKindOfClass:[NSArray class]]) {
result = [result objectAtIndex:0];
}
// When we ask for user infor this will happen.
if ([result isKindOfClass:[NSDictionary class]]){
//NSDictionary *hash = result;
NSLog(#"Birthday: %#", [result objectForKey:#"birthday"]);
NSLog(#"Name: %#", [result objectForKey:#"name"]);
}
if ([result isKindOfClass:[NSData class]])
{
NSLog(#"Profile Picture");
//[profilePicture release];
//profilePicture = [[UIImage alloc] initWithData: result];
}
NSLog(#"request returns %#",result);
//if ([result objectForKey:#"owner"]) {}
};
/**
* Called when an error prevents the Facebook API request from completing
* successfully.
*/
- (void)request:(FBRequest *)request didFailWithError:(NSError *)error {
//[self.label setText:[error localizedDescription]];
};
////////////////////////////////////////////////////////////////////////////////
// FBDialogDelegate
/**
* Called when a UIServer Dialog successfully return.
*/
- (void)dialogDidComplete:(FBDialog *)dialog {
//[self.label setText:#"publish successfully"];
}
#end
Thanks for this hint Yogesh!
In facebook.m you can also set the safariAuth param in the authorize method.
- (void)authorize:(NSArray *)permissions
delegate:(id<FBSessionDelegate>)delegate {
...
[self authorizeWithFBAppAuth:YES safariAuth:NO];
}

Resources