From 08581d7efae166ec814e3df324f674914aeb7fad Mon Sep 17 00:00:00 2001 From: Yonatan Kogan Date: Mon, 27 May 2013 16:16:14 -0700 Subject: [PATCH 1/2] Version of simple-oauth1 that works for GoodReads. Some changes in the design are included because they're what I needed for my project, and I think overall they're a small improvement. --- .../Simple-OAuth1/OAuth1Controller.h | 15 ++- .../Simple-OAuth1/OAuth1Controller.m | 117 +++++++++++------- Simple-OAuth1/Simple-OAuth1/ViewController.m | 37 +++--- 3 files changed, 111 insertions(+), 58 deletions(-) diff --git a/Simple-OAuth1/Simple-OAuth1/OAuth1Controller.h b/Simple-OAuth1/Simple-OAuth1/OAuth1Controller.h index 1833ed5..fcb74d8 100644 --- a/Simple-OAuth1/Simple-OAuth1/OAuth1Controller.h +++ b/Simple-OAuth1/Simple-OAuth1/OAuth1Controller.h @@ -8,8 +8,21 @@ #import +@class OAuth1Controller; + +@protocol OAuth1ControllerDelegate + +- (void)startedLoadingRequest:(NSURLRequest *)request oauth1Controller:(OAuth1Controller *)controller; + +@end + @interface OAuth1Controller : NSObject +@property (nonatomic, strong) NSString *consumerKey; +@property (nonatomic, strong) NSString *consumerSecret; +@property (nonatomic, strong) NSString *oauthCallback; +@property (nonatomic, weak) id controllerDelegate; + - (void)loginWithWebView:(UIWebView *)webWiew completion:(void (^)(NSDictionary *oauthTokens, NSError *error))completion; @@ -18,7 +31,7 @@ oauthVerifier:(NSString *)oauth_verifier completion:(void (^)(NSError *error, NSDictionary *responseParams))completion; -+ (NSURLRequest *)preparedRequestForPath:(NSString *)path +- (NSURLRequest *)preparedRequestForPath:(NSString *)path parameters:(NSDictionary *)parameters HTTPmethod:(NSString *)method oauthToken:(NSString *)oauth_token diff --git a/Simple-OAuth1/Simple-OAuth1/OAuth1Controller.m b/Simple-OAuth1/Simple-OAuth1/OAuth1Controller.m index 5714244..629714f 100644 --- a/Simple-OAuth1/Simple-OAuth1/OAuth1Controller.m +++ b/Simple-OAuth1/Simple-OAuth1/OAuth1Controller.m @@ -4,6 +4,7 @@ // // Created by Christian Hansen on 02/12/12. // Copyright (c) 2012 Christian-Hansen. All rights reserved. +// Modified for GoodReads 05/26/2013 by Yonatan Kogan // #import "OAuth1Controller.h" @@ -13,30 +14,13 @@ typedef void (^WebWiewDelegateHandler)(NSDictionary *oauthParams); -// Go to: http://www.tumblr.com/oauth/apps and get your API keys - -//#define OAUTH_CALLBACK @"tumblr_callback" //Sometimes this has to be the same as the registered app callback url -//#define CONSUMER_KEY @"OAuth consumer key" -//#define CONSUMER_SECRET @"OAuth consumer secret" -//#define AUTH_URL @"http://www.tumblr.com/oauth/" -//#define REQUEST_TOKEN_URL @"request_token" -//#define AUTHENTICATE_URL @"authorize" -//#define ACCESS_TOKEN_URL @"access_token" -//#define API_URL @"http://api.tumblr.com/v2/" -//#define OAUTH_SCOPE_PARAM @"" - -// Go to: https://www.linkedin.com/secure/developer and get your API keys - -#define OAUTH_CALLBACK @"linkedin_oauth" //Sometimes this has to be the same as the registered app callback url -#define CONSUMER_KEY @"API Key" -#define CONSUMER_SECRET @"Secret Key" -#define AUTH_URL @"https://api.linkedin.com/uas/" -#define REQUEST_TOKEN_URL @"oauth/requestToken" -#define AUTHENTICATE_URL @"oauth/authorize" -#define ACCESS_TOKEN_URL @"oauth/accessToken" -#define API_URL @"http://api.linkedin.com/v1/" -#define OAUTH_SCOPE_PARAM @"r_fullprofile r_emailaddress rw_nus r_network w_messages" - +#define AUTH_URL @"https://www.goodreads.com/oauth/" +#define REQUEST_TOKEN_URL @"request_token" +#define AUTHENTICATE_URL @"authorize" +#define ACCESS_TOKEN_URL @"access_token" +#define API_URL @"http://www.goodreads.com/" +#define OAUTH_SCOPE_PARAM @"" +#define URL_SUFFIX @"" #define REQUEST_TOKEN_METHOD @"POST" #define ACCESS_TOKEN_METHOD @"POST" @@ -218,16 +202,26 @@ - (void)loginWithWebView:(UIWebView *)webWiew completion:(void (^)(NSDictionary - (void)obtainRequestTokenWithCompletion:(void (^)(NSError *error, NSDictionary *responseParams))completion { NSString *request_url = [AUTH_URL stringByAppendingString:REQUEST_TOKEN_URL]; - NSString *oauth_consumer_secret = CONSUMER_SECRET; + if (!self.consumerSecret) { + if (!completion) { + return; + } + NSMutableDictionary* details = [NSMutableDictionary dictionary]; + [details setValue:@"Consumer Secret is not set" forKey:NSLocalizedDescriptionKey]; + // populate the error object with the details + NSError *error = [NSError errorWithDomain:@"oauthInfo" code:200 userInfo:details]; + return completion(error,nil); + } + NSString *oauth_consumer_secret = self.consumerSecret; - NSMutableDictionary *allParameters = [self.class standardOauthParameters]; + NSMutableDictionary *allParameters = [self standardOauthParameters]; if ([OAUTH_SCOPE_PARAM length] > 0) [allParameters setValue:OAUTH_SCOPE_PARAM forKey:@"scope"]; NSString *parametersString = CHQueryStringFromParametersWithEncoding(allParameters, NSUTF8StringEncoding); NSString *baseString = [REQUEST_TOKEN_METHOD stringByAppendingFormat:@"&%@&%@", request_url.utf8AndURLEncode, parametersString.utf8AndURLEncode]; NSString *secretString = [oauth_consumer_secret.utf8AndURLEncode stringByAppendingString:@"&"]; - NSString *oauth_signature = [self.class signClearText:baseString withSecret:secretString]; + NSString *oauth_signature = [self signClearText:baseString withSecret:secretString]; [allParameters setValue:oauth_signature forKey:@"oauth_signature"]; parametersString = CHQueryStringFromParametersWithEncoding(allParameters, NSUTF8StringEncoding); @@ -254,7 +248,17 @@ - (void)obtainRequestTokenWithCompletion:(void (^)(NSError *error, NSDictionary #pragma mark - Step 2 Show login to the user to authorize our app - (void)authenticateToken:(NSString *)oauthToken withCompletion:(void (^)(NSError *error, NSDictionary *responseParams))completion { - NSString *oauth_callback = OAUTH_CALLBACK; + if (!self.oauthCallback) { + if (!completion) { + return; + } + NSMutableDictionary* details = [NSMutableDictionary dictionary]; + [details setValue:@"OAuth Callback is not set" forKey:NSLocalizedDescriptionKey]; + // populate the error object with the details + NSError *error = [NSError errorWithDomain:@"oauthInfo" code:200 userInfo:details]; + return completion(error,nil); + } + NSString *oauth_callback = self.oauthCallback; NSString *authenticate_url = [AUTH_URL stringByAppendingString:AUTHENTICATE_URL]; authenticate_url = [authenticate_url stringByAppendingFormat:@"?oauth_token=%@", oauthToken]; authenticate_url = [authenticate_url stringByAppendingFormat:@"&oauth_callback=%@", oauth_callback.utf8AndURLEncode]; @@ -263,8 +267,8 @@ - (void)authenticateToken:(NSString *)oauthToken withCompletion:(void (^)(NSErro [request setValue:[NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey] ?: [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleIdentifierKey], (__bridge id)CFBundleGetValueForInfoDictionaryKey(CFBundleGetMainBundle(), kCFBundleVersionKey) ?: [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] ? [[UIScreen mainScreen] scale] : 1.0f)] forHTTPHeaderField:@"User-Agent"]; _delegateHandler = ^(NSDictionary *oauthParams) { - if (oauthParams[@"oauth_verifier"] == nil) { - NSError *authenticateError = [NSError errorWithDomain:@"com.ideaflasher.oauth.authenticate" code:0 userInfo:@{@"userInfo" : @"oauth_verifier not received and/or user denied access"}]; + if (oauthParams[@"oauth_token"] == nil || [oauthParams[@"authorize"] isEqualToString:@"0"]) { + NSError *authenticateError = [NSError errorWithDomain:@"com.ideaflasher.oauth.authenticate" code:0 userInfo:@{@"userInfo" : @"oauth_token not received and/or user denied access"}]; completion(authenticateError, oauthParams); } else { completion(nil, oauthParams); @@ -286,10 +290,22 @@ - (void)webViewDidFinishLoad:(UIWebView *)webView #pragma mark Used to detect call back in step 2 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + // Added this delegate call so that we can (maybe) gracefully handle if the user is already signed in + // --Yonatan Kogan 5/27/2013 + [self.controllerDelegate startedLoadingRequest:request oauth1Controller:self]; + if (_delegateHandler) { - // For other Oauth 1.0a service providers than LinkedIn, the call back URL might be part of the query of the URL (after the "?"). In this case use index 1 below. In any case NSLog the request URL after the user taps 'Allow'/'Authenticate' after he/she entered his/her username and password and see where in the URL the call back is. Note for some services the callback URL is set once on their website when registering an app, and the OAUTH_CALLBACK set here is ignored. + + // For other Oauth 1.0a service providers than LinkedIn, the call back URL might be part of the query of the URL (after the "?"). In this case use index 1 below. In any case NSLog the request URL after the user taps 'Allow'/'Authenticate' after he/she entered his/her username and password and see where in the URL the call back is. Note for some services the callback URL is set once on their website when registering an app, and the self.oauthCallback set here is ignored. NSString *urlWithoutQueryString = [request.URL.absoluteString componentsSeparatedByString:@"?"][0]; - if ([urlWithoutQueryString rangeOfString:OAUTH_CALLBACK].location != NSNotFound) + if (!self.oauthCallback) { + return YES; + } + // The second part of this AND statement is needed for "login with facebook functionality" + // otherwise, the conditional evaluates to true when the facebook page redirects to goodreads but + // the user hasn't yet authorized or not authorized + if ([urlWithoutQueryString rangeOfString:self.oauthCallback].location != NSNotFound + && [urlWithoutQueryString rangeOfString:@"/user/new"].location == NSNotFound) { NSString *queryString = [request.URL.absoluteString substringFromIndex:[request.URL.absoluteString rangeOfString:@"?"].location + 1]; NSDictionary *parameters = CHParametersFromQueryString(queryString); @@ -324,9 +340,19 @@ - (void)requestAccessToken:(NSString *)oauth_token_secret completion:(void (^)(NSError *error, NSDictionary *responseParams))completion { NSString *access_url = [AUTH_URL stringByAppendingString:ACCESS_TOKEN_URL]; - NSString *oauth_consumer_secret = CONSUMER_SECRET; + if (!self.consumerSecret) { + if (!completion) { + return; + } + NSMutableDictionary* details = [NSMutableDictionary dictionary]; + [details setValue:@"Consumer Secret is not set" forKey:NSLocalizedDescriptionKey]; + // populate the error object with the details + NSError *error = [NSError errorWithDomain:@"oauthInfo" code:200 userInfo:details]; + return completion(error,nil); + } + NSString *oauth_consumer_secret = self.consumerSecret; - NSMutableDictionary *allParameters = [self.class standardOauthParameters]; + NSMutableDictionary *allParameters = [self standardOauthParameters]; [allParameters setValue:oauth_verifier forKey:@"oauth_verifier"]; [allParameters setValue:oauth_token forKey:@"oauth_token"]; @@ -334,7 +360,7 @@ - (void)requestAccessToken:(NSString *)oauth_token_secret NSString *baseString = [ACCESS_TOKEN_METHOD stringByAppendingFormat:@"&%@&%@", access_url.utf8AndURLEncode, parametersString.utf8AndURLEncode]; NSString *secretString = [oauth_consumer_secret.utf8AndURLEncode stringByAppendingFormat:@"&%@", oauth_token_secret.utf8AndURLEncode]; - NSString *oauth_signature = [self.class signClearText:baseString withSecret:secretString]; + NSString *oauth_signature = [self signClearText:baseString withSecret:secretString]; [allParameters setValue:oauth_signature forKey:@"oauth_signature"]; parametersString = CHQueryStringFromParametersWithEncoding(allParameters, NSUTF8StringEncoding); @@ -360,11 +386,14 @@ - (void)requestAccessToken:(NSString *)oauth_token_secret } -+ (NSMutableDictionary *)standardOauthParameters +- (NSMutableDictionary *)standardOauthParameters { NSString *oauth_timestamp = [NSString stringWithFormat:@"%i", (NSUInteger)[NSDate.date timeIntervalSince1970]]; NSString *oauth_nonce = [NSString getNonce]; - NSString *oauth_consumer_key = CONSUMER_KEY; + if (!self.consumerKey) { + return nil; + } + NSString *oauth_consumer_key = self.consumerKey; NSString *oauth_signature_method = @"HMAC-SHA1"; NSString *oauth_version = @"1.0"; @@ -380,7 +409,7 @@ + (NSMutableDictionary *)standardOauthParameters #pragma mark build authorized API-requests -+ (NSURLRequest *)preparedRequestForPath:(NSString *)path +- (NSURLRequest *)preparedRequestForPath:(NSString *)path parameters:(NSDictionary *)queryParameters HTTPmethod:(NSString *)HTTPmethod oauthToken:(NSString *)oauth_token @@ -397,10 +426,14 @@ + (NSURLRequest *)preparedRequestForPath:(NSString *)path NSString *request_url = API_URL; if (path) request_url = [request_url stringByAppendingString:path]; - NSString *oauth_consumer_secret = CONSUMER_SECRET; + + if (!self.consumerSecret) { + return nil; + } + NSString *oauth_consumer_secret = self.consumerSecret; NSString *baseString = [HTTPmethod stringByAppendingFormat:@"&%@&%@", request_url.utf8AndURLEncode, parametersString.utf8AndURLEncode]; NSString *secretString = [oauth_consumer_secret.utf8AndURLEncode stringByAppendingFormat:@"&%@", oauth_token_secret.utf8AndURLEncode]; - NSString *oauth_signature = [self.class signClearText:baseString withSecret:secretString]; + NSString *oauth_signature = [self signClearText:baseString withSecret:secretString]; allParameters[@"oauth_signature"] = oauth_signature; NSString *queryString; @@ -426,7 +459,7 @@ + (NSURLRequest *)preparedRequestForPath:(NSString *)path } -+ (NSString *)URLStringWithoutQueryFromURL:(NSURL *) url +- (NSString *)URLStringWithoutQueryFromURL:(NSURL *) url { NSArray *parts = [[url absoluteString] componentsSeparatedByString:@"?"]; return [parts objectAtIndex:0]; @@ -434,7 +467,7 @@ + (NSString *)URLStringWithoutQueryFromURL:(NSURL *) url #pragma mark - -+ (NSString *)signClearText:(NSString *)text withSecret:(NSString *)secret +- (NSString *)signClearText:(NSString *)text withSecret:(NSString *)secret { NSData *secretData = [secret dataUsingEncoding:NSUTF8StringEncoding]; NSData *clearTextData = [text dataUsingEncoding:NSUTF8StringEncoding]; diff --git a/Simple-OAuth1/Simple-OAuth1/ViewController.m b/Simple-OAuth1/Simple-OAuth1/ViewController.m index ee24fa1..78e8abe 100644 --- a/Simple-OAuth1/Simple-OAuth1/ViewController.m +++ b/Simple-OAuth1/Simple-OAuth1/ViewController.m @@ -10,6 +10,9 @@ #import "OAuth1Controller.h" #import "LoginWebViewController.h" +#define MY_CONUSMER_KEY @"YOUR KEY HERE" +#define MY_CONUSMER_SECRET @"YOUR SECRET HERE" + @interface ViewController () @property (nonatomic, strong) OAuth1Controller *oauth1Controller; @@ -88,6 +91,9 @@ - (OAuth1Controller *)oauth1Controller { if (_oauth1Controller == nil) { _oauth1Controller = [[OAuth1Controller alloc] init]; + _oauth1Controller.consumerKey = MY_CONUSMER_KEY; + _oauth1Controller.consumerSecret = MY_CONUSMER_SECRET; + _oauth1Controller.oauthCallback = @"http://www.goodreads.com"; } return _oauth1Controller; } @@ -95,18 +101,20 @@ - (OAuth1Controller *)oauth1Controller - (IBAction)testGETRequest { - // Tumblr GET Request -// NSString *path = @"blog/chrhansen.tumblr.com/info"; // Insert your Tumblr name here -// NSDictionary *parameters = @{@"api_key" : @"The CONSUMER_KEY from OAuth1Controller.m"}; // Insert your Tumblr API-key/CONSUMER_KEY here + // Goodreads GET Requests that are signed with OAuth return HTML rather than XML... + + // GoodReads GET Request + NSString *path = @"user/show"; + NSDictionary *parameters = @{YOUR_GOODREADS_USERNAME : @"username", MY_CONUSMER_KEY : @"key"}; - // LinkedIn GET Request - NSString *path = @"people/~"; - NSDictionary *parameters = @{@"format" : @"json"}; + OAuth1Controller *oauth1Controller = [[OAuth1Controller alloc] init]; + oauth1Controller.consumerKey = MY_CONUSMER_KEY; + oauth1Controller.consumerSecret = MY_CONUSMER_SECRET; // Build authorized request based on path, parameters, tokens, timestamp etc. - NSURLRequest *preparedRequest = [OAuth1Controller preparedRequestForPath:path + NSURLRequest *preparedRequest = [oauth1Controller preparedRequestForPath:path parameters:parameters HTTPmethod:@"GET" oauthToken:self.oauthToken @@ -128,17 +136,16 @@ - (IBAction)testGETRequest - (IBAction)testPOSTRequest { // Tumblr POST Request - NSString *path = @"blog/YOUR_TUMBLR_NAME.tumblr.com/post"; // set your Tumblr name here - NSDictionary *parameters = @{@"type" : @"text", - @"title" : @"Simple OAuth1.0a for iOS by Christian Hansen", - @"body" : @"https://github.com/Christian-Hansen/simple-oauth1"}; + NSString *path = @"api/auth_user"; - // LinkedIn POST Request - // Not implemented, see http://developer.linkedin.com/ + OAuth1Controller *oauth1Controller = [[OAuth1Controller alloc] init]; + + oauth1Controller.consumerKey = MY_CONUSMER_KEY; + oauth1Controller.consumerSecret = MY_CONUSMER_SECRET; // Build authorized request based on path, parameters, tokens, timestamp etc. - NSURLRequest *preparedRequest = [OAuth1Controller preparedRequestForPath:path - parameters:parameters + NSURLRequest *preparedRequest = [oauth1Controller preparedRequestForPath:path + parameters:nil HTTPmethod:@"POST" oauthToken:self.oauthToken oauthSecret:self.oauthTokenSecret]; From 10c2074041128d9ef6606da9065050aebdbfa289 Mon Sep 17 00:00:00 2001 From: Yonatan Kogan Date: Mon, 27 May 2013 16:20:21 -0700 Subject: [PATCH 2/2] Include the original OAuth1Controller files --- .../Simple-OAuth1/LinkedInOAuth1Controller.h | 27 ++ .../Simple-OAuth1/LinkedInOAuth1Controller.m | 453 ++++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 Simple-OAuth1/Simple-OAuth1/LinkedInOAuth1Controller.h create mode 100644 Simple-OAuth1/Simple-OAuth1/LinkedInOAuth1Controller.m diff --git a/Simple-OAuth1/Simple-OAuth1/LinkedInOAuth1Controller.h b/Simple-OAuth1/Simple-OAuth1/LinkedInOAuth1Controller.h new file mode 100644 index 0000000..2902c8d --- /dev/null +++ b/Simple-OAuth1/Simple-OAuth1/LinkedInOAuth1Controller.h @@ -0,0 +1,27 @@ +// +// OAuth1Controller.h +// Simple-OAuth1 +// +// Created by Christian Hansen on 02/12/12. +// Copyright (c) 2012 Christian-Hansen. All rights reserved. +// + +#import + +@interface LinkedInOAuth1Controller : NSObject + +- (void)loginWithWebView:(UIWebView *)webWiew + completion:(void (^)(NSDictionary *oauthTokens, NSError *error))completion; + +- (void)requestAccessToken:(NSString *)oauth_token_secret + oauthToken:(NSString *)oauth_token + oauthVerifier:(NSString *)oauth_verifier + completion:(void (^)(NSError *error, NSDictionary *responseParams))completion; + ++ (NSURLRequest *)preparedRequestForPath:(NSString *)path + parameters:(NSDictionary *)parameters + HTTPmethod:(NSString *)method + oauthToken:(NSString *)oauth_token + oauthSecret:(NSString *)oauth_token_secret; + +@end diff --git a/Simple-OAuth1/Simple-OAuth1/LinkedInOAuth1Controller.m b/Simple-OAuth1/Simple-OAuth1/LinkedInOAuth1Controller.m new file mode 100644 index 0000000..af299ee --- /dev/null +++ b/Simple-OAuth1/Simple-OAuth1/LinkedInOAuth1Controller.m @@ -0,0 +1,453 @@ +// +// OAuth1Controller.m +// Simple-OAuth1 +// +// Created by Christian Hansen on 02/12/12. +// Copyright (c) 2012 Christian-Hansen. All rights reserved. +// + +#import "LinkedInOAuth1Controller.h" +#import "NSString+URLEncoding.h" +#include "hmac.h" +#include "Base64Transcoder.h" + +typedef void (^WebWiewDelegateHandler)(NSDictionary *oauthParams); + +// Go to: http://www.tumblr.com/oauth/apps and get your API keys + +//#define OAUTH_CALLBACK @"tumblr_callback" //Sometimes this has to be the same as the registered app callback url +//#define CONSUMER_KEY @"OAuth consumer key" +//#define CONSUMER_SECRET @"OAuth consumer secret" +//#define AUTH_URL @"http://www.tumblr.com/oauth/" +//#define REQUEST_TOKEN_URL @"request_token" +//#define AUTHENTICATE_URL @"authorize" +//#define ACCESS_TOKEN_URL @"access_token" +//#define API_URL @"http://api.tumblr.com/v2/" +//#define OAUTH_SCOPE_PARAM @"" + +// Go to: https://www.linkedin.com/secure/developer and get your API keys + +#define OAUTH_CALLBACK @"linkedin_oauth" //Sometimes this has to be the same as the registered app callback url +#define CONSUMER_KEY @"API Key" +#define CONSUMER_SECRET @"Secret Key" +#define AUTH_URL @"https://api.linkedin.com/uas/" +#define REQUEST_TOKEN_URL @"oauth/requestToken" +#define AUTHENTICATE_URL @"oauth/authorize" +#define ACCESS_TOKEN_URL @"oauth/accessToken" +#define API_URL @"http://api.linkedin.com/v1/" +#define OAUTH_SCOPE_PARAM @"r_fullprofile r_emailaddress rw_nus r_network w_messages" + + +#define REQUEST_TOKEN_METHOD @"POST" +#define ACCESS_TOKEN_METHOD @"POST" + + +//--- The part below is from AFNetworking--- +static NSString * CHPercentEscapedQueryStringPairMemberFromStringWithEncoding(NSString *string, NSStringEncoding encoding) { + static NSString * const kCHCharactersToBeEscaped = @":/?&=;+!@#$()~"; + static NSString * const kCHCharactersToLeaveUnescaped = @"[]."; + + return (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)string, (__bridge CFStringRef)kCHCharactersToLeaveUnescaped, (__bridge CFStringRef)kCHCharactersToBeEscaped, CFStringConvertNSStringEncodingToEncoding(encoding)); +} + +#pragma mark - + +@interface CHQueryStringPair : NSObject +@property (readwrite, nonatomic, strong) id field; +@property (readwrite, nonatomic, strong) id value; + +- (id)initWithField:(id)field value:(id)value; + +- (NSString *)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding; +@end + +@implementation CHQueryStringPair +@synthesize field = _field; +@synthesize value = _value; + +- (id)initWithField:(id)field value:(id)value { + self = [super init]; + if (!self) { + return nil; + } + + self.field = field; + self.value = value; + + return self; +} + +- (NSString *)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding { + if (!self.value || [self.value isEqual:[NSNull null]]) { + return CHPercentEscapedQueryStringPairMemberFromStringWithEncoding([self.field description], stringEncoding); + } else { + return [NSString stringWithFormat:@"%@=%@", CHPercentEscapedQueryStringPairMemberFromStringWithEncoding([self.field description], stringEncoding), CHPercentEscapedQueryStringPairMemberFromStringWithEncoding([self.value description], stringEncoding)]; + } +} + +@end + +#pragma mark - + +extern NSArray * CHQueryStringPairsFromDictionary(NSDictionary *dictionary); +extern NSArray * CHQueryStringPairsFromKeyAndValue(NSString *key, id value); + +NSString * CHQueryStringFromParametersWithEncoding(NSDictionary *parameters, NSStringEncoding stringEncoding) { + NSMutableArray *mutablePairs = [NSMutableArray array]; + for (CHQueryStringPair *pair in CHQueryStringPairsFromDictionary(parameters)) { + [mutablePairs addObject:[pair URLEncodedStringValueWithEncoding:stringEncoding]]; + } + + return [mutablePairs componentsJoinedByString:@"&"]; +} + +NSArray * CHQueryStringPairsFromDictionary(NSDictionary *dictionary) { + return CHQueryStringPairsFromKeyAndValue(nil, dictionary); +} + +NSArray * CHQueryStringPairsFromKeyAndValue(NSString *key, id value) { + NSMutableArray *mutableQueryStringComponents = [NSMutableArray array]; + + if([value isKindOfClass:[NSDictionary class]]) { + // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(caseInsensitiveCompare:)]; + [[[value allKeys] sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]] enumerateObjectsUsingBlock:^(id nestedKey, NSUInteger idx, BOOL *stop) { + id nestedValue = [value objectForKey:nestedKey]; + if (nestedValue) { + [mutableQueryStringComponents addObjectsFromArray:CHQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)]; + } + }]; + } else if([value isKindOfClass:[NSArray class]]) { + [value enumerateObjectsUsingBlock:^(id nestedValue, NSUInteger idx, BOOL *stop) { + [mutableQueryStringComponents addObjectsFromArray:CHQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)]; + }]; + } else { + [mutableQueryStringComponents addObject:[[CHQueryStringPair alloc] initWithField:key value:value]]; + } + + return mutableQueryStringComponents; +} + + +static inline NSDictionary *CHParametersFromQueryString(NSString *queryString) +{ + NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; + if (queryString) + { + NSScanner *parameterScanner = [[NSScanner alloc] initWithString:queryString]; + NSString *name = nil; + NSString *value = nil; + + while (![parameterScanner isAtEnd]) { + name = nil; + [parameterScanner scanUpToString:@"=" intoString:&name]; + [parameterScanner scanString:@"=" intoString:NULL]; + + value = nil; + [parameterScanner scanUpToString:@"&" intoString:&value]; + [parameterScanner scanString:@"&" intoString:NULL]; + + if (name && value) + { + [parameters setValue:[value stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] + forKey:[name stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + } + } + } + return parameters; +} + +//--- The part above is from AFNetworking--- + +@interface LinkedInOAuth1Controller () + +@property (nonatomic, weak) UIWebView *webView; +@property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator; +@property (nonatomic, strong) WebWiewDelegateHandler delegateHandler; + +@end + +@implementation LinkedInOAuth1Controller + +- (void)loginWithWebView:(UIWebView *)webWiew completion:(void (^)(NSDictionary *oauthTokens, NSError *error))completion +{ + self.webView = webWiew; + self.webView.delegate = self; + + self.loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + self.loadingIndicator.color = [UIColor grayColor]; + [self.loadingIndicator startAnimating]; + self.loadingIndicator.center = self.webView.center; + [self.webView addSubview:self.loadingIndicator]; + + [self obtainRequestTokenWithCompletion:^(NSError *error, NSDictionary *responseParams) + { + NSString *oauth_token_secret = responseParams[@"oauth_token_secret"]; + NSString *oauth_token = responseParams[@"oauth_token"]; + if (oauth_token_secret + && oauth_token) + { + [self authenticateToken:oauth_token withCompletion:^(NSError *error, NSDictionary *responseParams) + { + if (!error) + { + [self requestAccessToken:oauth_token_secret + oauthToken:responseParams[@"oauth_token"] + oauthVerifier:responseParams[@"oauth_verifier"] + completion:^(NSError *error, NSDictionary *responseParams) + { + completion(responseParams, error); + }]; + } + else + { + completion(responseParams, error); + } + }]; + } + else + { + if (!error) error = [NSError errorWithDomain:@"oauth.requestToken" code:0 userInfo:@{@"userInfo" : @"oauth_token and oauth_token_secret were not both returned from request token step"}]; + completion(responseParams, error); + } + }]; +} + + +#pragma mark - Step 1 Obtaining a request token +- (void)obtainRequestTokenWithCompletion:(void (^)(NSError *error, NSDictionary *responseParams))completion +{ + NSString *request_url = [AUTH_URL stringByAppendingString:REQUEST_TOKEN_URL]; + NSString *oauth_consumer_secret = CONSUMER_SECRET; + + NSMutableDictionary *allParameters = [self.class standardOauthParameters]; + if ([OAUTH_SCOPE_PARAM length] > 0) [allParameters setValue:OAUTH_SCOPE_PARAM forKey:@"scope"]; + + NSString *parametersString = CHQueryStringFromParametersWithEncoding(allParameters, NSUTF8StringEncoding); + + NSString *baseString = [REQUEST_TOKEN_METHOD stringByAppendingFormat:@"&%@&%@", request_url.utf8AndURLEncode, parametersString.utf8AndURLEncode]; + NSString *secretString = [oauth_consumer_secret.utf8AndURLEncode stringByAppendingString:@"&"]; + NSString *oauth_signature = [self.class signClearText:baseString withSecret:secretString]; + [allParameters setValue:oauth_signature forKey:@"oauth_signature"]; + + parametersString = CHQueryStringFromParametersWithEncoding(allParameters, NSUTF8StringEncoding); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:request_url]]; + request.HTTPMethod = REQUEST_TOKEN_METHOD; + + NSMutableArray *parameterPairs = [NSMutableArray array]; + for (NSString *name in allParameters) { + NSString *aPair = [name stringByAppendingFormat:@"=\"%@\"", [allParameters[name] utf8AndURLEncode]]; + [parameterPairs addObject:aPair]; + } + NSString *oAuthHeader = [@"OAuth " stringByAppendingFormat:@"%@", [parameterPairs componentsJoinedByString:@", "]]; + [request setValue:oAuthHeader forHTTPHeaderField:@"Authorization"]; + + [NSURLConnection sendAsynchronousRequest:request + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { + NSString *reponseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + completion(nil, CHParametersFromQueryString(reponseString)); + }]; +} + +#pragma mark - Step 2 Show login to the user to authorize our app +- (void)authenticateToken:(NSString *)oauthToken withCompletion:(void (^)(NSError *error, NSDictionary *responseParams))completion +{ + NSString *oauth_callback = OAUTH_CALLBACK; + NSString *authenticate_url = [AUTH_URL stringByAppendingString:AUTHENTICATE_URL]; + authenticate_url = [authenticate_url stringByAppendingFormat:@"?oauth_token=%@", oauthToken]; + authenticate_url = [authenticate_url stringByAppendingFormat:@"&oauth_callback=%@", oauth_callback.utf8AndURLEncode]; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:authenticate_url]]; + [request setValue:[NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey] ?: [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleIdentifierKey], (__bridge id)CFBundleGetValueForInfoDictionaryKey(CFBundleGetMainBundle(), kCFBundleVersionKey) ?: [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] ? [[UIScreen mainScreen] scale] : 1.0f)] forHTTPHeaderField:@"User-Agent"]; + + _delegateHandler = ^(NSDictionary *oauthParams) { + if (oauthParams[@"oauth_verifier"] == nil) { + NSError *authenticateError = [NSError errorWithDomain:@"com.ideaflasher.oauth.authenticate" code:0 userInfo:@{@"userInfo" : @"oauth_verifier not received and/or user denied access"}]; + completion(authenticateError, oauthParams); + } else { + completion(nil, oauthParams); + } + }; + [self.webView loadRequest:request]; +} + + + +#pragma mark - Webview delegate +#pragma mark Turn off spinner +- (void)webViewDidFinishLoad:(UIWebView *)webView +{ + [self.loadingIndicator removeFromSuperview]; + self.loadingIndicator = nil; +} + +#pragma mark Used to detect call back in step 2 +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType +{ + if (_delegateHandler) { + // For other Oauth 1.0a service providers than LinkedIn, the call back URL might be part of the query of the URL (after the "?"). In this case use index 1 below. In any case NSLog the request URL after the user taps 'Allow'/'Authenticate' after he/she entered his/her username and password and see where in the URL the call back is. Note for some services the callback URL is set once on their website when registering an app, and the OAUTH_CALLBACK set here is ignored. + NSString *urlWithoutQueryString = [request.URL.absoluteString componentsSeparatedByString:@"?"][0]; + if ([urlWithoutQueryString rangeOfString:OAUTH_CALLBACK].location != NSNotFound) + { + NSString *queryString = [request.URL.absoluteString substringFromIndex:[request.URL.absoluteString rangeOfString:@"?"].location + 1]; + NSDictionary *parameters = CHParametersFromQueryString(queryString); + parameters = [self removeAppendedSubstringOnVerifierIfPresent:parameters]; + + _delegateHandler(parameters); + _delegateHandler = nil; + } + } + return YES; +} + +#define FacebookAndTumblrAppendedString @"#_=_" + +- (NSDictionary *)removeAppendedSubstringOnVerifierIfPresent:(NSDictionary *)parameters +{ + NSString *oauthVerifier = parameters[@"oauth_verifier"]; + if ([oauthVerifier hasSuffix:FacebookAndTumblrAppendedString] + && [oauthVerifier length] > FacebookAndTumblrAppendedString.length) { + NSMutableDictionary *mutableParameters = parameters.mutableCopy; + mutableParameters[@"oauth_verifier"] = [oauthVerifier substringToIndex:oauthVerifier.length - FacebookAndTumblrAppendedString.length]; + parameters = mutableParameters; + } + return parameters; +} + + +#pragma mark - Step 3 Request access token now that user has authorized the app +- (void)requestAccessToken:(NSString *)oauth_token_secret + oauthToken:(NSString *)oauth_token + oauthVerifier:(NSString *)oauth_verifier + completion:(void (^)(NSError *error, NSDictionary *responseParams))completion +{ + NSString *access_url = [AUTH_URL stringByAppendingString:ACCESS_TOKEN_URL]; + NSString *oauth_consumer_secret = CONSUMER_SECRET; + + NSMutableDictionary *allParameters = [self.class standardOauthParameters]; + [allParameters setValue:oauth_verifier forKey:@"oauth_verifier"]; + [allParameters setValue:oauth_token forKey:@"oauth_token"]; + + NSString *parametersString = CHQueryStringFromParametersWithEncoding(allParameters, NSUTF8StringEncoding); + + NSString *baseString = [ACCESS_TOKEN_METHOD stringByAppendingFormat:@"&%@&%@", access_url.utf8AndURLEncode, parametersString.utf8AndURLEncode]; + NSString *secretString = [oauth_consumer_secret.utf8AndURLEncode stringByAppendingFormat:@"&%@", oauth_token_secret.utf8AndURLEncode]; + NSString *oauth_signature = [self.class signClearText:baseString withSecret:secretString]; + [allParameters setValue:oauth_signature forKey:@"oauth_signature"]; + + parametersString = CHQueryStringFromParametersWithEncoding(allParameters, NSUTF8StringEncoding); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:access_url]]; + request.HTTPMethod = ACCESS_TOKEN_METHOD; + + NSMutableArray *parameterPairs = [NSMutableArray array]; + for (NSString *name in allParameters) + { + NSString *aPair = [name stringByAppendingFormat:@"=\"%@\"", [allParameters[name] utf8AndURLEncode]]; + [parameterPairs addObject:aPair]; + } + NSString *oAuthHeader = [@"OAuth " stringByAppendingFormat:@"%@", [parameterPairs componentsJoinedByString:@", "]]; + [request setValue:oAuthHeader forHTTPHeaderField:@"Authorization"]; + + [NSURLConnection sendAsynchronousRequest:request + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { + NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + completion(nil, CHParametersFromQueryString(responseString)); + }]; +} + + ++ (NSMutableDictionary *)standardOauthParameters +{ + NSString *oauth_timestamp = [NSString stringWithFormat:@"%i", (NSUInteger)[NSDate.date timeIntervalSince1970]]; + NSString *oauth_nonce = [NSString getNonce]; + NSString *oauth_consumer_key = CONSUMER_KEY; + NSString *oauth_signature_method = @"HMAC-SHA1"; + NSString *oauth_version = @"1.0"; + + NSMutableDictionary *standardParameters = [NSMutableDictionary dictionary]; + [standardParameters setValue:oauth_consumer_key forKey:@"oauth_consumer_key"]; + [standardParameters setValue:oauth_nonce forKey:@"oauth_nonce"]; + [standardParameters setValue:oauth_signature_method forKey:@"oauth_signature_method"]; + [standardParameters setValue:oauth_timestamp forKey:@"oauth_timestamp"]; + [standardParameters setValue:oauth_version forKey:@"oauth_version"]; + + return standardParameters; +} + + +#pragma mark build authorized API-requests ++ (NSURLRequest *)preparedRequestForPath:(NSString *)path + parameters:(NSDictionary *)queryParameters + HTTPmethod:(NSString *)HTTPmethod + oauthToken:(NSString *)oauth_token + oauthSecret:(NSString *)oauth_token_secret +{ + if (!HTTPmethod + || !oauth_token) return nil; + + NSMutableDictionary *allParameters = [self standardOauthParameters]; + allParameters[@"oauth_token"] = oauth_token; + if (queryParameters) [allParameters addEntriesFromDictionary:queryParameters]; + + NSString *parametersString = CHQueryStringFromParametersWithEncoding(allParameters, NSUTF8StringEncoding); + + NSString *request_url = API_URL; + if (path) request_url = [request_url stringByAppendingString:path]; + NSString *oauth_consumer_secret = CONSUMER_SECRET; + NSString *baseString = [HTTPmethod stringByAppendingFormat:@"&%@&%@", request_url.utf8AndURLEncode, parametersString.utf8AndURLEncode]; + NSString *secretString = [oauth_consumer_secret.utf8AndURLEncode stringByAppendingFormat:@"&%@", oauth_token_secret.utf8AndURLEncode]; + NSString *oauth_signature = [self.class signClearText:baseString withSecret:secretString]; + allParameters[@"oauth_signature"] = oauth_signature; + + NSString *queryString; + if (queryParameters) queryString = CHQueryStringFromParametersWithEncoding(queryParameters, NSUTF8StringEncoding); + if (queryString) request_url = [request_url stringByAppendingFormat:@"?%@", queryString]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:request_url]]; + request.HTTPMethod = HTTPmethod; + + NSMutableArray *parameterPairs = [NSMutableArray array]; + [allParameters removeObjectsForKeys:queryParameters.allKeys]; + for (NSString *name in allParameters) { + NSString *aPair = [name stringByAppendingFormat:@"=\"%@\"", [allParameters[name] utf8AndURLEncode]]; + [parameterPairs addObject:aPair]; + } + NSString *oAuthHeader = [@"OAuth " stringByAppendingFormat:@"%@", [parameterPairs componentsJoinedByString:@", "]]; + [request setValue:oAuthHeader forHTTPHeaderField:@"Authorization"]; + if ([HTTPmethod isEqualToString:@"POST"] + && queryParameters != nil) { + NSData *body = [queryString dataUsingEncoding:NSUTF8StringEncoding]; + [request setHTTPBody:body]; + } + return request; +} + + ++ (NSString *)URLStringWithoutQueryFromURL:(NSURL *) url +{ + NSArray *parts = [[url absoluteString] componentsSeparatedByString:@"?"]; + return [parts objectAtIndex:0]; +} + + +#pragma mark - ++ (NSString *)signClearText:(NSString *)text withSecret:(NSString *)secret +{ + NSData *secretData = [secret dataUsingEncoding:NSUTF8StringEncoding]; + NSData *clearTextData = [text dataUsingEncoding:NSUTF8StringEncoding]; + unsigned char result[20]; + hmac_sha1((unsigned char *)[clearTextData bytes], [clearTextData length], (unsigned char *)[secretData bytes], [secretData length], result); + + //Base64 Encoding + char base64Result[32]; + size_t theResultLength = 32; + Base64EncodeData(result, 20, base64Result, &theResultLength); + NSData *theData = [NSData dataWithBytes:base64Result length:theResultLength]; + + return [NSString.alloc initWithData:theData encoding:NSUTF8StringEncoding]; +} + +@end \ No newline at end of file