views:

647

answers:

3

I'm trying to implement a custom login view to Twitter (I don't want that UIWebView). I've downloaded many classes, and I'm so far having a nightmare with this. Now I'm trying to make Twitter + oAuth work. Here's the demo code (that works):

_engine = [[SA_OAuthTwitterEngine alloc] initOAuthWithDelegate: self];
_engine.consumerKey = kOAuthConsumerKey;
_engine.consumerSecret = kOAuthConsumerSecret;

[_engine requestRequestToken];

UIViewController *controller = [SA_OAuthTwitterController controllerToEnterCredentialsWithTwitterEngine: _engine delegate: self];

if (controller) 
 [self presentModalViewController: controller animated: YES];
else
 [_engine sendUpdate: [NSString stringWithFormat: @"Already Updated. %@", [NSDate date]]];

Now what I wanna do is replace that SA_OAuthTwitterController with custom UITextFields. So I'm trying this:

_engine = [[SA_OAuthTwitterEngine alloc] initOAuthWithDelegate: self];
_engine.consumerKey = kOAuthConsumerKey;
_engine.consumerSecret = kOAuthConsumerSecret;

[_engine requestRequestToken];
[_engine requestAccessToken];
[_engine setUsername:@"username" password:@"password"];
[_engine sendUpdate:@"tweet"];

But I keep getting error 401. I'm probably missing a step. Anyone?

+1  A: 

The view controller you are eliminating loads a webview that prompts the user to authenticate your application with twitter. If you don't ever have the user authenticate, you won't be able to make authenticated twitter calls. There is a way to do what you are wanting, however, you have to go through all of the OAuth steps yourself. Here are the steps as specified from a different web service (Vimeo), but the same rules apply:

  1. Your application sends a request with your consumer key, and signed with your consumer secret, for a something called a request token. If we verify your application correctly, we'll send you back a request token and a token secret.

  2. You'll then create a link for the user to click on with the request token.

  3. When the user gets to Vimeo, they'll be prompted to allow your application access to their account. If they click yes, we'll send them back to your application, along with a verifier.

  4. You'll then use the request token, verifier, and token secret to make
    another call to us to get an access token. The access token is what you'll use to access the user's information on Vimeo.

OAuth is a real pain in the rump, so good luck. ;-)

Matt Long
+1  A: 

I think the bit you're missing is here, SA_OAuthTwitterEngine.m:103:

    //This generates a URL request that can be passed to a UIWebView. It will open a page in which the user must enter their Twitter creds to validate
- (NSURLRequest *) authorizeURLRequest {
        if (!_requestToken.key && _requestToken.secret) return nil;     // we need a valid request token to generate the URL

        OAMutableURLRequest *request = [[[OAMutableURLRequest alloc] initWithURL: self.authorizeURL consumer: nil token: _requestToken realm: nil signatureProvider: nil] autorelease];     

        [request setParameters: [NSArray arrayWithObject: [[[OARequestParameter alloc] initWithName: @"oauth_token" value: _requestToken.key] autorelease]]];   
        return request;
}

You will have to "fake" this user login out yourself, I believe by sending twitter the login credentials as a separate request. It appears that the setUsername method you're calling is actually called as a post operation once a valid access token has been received. See SA_OAuthTwitterEngine.m:185

//
// access token callback
// when twitter sends us an access token this callback will fire
// we store it in our ivar as well as writing it to the keychain
// 
- (void) setAccessToken: (OAServiceTicket *) ticket withData: (NSData *) data {
        if (!ticket.didSucceed || !data) return;

        NSString *dataString = [[[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding] autorelease];
        if (!dataString) return;

        if (self.pin.length && [dataString rangeOfString: @"oauth_verifier"].location == NSNotFound) dataString = [dataString stringByAppendingFormat: @"&oauth_verifier=%@", self.pin];

        NSString                                *username = [self extractUsernameFromHTTPBody:dataString];

        if (username.length > 0) {
                [[self class] setUsername: username password: nil];
                if ([_delegate respondsToSelector: @selector(storeCachedTwitterOAuthData:forUsername:)]) [(id) _delegate storeCachedTwitterOAuthData: dataString forUsername: username];
        }

        [_accessToken release];
        _accessToken = [[OAToken alloc] initWithHTTPResponseBody:dataString];
}

So the steps are as follows:

  1. Request request token
  2. Fake user login to twitter and get the pin value
  3. Set the pin on the engine
  4. Request access token

You can see where SA_OAuthTwitterController parses the pin out of the webview content in SA_OAuthTwitterController.m:156

#pragma mark Webview Delegate stuff
- (void) webViewDidFinishLoad: (UIWebView *) webView {
        NSError *error;
        NSString *path = [[NSBundle mainBundle] pathForResource: @"jQueryInject" ofType: @"txt"];
    NSString *dataSource = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];

    if (dataSource == nil) {
        NSLog(@"An error occured while processing the jQueryInject file");
    }

        [_webView stringByEvaluatingJavaScriptFromString:dataSource]; //This line injects the jQuery to make it look better

        NSString                                        *authPin = [[_webView stringByEvaluatingJavaScriptFromString: @"document.getElementById('oauth_pin').innerHTML"] stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];

        if (authPin.length == 0) authPin = [[_webView stringByEvaluatingJavaScriptFromString: @"document.getElementById('oauth_pin').getElementsByTagName('a')[0].innerHTML"] stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];

        [_activityIndicator stopAnimating];
        if (authPin.length) {
                [self gotPin: authPin];
        } 
        if ([_webView isLoading] || authPin.length) {
                [_webView setHidden:YES];
        } else {
                [_webView setHidden:NO];
        }
}

hope this helps.

ImHuntingWabbits
Thanks for awesome answer. However I ended up using the UIWebView. I thought most apps were doing using oAuth on those UITextFields, but they're not. Hence the "posted via API".
Sam V
+1  A: 

You have to use xAuth for custom user/pass controls. But it requires the permission from Twitter.

Andrew Kovzel