1
2
3
4
5
6
7
8
9
10
11
12
13 package sparklr.common;
14
15 import static org.junit.Assert.assertEquals;
16 import static org.junit.Assert.assertFalse;
17 import static org.junit.Assert.assertNotNull;
18 import static org.junit.Assert.assertNull;
19 import static org.junit.Assert.assertTrue;
20 import static org.junit.Assert.fail;
21
22 import java.io.IOException;
23 import java.nio.charset.Charset;
24 import java.util.Arrays;
25 import java.util.concurrent.atomic.AtomicReference;
26
27 import org.junit.Test;
28 import org.springframework.http.HttpHeaders;
29 import org.springframework.http.HttpStatus;
30 import org.springframework.http.MediaType;
31 import org.springframework.http.ResponseEntity;
32 import org.springframework.http.client.ClientHttpResponse;
33 import org.springframework.security.crypto.codec.Base64;
34 import org.springframework.security.oauth2.client.OAuth2RestTemplate;
35 import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException;
36 import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
37 import org.springframework.security.oauth2.client.test.BeforeOAuth2Context;
38 import org.springframework.security.oauth2.client.test.OAuth2ContextConfiguration;
39 import org.springframework.security.oauth2.client.token.AccessTokenRequest;
40 import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
41 import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
42 import org.springframework.security.oauth2.common.OAuth2AccessToken;
43 import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException;
44 import org.springframework.security.oauth2.common.util.OAuth2Utils;
45 import org.springframework.util.LinkedMultiValueMap;
46 import org.springframework.util.StreamUtils;
47 import org.springframework.web.client.DefaultResponseErrorHandler;
48 import org.springframework.web.client.HttpClientErrorException;
49 import org.springframework.web.client.ResourceAccessException;
50 import org.springframework.web.client.ResponseErrorHandler;
51 import org.springframework.web.client.ResponseExtractor;
52
53 import sparklr.common.HttpTestUtils.UriBuilder;
54
55
56
57
58
59 public abstract class AbstractAuthorizationCodeProviderTests extends AbstractIntegrationTests {
60
61 private AuthorizationCodeAccessTokenProvider accessTokenProvider;
62
63 private ClientHttpResponse tokenEndpointResponse;
64
65 @BeforeOAuth2Context
66 public void setupAccessTokenProvider() {
67 accessTokenProvider = new AuthorizationCodeAccessTokenProvider() {
68
69 private ResponseExtractor<OAuth2AccessToken> extractor = super.getResponseExtractor();
70
71 private ResponseExtractor<ResponseEntity<Void>> authExtractor = super.getAuthorizationResponseExtractor();
72
73 private ResponseErrorHandler errorHandler = super.getResponseErrorHandler();
74
75 @Override
76 protected ResponseErrorHandler getResponseErrorHandler() {
77 return new DefaultResponseErrorHandler() {
78 public void handleError(ClientHttpResponse response) throws IOException {
79 response.getHeaders();
80 response.getStatusCode();
81 tokenEndpointResponse = response;
82 errorHandler.handleError(response);
83 }
84 };
85 }
86
87 @Override
88 protected ResponseExtractor<OAuth2AccessToken> getResponseExtractor() {
89 return new ResponseExtractor<OAuth2AccessToken>() {
90
91 public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException {
92 try {
93 response.getHeaders();
94 response.getStatusCode();
95 tokenEndpointResponse = response;
96 return extractor.extractData(response);
97 }
98 catch (ResourceAccessException e) {
99 return null;
100 }
101 }
102
103 };
104 }
105
106 @Override
107 protected ResponseExtractor<ResponseEntity<Void>> getAuthorizationResponseExtractor() {
108 return new ResponseExtractor<ResponseEntity<Void>>() {
109
110 public ResponseEntity<Void> extractData(ClientHttpResponse response) throws IOException {
111 response.getHeaders();
112 response.getStatusCode();
113 tokenEndpointResponse = response;
114 return authExtractor.extractData(response);
115 }
116 };
117 }
118 };
119 context.setAccessTokenProvider(accessTokenProvider);
120 }
121
122 @Test
123 @OAuth2ContextConfiguration(resource = MyTrustedClient.class, initialize = false)
124 public void testUnauthenticatedAuthorizationRespondsUnauthorized() throws Exception {
125
126 AccessTokenRequest request = context.getAccessTokenRequest();
127 request.setCurrentUri("http://anywhere");
128 request.add(OAuth2Utils.USER_OAUTH_APPROVAL, "true");
129
130 try {
131 String code = accessTokenProvider.obtainAuthorizationCode(context.getResource(), request);
132 assertNotNull(code);
133 fail("Expected UserRedirectRequiredException");
134 }
135 catch (HttpClientErrorException e) {
136 assertEquals(HttpStatus.UNAUTHORIZED, e.getStatusCode());
137 }
138
139 }
140
141 @Test
142 @OAuth2ContextConfiguration(resource = MyTrustedClient.class, initialize = false)
143 public void testSuccessfulAuthorizationCodeFlow() throws Exception {
144
145
146 approveAccessTokenGrant("http://anywhere", true);
147
148
149 assertNotNull(context.getAccessToken());
150
151 AccessTokenRequest request = context.getAccessTokenRequest();
152 assertNotNull(request.getAuthorizationCode());
153 assertEquals(HttpStatus.OK, http.getStatusCode("/admin/beans"));
154
155 }
156
157 @Test
158 @OAuth2ContextConfiguration(resource = MyTrustedClient.class, initialize = false)
159 public void testWrongRedirectUri() throws Exception {
160 approveAccessTokenGrant("http://anywhere", true);
161 AccessTokenRequest request = context.getAccessTokenRequest();
162
163 context.getOAuth2ClientContext().setPreservedState(request.getStateKey(), "http://nowhere");
164
165 try {
166 assertNotNull(context.getAccessToken());
167 fail("Expected RedirectMismatchException");
168 }
169 catch (RedirectMismatchException e) {
170
171 }
172 assertEquals(HttpStatus.BAD_REQUEST, tokenEndpointResponse.getStatusCode());
173 }
174
175 @Test
176 @OAuth2ContextConfiguration(resource = MyTrustedClient.class, initialize = false)
177 public void testUserDeniesConfirmation() throws Exception {
178 approveAccessTokenGrant("http://anywhere", false);
179 String location = null;
180 try {
181 assertNotNull(context.getAccessToken());
182 fail("Expected UserRedirectRequiredException");
183 }
184 catch (UserRedirectRequiredException e) {
185 location = e.getRedirectUri();
186 }
187 assertTrue("Wrong location: " + location, location.contains("state="));
188 assertTrue(location.startsWith("http://anywhere"));
189 assertTrue(location.substring(location.indexOf('?')).contains("error=access_denied"));
190
191 assertEquals(HttpStatus.FOUND, tokenEndpointResponse.getStatusCode());
192 }
193
194 @Test
195 public void testNoClientIdProvided() throws Exception {
196 ResponseEntity<String> response = attemptToGetConfirmationPage(null, "http://anywhere");
197
198 assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
199 String body = response.getBody();
200 assertTrue("Wrong body: " + body, body.contains("<html"));
201 assertTrue("Wrong body: " + body, body.contains("Bad client credentials"));
202 }
203
204 @Test
205 public void testNoRedirect() throws Exception {
206 ResponseEntity<String> response = attemptToGetConfirmationPage("my-trusted-client", null);
207
208
209 assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
210 String body = response.getBody();
211 assertTrue("Wrong body: " + body, body.contains("<html"));
212 assertTrue("Wrong body: " + body, body.contains("invalid_request"));
213 }
214
215 @Test
216 public void testIllegalAttemptToApproveWithoutUsingAuthorizationRequest() throws Exception {
217
218 HttpHeaders headers = getAuthenticatedHeaders();
219
220 String authorizeUrl = getAuthorizeUrl("my-trusted-client", "http://anywhere.com", "read");
221 authorizeUrl = authorizeUrl + "&user_oauth_approval=true";
222 ResponseEntity<Void> response = http.postForStatus(authorizeUrl, headers,
223 new LinkedMultiValueMap<String, String>());
224 assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
225 }
226
227 @Test
228 @OAuth2ContextConfiguration(resource = MyClientWithRegisteredRedirect.class, initialize = false)
229 public void testSuccessfulFlowWithRegisteredRedirect() throws Exception {
230
231
232 approveAccessTokenGrant(null, true);
233
234
235 assertNotNull(context.getAccessToken());
236
237 AccessTokenRequest request = context.getAccessTokenRequest();
238 assertNotNull(request.getAuthorizationCode());
239 assertEquals(HttpStatus.OK, http.getStatusCode("/admin/beans"));
240
241 }
242
243 @Test
244 public void testInvalidScopeInAuthorizationRequest() throws Exception {
245
246 HttpHeaders headers = getAuthenticatedHeaders();
247 headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
248
249 String scope = "bogus";
250 String redirectUri = "http://anywhere?key=value";
251 String clientId = "my-client-with-registered-redirect";
252
253 UriBuilder uri = http.buildUri(authorizePath()).queryParam("response_type", "code")
254 .queryParam("state", "mystateid").queryParam("scope", scope);
255 if (clientId != null) {
256 uri.queryParam("client_id", clientId);
257 }
258 if (redirectUri != null) {
259 uri.queryParam("redirect_uri", redirectUri);
260 }
261 ResponseEntity<String> response = http.getForString(uri.pattern(), headers, uri.params());
262 assertEquals(HttpStatus.FOUND, response.getStatusCode());
263 String location = response.getHeaders().getLocation().toString();
264 assertTrue(location.startsWith("http://anywhere"));
265 assertTrue(location.contains("error=invalid_scope"));
266 assertFalse(location.contains("redirect_uri="));
267 }
268
269 @Test
270 public void testInvalidAccessToken() throws Exception {
271
272
273 HttpHeaders headers = new HttpHeaders();
274 headers.set("Authorization", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, "FOO"));
275 ResponseEntity<String> response = http.getForString("/admin/beans", headers);
276 assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
277
278 String authenticate = response.getHeaders().getFirst("WWW-Authenticate");
279 assertNotNull(authenticate);
280 assertTrue(authenticate.startsWith("Bearer"));
281
282 assertFalse(authenticate.contains("scope=\""));
283
284 }
285
286 @Test
287 @OAuth2ContextConfiguration(resource = MyClientWithRegisteredRedirect.class, initialize = false)
288 public void testRegisteredRedirectWithWrongRequestedRedirect() throws Exception {
289 try {
290 approveAccessTokenGrant("http://nowhere", true);
291 fail("Expected RedirectMismatchException");
292 }
293 catch (HttpClientErrorException e) {
294 assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode());
295 }
296 }
297
298 @Test
299 @OAuth2ContextConfiguration(resource = MyClientWithRegisteredRedirect.class, initialize = false)
300 public void testRegisteredRedirectWithWrongOneInTokenEndpoint() throws Exception {
301 approveAccessTokenGrant("http://anywhere?key=value", true);
302
303 context.getAccessTokenRequest().set("redirect_uri", "http://nowhere.com");
304 try {
305 assertNotNull(context.getAccessToken());
306 fail("Expected RedirectMismatchException");
307 }
308 catch (RedirectMismatchException e) {
309 assertEquals(HttpStatus.BAD_REQUEST.value(), e.getHttpErrorCode());
310 assertEquals("invalid_grant", e.getOAuth2ErrorCode());
311 }
312 }
313
314 private ResponseEntity<String> attemptToGetConfirmationPage(String clientId, String redirectUri) {
315 HttpHeaders headers = getAuthenticatedHeaders();
316 return http.getForString(getAuthorizeUrl(clientId, redirectUri, "read"), headers);
317 }
318
319 private HttpHeaders getAuthenticatedHeaders() {
320 HttpHeaders headers = new HttpHeaders();
321 headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
322 headers.set("Authorization", "Basic " + new String(Base64.encode("user:password".getBytes())));
323 if (context.getRestTemplate() != null) {
324 context.getAccessTokenRequest().setHeaders(headers);
325 }
326 return headers;
327 }
328
329 private String getAuthorizeUrl(String clientId, String redirectUri, String scope) {
330 UriBuilder uri = http.buildUri(authorizePath()).queryParam("response_type", "code")
331 .queryParam("state", "mystateid").queryParam("scope", scope);
332 if (clientId != null) {
333 uri.queryParam("client_id", clientId);
334 }
335 if (redirectUri != null) {
336 uri.queryParam("redirect_uri", redirectUri);
337 }
338 return uri.build().toString();
339 }
340
341 protected void approveAccessTokenGrant(String currentUri, boolean approved) {
342
343 AccessTokenRequest request = context.getAccessTokenRequest();
344 request.setHeaders(getAuthenticatedHeaders());
345 AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) context.getResource();
346
347 if (currentUri != null) {
348 request.setCurrentUri(currentUri);
349 }
350
351 String location = null;
352
353 try {
354
355 assertNotNull(context.getAccessToken());
356 fail("Expected UserRedirectRequiredException");
357 }
358 catch (UserRedirectRequiredException e) {
359
360 location = e.getRedirectUri();
361 }
362
363 assertTrue(location.startsWith(resource.getUserAuthorizationUri()));
364 assertNull(request.getAuthorizationCode());
365
366 verifyAuthorizationPage(context.getRestTemplate(), location);
367
368 try {
369
370 assertNotNull(context.getAccessToken());
371 fail("Expected UserRedirectRequiredException");
372 }
373 catch (UserApprovalRequiredException e) {
374
375 location = e.getApprovalUri();
376 }
377
378 assertTrue(location.startsWith(resource.getUserAuthorizationUri()));
379 assertNull(request.getAuthorizationCode());
380
381
382 request.set(OAuth2Utils.USER_OAUTH_APPROVAL, "" + approved);
383
384 }
385
386 private void verifyAuthorizationPage(OAuth2RestTemplate restTemplate, String location) {
387 final AtomicReference<String> confirmationPage = new AtomicReference<String>();
388 AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider() {
389 @Override
390 protected ResponseExtractor<ResponseEntity<Void>> getAuthorizationResponseExtractor() {
391 return new ResponseExtractor<ResponseEntity<Void>>() {
392 public ResponseEntity<Void> extractData(ClientHttpResponse response) throws IOException {
393 confirmationPage.set(StreamUtils.copyToString(response.getBody(), Charset.forName("UTF-8")));
394 return new ResponseEntity<Void>(response.getHeaders(), response.getStatusCode());
395 }
396 };
397 }
398 };
399 try {
400 provider.obtainAuthorizationCode(restTemplate.getResource(), restTemplate.getOAuth2ClientContext().getAccessTokenRequest());
401 } catch (UserApprovalRequiredException e) {
402
403 }
404 String page = confirmationPage.get();
405 verifyAuthorizationPage(page);
406 }
407
408 protected void verifyAuthorizationPage(String page) {
409 }
410
411 protected static class MyTrustedClient extends AuthorizationCodeResourceDetails {
412 public MyTrustedClient(Object target) {
413 super();
414 setClientId("my-trusted-client");
415 setScope(Arrays.asList("read"));
416 setId(getClientId());
417 }
418 }
419
420 protected static class MyClientWithRegisteredRedirect extends MyTrustedClient {
421 public MyClientWithRegisteredRedirect(Object target) {
422 super(target);
423 setClientId("my-client-with-registered-redirect");
424 setPreEstablishedRedirectUri("http://anywhere?key=value");
425 }
426 }
427 }