agorapulse / grails-facebook-sdk

Facebook SDK Grails Plugin
http://agorapulse.github.com/grails-facebook-sdk/guide
30 stars 13 forks source link

Error getting user access token from signed request code with 2 simultaneous requests #44

Closed elegorod closed 11 years ago

elegorod commented 11 years ago

Hi, I'm using you facebook plugin and encuntered a concurrency error. When my application makes 2 simultaneous AJAX requests on 1 page, first request finishes normally, but second can't get user access token and clears the session, kicking the user to login page.

How to reproduce:

  1. Main page loads with some code (context.signedRequest.code), name it code 1.
  2. Second page loads with same code 1.
  3. There is a button on second page, and when user pressed it, 2 simultaneous AJAX requests are performed. Both AJAX requests have new code, code 2. Server processed them in 2 threads.
  4. As code is new (not stored in the session), FacebookContextUser.getTokenFromCode() method is invoked in both threads. This method makes Facebook graph request to get token from code.
  5. First thread gets access token normally. Second thread gets FacebookOAuthException with code 100: This authorization code has been used. This is because first thread used the code, and code can be used only once. Second thread calls invalidate(), clears all plugin session attributes, and user is kicked to login page.

I have developed a fix for this. I'he not used synchronization because application works in the cloud, with possible session replication. If thread gets FacebookOAuthException with code 100, it retries to get a token from session code 10 times.

Please, add this fix to the plugin or develop a better solution

In FacebookContextUser.groovy :

# This patch file was generated by NetBeans IDE
# It uses platform neutral UTF-8 encoding and \n newlines.
--- <html>FacebookContextUser.groovy (<b>Jan 3, 2013 9:17:34 AM</b>)</html>
+++ <html><b>Current File</b></html>
@@ -99,15 +99,8 @@
                     context.session.setData('token', token)
                 } else if (context.signedRequest.code) {
                     // Facebook Javascript SDK puts an authorization code in signed request
-                    if (context.signedRequest.code == context.session.getData('code')) {
-                        if (!isTokenExpired()) {
-                            _token = context.session.getData('token')
-                            log.debug "Got token from code (token=$_token)"
-                            if (isTokenExpiredSoon()) {
-                                exchangeToken()
-                            }
-                        }
-                    } else {
+                    boolean loadedFromCode = loadSavedTokenFromCode(context.signedRequest.code)
+                    if (!loadedFromCode) {
                         _token = getTokenFromCode(context.signedRequest.code)
                         log.debug "Got token from signed request code (token=$_token)"
                     }
@@ -144,6 +137,24 @@
         return _token
     }

+  private boolean loadSavedTokenFromCode(String code)
+  {
+    boolean result = false
+    String sessionCode = context.session.getData('code')
+    // log.debug "Requested/Session code\n" + code + "\n" + sessionCode
+    if (code == sessionCode) {
+      result = true
+      if (!isTokenExpired()) {
+        _token = context.session.getData('token')
+        log.debug "Got token from code (token=$_token)"
+        if (isTokenExpiredSoon()) {
+          exchangeToken()
+        }
+      }
+    }
+    result
+  }
+
     /*
      * @description Get the token expiration time of the connected user.
      */
@@ -207,8 +218,35 @@
     }

     private String getTokenFromCode(String code, String redirectUri = '') {
+    
         String accessToken = ''
         try {
+      accessToken = loadTokenFromCode(code, redirectUri)
+    } catch (FacebookOAuthException exception) {
+      if(exception.errorCode == 100)
+      {
+        // token was received by another concurrent process, trying to extract it from session
+        // retries were made for distributed session replication
+        for(i in 0..<10)
+        {
+          sleep(100 + 300 * i)
+          log.debug "Retry $i to load from code"
+          boolean loadedFromCode = loadSavedTokenFromCode(code)
+          if(loadedFromCode)
+          {
+            return _token
+          }
+        }
+      }
+      log.warn "Could not get token from code: $exception.errorCode $exception.errorMessage"
+      invalidate()
+    }
+    
+    return accessToken
+  }
+  
+  private String loadTokenFromCode(String code, String redirectUri = '') {
+    String accessToken = ''
             def result = graphClient.fetchObject('oauth/access_token', [
                     client_id: context.app.id,
                     client_secret: context.app.secret,
@@ -225,10 +263,6 @@
                     context.session.setData('expirationTime', expirationTime)
                 }
             }
-        } catch (FacebookOAuthException exception) {
-            log.warn "Could not get token from code: $exception.errorMessage"
-            invalidate()
-        }
         return accessToken
     }
benorama commented 11 years ago

Indeed... Thanks for the report. I'll have a look at this next week.

benorama commented 11 years ago

0.4.9 has been released with this bug fix.

elegorod commented 11 years ago

thank you)