Added a fix to repair the login-redirect when using the Chrome-browser
authorKai Moritz <kai@juplo.de>
Thu, 26 May 2016 09:45:06 +0000 (11:45 +0200)
committerKai Moritz <kai@juplo.de>
Thu, 26 May 2016 13:50:14 +0000 (15:50 +0200)
src/main/java/org/springframework/social/facebook/web/CanvasSignInController.java [new file with mode: 0644]

diff --git a/src/main/java/org/springframework/social/facebook/web/CanvasSignInController.java b/src/main/java/org/springframework/social/facebook/web/CanvasSignInController.java
new file mode 100644 (file)
index 0000000..9858b4e
--- /dev/null
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.social.facebook.web;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.social.connect.Connection;
+import org.springframework.social.connect.ConnectionFactoryLocator;
+import org.springframework.social.connect.UsersConnectionRepository;
+import org.springframework.social.connect.support.OAuth2ConnectionFactory;
+import org.springframework.social.connect.web.SignInAdapter;
+import org.springframework.social.facebook.api.Facebook;
+import org.springframework.social.oauth2.AccessGrant;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.view.RedirectView;
+
+/**
+ * Sign in controller that uses the signed_request parameter that Facebook gives to Canvas applications to obtain an access token.
+ * If no access token exists in signed_request, this controller will redirect the top-level browser window to Facebook's authorization dialog.
+ * When Facebook redirects back from the authorization dialog, the signed_request parameter should contain an access token.
+ * @author Craig Walls
+ */
+@Controller
+@RequestMapping(value="/canvas")
+public class CanvasSignInController {
+       
+       private final static Log logger = LogFactory.getLog(CanvasSignInController.class);
+
+       private final String clientId;
+       
+       private final String canvasPage;
+
+       private final ConnectionFactoryLocator connectionFactoryLocator;
+
+       private final UsersConnectionRepository usersConnectionRepository;
+
+       private final SignInAdapter signInAdapter;
+       
+       private final SignedRequestDecoder signedRequestDecoder;
+       
+       private String postSignInUrl = "/";
+       
+       private String postDeclineUrl = "http://www.facebook.com";
+
+       private String scope;
+
+       @Inject
+       public CanvasSignInController(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository, SignInAdapter signInAdapter, String clientId, String clientSecret, String canvasPage) {
+               this.usersConnectionRepository = usersConnectionRepository;
+               this.signInAdapter = signInAdapter;
+               this.clientId = clientId;
+               this.canvasPage = canvasPage;
+               this.connectionFactoryLocator = connectionFactoryLocator;
+               this.signedRequestDecoder = new SignedRequestDecoder(clientSecret);
+       }
+       
+       /**
+        * The URL or path to redirect to after successful canvas authorization.
+        * @param postSignInUrl the url to redirect to after successful canvas authorization. Defaults to "/".
+        */
+       public void setPostSignInUrl(String postSignInUrl) {
+               this.postSignInUrl = postSignInUrl;
+       }
+       
+       /**
+        * The URL or path to redirect to if a user declines authorization.
+        * The redirect will happen in the top-level window. 
+        * If you want the redirect to happen in the canvas iframe, then override the {@link #postDeclineView()} method to return a different implementation of {@link View}.
+        * @param postDeclineUrl the url to redirect to after a user declines authorization. Defaults to "http://www.facebook.com".
+        */
+       public void setPostDeclineUrl(String postDeclineUrl) {
+               this.postDeclineUrl = postDeclineUrl;
+       }
+       
+       /**
+        * The scope to request during authorization.
+        * @param scope the scope to request. Defaults to null (no scope will be requested; Facebook will offer their default scope).
+        */
+       public void setScope(String scope) {
+               this.scope = scope;
+       }
+
+       @RequestMapping(method={ RequestMethod.POST, RequestMethod.GET }, params={"signed_request", "!error"})
+       public View signin(Model model, NativeWebRequest request) throws SignedRequestException {
+               String signedRequest = request.getParameter("signed_request");
+               if (signedRequest == null) {
+                       debug("Expected a signed_request parameter, but none given. Redirecting to the application's Canvas Page: " + canvasPage);
+                       return new RedirectView(canvasPage, false);
+               }
+               
+               Map<String, ?> decodedSignedRequest = signedRequestDecoder.decodeSignedRequest(signedRequest);
+               String accessToken = (String) decodedSignedRequest.get("oauth_token");
+               if (accessToken == null) {
+                       debug("No access token in the signed_request parameter. Redirecting to the authorization dialog.");
+                       model.addAttribute("clientId", clientId);
+                       model.addAttribute("canvasPage", canvasPage);
+                       if (scope != null) {
+                               model.addAttribute("scope", scope);
+                       }
+                       return new TopLevelWindowRedirect() {
+                               @Override
+                               protected String getRedirectUrl(Map<String, ?> model) {
+                                       String clientId = (String) model.get("clientId");
+                                       String canvasPage = (String) model.get("canvasPage");
+                                       String scope = (String) model.get("scope");
+                                       String redirectUrl = "https://www.facebook.com/v2.5/dialog/oauth?client_id=" + clientId + "&redirect_uri=" + canvasPage;
+                                       if (scope != null) {
+                                               redirectUrl += "&scope=" + formEncode(scope);
+                                       }
+                                       return redirectUrl;
+                               }
+                       };
+               }
+
+               debug("Access token available in signed_request parameter. Creating connection and signing in.");
+               OAuth2ConnectionFactory<Facebook> connectionFactory = (OAuth2ConnectionFactory<Facebook>) connectionFactoryLocator.getConnectionFactory(Facebook.class);
+               AccessGrant accessGrant = new AccessGrant(accessToken);
+               // TODO: Maybe should create via ConnectionData instead?
+               Connection<Facebook> connection = connectionFactory.createConnection(accessGrant);
+               handleSignIn(connection, request);
+               debug("Signed in. Redirecting to post-signin page.");
+               return new RedirectView(postSignInUrl, true); 
+       }
+
+       @RequestMapping(method={ RequestMethod.POST, RequestMethod.GET }, params="error")
+       public View error(@RequestParam("error") String error, @RequestParam("error_description") String errorDescription) {
+               String string = "User declined authorization: '" + errorDescription + "'. Redirecting to " + postDeclineUrl;
+               debug(string);
+               return postDeclineView();
+       }
+
+       /**
+        * View that redirects the top level window to the URL defined in postDeclineUrl property after user declines to authorize application.
+        * May be overridden for custom views, particularly in the case where the post-decline view should be rendered in-canvas.
+        * @return a view to display after a user declines authoriation. Defaults as a redirect to postDeclineUrl
+        */
+       protected View postDeclineView() {
+               return new TopLevelWindowRedirect() {
+                       @Override
+                       protected String getRedirectUrl(Map<String, ?> model) {
+                               return postDeclineUrl;
+                       }
+               };
+       }
+       
+       private void debug(String string) {
+               if (logger.isDebugEnabled()) {
+                       logger.debug(string);
+               }
+       }
+
+       private void handleSignIn(Connection<Facebook> connection, NativeWebRequest request) {
+               List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
+               if (userIds.size() == 1) {
+                       usersConnectionRepository.createConnectionRepository(userIds.get(0)).updateConnection(connection);
+                       signInAdapter.signIn(userIds.get(0), connection, request);
+               } else {
+                       // TODO: This should never happen, but need to figure out what to do if it does happen. 
+                       logger.error("Expected exactly 1 matching user. Got " + userIds.size() + " metching users.");
+               }
+       }
+       
+       private static abstract class TopLevelWindowRedirect implements View {
+               
+               public String getContentType() {
+                       return "text/html";
+               }
+
+               public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
+                       response.setContentType("text/html");
+                       response.getWriter().write("<script>");
+                       response.getWriter().write("top.location.href='" + getRedirectUrl(model) + "';");
+                       response.getWriter().write("</script>");
+                       response.flushBuffer();
+               }
+               
+               protected abstract String getRedirectUrl(Map<String, ?> model);
+
+       }
+
+       private String formEncode(String data) {
+               try {
+                       return URLEncoder.encode(data, "UTF-8");
+               }
+               catch (UnsupportedEncodingException ex) {
+                       // should not happen, UTF-8 is always supported
+                       throw new IllegalStateException(ex);
+               }
+       }
+
+}