浏览代码

Add opaqueToken MockMvc Test Support

Fixes gh-7712
Josh Cummings 5 年之前
父节点
当前提交
e1fdb24b5d

+ 7 - 0
samples/boot/oauth2resourceserver-opaque/src/main/java/sample/OAuth2ResourceServerController.java

@@ -18,6 +18,8 @@ package sample;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.security.core.annotation.AuthenticationPrincipal;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
 
 
 /**
 /**
@@ -35,4 +37,9 @@ public class OAuth2ResourceServerController {
 	public String message() {
 	public String message() {
 		return "secret message";
 		return "secret message";
 	}
 	}
+
+	@PostMapping("/message")
+	public String createMessage(@RequestBody String message) {
+		return String.format("Message was created. Content: %s", message);
+	}
 }
 }

+ 3 - 1
samples/boot/oauth2resourceserver-opaque/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java

@@ -16,6 +16,7 @@
 package sample;
 package sample;
 
 
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpMethod;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@@ -36,7 +37,8 @@ public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfig
 		http
 		http
 			.authorizeRequests(authorizeRequests ->
 			.authorizeRequests(authorizeRequests ->
 				authorizeRequests
 				authorizeRequests
-					.mvcMatchers("/message/**").hasAuthority("SCOPE_message:read")
+					.antMatchers(HttpMethod.GET, "/message/**").hasAuthority("SCOPE_message:read")
+					.antMatchers(HttpMethod.POST, "/message/**").hasAuthority("SCOPE_message:write")
 					.anyRequest().authenticated()
 					.anyRequest().authenticated()
 			)
 			)
 			.oauth2ResourceServer(oauth2ResourceServer ->
 			.oauth2ResourceServer(oauth2ResourceServer ->

+ 92 - 0
samples/boot/oauth2resourceserver-opaque/src/test/java/sample/OAuth2ResourceServerControllerTests.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright 2002-2019 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 sample;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.opaqueToken;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Josh Cummings
+ * @since 5.3
+ */
+@RunWith(SpringRunner.class)
+@WebMvcTest(OAuth2ResourceServerController.class)
+public class OAuth2ResourceServerControllerTests {
+
+	@Autowired
+	MockMvc mvc;
+
+	@Test
+	public void indexGreetsAuthenticatedUser() throws Exception {
+		this.mvc.perform(get("/").with(opaqueToken().attribute("sub", "ch4mpy")))
+				.andExpect(content().string(is("Hello, ch4mpy!")));
+	}
+
+	@Test
+	public void messageCanBeReadWithScopeMessageReadAuthority() throws Exception {
+		this.mvc.perform(get("/message").with(opaqueToken().scopes("message:read")))
+				.andExpect(content().string(is("secret message")));
+
+		this.mvc.perform(get("/message")
+				.with(jwt().authorities(new SimpleGrantedAuthority(("SCOPE_message:read")))))
+				.andExpect(content().string(is("secret message")));
+	}
+
+	@Test
+	public void messageCanNotBeReadWithoutScopeMessageReadAuthority() throws Exception {
+		this.mvc.perform(get("/message").with(opaqueToken()))
+				.andExpect(status().isForbidden());
+	}
+
+	@Test
+	public void messageCanNotBeCreatedWithoutAnyScope() throws Exception {
+		this.mvc.perform(post("/message")
+				.content("Hello message")
+				.with(opaqueToken()))
+				.andExpect(status().isForbidden());
+	}
+
+	@Test
+	public void messageCanNotBeCreatedWithScopeMessageReadAuthority() throws Exception {
+		this.mvc.perform(post("/message")
+				.content("Hello message")
+				.with(opaqueToken().scopes("message:read")))
+				.andExpect(status().isForbidden());
+	}
+
+	@Test
+	public void messageCanBeCreatedWithScopeMessageWriteAuthority() throws Exception {
+		this.mvc.perform(post("/message")
+				.content("Hello message")
+				.with(opaqueToken().scopes("message:write")))
+				.andExpect(status().isOk())
+				.andExpect(content().string(is("Message was created. Content: Hello message")));
+	}
+}

