/* * Copyright 2013 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 com.chughes.dip.user; 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.beans.factory.annotation.Autowired; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; 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.facebook.web.SignedRequestDecoder; import org.springframework.social.facebook.web.SignedRequestException; 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.AbstractView; 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 @PropertySource("application.properties") public class CanvasSignInFix { @Autowired private Environment environment; private final static Log logger = LogFactory.getLog(CanvasSignInFix.class); // private final String clientId; // // 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 CanvasSignInFix(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. * 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}. * Defaults to "http://www.facebook.com". */ public void setPostDeclineUrl(String postDeclineUrl) { this.postDeclineUrl = postDeclineUrl; } /** * The scope to request during authorization. * Defaults to null (no scope will be requested; Facebook will offer their default scope). */ public void setScope(String scope) { this.scope = scope; } // @RequestMapping(value="/fbcanvas", method={ RequestMethod.POST, RequestMethod.GET }, params={"signed_request", "!error"}) // public View signMeIn(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/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(value="/fbcanvas", method={ RequestMethod.POST, RequestMethod.GET }, params="error") // public View errorMe(@RequestParam("error") String error, @RequestParam("error_description") String errorDescription) { // String string = "User declined authorization: '" + errorDescription + "'. Redirecting to " + postDeclineUrl; // debug(string); // return postDeclineView(); // } @RequestMapping(value="/fbauth") public View authorize(){ return new TopLevelWindowRedirect() { @Override protected String getRedirectUrl(Map<String, ?> model) { String clientId = environment.getProperty("facebook.clientId"); String canvasPage = environment.getProperty("facebook.canvasPage"); String redirectUrl = "https://www.facebook.com/dialog/oauth?client_id=" + clientId + "&redirect_uri=" + canvasPage; // if (scope != null) { // redirectUrl += "&scope=" + formEncode(scope); // } return redirectUrl; } }; } /** * 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. */ 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 extends AbstractView { @Override public String getContentType() { return "text/html"; } @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType("text/html"); response.getWriter().write("<html>"); response.getWriter().write("<script type='text/javascript'>"); response.getWriter().write("top.location.href='" + getRedirectUrl(model) + "';"); response.getWriter().write("</script>"); response.getWriter().write("</html>"); 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); } } }