how to refresh an oauth token when using the Facebook iPhone SDK - iphone

I'm using the Facebook SDK for iOS in my app: http://github.com/facebook/facebook-ios-sdk
The oAuth token expires after about 2 hours. How can I "refresh" the oAuth token without having to call the [Facebook authorize...] method again - which shows an empty Facebook dialog briefly if the user had previously logged in? What I want to avoid is requiring the user to re-login to FB each time they use the app - say, day to day.
I am already saving / restoring oAuth tokens when the app exits / starts. And I can check to see if the token is valid using [Facebook isSessionValid], or by checking the expire time on the token. But what to do if the token has expired? I've read that it is possible to "refresh" the token, but I don't understand how this is done.
I don't want to request "offline_access" permission, which would give me a "forever" token.
Help!?

Facebook's implementation of OAuth doesn't support token refresh.
You have 2 types of access_tokens in Facebook. Short term token, which is given by default and a long term token which is given if you request offline_access. If refresh token were supported, it was the same as giving a offline_access token for all apps.
As long as the user has an active facebook session on your web control, you can request a new access_token just by accessing https://www.facebook.com/dialog/oauth?client_id=YOUR_APP_ID&redirect_uri=http://www.facebook.com/connect/login_success.html&response_type=token or probably some iOS SDK command that does the same (never worked with it so I can't tell). This kind of request will not ask the user to login again, but will use the previous session that was created during the first login.

