2 * Copyright 2015 the original author or authors.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org.springframework.social.facebook.web;
18 import java.io.UnsupportedEncodingException;
19 import java.net.URLEncoder;
20 import java.util.List;
23 import javax.inject.Inject;
24 import javax.servlet.http.HttpServletRequest;
25 import javax.servlet.http.HttpServletResponse;
27 import org.apache.commons.logging.Log;
28 import org.apache.commons.logging.LogFactory;
29 import org.springframework.social.connect.Connection;
30 import org.springframework.social.connect.ConnectionFactoryLocator;
31 import org.springframework.social.connect.UsersConnectionRepository;
32 import org.springframework.social.connect.support.OAuth2ConnectionFactory;
33 import org.springframework.social.connect.web.SignInAdapter;
34 import org.springframework.social.facebook.api.Facebook;
35 import org.springframework.social.oauth2.AccessGrant;
36 import org.springframework.stereotype.Controller;
37 import org.springframework.ui.Model;
38 import org.springframework.web.bind.annotation.RequestMapping;
39 import org.springframework.web.bind.annotation.RequestMethod;
40 import org.springframework.web.bind.annotation.RequestParam;
41 import org.springframework.web.context.request.NativeWebRequest;
42 import org.springframework.web.servlet.View;
43 import org.springframework.web.servlet.view.RedirectView;
46 * Sign in controller that uses the signed_request parameter that Facebook gives to Canvas applications to obtain an access token.
47 * If no access token exists in signed_request, this controller will redirect the top-level browser window to Facebook's authorization dialog.
48 * When Facebook redirects back from the authorization dialog, the signed_request parameter should contain an access token.
52 @RequestMapping(value="/canvas")
53 public class CanvasSignInController {
55 private final static Log logger = LogFactory.getLog(CanvasSignInController.class);
57 private final String clientId;
59 private final String canvasPage;
61 private final ConnectionFactoryLocator connectionFactoryLocator;
63 private final UsersConnectionRepository usersConnectionRepository;
65 private final SignInAdapter signInAdapter;
67 private final SignedRequestDecoder signedRequestDecoder;
69 private String postSignInUrl = "/";
71 private String postDeclineUrl = "http://www.facebook.com";
76 public CanvasSignInController(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository, SignInAdapter signInAdapter, String clientId, String clientSecret, String canvasPage) {
77 this.usersConnectionRepository = usersConnectionRepository;
78 this.signInAdapter = signInAdapter;
79 this.clientId = clientId;
80 this.canvasPage = canvasPage;
81 this.connectionFactoryLocator = connectionFactoryLocator;
82 this.signedRequestDecoder = new SignedRequestDecoder(clientSecret);
86 * The URL or path to redirect to after successful canvas authorization.
87 * @param postSignInUrl the url to redirect to after successful canvas authorization. Defaults to "/".
89 public void setPostSignInUrl(String postSignInUrl) {
90 this.postSignInUrl = postSignInUrl;
94 * The URL or path to redirect to if a user declines authorization.
95 * The redirect will happen in the top-level window.
96 * 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}.
97 * @param postDeclineUrl the url to redirect to after a user declines authorization. Defaults to "http://www.facebook.com".
99 public void setPostDeclineUrl(String postDeclineUrl) {
100 this.postDeclineUrl = postDeclineUrl;
104 * The scope to request during authorization.
105 * @param scope the scope to request. Defaults to null (no scope will be requested; Facebook will offer their default scope).
107 public void setScope(String scope) {
111 @RequestMapping(method={ RequestMethod.POST, RequestMethod.GET }, params={"signed_request", "!error"})
112 public View signin(Model model, NativeWebRequest request) throws SignedRequestException {
113 String signedRequest = request.getParameter("signed_request");
114 if (signedRequest == null) {
115 debug("Expected a signed_request parameter, but none given. Redirecting to the application's Canvas Page: " + canvasPage);
116 return new RedirectView(canvasPage, false);
119 Map<String, ?> decodedSignedRequest = signedRequestDecoder.decodeSignedRequest(signedRequest);
120 String accessToken = (String) decodedSignedRequest.get("oauth_token");
121 if (accessToken == null) {
122 debug("No access token in the signed_request parameter. Redirecting to the authorization dialog.");
123 model.addAttribute("clientId", clientId);
124 model.addAttribute("canvasPage", canvasPage);
126 model.addAttribute("scope", scope);
128 return new TopLevelWindowRedirect() {
130 protected String getRedirectUrl(Map<String, ?> model) {
131 String clientId = (String) model.get("clientId");
132 String canvasPage = (String) model.get("canvasPage");
133 String scope = (String) model.get("scope");
134 String redirectUrl = "https://www.facebook.com/v2.5/dialog/oauth?client_id=" + clientId + "&redirect_uri=" + canvasPage;
136 redirectUrl += "&scope=" + formEncode(scope);
143 debug("Access token available in signed_request parameter. Creating connection and signing in.");
144 OAuth2ConnectionFactory<Facebook> connectionFactory = (OAuth2ConnectionFactory<Facebook>) connectionFactoryLocator.getConnectionFactory(Facebook.class);
145 AccessGrant accessGrant = new AccessGrant(accessToken);
146 // TODO: Maybe should create via ConnectionData instead?
147 Connection<Facebook> connection = connectionFactory.createConnection(accessGrant);
148 handleSignIn(connection, request);
149 debug("Signed in. Redirecting to post-signin page.");
150 return new RedirectView(postSignInUrl, true);
153 @RequestMapping(method={ RequestMethod.POST, RequestMethod.GET }, params="error")
154 public View error(@RequestParam("error") String error, @RequestParam("error_description") String errorDescription) {
155 String string = "User declined authorization: '" + errorDescription + "'. Redirecting to " + postDeclineUrl;
157 return postDeclineView();
161 * View that redirects the top level window to the URL defined in postDeclineUrl property after user declines to authorize application.
162 * May be overridden for custom views, particularly in the case where the post-decline view should be rendered in-canvas.
163 * @return a view to display after a user declines authoriation. Defaults as a redirect to postDeclineUrl
165 protected View postDeclineView() {
166 return new TopLevelWindowRedirect() {
168 protected String getRedirectUrl(Map<String, ?> model) {
169 return postDeclineUrl;
174 private void debug(String string) {
175 if (logger.isDebugEnabled()) {
176 logger.debug(string);
180 private void handleSignIn(Connection<Facebook> connection, NativeWebRequest request) {
181 List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
182 if (userIds.size() == 1) {
183 usersConnectionRepository.createConnectionRepository(userIds.get(0)).updateConnection(connection);
184 signInAdapter.signIn(userIds.get(0), connection, request);
186 // TODO: This should never happen, but need to figure out what to do if it does happen.
187 logger.error("Expected exactly 1 matching user. Got " + userIds.size() + " metching users.");
191 private static abstract class TopLevelWindowRedirect implements View {
193 public String getContentType() {
197 public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
198 response.setContentType("text/html");
199 response.getWriter().write("<script>");
200 response.getWriter().write("top.location.href='" + getRedirectUrl(model) + "';");
201 response.getWriter().write("</script>");
202 response.flushBuffer();
205 protected abstract String getRedirectUrl(Map<String, ?> model);
209 private String formEncode(String data) {
211 return URLEncoder.encode(data, "UTF-8");
213 catch (UnsupportedEncodingException ex) {
214 // should not happen, UTF-8 is always supported
215 throw new IllegalStateException(ex);