/* * 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.8/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.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); } } }