+ 178 - 0
test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java

@@ -21,18 +21,24 @@ import java.nio.charset.StandardCharsets;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.Base64;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponse;
 
 
+import com.nimbusds.oauth2.sdk.util.StringUtils;
+
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.core.io.DefaultResourceLoader;
 import org.springframework.core.io.DefaultResourceLoader;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.Resource;
@@ -55,7 +61,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
 import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository;
 import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository;
 import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
 import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
 import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
 import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
@@ -63,8 +71,10 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
 import org.springframework.security.oauth2.core.oidc.user.OidcUser;
 import org.springframework.security.oauth2.core.oidc.user.OidcUser;
 import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
 import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
 import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
+import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
 import org.springframework.security.test.context.TestSecurityContextHolder;
 import org.springframework.security.test.context.TestSecurityContextHolder;
 import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
 import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
 import org.springframework.security.test.web.support.WebTestUtils;
 import org.springframework.security.test.web.support.WebTestUtils;
@@ -246,6 +256,34 @@ public final class SecurityMockMvcRequestPostProcessors {
 		return new JwtRequestPostProcessor();
 		return new JwtRequestPostProcessor();
 	}
 	}
 
 
+	/**
+	 * Establish a {@link SecurityContext} that has a
+	 * {@link BearerTokenAuthentication} for the
+	 * {@link Authentication} and a {@link OAuth2AuthenticatedPrincipal} for the
+	 * {@link Authentication#getPrincipal()}. All details are
+	 * declarative and do not require the token to be valid
+	 *
+	 * <p>
+	 * The support works by associating the authentication to the HttpServletRequest. To associate
+	 * the request to the SecurityContextHolder you need to ensure that the
+	 * SecurityContextPersistenceFilter is associated with the MockMvc instance. A few
+	 * ways to do this are:
+	 * </p>
+	 *
+	 * <ul>
+	 * <li>Invoking apply {@link SecurityMockMvcConfigurers#springSecurity()}</li>
+	 * <li>Adding Spring Security's FilterChainProxy to MockMvc</li>
+	 * <li>Manually adding {@link SecurityContextPersistenceFilter} to the MockMvc
+	 * instance may make sense when using MockMvcBuilders standaloneSetup</li>
+	 * </ul>
+	 *
+	 * @return the {@link OpaqueTokenRequestPostProcessor} for additional customization
+	 * @since 5.3
+	 */
+	public static OpaqueTokenRequestPostProcessor opaqueToken() {
+		return new OpaqueTokenRequestPostProcessor();
+	}
+
 	/**
 	/**
 	 * Establish a {@link SecurityContext} that uses the specified {@link Authentication}
 	 * Establish a {@link SecurityContext} that uses the specified {@link Authentication}
 	 * for the {@link Authentication#getPrincipal()} and a custom {@link UserDetails}. All
 	 * for the {@link Authentication#getPrincipal()} and a custom {@link UserDetails}. All
@@ -1070,6 +1108,146 @@ public final class SecurityMockMvcRequestPostProcessors {
 
 
 	}
 	}
 
 
+	/**
+	 * @author Josh Cummings
+	 * @since 5.3
+	 */
+	public final static class OpaqueTokenRequestPostProcessor implements RequestPostProcessor {
+		private final Map<String, Object> attributes = new HashMap<>();
+		private Converter<Map<String, Object>, Instant> expiresAtConverter =
+				attributes -> getInstant(attributes, "exp");
+		private Converter<Map<String, Object>, Instant> issuedAtConverter =
+				attributes -> getInstant(attributes, "iat");
+		private Converter<Map<String, Object>, Collection<GrantedAuthority>> authoritiesConverter =
+				attributes -> getAuthorities(attributes);
+
+		private OAuth2AuthenticatedPrincipal principal;
+
+		private OpaqueTokenRequestPostProcessor() {
+			this.attributes.put(OAuth2IntrospectionClaimNames.SUBJECT, "user");
+			this.attributes.put(OAuth2IntrospectionClaimNames.SCOPE, "read");
+		}
+
+		/**
+		 * Add the provided attribute to the resulting principal
+		 * @param name the attribute name
+		 * @param value the attribute value
+		 * @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
+		 */
+		public OpaqueTokenRequestPostProcessor attribute(String name, Object value) {
+			Assert.notNull(name, "name cannot be null");
+			this.attributes.put(name, value);
+			return this;
+		}
+
+		/**
+		 * Use the provided authorities in the resulting principal
+		 * @param authorities the authorities to use
+		 * @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
+		 */
+		public OpaqueTokenRequestPostProcessor authorities(Collection<GrantedAuthority> authorities) {
+			Assert.notNull(authorities, "authorities cannot be null");
+			this.authoritiesConverter = attributes -> authorities;
+			return this;
+		}
+
+		/**
+		 * Use the provided authorities in the resulting principal
+		 * @param authorities the authorities to use
+		 * @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
+		 */
+		public OpaqueTokenRequestPostProcessor authorities(GrantedAuthority... authorities) {
+			Assert.notNull(authorities, "authorities cannot be null");
+			this.authoritiesConverter = attributes -> Arrays.asList(authorities);
+			return this;
+		}
+
+		/**
+		 * Use the provided scopes as the authorities in the resulting principal
+		 * @param scopes the scopes to use
+		 * @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
+		 */
+		public OpaqueTokenRequestPostProcessor scopes(String... scopes) {
+			Assert.notNull(scopes, "scopes cannot be null");
+			this.authoritiesConverter = attributes -> getAuthorities(Arrays.asList(scopes));
+			return this;
+		}
+
+		/**
+		 * Use the provided principal
+		 *
+		 * Providing the principal takes precedence over
+		 * any authorities or attributes provided via {@link #attribute(String, Object)},
+		 * {@link #authorities} or {@link #scopes}.
+		 *
+		 * @param principal the principal to use
+		 * @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
+		 */
+		public OpaqueTokenRequestPostProcessor principal(OAuth2AuthenticatedPrincipal principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return this;
+		}
+
+		@Override
+		public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+			CsrfFilter.skipRequest(request);
+			OAuth2AuthenticatedPrincipal principal = getPrincipal();
+			OAuth2AccessToken accessToken = getOAuth2AccessToken(principal);
+			BearerTokenAuthentication token = new BearerTokenAuthentication
+					(principal, accessToken, principal.getAuthorities());
+			return new AuthenticationRequestPostProcessor(token).postProcessRequest(request);
+		}
+
+		private OAuth2AuthenticatedPrincipal getPrincipal() {
+			if (this.principal != null) {
+				return this.principal;
+			}
+
+			return new DefaultOAuth2AuthenticatedPrincipal
+					(this.attributes, this.authoritiesConverter.convert(this.attributes));
+		}
+
+		private Collection<GrantedAuthority> getAuthorities(Map<String, Object> attributes) {
+			Object scope = attributes.get(OAuth2IntrospectionClaimNames.SCOPE);
+			if (scope == null) {
+				return Collections.emptyList();
+			}
+			if (scope instanceof Collection) {
+				return getAuthorities((Collection) scope);
+			}
+			String scopes = scope.toString();
+			if (StringUtils.isBlank(scopes)) {
+				return Collections.emptyList();
+			}
+			return getAuthorities(Arrays.asList(scopes.split(" ")));
+		}
+
+		private Collection<GrantedAuthority> getAuthorities(Collection<?> scopes) {
+			return scopes.stream()
+					.map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
+					.collect(Collectors.toList());
+		}
+
+		private Instant getInstant(Map<String, Object> attributes, String name) {
+			Object value = attributes.get(name);
+			if (value == null) {
+				return null;
+			}
+			if (value instanceof Instant) {
+				return (Instant) value;
+			}
+			throw new IllegalArgumentException(name + " attribute must be of type Instant");
+		}
+
+		private OAuth2AccessToken getOAuth2AccessToken(OAuth2AuthenticatedPrincipal principal) {
+			Instant expiresAt = this.expiresAtConverter.convert(principal.getAttributes());
+			Instant issuedAt = this.issuedAtConverter.convert(principal.getAttributes());
+			return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
+					"token", issuedAt, expiresAt);
+		}
+	}
+
 	/**
 	/**
 	 * @author Josh Cummings
 	 * @author Josh Cummings
 	 * @since 5.3
 	 * @since 5.3

+ 154 - 0
test/src/test/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessorsOpaqueTokenTests.java

@@ -0,0 +1,154 @@
+/*
+ * Copyright 2002-2019 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.security.test.web.servlet.request;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+import static org.mockito.Mockito.mock;
+import static org.powermock.api.mockito.PowerMockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.opaqueToken;
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link SecurityMockMvcRequestPostProcessors#opaqueToken()}
+ *
+ * @author Josh Cummings
+ * @since 5.3
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+@WebAppConfiguration
+public class SecurityMockMvcRequestPostProcessorsOpaqueTokenTests {
+	@Autowired
+	WebApplicationContext context;
+
+	MockMvc mvc;
+
+	@Before
+	public void setup() {
+		// @formatter:off
+		this.mvc = MockMvcBuilders
+			.webAppContextSetup(this.context)
+			.apply(springSecurity())
+			.build();
+		// @formatter:on
+	}
+
+	@Test
+	public void opaqueTokenWhenUsingDefaultsThenProducesDefaultAuthentication()
+			throws Exception {
+
+		this.mvc.perform(get("/name").with(opaqueToken()))
+				.andExpect(content().string("user"));
+		this.mvc.perform(get("/admin/scopes").with(opaqueToken()))
+				.andExpect(status().isForbidden());
+	}
+
+	@Test
+	public void opaqueTokenWhenAuthoritiesSpecifiedThenGrantsAccess() throws Exception {
+		this.mvc.perform(get("/admin/scopes")
+				.with(opaqueToken().scopes("admin", "read")))
+				.andExpect(content().string("[\"SCOPE_admin\",\"SCOPE_read\"]"));
+	}
+
+	@Test
+	public void opaqueTokenWhenAttributeSpecifiedThenUserHasAttribute() throws Exception {
+		this.mvc.perform(get("/opaque-token/iss")
+				.with(opaqueToken().attribute("iss", "https://idp.example.org")))
+				.andExpect(content().string("https://idp.example.org"));
+	}
+
+	@Test
+	public void opaqueTokenWhenPrincipalSpecifiedThenAuthenticationHasPrincipal() throws Exception {
+		Collection authorities = Collections.singleton(new SimpleGrantedAuthority("SCOPE_read"));
+		OAuth2AuthenticatedPrincipal principal = mock(OAuth2AuthenticatedPrincipal.class);
+		when(principal.getName()).thenReturn("ben");
+		when(principal.getAuthorities()).thenReturn(authorities);
+
+		this.mvc.perform(get("/name").with(opaqueToken().principal(principal)))
+				.andExpect(content().string("ben"));
+	}
+
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class OAuth2LoginConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			http
+				.authorizeRequests()
+					.mvcMatchers("/admin/**").hasAuthority("SCOPE_admin")
+					.anyRequest().hasAuthority("SCOPE_read")
+					.and()
+				.oauth2ResourceServer()
+					.opaqueToken()
+						.introspector(mock(OpaqueTokenIntrospector.class));
+		}
+
+		@RestController
+		static class PrincipalController {
+			@GetMapping("/name")
+			String name(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
+				return principal.getName();
+			}
+
+			@GetMapping("/opaque-token/{attribute}")
+			String tokenAttribute(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal,
+					@PathVariable("attribute") String attribute) {
+
+				return principal.getAttribute(attribute);
+			}
+
+			@GetMapping("/admin/scopes")
+			List<String> scopes(@AuthenticationPrincipal(expression = "authorities")
+					Collection<GrantedAuthority> authorities) {
+
+				return authorities.stream().map(GrantedAuthority::getAuthority)
+						.collect(Collectors.toList());
+			}
+		}
+	}
+}