Skip to content

Commit bc03d02

Browse files
author
Craig Walls
committed
Update copyright to 2014
1 parent 88a1769 commit bc03d02

File tree

198 files changed

+21091
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

198 files changed

+21091
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
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
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
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.
15+
*/
16+
package org.springframework.social.facebook.web;
17+
18+
import java.io.UnsupportedEncodingException;
19+
import java.net.URLEncoder;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import javax.inject.Inject;
24+
import javax.servlet.http.HttpServletRequest;
25+
import javax.servlet.http.HttpServletResponse;
26+
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;
44+
45+
/**
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.
49+
* @author Craig Walls
50+
*/
51+
@Controller
52+
@RequestMapping(value="/canvas")
53+
public class CanvasSignInController {
54+
55+
private final static Log logger = LogFactory.getLog(CanvasSignInController.class);
56+
57+
private final String clientId;
58+
59+
private final String canvasPage;
60+
61+
private final ConnectionFactoryLocator connectionFactoryLocator;
62+
63+
private final UsersConnectionRepository usersConnectionRepository;
64+
65+
private final SignInAdapter signInAdapter;
66+
67+
private final SignedRequestDecoder signedRequestDecoder;
68+
69+
private String postSignInUrl = "/";
70+
71+
private String postDeclineUrl = "http://www.facebook.com";
72+
73+
private String scope;
74+
75+
@Inject
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);
83+
}
84+
85+
/**
86+
* The URL or path to redirect to after successful canvas authorization.
87+
* Defaults to "/".
88+
*/
89+
public void setPostSignInUrl(String postSignInUrl) {
90+
this.postSignInUrl = postSignInUrl;
91+
}
92+
93+
/**
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+
* Defaults to "http://www.facebook.com".
98+
*/
99+
public void setPostDeclineUrl(String postDeclineUrl) {
100+
this.postDeclineUrl = postDeclineUrl;
101+
}
102+
103+
/**
104+
* The scope to request during authorization.
105+
* Defaults to null (no scope will be requested; Facebook will offer their default scope).
106+
*/
107+
public void setScope(String scope) {
108+
this.scope = scope;
109+
}
110+
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);
117+
}
118+
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);
125+
if (scope != null) {
126+
model.addAttribute("scope", scope);
127+
}
128+
return new TopLevelWindowRedirect() {
129+
@Override
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/dialog/oauth?client_id=" + clientId + "&redirect_uri=" + canvasPage;
135+
if (scope != null) {
136+
redirectUrl += "&scope=" + formEncode(scope);
137+
}
138+
return redirectUrl;
139+
}
140+
};
141+
}
142+
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);
151+
}
152+
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;
156+
debug(string);
157+
return postDeclineView();
158+
}
159+
160+
/**
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+
*/
164+
protected View postDeclineView() {
165+
return new TopLevelWindowRedirect() {
166+
@Override
167+
protected String getRedirectUrl(Map<String, ?> model) {
168+
return postDeclineUrl;
169+
}
170+
};
171+
}
172+
173+
private void debug(String string) {
174+
if (logger.isDebugEnabled()) {
175+
logger.debug(string);
176+
}
177+
}
178+
179+
private void handleSignIn(Connection<Facebook> connection, NativeWebRequest request) {
180+
List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
181+
if (userIds.size() == 1) {
182+
usersConnectionRepository.createConnectionRepository(userIds.get(0)).updateConnection(connection);
183+
signInAdapter.signIn(userIds.get(0), connection, request);
184+
} else {
185+
// TODO: This should never happen, but need to figure out what to do if it does happen.
186+
logger.error("Expected exactly 1 matching user. Got " + userIds.size() + " metching users.");
187+
}
188+
}
189+
190+
private static abstract class TopLevelWindowRedirect implements View {
191+
192+
public String getContentType() {
193+
return "text/html";
194+
}
195+
196+
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
197+
response.getWriter().write("<script>");
198+
response.getWriter().write("top.location.href='" + getRedirectUrl(model) + "';");
199+
response.getWriter().write("</script>");
200+
response.flushBuffer();
201+
}
202+
203+
protected abstract String getRedirectUrl(Map<String, ?> model);
204+
205+
}
206+
207+
private String formEncode(String data) {
208+
try {
209+
return URLEncoder.encode(data, "UTF-8");
210+
}
211+
catch (UnsupportedEncodingException ex) {
212+
// should not happen, UTF-8 is always supported
213+
throw new IllegalStateException(ex);
214+
}
215+
}
216+
217+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
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
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
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.
15+
*/
16+
package org.springframework.social.facebook.web;
17+
18+
import java.util.HashSet;
19+
import java.util.Map;
20+
import java.util.Set;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import org.springframework.http.HttpStatus;
25+
import org.springframework.http.ResponseEntity;
26+
import org.springframework.social.connect.ConnectionKey;
27+
import org.springframework.social.connect.ConnectionRepository;
28+
import org.springframework.social.connect.UsersConnectionRepository;
29+
import org.springframework.stereotype.Controller;
30+
import org.springframework.web.bind.annotation.RequestMapping;
31+
import org.springframework.web.bind.annotation.RequestMethod;
32+
import org.springframework.web.bind.annotation.RequestParam;
33+
34+
/**
35+
* Spring MVC controller for handling deauthorization callbacks from Facebook as described at https://developers.facebook.com/docs/authentication/.
36+
* Handles a POST request to /disconnect/facebook with a single signed_request parameter, extracts the Facebook user ID from the signed_request,
37+
* then removes all connections for the given Facebook user.
38+
* @author habuma
39+
*/
40+
@Controller
41+
@RequestMapping("/disconnect/facebook")
42+
public class DisconnectController {
43+
44+
private final static Log logger = LogFactory.getLog(DisconnectController.class);
45+
46+
private final SignedRequestDecoder signedRequestDecoder;
47+
48+
private final UsersConnectionRepository usersConnectionRepository;
49+
50+
/**
51+
* Constructs a DisconnectController.
52+
* @param usersConnectionRepository the current user's {@link UsersConnectionRepository} needed to persist connections; must be a proxy to a request-scoped bean
53+
* @param applicationSecret the application's secret as assigned by Facebook at registration time. Used to validate signed_request.
54+
*/
55+
public DisconnectController(UsersConnectionRepository usersConnectionRepository, String applicationSecret) {
56+
this.usersConnectionRepository = usersConnectionRepository;
57+
this.signedRequestDecoder = new SignedRequestDecoder(applicationSecret);
58+
}
59+
60+
@RequestMapping(method=RequestMethod.POST)
61+
public ResponseEntity<String> disconnect(@RequestParam("signed_request") String signedRequest) {
62+
try {
63+
String userId = getUserId(signedRequest);
64+
logger.info("Deauthorization request received for Facebook User ID: " + userId + "; Removing connections.");
65+
Set<String> providerUserIds = new HashSet<String>();
66+
providerUserIds.add(userId);
67+
Set<String> localUserIds = usersConnectionRepository.findUserIdsConnectedTo("facebook", providerUserIds);
68+
for (String localUserId : localUserIds) {
69+
ConnectionRepository connectionRepository = usersConnectionRepository.createConnectionRepository(localUserId);
70+
logger.info(" Removing Facebook connection for local user '" + localUserId + "'");
71+
connectionRepository.removeConnection(new ConnectionKey("facebook", userId));
72+
}
73+
return new ResponseEntity<String>(HttpStatus.NO_CONTENT);
74+
} catch (SignedRequestException e) {
75+
return new ResponseEntity<String>(HttpStatus.BAD_REQUEST);
76+
}
77+
}
78+
79+
private String getUserId(String signedRequest) throws SignedRequestException {
80+
return (String) signedRequestDecoder.decodeSignedRequest(signedRequest, Map.class).get("user_id");
81+
}
82+
83+
}

0 commit comments

Comments
 (0)