diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 826f91f..6eb4463 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -31,6 +31,7 @@ PODS: - SimpleAuth/Tumblr (= 0.3.9) - SimpleAuth/Twitter (= 0.3.9) - SimpleAuth/TwitterWeb (= 0.3.9) + - SimpleAuth/VKontakteWeb (= 0.3.9) - SimpleAuth/BoxWeb (0.3.9): - SimpleAuth/Core - SimpleAuth/Core (0.3.9): @@ -72,13 +73,15 @@ PODS: - SimpleAuth/TwitterWeb (0.3.9): - cocoa-oauth - SimpleAuth/Core + - SimpleAuth/VKontakteWeb (0.3.9): + - SimpleAuth/Core DEPENDENCIES: - SimpleAuth (from `..`) EXTERNAL SOURCES: SimpleAuth: - :path: ".." + :path: .. SPEC CHECKSUMS: CMDQueryStringSerialization: 03c5d9847a4eaa2b3e4e439ce1ae24f914cc7fe1 @@ -86,6 +89,6 @@ SPEC CHECKSUMS: ISO8601: 8805b6cd6b2d0f7e594f7c5b50e8b00b51695ac0 NSData+Base64: 4e84902c4db907a15673474677e57763ef3903e4 ReactiveCocoa: e2db045570aa97c695e7aa97c2bcab222ae51f4a - SimpleAuth: bc7d594f90711a7e6ef7624dce5aa8c8b09770ab + SimpleAuth: 84c0a98a8797f3b9400d699f3a2c7e3be5dde1ca -COCOAPODS: 0.36.4 +COCOAPODS: 0.37.2 diff --git a/Example/SimpleAuth/Providers.plist b/Example/SimpleAuth/Providers.plist index efa2451..624ff1a 100644 --- a/Example/SimpleAuth/Providers.plist +++ b/Example/SimpleAuth/Providers.plist @@ -19,5 +19,6 @@ trello-web box-web onedrive-web + vkontakte-web diff --git a/Example/SimpleAuth/SADAppDelegate.m b/Example/SimpleAuth/SADAppDelegate.m index 98a5cb2..a6919a6 100644 --- a/Example/SimpleAuth/SADAppDelegate.m +++ b/Example/SimpleAuth/SADAppDelegate.m @@ -51,6 +51,7 @@ - (void)configureAuthorizaionProviders { // app_id is required SimpleAuth.configuration[@"facebook"] = @{}; + // app_secret optional required for re-authorization of token SimpleAuth.configuration[@"facebook-web"] = @{}; // client_id and redirect_uri are required @@ -88,6 +89,9 @@ - (void)configureAuthorizaionProviders { // client_id and client_secret are required SimpleAuth.configuration[@"onedrive-web"] = @{}; + + // client_id is required + SimpleAuth.configuration[@"vkontakte-web"] = @{}; } diff --git a/Pod/Core/SimpleAuth.h b/Pod/Core/SimpleAuth.h index 38edcea..c8965ea 100644 --- a/Pod/Core/SimpleAuth.h +++ b/Pod/Core/SimpleAuth.h @@ -46,6 +46,30 @@ */ + (void)authorize:(NSString *)provider options:(NSDictionary *)options completion:(SimpleAuthRequestHandler)completion; +/** + Perform re-authorization with the given provider and all previously configured + and default provider options. This is for providers where the token expires and you + want to renew the non expired token. + + @param token the unexpired token recieved from the service + @param completion Called on the main queue when the operation is complete. + + @see +authorize:options:completion: + */ ++ (void)reAuthorize:(NSString *)provider token:(NSString *)token completion:(SimpleAuthRequestHandler)completion; + +/** + Perform re-authorization with the given provider and all previously configured + and default provider options. This is for providers where the token expires and you + want to renew the non expired token. + + @param token the unexpired token recieved from the service + @param completion Called on the main queue when the operation is complete. + + @see +authorize:options:completion: + */ ++ (void)reAuthorize:(NSString *)type options:(NSDictionary *)options token:(NSString *)token completion:(SimpleAuthRequestHandler)completion; + /** Determine whether the provider can handle the callback URL or not. diff --git a/Pod/Core/SimpleAuth.m b/Pod/Core/SimpleAuth.m index 3037b4f..f35f977 100644 --- a/Pod/Core/SimpleAuth.m +++ b/Pod/Core/SimpleAuth.m @@ -62,6 +62,33 @@ + (void)authorize:(NSString *)type options:(NSDictionary *)options completion:(S }]; } ++ (void)reAuthorize:(NSString *)type token:(NSString *)token completion:(SimpleAuthRequestHandler)completion { + [self reAuthorize:type options:nil token:token completion:completion]; +} + + ++ (void)reAuthorize:(NSString *)type options:(NSDictionary *)options token:(NSString *)token completion:(SimpleAuthRequestHandler)completion{ + // Load the provider class + Class klass = [self providers][type]; + NSAssert(klass, @"There is no class registered to handle %@ requests.", type); + + // Create options dictionary + NSDictionary *defaultOptions = [klass defaultOptions]; + NSDictionary *registeredOptions = [self configuration][type]; + NSMutableDictionary *resolvedOptions = [NSMutableDictionary new]; + [resolvedOptions addEntriesFromDictionary:defaultOptions]; + [resolvedOptions addEntriesFromDictionary:registeredOptions]; + [resolvedOptions addEntriesFromDictionary:options]; + + // Create the provider and run authorization + SimpleAuthProvider *provider = [(SimpleAuthProvider *)[klass alloc] initWithOptions:resolvedOptions]; + [provider reAuthorizeWithToken:token completionHandler:^(id responseObject, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(responseObject, error); + }); + [provider class]; // Kepp the provider around until the callback is complete + }]; +} + (BOOL)handleCallback:(NSURL *)URL { NSParameterAssert(URL != nil); diff --git a/Pod/Core/SimpleAuthProvider.h b/Pod/Core/SimpleAuthProvider.h index a531d37..57534ce 100644 --- a/Pod/Core/SimpleAuthProvider.h +++ b/Pod/Core/SimpleAuthProvider.h @@ -20,5 +20,6 @@ - (instancetype)initWithOptions:(NSDictionary *)options; - (void)authorizeWithCompletion:(SimpleAuthRequestHandler)completion; +- (void)reAuthorizeWithToken:(NSString *)token completionHandler:(SimpleAuthRequestHandler)completion; @end diff --git a/Pod/Core/SimpleAuthProvider.m b/Pod/Core/SimpleAuthProvider.m index e1ec723..05ac21b 100644 --- a/Pod/Core/SimpleAuthProvider.m +++ b/Pod/Core/SimpleAuthProvider.m @@ -53,4 +53,9 @@ - (void)authorizeWithCompletion:(SimpleAuthRequestHandler)completion { [self doesNotRecognizeSelector:_cmd]; } +- (void)reAuthorizeWithToken:(NSString *)token completionHandler:(SimpleAuthRequestHandler)completion { + // Provider doesn't have a re authorize token method setup revert back to standard authorization + [self authorizeWithCompletion:completion]; +} + @end diff --git a/Pod/Providers/Facebook/SimpleAuthFacebookProvider.m b/Pod/Providers/Facebook/SimpleAuthFacebookProvider.m index a9cd267..6c00b1c 100644 --- a/Pod/Providers/Facebook/SimpleAuthFacebookProvider.m +++ b/Pod/Providers/Facebook/SimpleAuthFacebookProvider.m @@ -65,7 +65,10 @@ - (RACSignal *)systemAccount { - (RACSignal *)remoteAccountWithSystemAccount:(ACAccount *)account { return [RACSignal createSignal:^RACDisposable *(id subscriber) { NSURL *URL = [NSURL URLWithString:@"https://graph.facebook.com/me"]; - SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeFacebook requestMethod:SLRequestMethodGET URL:URL parameters:nil]; + NSDictionary *parameters = @{ + @"fields" : @"name,first_name,last_name,verified,email,location,link" + }; + SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeFacebook requestMethod:SLRequestMethodGET URL:URL parameters:parameters]; request.account = account; [request performRequestWithHandler:^(NSData *data, NSHTTPURLResponse *response, NSError *connectionError) { NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 99)]; @@ -140,7 +143,8 @@ - (NSDictionary *)infoDictionaryWithRemoteAccount:(NSDictionary *)remoteAccount if (location) { dictionary[@"location"] = location; } - + if (remoteAccount[@"verified"]) + dictionary[@"verified"] = remoteAccount[@"verified"]; dictionary[@"urls"] = @{ @"Facebook": remoteAccount[@"link"] }; diff --git a/Pod/Providers/FacebookWeb/SimpleAuthFaceBookWebProvider.m b/Pod/Providers/FacebookWeb/SimpleAuthFaceBookWebProvider.m index 411cfdf..9fa7334 100644 --- a/Pod/Providers/FacebookWeb/SimpleAuthFaceBookWebProvider.m +++ b/Pod/Providers/FacebookWeb/SimpleAuthFaceBookWebProvider.m @@ -62,6 +62,27 @@ - (void)authorizeWithCompletion:(SimpleAuthRequestHandler)completion { }]; } +- (void)reAuthorizeWithToken:(NSString *)token completionHandler:(SimpleAuthRequestHandler)completion{ + if (token.length <= 0) { + // There is no token so we are going to use the standard login method + [self authorizeWithCompletion:completion]; + return; + } + [[[self reAuthAccessTokenWithToken:token] + flattenMap:^(NSDictionary *response) { + NSArray *signals = @[ + [self accountWithAccessToken:response], + [RACSignal return:response] + ]; + return [self rac_liftSelector:@selector(dictionaryWithAccount:accessToken:) withSignalsFromArray:signals]; + }] + subscribeNext:^(id x) { + completion(x, nil); + } + error:^(NSError *error) { + completion(nil, error); + }]; +} #pragma mark - Private @@ -98,10 +119,55 @@ - (RACSignal *)accessToken { }]; } +- (RACSignal *)reAuthAccessTokenWithToken:(NSString *)preToken { + return [RACSignal createSignal:^RACDisposable *(id subscriber) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSDictionary *parameters = @{ + @"client_id" : self.options[@"app_id"], + @"grant_type" :@"fb_exchange_token", + @"response_type" : @"token", + @"grant_type" :@"fb_exchange_token", + @"client_secret" : self.options[@"app_secret"], + @"fb_exchange_token" : preToken + }; + + NSString *URLString = [NSString stringWithFormat: + @"https://graph.facebook.com/oauth/access_token?%@", + [CMDQueryStringSerialization queryStringWithDictionary:parameters]]; + NSError *responseError = nil; + NSURLResponse *response = nil; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:URLString]]; + NSData *dataResponse = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&responseError]; + if (!responseError){ + NSString *responseString = [[NSString alloc] initWithData:dataResponse encoding:NSUTF8StringEncoding]; + NSDictionary *dictionary = [CMDQueryStringSerialization dictionaryWithQueryString:responseString]; + id token = dictionary[@"access_token"]; + id expiration = dictionary[@"expires"]; + + // Check for error + if (!token || !expiration) { + [subscriber sendError:[NSError errorWithDomain:@"Missing token or experation from facebook" code:101 userInfo:nil]]; + return; + } + // Send completion + [subscriber sendNext:@{@"access_token" : token, @"expires_in" : expiration}]; + [subscriber sendCompleted]; + } + else { + [subscriber sendError:responseError]; + return ; + } + }); + return nil; + }]; +} - (RACSignal *)accountWithAccessToken:(NSDictionary *)accessToken { return [RACSignal createSignal:^RACDisposable *(id subscriber) { - NSDictionary *parameters = @{ @"access_token" : accessToken[@"access_token"] }; + NSDictionary *parameters = @{ + @"access_token" : accessToken[@"access_token"], + @"fields" : @"name,first_name,last_name,verified,email,location,link" + }; NSString *URLString = [NSString stringWithFormat: @"https://graph.facebook.com/me?%@", [CMDQueryStringSerialization queryStringWithDictionary:parameters]]; diff --git a/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebLoginViewController.h b/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebLoginViewController.h new file mode 100644 index 0000000..89d5fca --- /dev/null +++ b/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebLoginViewController.h @@ -0,0 +1,12 @@ +// +// SimpleAuthVKontakteWebLoginViewController.h +// SimpleAuth +// +// Created by Mikhail Kupriyanov on 7/7/15. +// + +#import "SimpleAuthWebViewController.h" + +@interface SimpleAuthVKontakteWebLoginViewController : SimpleAuthWebViewController + +@end diff --git a/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebLoginViewController.m b/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebLoginViewController.m new file mode 100644 index 0000000..3fa2d84 --- /dev/null +++ b/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebLoginViewController.m @@ -0,0 +1,39 @@ +// +// SimpleAuthVKontakteWebLoginViewController.m +// SimpleAuth +// +// Created by Mikhail Kupriyanov on 7/7/15. +// + +#import "SimpleAuthVKontakteWebLoginViewController.h" + +@implementation SimpleAuthVKontakteWebLoginViewController + +#pragma mark - SimpleAuthWebViewController + +- (instancetype)initWithOptions:(NSDictionary *)options requestToken:(NSDictionary *)requestToken { + if ((self = [super initWithOptions:options requestToken:requestToken])) { + self.title = @"VKontakte"; + } + return self; +} + +- (NSURLRequest *)initialRequest { + NSDictionary *parameters = @{ + @"client_id" : self.options[@"client_id"], + @"redirect_uri" : self.options[SimpleAuthRedirectURIKey], + @"response_type" : @"token", + @"scope" : [self.options[@"permission"] componentsJoinedByString:@","], + @"display" : @"mobile", + @"v" : @"5.34" + }; + + NSString *URLString = [NSString stringWithFormat: + @"https://oauth.vk.com/authorize?%@", + [CMDQueryStringSerialization queryStringWithDictionary:parameters]]; + NSURL *URL = [NSURL URLWithString:URLString]; + + return [NSURLRequest requestWithURL:URL]; +} + +@end diff --git a/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebProvider.h b/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebProvider.h new file mode 100644 index 0000000..a3cff9c --- /dev/null +++ b/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebProvider.h @@ -0,0 +1,12 @@ +// +// SimpleAuthVKontakteWebProvider.h +// SimpleAuth +// +// Created by Mikhail Kupriyanov on 7/7/15. +// + +#import "SimpleAuthProvider.h" + +@interface SimpleAuthVKontakteWebProvider : SimpleAuthProvider + +@end diff --git a/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebProvider.m b/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebProvider.m new file mode 100644 index 0000000..12763f4 --- /dev/null +++ b/Pod/Providers/VKontakteWeb/SimpleAuthVKontakteWebProvider.m @@ -0,0 +1,169 @@ +// +// SimpleAuthVKontakteWebProvider.m +// SimpleAuth +// +// Created by Mikhail Kupriyanov on 7/7/15. +// + +#import "SimpleAuthVKontakteWebProvider.h" +#import "SimpleAuthVKontakteWebLoginViewController.h" + +#import "UIViewController+SimpleAuthAdditions.h" +#import + +static const NSString *kVersion_api = @"5.34"; + +@implementation SimpleAuthVKontakteWebProvider + +#pragma mark - SimpleAuthProvider + ++ (NSString *)type { + return @"vkontakte-web"; +} + + ++ (NSDictionary *)defaultOptions { + + // Default present block + SimpleAuthInterfaceHandler presentBlock = ^(UIViewController *controller) { + UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:controller]; + navigation.modalPresentationStyle = UIModalPresentationFormSheet; + UIViewController *presented = [UIViewController SimpleAuth_presentedViewController]; + [presented presentViewController:navigation animated:YES completion:nil]; + }; + + // Default dismiss block + SimpleAuthInterfaceHandler dismissBlock = ^(id controller) { + [controller dismissViewControllerAnimated:YES completion:nil]; + }; + + NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithDictionary:[super defaultOptions]]; + dictionary[SimpleAuthPresentInterfaceBlockKey] = presentBlock; + dictionary[SimpleAuthDismissInterfaceBlockKey] = dismissBlock; + dictionary[SimpleAuthRedirectURIKey] = @"https://oauth.vk.com/blank.html"; + dictionary[@"permission"] = @[ @"email, offline" ]; + dictionary[@"v"] = [kVersion_api copy]; + return dictionary; +} + +- (void)authorizeWithCompletion:(SimpleAuthRequestHandler)completion { + [[[self accessToken] + flattenMap:^RACStream *(NSString *response) { + NSArray *signals = @[ + [self accountWithAccessToken:response], + [RACSignal return:response] + ]; + return [self rac_liftSelector:@selector(dictionaryWithAccount:accessToken:) withSignalsFromArray:signals]; + }] + subscribeNext:^(NSDictionary *response) { + completion(response, nil); + } + error:^(NSError *error) { + completion(nil, error); + }]; +} + + +#pragma mark - Private + +- (RACSignal *)accessToken { + return [RACSignal createSignal:^RACDisposable *(id subscriber) { + dispatch_async(dispatch_get_main_queue(), ^{ + SimpleAuthVKontakteWebLoginViewController *login = [[SimpleAuthVKontakteWebLoginViewController alloc] initWithOptions:self.options]; + login.completion = ^(UIViewController *login, NSURL *URL, NSError *error) { + SimpleAuthInterfaceHandler dismissBlock = self.options[SimpleAuthDismissInterfaceBlockKey]; + dismissBlock(login); + + // Parse URL + NSString *fragment = [URL fragment]; + NSDictionary *dictionary = [CMDQueryStringSerialization dictionaryWithQueryString:fragment]; + NSString *access_token = dictionary[@"access_token"]; + + // Check for error + if (![access_token length]) { + [subscriber sendError:error]; + return; + } + + // Send completion + [subscriber sendNext:dictionary]; + [subscriber sendCompleted]; + }; + + SimpleAuthInterfaceHandler block = self.options[SimpleAuthPresentInterfaceBlockKey]; + block(login); + }); + return nil; + }]; +} + + +- (RACSignal *)accountWithAccessToken:(NSDictionary *) dictionaryWithAccessToken { + return [RACSignal createSignal:^RACDisposable *(id subscriber) { + NSString *accessToken = dictionaryWithAccessToken[@"access_token"]; + NSString *user_id = dictionaryWithAccessToken[@"user_id"]; + NSString *fields = @"sex, bdate, city, country, photo_50, photo_100, photo_200_orig, photo_200, photo_400_orig, photo_max, photo_max_orig, photo_id, online, online_mobile, domain, has_mobile, contacts, connections, site, education, universities, schools, can_post, can_see_all_posts, can_see_audio, can_write_private_message, status, last_seen, common_count, relation, relatives, counters, screen_name, maiden_name, timezone, occupation,activities, interests, music, movies, tv, books, games, about, quotes, personal, friend_status"; + + NSDictionary *parameters = @{ @"access_token" : accessToken , @"user_id" : user_id, @"fields" : fields, @"v" : self.options[@"v"] }; + NSString *query = [CMDQueryStringSerialization queryStringWithDictionary:parameters]; + NSString *URLString = [NSString stringWithFormat:@"https://api.vk.com/method/users.get?%@", query]; + NSURL *URL = [NSURL URLWithString:URLString]; + NSURLRequest *request = [NSURLRequest requestWithURL:URL]; + [NSURLConnection sendAsynchronousRequest:request queue:self.operationQueue + completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { + NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 99)]; + NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; + if ([indexSet containsIndex:statusCode] && data) { + NSError *parseError = nil; + NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&parseError]; + if (dictionary) { + [subscriber sendNext:dictionary]; + [subscriber sendCompleted]; + } + else { + [subscriber sendError:parseError]; + } + } + else { + [subscriber sendError:connectionError]; + } + }]; + return nil; + }]; +} + + +#pragma mark - Private + +- (NSDictionary *)dictionaryWithAccount:(NSDictionary *)account accessToken:(NSDictionary *)accessToken{ + NSMutableDictionary *dictionary = [NSMutableDictionary new]; + NSArray *dataArray = (NSArray *) account[@"response"]; + NSDictionary *data = dataArray.lastObject; + // Provider + dictionary[@"provider"] = [[self class] type]; + + // Credentials + dictionary[@"credentials"] = @{ + @"access_token" : accessToken[@"access_token"], + @"expires_in" : accessToken[@"expires_in"] + }; + + // User ID + dictionary[@"user_id"] = accessToken[@"user_id"];; + + // Raw response + dictionary[@"extra"] = @{ + @"raw_info" : data + }; + + // User info + NSMutableDictionary *user = [NSMutableDictionary new]; + user[@"first_name"] = data[@"first_name"]; + user[@"last_name"] = data[@"last_name"]; + user[@"photo_max_orig"] = data[@"photo_max_orig"]; + + dictionary[@"user_info"] = user; + + return dictionary; +} +@end diff --git a/Readme.markdown b/Readme.markdown index 2a88cc2..434e48a 100644 --- a/Readme.markdown +++ b/Readme.markdown @@ -16,6 +16,7 @@ SimpleAuth currently has the following providers: - [Google](https://github.com/calebd/SimpleAuth/wiki/Google) - [Box](https://github.com/calebd/SimpleAuth/wiki/Box) - [OneDrive](https://github.com/calebd/SimpleAuth/wiki/OneDrive) +- [VKontakte](https://github.com/calebd/SimpleAuth/wiki/VKontakte) ## Installing @@ -97,3 +98,4 @@ Special thanks to my friend [@soffes](https://twitter.com/soffes) for advising o - [mouhcine](https://github.com/mouhcine): Meetup provider - [iamabhiee](https://github.com/iamabhiee): LinkedIn provider - [aschuch](https://github.com/aschuch): Sina Weibo provider +- [Mikhail_Kupriyanov](https://github.com/MikhailKupriyanov): VKontakte provider diff --git a/SimpleAuth.podspec b/SimpleAuth.podspec index 6125ab9..576a92b 100755 --- a/SimpleAuth.podspec +++ b/SimpleAuth.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |s| s.subspec 'Core' do |ss| ss.source_files = 'Pod/Core' ss.public_header_files = 'Pod/Core/SimpleAuth.h', 'Pod/Core/SimpleAuthDefines.h' - ss.dependency 'ReactiveCocoa' + ss.dependency 'ReactiveCocoa', '~>2.5' ss.dependency 'CMDQueryStringSerialization' ss.ios.frameworks = 'UIKit' @@ -128,4 +128,10 @@ Pod::Spec.new do |s| ss.private_header_files = 'Pod/Providers/OneDriveWeb/*.h' end + s.subspec 'VKontakteWeb' do |ss| + ss.dependency 'SimpleAuth/Core' + ss.source_files = 'Pod/Providers/VKontakteWeb' + ss.private_header_files = 'Pod/Providers/VKontakteWeb/*.h' + end + end