From 4969a9928e5182eefa347158e4491dc46dfc9a37 Mon Sep 17 00:00:00 2001 From: Kamil Kraszewski Date: Mon, 19 Dec 2011 13:55:13 -0800 Subject: [PATCH] Added the extendAccessToken method to facebook-ios-sdk Summary: Whenever user makes an API call the SDK checks if the access token is about to expire. If that's the case SDK will try to silently refresh the token. Developer can also force the refreshing process by calling [facebook extendAccessToken] method (this might be useful for apps that doesn't make frequent API calls). Also provided an example of this functionality in the Hackbook. Test Plan: With prod: tried refreshing the token and check if the expirationDate is correct. With sandbox: - disabling checking the ssl certificates in the sdk (done by following lines (src/FBRequest.m) - I'm sure there's an easier way, but I don't know it :P): LANG=C - (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace { return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]; } - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge]; [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge]; } - changing the rest server url (in my case:) LANG=C static NSString* kRestserverBaseURL = @"https://api.kamil.devrs332.facebook.com/method/"; - after that I switched to app which access expires after 10 minutes and tested if the refreshing process behaves correctly (had to modify isSessionOld function) Reviewers: yariv, jimbru, brent, ekoneil Reviewed By: yariv CC: lshepard, ekoneil, jgabbard, brent, yariv, kamil, trvish Differential Revision: https://phabricator.fb.com/D371702 Task ID: 823099 --- .../Hackbook/APIResultsViewController.h | 1 - .../Hackbook/Hackbook/HackbookAppDelegate.m | 57 +++++---- sample/Hackbook/Hackbook/RootViewController.m | 41 ++++--- src/Facebook.h | 24 +++- src/Facebook.m | 112 +++++++++++++++++- 5 files changed, 183 insertions(+), 52 deletions(-) diff --git a/sample/Hackbook/Hackbook/APIResultsViewController.h b/sample/Hackbook/Hackbook/APIResultsViewController.h index 056839a611..055ba34986 100644 --- a/sample/Hackbook/Hackbook/APIResultsViewController.h +++ b/sample/Hackbook/Hackbook/APIResultsViewController.h @@ -19,7 +19,6 @@ @interface APIResultsViewController : UIViewController { NSMutableArray *myData; diff --git a/sample/Hackbook/Hackbook/HackbookAppDelegate.m b/sample/Hackbook/Hackbook/HackbookAppDelegate.m index 76a1ae61cc..d93debee27 100644 --- a/sample/Hackbook/Hackbook/HackbookAppDelegate.m +++ b/sample/Hackbook/Hackbook/HackbookAppDelegate.m @@ -43,37 +43,44 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; [navController.navigationBar setTintColor:[UIColor colorWithRed:0/255.0 green:51.0/255.0 - blue:102.0/255.0 + blue:102.0/255.0 alpha:1.0]]; [navController.navigationBar setBarStyle:UIBarStyleBlackTranslucent]; self.navigationController = navController; [rootViewController release]; [navController release]; - + // Initialize Facebook facebook = [[Facebook alloc] initWithAppId:kAppId andDelegate:rootViewController]; - + + // Check and retrieve authorization information + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + if ([defaults objectForKey:@"FBAccessTokenKey"] && [defaults objectForKey:@"FBExpirationDateKey"]) { + facebook.accessToken = [defaults objectForKey:@"FBAccessTokenKey"]; + facebook.expirationDate = [defaults objectForKey:@"FBExpirationDateKey"]; + } + // Initialize API data (for views, etc.) apiData = [[DataSet alloc] init]; - + // Initialize user permissions userPermissions = [[NSMutableDictionary alloc] initWithCapacity:1]; - + // Override point for customization after application launch. // Add the navigation controller's view to the window and display. self.window.rootViewController = self.navigationController; [self.window makeKeyAndVisible]; - + // Check App ID: // This is really a warning for the developer, this should not // happen in a completed app if (!kAppId) { - UIAlertView *alertView = [[UIAlertView alloc] - initWithTitle:@"Setup Error" - message:@"Missing app ID. You cannot run the app until you provide this in the code." - delegate:self - cancelButtonTitle:@"OK" - otherButtonTitles:nil, + UIAlertView *alertView = [[UIAlertView alloc] + initWithTitle:@"Setup Error" + message:@"Missing app ID. You cannot run the app until you provide this in the code." + delegate:self + cancelButtonTitle:@"OK" + otherButtonTitles:nil, nil]; [alertView show]; [alertView release]; @@ -83,7 +90,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( NSString *url = [NSString stringWithFormat:@"fb%@://authorize",kAppId]; BOOL bSchemeInPlist = NO; // find out if the sceme is in the plist file. NSArray* aBundleURLTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; - if ([aBundleURLTypes isKindOfClass:[NSArray class]] && + if ([aBundleURLTypes isKindOfClass:[NSArray class]] && ([aBundleURLTypes count] > 0)) { NSDictionary* aBundleURLTypes0 = [aBundleURLTypes objectAtIndex:0]; if ([aBundleURLTypes0 isKindOfClass:[NSDictionary class]]) { @@ -91,7 +98,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( if ([aBundleURLSchemes isKindOfClass:[NSArray class]] && ([aBundleURLSchemes count] > 0)) { NSString *scheme = [aBundleURLSchemes objectAtIndex:0]; - if ([scheme isKindOfClass:[NSString class]] && + if ([scheme isKindOfClass:[NSString class]] && [url hasPrefix:scheme]) { bSchemeInPlist = YES; } @@ -101,21 +108,29 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // Check if the authorization callback will work BOOL bCanOpenUrl = [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString: url]]; if (!bSchemeInPlist || !bCanOpenUrl) { - UIAlertView *alertView = [[UIAlertView alloc] - initWithTitle:@"Setup Error" - message:@"Invalid or missing URL scheme. You cannot run the app until you set up a valid URL scheme in your .plist." - delegate:self - cancelButtonTitle:@"OK" - otherButtonTitles:nil, + UIAlertView *alertView = [[UIAlertView alloc] + initWithTitle:@"Setup Error" + message:@"Invalid or missing URL scheme. You cannot run the app until you set up a valid URL scheme in your .plist." + delegate:self + cancelButtonTitle:@"OK" + otherButtonTitles:nil, nil]; [alertView show]; [alertView release]; } } - + return YES; } +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Although the SDK attempts to refresh its access tokens when it makes API calls, + // it's a good practice to refresh the access token also when the app becomes active. + // This gives apps that seldom make api calls a higher chance of having a non expired + // access token. + [[self facebook] extendAccessTokenIfNeeded]; +} + - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { return [self.facebook handleOpenURL:url]; } diff --git a/sample/Hackbook/Hackbook/RootViewController.m b/sample/Hackbook/Hackbook/RootViewController.m index bc80c4557b..d0e3dc26ce 100644 --- a/sample/Hackbook/Hackbook/RootViewController.m +++ b/sample/Hackbook/Hackbook/RootViewController.m @@ -103,7 +103,7 @@ - (void)showLoggedOut { nameLabel.text = @""; // Get the profile image [profilePhotoImageView setImage:nil]; - + [[self navigationController] popToRootViewControllerAnimated:YES]; } @@ -245,19 +245,11 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; HackbookAppDelegate *delegate = (HackbookAppDelegate *)[[UIApplication sharedApplication] delegate]; - // Check and retrieve authorization information - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - if ([defaults objectForKey:@"FBAccessTokenKey"] - && [defaults objectForKey:@"FBExpirationDateKey"]) { - [delegate facebook].accessToken = [defaults objectForKey:@"FBAccessTokenKey"]; - [delegate facebook].expirationDate = [defaults objectForKey:@"FBExpirationDateKey"]; - } if (![[delegate facebook] isSessionValid]) { [self showLoggedOut]; } else { [self showLoggedIn]; } - } - (void)viewWillDisappear:(BOOL)animated { @@ -315,6 +307,13 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } +- (void)storeAuthData:(NSString *)accessToken expiresAt:(NSDate *)expiresAt { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:accessToken forKey:@"FBAccessTokenKey"]; + [defaults setObject:expiresAt forKey:@"FBExpirationDateKey"]; + [defaults synchronize]; +} + #pragma mark - FBSessionDelegate Methods /** * Called when the user has logged in successfully. @@ -323,16 +322,16 @@ - (void)fbDidLogin { [self showLoggedIn]; HackbookAppDelegate *delegate = (HackbookAppDelegate *)[[UIApplication sharedApplication] delegate]; + [self storeAuthData:[[delegate facebook] accessToken] expiresAt:[[delegate facebook] expirationDate]]; - // Save authorization information - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - [defaults setObject:[[delegate facebook] accessToken] forKey:@"FBAccessTokenKey"]; - [defaults setObject:[[delegate facebook] expirationDate] forKey:@"FBExpirationDateKey"]; - [defaults synchronize]; - [pendingApiCallsController userDidGrantPermission]; } +-(void)fbDidExtendToken:(NSString *)accessToken expiresAt:(NSDate *)expiresAt { + NSLog(@"token extended"); + [self storeAuthData:accessToken expiresAt:expiresAt]; +} + /** * Called when the user canceled the authorization dialog. */ @@ -359,13 +358,13 @@ - (void)fbDidLogout { /** * Called when the session has expired. */ -- (void)fbSessionInvalidated { +- (void)fbSessionInvalidated { UIAlertView *alertView = [[UIAlertView alloc] - initWithTitle:@"Auth Exception" - message:@"Your session has expired." - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil, + initWithTitle:@"Auth Exception" + message:@"Your session has expired." + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil, nil]; [alertView show]; [alertView release]; diff --git a/src/Facebook.h b/src/Facebook.h index f4cea27c89..8620fc9739 100644 --- a/src/Facebook.h +++ b/src/Facebook.h @@ -25,7 +25,7 @@ * and Graph APIs, and start user interface interactions (such as * pop-ups promoting for credentials, permissions, stream posts, etc.) */ -@interface Facebook : NSObject{ +@interface Facebook : NSObject{ NSString* _accessToken; NSDate* _expirationDate; id _sessionDelegate; @@ -35,6 +35,8 @@ NSString* _appId; NSString* _urlSchemeSuffix; NSArray* _permissions; + BOOL _isExtendingAccessToken; + NSDate* _lastAccessTokenUpdate; } @property(nonatomic, copy) NSString* accessToken; @@ -51,6 +53,12 @@ - (void)authorize:(NSArray *)permissions; +- (void)extendAccessToken; + +- (void)extendAccessTokenIfNeeded; + +- (BOOL)shouldExtendAccessToken; + - (BOOL)handleOpenURL:(NSURL *)url; - (void)logout; @@ -93,8 +101,6 @@ */ @protocol FBSessionDelegate -@optional - /** * Called when the user successfully logged in. */ @@ -105,6 +111,16 @@ */ - (void)fbDidNotLogin:(BOOL)cancelled; +/** + * Called after the access token was extended. If your application has any + * references to the previous access token (for example, if your application + * stores the previous access token in persistent storage), your application + * should overwrite the old access token with the new one in this method. + * See extendAccessToken for more details. + */ +- (void)fbDidExtendToken:(NSString*)accessToken + expiresAt:(NSDate*)expiresAt; + /** * Called when the user logged out. */ @@ -112,7 +128,7 @@ /** * Called when the current session has expired. This might happen when: - * - the access token expired + * - the access token expired * - the app has been disabled * - the user revoked the app's permissions * - the user changed his or her password diff --git a/src/Facebook.m b/src/Facebook.m index 690234df80..4380e84afa 100644 --- a/src/Facebook.m +++ b/src/Facebook.m @@ -30,6 +30,10 @@ static NSString* kSDK = @"ios"; static NSString* kSDKVersion = @"2"; +// If the last time we extended the access token was more than 24 hours ago +// we try to refresh the access token again. +static const int kTokenExtendThreshold = 24; + static NSString *requestFinishedKeyPath = @"state"; static void *finishedContext = @"finishedContext"; @@ -93,10 +97,11 @@ - (id)initWithAppId:(NSString *)appId - (id)initWithAppId:(NSString *)appId urlSchemeSuffix:(NSString *)urlSchemeSuffix andDelegate:(id)delegate { - + self = [super init]; if (self) { _requests = [[NSMutableSet alloc] init]; + _lastAccessTokenUpdate = [[NSDate distantPast] retain]; self.appId = appId; self.sessionDelegate = delegate; self.urlSchemeSuffix = urlSchemeSuffix; @@ -111,6 +116,7 @@ - (void)dealloc { for (FBRequest* _request in _requests) { [_request removeObserver:self forKeyPath:requestFinishedKeyPath]; } + [_lastAccessTokenUpdate release]; [_accessToken release]; [_expirationDate release]; [_requests release]; @@ -125,11 +131,11 @@ - (void)dealloc { - (void)invalidateSession { self.accessToken = nil; self.expirationDate = nil; - + NSHTTPCookieStorage* cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage]; NSArray* facebookCookies = [cookies cookiesForURL: [NSURL URLWithString:@"http://login.facebook.com"]]; - + for (NSHTTPCookie* cookie in facebookCookies) { [cookies deleteCookie:cookie]; } @@ -160,6 +166,8 @@ - (FBRequest*)openUrl:(NSString *)url [params setValue:self.accessToken forKey:@"access_token"]; } + [self extendAccessTokenIfNeeded]; + FBRequest* _request = [FBRequest getRequestWithParams:params httpMethod:httpMethod delegate:delegate @@ -221,7 +229,7 @@ - (void)authorizeWithFBAppAuth:(BOOL)tryFBAppAuth if (_urlSchemeSuffix) { [params setValue:_urlSchemeSuffix forKey:@"local_client_id"]; } - + // If the device is running a version of iOS that supports multitasking, // try to obtain the access token from the Facebook app installed // on the device. @@ -318,6 +326,56 @@ - (void)authorize:(NSArray *)permissions { [self authorizeWithFBAppAuth:YES safariAuth:YES]; } +/** + * Attempt to extend the access token. + * + * Access tokens typically expire within 30-60 days. When the user uses the + * app, the app should periodically try to obtain a new access token. Once an + * access token has expired, the app can no longer renew it. The app then has + * to ask the user to re-authorize it to obtain a new access token. + * + * To ensure your app always has a fresh access token for active users, it's + * recommended that you call extendAccessTokenIfNeeded in your application's + * applicationDidBecomeActive: UIApplicationDelegate method. + */ +- (void)extendAccessToken { + if (_isExtendingAccessToken) { + return; + } + _isExtendingAccessToken = YES; + NSMutableDictionary* params = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @"auth.extendSSOAccessToken", @"method", + nil]; + [self requestWithParams:params andDelegate:self]; +} + +/** + * Calls extendAccessToken if shouldExtendAccessToken returns YES. + */ +- (void)extendAccessTokenIfNeeded { + if ([self shouldExtendAccessToken]) { + [self extendAccessToken]; + } +} + +/** + * Returns YES if the last time a new token was obtained was over 24 hours ago. + */ +- (BOOL)shouldExtendAccessToken { + if ([self isSessionValid]){ + NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease]; + NSDateComponents *components = [calendar components:NSHourCalendarUnit + fromDate:_lastAccessTokenUpdate + toDate:[NSDate date] + options:0]; + + if (components.hour >= kTokenExtendThreshold) { + return YES; + } + } + return NO; +} + /** * This function processes the URL the Facebook application or Safari used to * open your application during a single sign-on flow. @@ -408,7 +466,7 @@ - (BOOL)handleOpenURL:(NSURL *)url { */ - (void)logout { [self invalidateSession]; - + if ([self.sessionDelegate respondsToSelector:@selector(fbDidLogout)]) { [self.sessionDelegate fbDidLogout]; } @@ -623,6 +681,7 @@ - (void)dialog:(NSString *)action if ([self isSessionValid]) { [params setValue:[self.accessToken stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] forKey:@"access_token"]; + [self extendAccessTokenIfNeeded]; } _fbDialog = [[FBDialog alloc] initWithURL:dialogURL params:params delegate:delegate]; } @@ -648,6 +707,8 @@ - (BOOL)isSessionValid { - (void)fbDialogLogin:(NSString *)token expirationDate:(NSDate *)expirationDate { self.accessToken = token; self.expirationDate = expirationDate; + [_lastAccessTokenUpdate release]; + _lastAccessTokenUpdate = [[NSDate date] retain]; if ([self.sessionDelegate respondsToSelector:@selector(fbDidLogin)]) { [self.sessionDelegate fbDidLogin]; } @@ -663,4 +724,45 @@ - (void)fbDialogNotLogin:(BOOL)cancelled { } } +#pragma mark - FBRequestDelegate Methods +// These delegate methods are only called for requests that extendAccessToken initiated + +- (void)request:(FBRequest *)request didFailWithError:(NSError *)error { + _isExtendingAccessToken = NO; +} + +- (void)request:(FBRequest *)request didLoad:(id)result { + _isExtendingAccessToken = NO; + NSString* accessToken = [result objectForKey:@"access_token"]; + NSString* expTime = [result objectForKey:@"expires_at"]; + + if (accessToken == nil || expTime == nil) { + return; + } + + self.accessToken = accessToken; + + NSTimeInterval timeInterval = [expTime doubleValue]; + NSDate *expirationDate = [NSDate distantFuture]; + if (timeInterval != 0) { + expirationDate = [NSDate dateWithTimeIntervalSince1970:timeInterval]; + } + self.expirationDate = expirationDate; + [_lastAccessTokenUpdate release]; + _lastAccessTokenUpdate = [[NSDate date] retain]; + + if ([self.sessionDelegate respondsToSelector:@selector(fbDidExtendToken:expiresAt:)]) { + [self.sessionDelegate fbDidExtendToken:accessToken expiresAt:expirationDate]; + } +} + +- (void)request:(FBRequest *)request didLoadRawResponse:(NSData *)data { +} + +- (void)request:(FBRequest *)request didReceiveResponse:(NSURLResponse *)response{ +} + +- (void)requestLoading:(FBRequest *)request{ +} + @end