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 353
/
ConnectSupport.java
276 lines (243 loc) · 12.8 KB
/
ConnectSupport.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
/*
* 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 org.springframework.social.connect.web;
import static java.util.Arrays.*;
import java.util.List;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.support.OAuth1ConnectionFactory;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth1.AuthorizedRequestToken;
import org.springframework.social.oauth1.OAuth1Operations;
import org.springframework.social.oauth1.OAuth1Parameters;
import org.springframework.social.oauth1.OAuth1Version;
import org.springframework.social.oauth1.OAuthToken;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.GrantType;
import org.springframework.social.oauth2.OAuth2Operations;
import org.springframework.social.oauth2.OAuth2Parameters;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
/**
* Provides common connect support and utilities for Java web/servlet environments.
* Used by {@link ConnectController} and {@link ProviderSignInController}.
* @author Keith Donald
*/
public class ConnectSupport {
private final static Log logger = LogFactory.getLog(ConnectSupport.class);
private boolean useAuthenticateUrl;
private String applicationUrl;
private String callbackUrl;
/**
* Flag indicating if this instance will support OAuth-based authentication instead of the traditional user authorization.
* Some providers expose a special "authenticateUrl" the user should be redirected to as part of an OAuth-based authentication attempt.
* Setting this flag to true has {@link #buildOAuthUrl(ConnectionFactory, NativeWebRequest) oauthUrl} return this authenticate URL.
* @param useAuthenticateUrl whether to use the authenticat url or not
* @see OAuth1Operations#buildAuthenticateUrl(String, OAuth1Parameters)
* @see OAuth2Operations#buildAuthenticateUrl(GrantType, OAuth2Parameters)
*/
public void setUseAuthenticateUrl(boolean useAuthenticateUrl) {
this.useAuthenticateUrl = useAuthenticateUrl;
}
/**
* 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 {@link HttpServletRequest HttpServletRequests}.
* You may wish to set this property if requests into your application flow through a proxy to your application server.
* In this case, the HttpServletRequest 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;
}
/**
* Configures a specific callback URL that is to be used instead of calculating one based on the application URL or current request URL.
* When set this URL will override the default behavior where the callback URL is derived from the current request and/or a specified application URL.
* When set along with applicationUrl, the applicationUrl will be ignored.
* @param callbackUrl the callback URL to send to providers during authorization. Default is null.
*/
public void setCallbackUrl(String callbackUrl) {
this.callbackUrl = callbackUrl;
}
/**
* Builds the provider URL to redirect the user to for connection authorization.
* @param connectionFactory the service provider's connection factory e.g. FacebookConnectionFactory
* @param request the current web request
* @return the URL to redirect the user to for authorization
* @throws IllegalArgumentException if the connection factory is not OAuth1 based.
*/
public String buildOAuthUrl(ConnectionFactory<?> connectionFactory, NativeWebRequest request) {
return buildOAuthUrl(connectionFactory, request, null);
}
/**
* Builds the provider URL to redirect the user to for connection authorization.
* @param connectionFactory the service provider's connection factory e.g. FacebookConnectionFactory
* @param request the current web request
* @param additionalParameters parameters to add to the authorization URL.
* @return the URL to redirect the user to for authorization
* @throws IllegalArgumentException if the connection factory is not OAuth1 based.
*/
public String buildOAuthUrl(ConnectionFactory<?> connectionFactory, NativeWebRequest request, MultiValueMap<String, String> additionalParameters) {
if (connectionFactory instanceof OAuth1ConnectionFactory) {
return buildOAuth1Url((OAuth1ConnectionFactory<?>) connectionFactory, request, additionalParameters);
} else if (connectionFactory instanceof OAuth2ConnectionFactory) {
return buildOAuth2Url((OAuth2ConnectionFactory<?>) connectionFactory, request, additionalParameters);
} else {
throw new IllegalArgumentException("ConnectionFactory not supported");
}
}
/**
* Complete the connection to the OAuth1 provider.
* @param connectionFactory the service provider's connection factory e.g. FacebookConnectionFactory
* @param request the current web request
* @return a new connection to the service provider
*/
public Connection<?> completeConnection(OAuth1ConnectionFactory<?> connectionFactory, NativeWebRequest request) {
String verifier = request.getParameter("oauth_verifier");
AuthorizedRequestToken requestToken = new AuthorizedRequestToken(extractCachedRequestToken(request), verifier);
OAuthToken accessToken = connectionFactory.getOAuthOperations().exchangeForAccessToken(requestToken, null);
return connectionFactory.createConnection(accessToken);
}
/**
* Complete the connection to the OAuth2 provider.
* @param connectionFactory the service provider's connection factory e.g. FacebookConnectionFactory
* @param request the current web request
* @return a new connection to the service provider
*/
public Connection<?> completeConnection(OAuth2ConnectionFactory<?> connectionFactory, NativeWebRequest request) {
if (connectionFactory.supportsStateParameter()) {
verifyStateParameter(request);
}
String code = request.getParameter("code");
try {
AccessGrant accessGrant = connectionFactory.getOAuthOperations().exchangeForAccess(code, callbackUrl(request), null);
return connectionFactory.createConnection(accessGrant);
} catch (HttpClientErrorException e) {
logger.warn("HttpClientErrorException while completing connection: " + e.getMessage());
logger.warn(" Response body: " + e.getResponseBodyAsString());
throw e;
}
}
private void verifyStateParameter(NativeWebRequest request) {
String state = request.getParameter("state");
String originalState = extractCachedOAuth2State(request);
if (state != null && !state.equals(originalState)) {
throw new IllegalStateException("The OAuth2 'state' parameter doesn't match.");
}
}
protected String callbackUrl(NativeWebRequest request) {
if (callbackUrl != null) {
return callbackUrl;
}
HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class);
if (applicationUrl != null) {
return applicationUrl + connectPath(nativeRequest);
} else {
return nativeRequest.getRequestURL().toString();
}
}
// internal helpers
private String buildOAuth1Url(OAuth1ConnectionFactory<?> connectionFactory, NativeWebRequest request, MultiValueMap<String, String> additionalParameters) {
OAuth1Operations oauthOperations = connectionFactory.getOAuthOperations();
MultiValueMap<String, String> requestParameters = getRequestParameters(request);
OAuth1Parameters parameters = getOAuth1Parameters(request, additionalParameters);
parameters.putAll(requestParameters);
if (oauthOperations.getVersion() == OAuth1Version.CORE_10) {
parameters.setCallbackUrl(callbackUrl(request));
}
OAuthToken requestToken = fetchRequestToken(request, requestParameters, oauthOperations);
request.setAttribute(OAUTH_TOKEN_ATTRIBUTE, requestToken, RequestAttributes.SCOPE_SESSION);
return buildOAuth1Url(oauthOperations, requestToken.getValue(), parameters);
}
private OAuth1Parameters getOAuth1Parameters(NativeWebRequest request, MultiValueMap<String, String> additionalParameters) {
OAuth1Parameters parameters = new OAuth1Parameters(additionalParameters);
parameters.putAll(getRequestParameters(request));
return parameters;
}
private OAuthToken fetchRequestToken(NativeWebRequest request, MultiValueMap<String, String> requestParameters, OAuth1Operations oauthOperations) {
if (oauthOperations.getVersion() == OAuth1Version.CORE_10_REVISION_A) {
return oauthOperations.fetchRequestToken(callbackUrl(request), requestParameters);
}
return oauthOperations.fetchRequestToken(null, requestParameters);
}
private String buildOAuth2Url(OAuth2ConnectionFactory<?> connectionFactory, NativeWebRequest request, MultiValueMap<String, String> additionalParameters) {
OAuth2Operations oauthOperations = connectionFactory.getOAuthOperations();
String defaultScope = connectionFactory.getScope();
OAuth2Parameters parameters = getOAuth2Parameters(request, defaultScope, additionalParameters);
String state = connectionFactory.generateState();
parameters.add("state", state);
request.setAttribute(OAUTH2_STATE_ATTRIBUTE, state, RequestAttributes.SCOPE_SESSION);
if (useAuthenticateUrl) {
return oauthOperations.buildAuthenticateUrl(GrantType.AUTHORIZATION_CODE, parameters);
} else {
return oauthOperations.buildAuthorizeUrl(GrantType.AUTHORIZATION_CODE, parameters);
}
}
private OAuth2Parameters getOAuth2Parameters(NativeWebRequest request, String defaultScope, MultiValueMap<String, String> additionalParameters) {
OAuth2Parameters parameters = new OAuth2Parameters(additionalParameters);
parameters.putAll(getRequestParameters(request, "scope"));
parameters.setRedirectUri(callbackUrl(request));
String scope = request.getParameter("scope");
if (scope != null) {
parameters.setScope(scope);
} else if (defaultScope != null) {
parameters.setScope(defaultScope);
}
return parameters;
}
private String connectPath(HttpServletRequest request) {
String pathInfo = request.getPathInfo();
return request.getServletPath() + (pathInfo != null ? pathInfo : "");
}
private String buildOAuth1Url(OAuth1Operations oauthOperations, String requestToken, OAuth1Parameters parameters) {
if (useAuthenticateUrl) {
return oauthOperations.buildAuthenticateUrl(requestToken, parameters);
} else {
return oauthOperations.buildAuthorizeUrl(requestToken, parameters);
}
}
private OAuthToken extractCachedRequestToken(WebRequest request) {
OAuthToken requestToken = (OAuthToken) request.getAttribute(OAUTH_TOKEN_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);
request.removeAttribute(OAUTH_TOKEN_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);
return requestToken;
}
private String extractCachedOAuth2State(WebRequest request) {
String state = (String) request.getAttribute(OAUTH2_STATE_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);
request.removeAttribute(OAUTH2_STATE_ATTRIBUTE, RequestAttributes.SCOPE_SESSION);
return state;
}
private MultiValueMap<String, String> getRequestParameters(NativeWebRequest request, String... ignoredParameters) {
List<String> ignoredParameterList = asList(ignoredParameters);
MultiValueMap<String, String> convertedMap = new LinkedMultiValueMap<String, String>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
if (!ignoredParameterList.contains(entry.getKey())) {
convertedMap.put(entry.getKey(), asList(entry.getValue()));
}
}
return convertedMap;
}
private static final String OAUTH_TOKEN_ATTRIBUTE = "oauthToken";
private static final String OAUTH2_STATE_ATTRIBUTE = "oauth2State";
}