From 96ba5ed32c7bc26efae5ecc275c0606cb5625b26 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 29 Oct 2015 08:42:36 +0000 Subject: [PATCH] Ensure OAuth2 context state is not reset if access tokens match We can expect that occasionally a user clicks on a login link when the token is expired. If the UserInfoTokenServices tries to authenticate the user at that point, it can retain the option to refresh the expired token if we preserve the state. Since the state is in session scope all we need to do is to check that the token that is being checked has the same value as the one in the OAuth2ClientContext and re-use the context if it is. Fixes gh-4251 --- .../resource/UserInfoTokenServices.java | 12 +- ...serInfoTokenServicesRefreshTokenTests.java | 152 ++++++++++++++++++ 2 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServicesRefreshTokenTests.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java index facf263e50..095f62655f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java @@ -22,7 +22,6 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; @@ -123,9 +122,14 @@ public class UserInfoTokenServices implements ResourceServerTokenServices { resource.setClientId(this.clientId); restTemplate = new OAuth2RestTemplate(resource); } - DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken); - token.setTokenType(this.tokenType); - restTemplate.getOAuth2ClientContext().setAccessToken(token); + OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext() + .getAccessToken(); + if (existingToken == null || !accessToken.equals(existingToken.getValue())) { + DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken( + accessToken); + token.setTokenType(this.tokenType); + restTemplate.getOAuth2ClientContext().setAccessToken(token); + } return restTemplate.getForEntity(path, Map.class).getBody(); } catch (Exception ex) { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServicesRefreshTokenTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServicesRefreshTokenTests.java new file mode 100644 index 0000000000..86f6b2ad34 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServicesRefreshTokenTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-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 + * + * 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.boot.autoconfigure.security.oauth2.resource; + +import java.util.Date; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServicesRefreshTokenTests.Application; +import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@link UserInfoTokenServices}. + * + * @author Dave Syer + */ +@SpringApplicationConfiguration(classes = Application.class) +@RunWith(SpringJUnit4ClassRunner.class) +@WebIntegrationTest({ "server.port=0", + "security.oauth2.resource.userInfoUri:http://example.com", + "security.oauth2.client.clientId=foo" }) +@DirtiesContext +public class UserInfoTokenServicesRefreshTokenTests { + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Value("${local.server.port}") + private int port; + + private UserInfoTokenServices services; + + @Before + public void init() { + this.services = new UserInfoTokenServices( + "http://localhost:" + this.port + "/user", "foo"); + } + + @Test + public void sunnyDay() { + assertEquals("me", this.services.loadAuthentication("FOO").getName()); + } + + @Test + public void withRestTemplate() { + OAuth2ProtectedResourceDetails resource = new AuthorizationCodeResourceDetails(); + OAuth2ClientContext context = new DefaultOAuth2ClientContext(); + DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken("FOO"); + token.setRefreshToken(new DefaultExpiringOAuth2RefreshToken("BAR", new Date(0L))); + context.setAccessToken(token); + this.services.setRestTemplate(new OAuth2RestTemplate(resource, context)); + assertEquals("me", this.services.loadAuthentication("FOO").getName()); + assertEquals("FOO", context.getAccessToken().getValue()); + // The refresh token is still intact + assertEquals(token.getRefreshToken(), context.getAccessToken().getRefreshToken()); + } + + @Test + public void withRestTemplateChangesState() { + OAuth2ProtectedResourceDetails resource = new AuthorizationCodeResourceDetails(); + OAuth2ClientContext context = new DefaultOAuth2ClientContext(); + context.setAccessToken(new DefaultOAuth2AccessToken("FOO")); + this.services.setRestTemplate(new OAuth2RestTemplate(resource, context)); + assertEquals("me", this.services.loadAuthentication("BAR").getName()); + assertEquals("BAR", context.getAccessToken().getValue()); + } + + @Configuration + @Import({ EmbeddedServletContainerAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + + @RestController + protected static class Application { + + @RequestMapping(value = "/user") + public User user(@RequestHeader("Authorization") String authorization) { + if (authorization.endsWith("EXPIRED")) { + throw new InvalidTokenException("Expired"); + } + return new User(); + } + + @ExceptionHandler(InvalidTokenException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public void expired() { + } + + } + + public static class User { + + private String userid = "me"; + + public String getUserid() { + return this.userid; + } + + public void setUserid(String userid) { + this.userid = userid; + } + + } + +}