This repository has been archived by the owner on Apr 5, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 351
/
ProviderSignInController.java
324 lines (288 loc) · 15.7 KB
/
ProviderSignInController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/*
* 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
*
* https://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.connect.web;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.GenericTypeResolver;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.support.OAuth1ConnectionFactory;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.support.URIBuilder;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
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.context.request.WebRequest;
import org.springframework.web.servlet.view.RedirectView;
/**
* Spring MVC Controller for handling the provider user sign-in flow.
* <ul>
* <li>POST /signin/{providerId} - Initiate user sign-in with {providerId}.</li>
* <li>GET /signin/{providerId}?oauth_token&oauth_verifier||code - Receive {providerId} authentication callback and establish the connection.</li>
* </ul>
* @author Keith Donald
*/
@Controller
@RequestMapping("/signin")
public class ProviderSignInController implements InitializingBean {
private final static Log logger = LogFactory.getLog(ProviderSignInController.class);
private final ConnectionFactoryLocator connectionFactoryLocator;
private final UsersConnectionRepository usersConnectionRepository;
private final MultiValueMap<Class<?>, ProviderSignInInterceptor<?>> signInInterceptors = new LinkedMultiValueMap<Class<?>, ProviderSignInInterceptor<?>>();
private final SignInAdapter signInAdapter;
private String applicationUrl;
private String signInUrl = "/signin";
private String signUpUrl = "/signup";
private String postSignInUrl = "/";
private ConnectSupport connectSupport;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* Creates a new provider sign-in controller.
* @param connectionFactoryLocator the locator of {@link ConnectionFactory connection factories} used to support provider sign-in.
* Note: this reference should be a serializable proxy to a singleton-scoped target instance.
* This is because {@link ProviderSignInAttempt} are session-scoped objects that hold ConnectionFactoryLocator references.
* If these references cannot be serialized, NotSerializableExceptions can occur at runtime.
* @param usersConnectionRepository the global store for service provider connections across all users.
* Note: this reference should be a serializable proxy to a singleton-scoped target instance.
* @param signInAdapter handles user sign-in
*/
@Inject
public ProviderSignInController(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository, SignInAdapter signInAdapter) {
this.connectionFactoryLocator = connectionFactoryLocator;
this.usersConnectionRepository = usersConnectionRepository;
this.signInAdapter = signInAdapter;
}
/**
* Configure the list of sign in interceptors that should receive callbacks during the sign in process.
* Convenient when an instance of this class is configured using a tool that supports JavaBeans-based configuration.
* @param interceptors the sign in interceptors to add
*/
public void setSignInInterceptors(List<ProviderSignInInterceptor<?>> interceptors) {
for (ProviderSignInInterceptor<?> interceptor : interceptors) {
addSignInInterceptor(interceptor);
}
}
/**
* Sets the URL of the application's sign in page.
* Defaults to "/signin".
* @param signInUrl the signIn URL
*/
public void setSignInUrl(String signInUrl) {
this.signInUrl = signInUrl;
}
/**
* Sets the URL to redirect the user to if no local user account can be mapped when signing in using a provider.
* Defaults to "/signup".
* @param signUpUrl the signUp URL
*/
public void setSignUpUrl(String signUpUrl) {
this.signUpUrl = signUpUrl;
}
/**
* Sets the default URL to redirect the user to after signing in using a provider.
* Defaults to "/".
* @param postSignInUrl the postSignIn URL
*/
public void setPostSignInUrl(String postSignInUrl) {
this.postSignInUrl = postSignInUrl;
}
/**
* Configures the base secure URL for the application this controller is being used in e.g. <code>https://myapp.com</code>. Defaults to null.
* If specified, will be used to generate OAuth callback URLs.
* If not specified, OAuth callback URLs are generated from web request info.
* You may wish to set this property if requests into your application flow through a proxy to your application server.
* In this case, the request URI may contain a scheme, host, and/or port value that points to an internal server not appropriate for an external callback URL.
* If you have this problem, you can set this property to the base external URL for your application and it will be used to construct the callback URL instead.
* @param applicationUrl the application URL value
*/
public void setApplicationUrl(String applicationUrl) {
this.applicationUrl = applicationUrl;
}
/**
* Sets a strategy to use when persisting information that is to survive past the boundaries of a request.
* The default strategy is to set the data as attributes in the HTTP Session.
* @param sessionStrategy the session strategy.
*/
public void setSessionStrategy(SessionStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
/**
* Adds a ConnectInterceptor to receive callbacks during the connection process.
* Useful for programmatic configuration.
* @param interceptor the connect interceptor to add
*/
public void addSignInInterceptor(ProviderSignInInterceptor<?> interceptor) {
Class<?> serviceApiType = GenericTypeResolver.resolveTypeArgument(interceptor.getClass(), ProviderSignInInterceptor.class);
signInInterceptors.add(serviceApiType, interceptor);
}
/**
* Process a sign-in form submission by commencing the process of establishing a connection to the provider on behalf of the user.
* For OAuth1, fetches a new request token from the provider, temporarily stores it in the session, then redirects the user to the provider's site for authentication authorization.
* For OAuth2, redirects the user to the provider's site for authentication authorization.
* @param providerId the provider ID to authorize against
* @param request the request
* @return a RedirectView to the provider's authorization page or to the application's signin page if there is an error
*/
@RequestMapping(value="/{providerId}", method=RequestMethod.POST)
public RedirectView signIn(@PathVariable String providerId, NativeWebRequest request) {
try {
ConnectionFactory<?> connectionFactory = connectionFactoryLocator.getConnectionFactory(providerId);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();
preSignIn(connectionFactory, parameters, request);
return new RedirectView(connectSupport.buildOAuthUrl(connectionFactory, request, parameters));
} catch (Exception e) {
logger.error("Exception while building authorization URL: ", e);
return redirect(URIBuilder.fromUri(signInUrl).queryParam("error", "provider").build().toString());
}
}
/**
* Process the authentication callback from an OAuth 1 service provider.
* Called after the member authorizes the authentication, generally done once by having he or she click "Allow" in their web browser at the provider's site.
* Handles the provider sign-in callback by first determining if a local user account is associated with the connected provider account.
* If so, signs the local user in by delegating to {@link SignInAdapter#signIn(String, Connection, NativeWebRequest)}
* If not, redirects the user to a signup page to create a new account with {@link ProviderSignInAttempt} context exposed in the HttpSession.
* @param providerId the provider ID to authorize against
* @param request the request
* @return a RedirectView to the provider's authorization page or to the application's signin page if there is an error
* @see ProviderSignInAttempt
* @see ProviderSignInUtils
*/
@RequestMapping(value="/{providerId}", method=RequestMethod.GET, params="oauth_token")
public RedirectView oauth1Callback(@PathVariable String providerId, NativeWebRequest request) {
try {
OAuth1ConnectionFactory<?> connectionFactory = (OAuth1ConnectionFactory<?>) connectionFactoryLocator.getConnectionFactory(providerId);
Connection<?> connection = connectSupport.completeConnection(connectionFactory, request);
return handleSignIn(connection, connectionFactory, request);
} catch (Exception e) {
logger.error("Exception while completing OAuth 1.0(a) connection: ", e);
return redirect(URIBuilder.fromUri(signInUrl).queryParam("error", "provider").build().toString());
}
}
/**
* Process the authentication callback from an OAuth 2 service provider.
* Called after the user authorizes the authentication, generally done once by having he or she click "Allow" in their web browser at the provider's site.
* Handles the provider sign-in callback by first determining if a local user account is associated with the connected provider account.
* If so, signs the local user in by delegating to {@link SignInAdapter#signIn(String, Connection, NativeWebRequest)}.
* If not, redirects the user to a signup page to create a new account with {@link ProviderSignInAttempt} context exposed in the HttpSession.
* @see ProviderSignInAttempt
* @see ProviderSignInUtils
* @param providerId the provider ID to authorize against
* @param code the OAuth 2 authorization code
* @param request the web request
* @return A RedirectView to the target page or the signInUrl if an error occurs
*/
@RequestMapping(value="/{providerId}", method=RequestMethod.GET, params="code")
public RedirectView oauth2Callback(@PathVariable String providerId, @RequestParam("code") String code, NativeWebRequest request) {
try {
OAuth2ConnectionFactory<?> connectionFactory = (OAuth2ConnectionFactory<?>) connectionFactoryLocator.getConnectionFactory(providerId);
Connection<?> connection = connectSupport.completeConnection(connectionFactory, request);
return handleSignIn(connection, connectionFactory, request);
} catch (Exception e) {
logger.error("Exception while completing OAuth 2 connection: ", e);
return redirect(URIBuilder.fromUri(signInUrl).queryParam("error", "provider").build().toString());
}
}
/**
* Process an error callback from an OAuth 2 authorization as described at https://tools.ietf.org/html/rfc6749#section-4.1.2.1.
* Called after upon redirect from an OAuth 2 provider when there is some sort of error during authorization, typically because the user denied authorization.
* Simply carries the error parameters through to the sign-in page.
* @param providerId The Provider ID
* @param error An error parameter sent on the redirect from the provider
* @param errorDescription An optional error description sent from the provider
* @param errorUri An optional error URI sent from the provider
* @param request The web request
* @return a RedirectView to the signInUrl
*/
@RequestMapping(value="/{providerId}", method=RequestMethod.GET, params="error")
public RedirectView oauth2ErrorCallback(@PathVariable String providerId,
@RequestParam("error") String error,
@RequestParam(value="error_description", required=false) String errorDescription,
@RequestParam(value="error_uri", required=false) String errorUri,
NativeWebRequest request) {
logger.warn("Error during authorization: " + error);
URIBuilder uriBuilder = URIBuilder.fromUri(signInUrl).queryParam("error", error);
if (errorDescription != null ) { uriBuilder.queryParam("error_description", errorDescription); }
if (errorUri != null ) { uriBuilder.queryParam("error_uri", errorUri); }
return redirect(uriBuilder.build().toString());
}
/**
* Process the authentication callback when neither the oauth_token or code parameter is given, likely indicating that the user denied authorization with the provider.
* Redirects to application's sign in URL, as set in the signInUrl property.
* @return A RedirectView to the sign in URL
*/
@RequestMapping(value="/{providerId}", method=RequestMethod.GET)
public RedirectView canceledAuthorizationCallback() {
return redirect(signInUrl);
}
// From InitializingBean
public void afterPropertiesSet() throws Exception {
this.connectSupport = new ConnectSupport(sessionStrategy);
this.connectSupport.setUseAuthenticateUrl(true);
if (this.applicationUrl != null) {
this.connectSupport.setApplicationUrl(applicationUrl);
}
};
// internal helpers
private RedirectView handleSignIn(Connection<?> connection, ConnectionFactory<?> connectionFactory, NativeWebRequest request) {
List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
if (userIds.size() == 0) {
ProviderSignInAttempt signInAttempt = new ProviderSignInAttempt(connection);
sessionStrategy.setAttribute(request, ProviderSignInAttempt.SESSION_ATTRIBUTE, signInAttempt);
return redirect(signUpUrl);
} else if (userIds.size() == 1) {
usersConnectionRepository.createConnectionRepository(userIds.get(0)).updateConnection(connection);
String originalUrl = signInAdapter.signIn(userIds.get(0), connection, request);
postSignIn(connectionFactory, connection, (WebRequest) request);
return originalUrl != null ? redirect(originalUrl) : redirect(postSignInUrl);
} else {
return redirect(URIBuilder.fromUri(signInUrl).queryParam("error", "multiple_users").build().toString());
}
}
private RedirectView redirect(String url) {
return new RedirectView(url, true);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void preSignIn(ConnectionFactory<?> connectionFactory, MultiValueMap<String, String> parameters, WebRequest request) {
for (ProviderSignInInterceptor interceptor : interceptingSignInTo(connectionFactory)) {
interceptor.preSignIn(connectionFactory, parameters, request);
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void postSignIn(ConnectionFactory<?> connectionFactory, Connection<?> connection, WebRequest request) {
for (ProviderSignInInterceptor interceptor : interceptingSignInTo(connectionFactory)) {
interceptor.postSignIn(connection, request);
}
}
private List<ProviderSignInInterceptor<?>> interceptingSignInTo(ConnectionFactory<?> connectionFactory) {
Class<?> serviceType = GenericTypeResolver.resolveTypeArgument(connectionFactory.getClass(), ConnectionFactory.class);
List<ProviderSignInInterceptor<?>> typedInterceptors = signInInterceptors.get(serviceType);
if (typedInterceptors == null) {
typedInterceptors = Collections.emptyList();
}
return typedInterceptors;
}
}