Since none of these answers actually addressed the question I am going to detail how I have implemented OAuth token refresh using The Facebook SDK.
The SDK will automatically refresh your tokens when you make requests however, in my scenario we send the tokens to our servers and we need to use the latest token. So when our server indicates that we need new tokens this is what we do:
Note You can either pass the AppID into the FBSession or you can add the FacebookAppID key to your App's plist (this is what we do).
- (void)renewFacebookCredentials {
if (FBSession.activeSession.state == FBSessionStateOpen ||
FBSession.activeSession.state == FBSessionStateOpenTokenExtended) {
[self sessionStateChanged:[FBSession activeSession] state:[FBSession activeSession].state error:nil];
} else {
// Open a session showing the user the login UI
// You must ALWAYS ask for public_profile permissions when opening a session
[FBSession openActiveSessionWithReadPermissions:#[#"public_profile",#"email"]
allowLoginUI:NO
completionHandler:^(FBSession *session, FBSessionState state, NSError *error) {
//this block will run throughout the lifetime of the app.
[self sessionStateChanged:session state:state error:error];
}];
}
}
The you can use the sessionStateChanged: method that Facebook include in their documentation but a simplified handler looks like this:
- (void)sessionStateChanged:(FBSession *)session state:(FBSessionState) state error:(NSError *)error {
// If the session was opened successfully
NSString *accessToken;
if (!error && state == FBSessionStateOpen && [[session accessTokenData] accessToken]){
// Show the user the logged-in UI
//#see http://stackoverflow.com/questions/20623728/getting-username-and-profile-picture-from-facebook-ios-7
accessToken = [[session accessTokenData] accessToken];
//Now we have an access token, can send this to the server...
} else {
//No access token, show a dialog or something
}
//either call a delegate or a completion handler here with the accessToken
}
Be aware that some of the FBSession API calls check for thread affinity so I found that I had to wrap all my FBSession calls inside a dispatch_async(dispatch_get_main_queue(), ^{...

Just do [ [FBSession session] resume] if it return false do login again

Facebook iOS SDK doesn’t handle the session storage.
FBSessionDelegate is a callback interface that your application should implement. it's methods will be invoked on application's successful login or logout.
See the example application facebook-ios-sdk/sample/DemoApp/Classes/DemoAppViewController.m for fbDidLogin, fbDidNotLogin and fbDidLogout methods

As per the Facebook SDK documentation:
The Facebook SDK automatically refreshes the user's session if
necessary. When it does this, the state also transitions to the
FBSessionStateOpenTokenExtended state.
Also to further clarify (since SDK version 3.1):
The SDK refreshes session automatically on API calls.
It also refreshes the token data as needed when follow-on authentication or Facebook API calls made using the SDK.

As of today Facebook is supposed to refresh tokens automatically, requests to GraphAPI can be done without providing token string either (Facebook handles it under the hood).
Moreover, if it happens that user wasn't using app for a long time and his token managed to expire, on your next request to Graph API an alert will be shown by Facebook's SDK asking user to relogin (all that is handled by Facebook and when done - will return into FBSDKGraphRequest's closure).
However, if someone really has a reason to manually refresh access token, here's an example (Swift 4):
private var selfDestructableNotificationToken: NotificationTokenThatAutomaticallyRemovesObserver?
final class NotificationTokenThatAutomaticallyRemovesObserver: NSObject { // more info here: https://oleb.net/blog/2018/01/notificationcenter-removeobserver/
let token: Any
init(_ token: Any) { self.token = token }
deinit { NotificationCenter.default.removeObserver(token) }
}
...
if let currentFBToken = FBSDKAccessToken.current() { // if this is a Facebook user, not an email-based user
if FBSDKAccessToken.currentAccessTokenIsActive() { // and his token has not expired yet
let token = NotificationCenter.default.addObserver(forName: NSNotification.Name.FBSDKAccessTokenDidChange, object: nil, queue: OperationQueue.main) { notification in
if let userInfo = notification.userInfo, let refreshedToken = userInfo["FBSDKAccessToken"] as? FBSDKAccessToken {
self.fbAccessToken = refreshedToken.tokenString
} else {
self.fbAccessToken = currentFBToken.tokenString // falling back to using an old token (better than none)
}
}
self.selfDestructableNotificationToken = NotificationTokenThatAutomaticallyRemovesObserver(token)
FBSDKAccessToken.refreshCurrentAccessToken { _, _, error in
if let error = error {
print("failed to refresh Facebook token with error \(error.localizedDescription)")
self.fbAccessToken = currentFBToken.tokenString // falling back to an old token (better than none)
}
}
} else if FBSDKAccessToken.current().isExpired { // unlucky user. Probably returned to the app after > 2 months break
self.fbAccessToken = currentFBToken.tokenString // assigning expired token. Facebook will ask user to relogin as soon as we call Graph API with that expired token
}
}

Related

Is it a good practice to reload credentials before every request?

I'm working on an iOS app that works with a backend made in laravel, this api manage login and information exchange via JSON web tokens.
To prevent the user to access a web route with an expired token I check the user's credentials before every web call.
My question is, is it a good practice? Because when I started thinking about it the user is accessing twice the amount of times to my server.
For example this is the function to access they're information.
/// Get user's info form the database
///
/// - Parameter completed: Completition Block
func getInfo(completed: #escaping DownloadComplete){
let token = self.getToken()
print(self.getError())
if(token != nil && token != ""){
//Here is where I check credentials.
self.checkCredentials(completed: {
let url = "\(Base_URL)\(myInfo)\(self.getToken() ?? "")"
Alamofire.request(url, method: HTTPMethod.get).responseJSON { respone in
if let result = respone.result.value as? Dictionary<String, AnyObject>{
if let user = result["User"] as? Dictionary<String, AnyObject>{
var Info = Alumno()
if let id = user["id"] as? Int64!{
Info.id = id
}
...
self.userInfo = Info
completed()
}
}
}
})
}
}
The check credentials functions asks the server if the user's token is still valid and if it is it returns
{
"Status": "Success"
}
My answer assumes your server validates the token, and you are not relying on the app to validate the token.
With that assumption, I think it depends on what you can do if the token is expired.
If the only thing you can do is prompt the user to sign in again, then I would probably just make the web request and handle the 401 Not Authorized.
However, if you have the ability to silently refresh the user's token, you may want to preemptively refresh the token if is close to expiring.
In my application, before every web request the app checks if the user's access token will expire within the next 3 minutes, and if so, attempts to refresh it before making the request. If the refresh is successful, the web request is made with the new access token. If the refresh fails, the web request is not made and the app prompts the user to sign in instead. If for any reason the web request still fails after that with 401, the app prompts the user to sign in.
I would do it like once every time the user opens the app not every call.
Other options:
You could also give them a code that they enter that's good for so long.
You could have them sign in when they first download the app. Then they would always be logged in. Then when the token is expired a little popup would say "Something Something Something"

iOS Today Widget utilize Uber login token

I have an app with an Uber login that gives access to restricted API calls (info on the current ride). I'd like to share the login token with the associated Today Widget so it can make similar calls.
I'm already sharing data with a UserDefaults suite, and I'm using the UberRides SDK. In digging into the RidesClient object it seems to try to use the keychain for storing/sharing the login token, and I set up a shared keychain to try to take advantage of this, but no luck. Restricted API calls from the widget return as unauthorized. Any suggestions?
Here's some code from the widget (note the user already authenticated in the main app):
let rc = RidesClient()
rc.fetchCurrentRide { ride, response in
if ride == nil { print("NO CURRENT RIDE") }
print(response.response)
print(response.error?.title)
if let ride = ride {
// do something
} else {
self.ride = nil
}
}
This returns an unauthorized response. I traced into the RidesClient (which is an object in the UberRides SDK), and see the code where the token is "supposed" to come from the keychain, but it doesn't.
I also tried generating my own URL request in the widget, using the login token passed through shared UserDefaults. This followed the standard HTTP access approach, putting the token in the Authorization header. But I got the same unauthorized response.
Here's some more details on the SDK approach:
Main app uses the LoginButton in native mode:
let scopes: [RidesScope] = [.Profile, .Places, .Request, .AllTrips]
let loginManager = LoginManager(accessTokenIdentifier: Configuration.getDefaultAccessTokenIdentifier(), keychainAccessGroup: "com.MYCOMPANY.MYAPP.share", loginType: .native)
let loginButton = LoginButton(frame: loginFrame, scopes: scopes, loginManager: loginManager)
loginButton.presentingViewController = self
loginButton.delegate = self
view.addSubview(loginButton)
The login button does the right thing and authorizes in the Uber app. I can see the token returned in the delegate callback didCompleteLoginWithToken. However, I can then check for the token:
let token = TokenManager.fetchToken(Configuration.getDefaultAccessTokenIdentifier(), accessGroup: "com.MYCOMPANY.MYAPP.share")
print(token)
The token is "nil". I don't think the SDK is saving the token into the access group keychain.
When I use the default keychain (not the keychainAccessGroup), the login in the app works fine and I can get the login token back and make restricted calls to the API. However, that doesn't help the widget, which needs the token from the access group keychain.
Solved!! After many hours of debugging, and searching. What was not clear in ANY documentation is the keychainAccessGroup MUST include the AppIndentifierPrefix. That's the 10 character identifier associated with the App ID. So, instead of using "com.MYCOMPANY.MYAPP.share", it's "APPID.com.MYCOMPANY.MYAPP.share" for the keychainAccessGroup.

Check Facebook login status using Alloy or Titanium in an iOS app

I want to add Facebook login to my Alloy/ Titanium ios app.
So far I have the following code:
var fb = require('facebook');
fb.initialize();
fb.setLoginBehavior(fb.LOGIN_BEHAVIOR_NATIVE);
fb.permissions = ['email', 'public_profile'];
function doFacebookLogin() {
// occurs when a button is clicked
if (fb.loggedIn) {
facebookLogin(fb);
} else {
fb.authorize();
}
}
fb.addEventListener('login', function(event) {
if (event.success) {
facebookLogin(fb);
}
});
function facebookLogin(fb) {
Ti.API.info(fb.accessToken);
// send the fb.accessToken server-side and register & login the user
}
For the most part the code works fine!
The issue is when the user de-authorises the app, fb.login still return true and so is using the same access token.
How can I check if the user has de-authorised my app. I know it's easily possible using the JavaScript SDK using FB.getLoginStatus: https://developers.facebook.com/docs/reference/javascript/FB.getLoginStatus , but I can't find an equivalent in Alloy/Titanium: https://docs.appcelerator.com/platform/latest/#!/api/Modules.Facebook
You need to call fb.logout() to set fb.loggedIn to false.
fb.logout clears the access token and resets the login state to false.
Note: I am not sure whether clearing the access token in Titanium module will also clear the token on FB server. So even after calling fb.logout(), you might receive the same access token if and only if it has not expired on fb server.
Read more about Facebook Access Token here

Swift : How to check if Facebook access token has expired?

I'm currently using
if FBSDKAccessToken.currentAccessToken() != nil {
//Do Something
}
else {
//Passing the access token to my server
}
to check if a user is already logged in. But the problem in this approach is if a user opens my app after long time the currentAccessToken might have expired but since it is not nil it will send the expired token to the server.
How can I put a check if currentAccessToken is valid or not? I'm using version 2.4 of FBSDK
I think you may request https://graph.facebook.com/me?access_token=TOKEN
If token is not valid you will get oauth error in json response, otherwise you will get some info about user.
But better, for my mind, do the same on server side.

Google gdata api for iphone, how to save login credentials (username and password) or session?

Using GData, is there a built in way to store a session or credentials for interacting with the gdata api, or do I need to store credentials manually in the keychain? I'm using the YouTube upload API, and want to ensure the user doesn't have to enter username and pw each time. If there's a way to automatically get the user's Google login session, that's even better.
If you're using the GTM Oauth library (http://code.google.com/p/gtm-oauth/), they provide a method to store access token information in keychain by service name (e.g. "YouTubeAPI" or something).
Additionally, if you're using raw username / password, I would definitely store the details in keychain. Sci-Fi Hi-Fi has a nice, easy to use library that I've used in the past - http://github.com/ldandersen/scifihifi-iphone.
The GTM OAuth is newer, but the GData API's also support this via the setAuthorizer method. I didn't notice that until I dove into the source code.
//save to keychain
- (void)viewController:(GDataOAuthViewControllerTouch *)viewController
finishedWithAuth:(GDataOAuthAuthentication *)auth
error:(NSError *)error {
if (error != nil) {
// Authentication failed
} else {
[[self youTubeService] setAuthorizer:auth];
}
}
//check if authorized:
- (BOOL)isAuthorized
{
GDataOAuthAuthentication * auth = [GDataOAuthViewControllerTouch authForGoogleFromKeychainForName:kAppServiceName];
BOOL isSignedIn = [auth canAuthorize]; // returns NO if auth cannot authorize requests
if(isSignedIn) [[self youTubeService] setAuthorizer:auth];
return isSignedIn;
}