From 9c8202018997d8e18da4da86ccef926cb9f9de6f Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Thu, 26 May 2016 11:45:06 +0200 Subject: [PATCH] Added a fix to repair the login-redirect when using the Chrome-browser --- .../facebook/web/CanvasSignInController.java | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 src/main/java/org/springframework/social/facebook/web/CanvasSignInController.java 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 index 0000000..9858b4e --- /dev/null +++ b/src/main/java/org/springframework/social/facebook/web/CanvasSignInController.java @@ -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 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 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 connectionFactory = (OAuth2ConnectionFactory) connectionFactoryLocator.getConnectionFactory(Facebook.class); + AccessGrant accessGrant = new AccessGrant(accessToken); + // TODO: Maybe should create via ConnectionData instead? + Connection 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 model) { + return postDeclineUrl; + } + }; + } + + private void debug(String string) { + if (logger.isDebugEnabled()) { + logger.debug(string); + } + } + + private void handleSignIn(Connection connection, NativeWebRequest request) { + List 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 model, HttpServletRequest request, HttpServletResponse response) throws Exception { + response.setContentType("text/html"); + response.getWriter().write(""); + response.flushBuffer(); + } + + protected abstract String getRedirectUrl(Map 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); + } + } + +} -- 2.20.1