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