From 68ce62cc35e213667e562a151c7013fcc1dbeb0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Wacongne?= Date: Thu, 4 Apr 2019 12:47:32 +0200 Subject: [PATCH 1/2] Base branch for JWT secured components unit-testing framework --- oauth2-test/README.MD | 166 ++++++++++++++++++ ...ecurity-oauth2-resource-server-test.gradle | 16 ++ .../oauth2/support/AuthoritiesAndScopes.java | 119 +++++++++++++ .../oauth2/support/CollectionsSupport.java | 62 +++++++ .../DefaultOAuth2AuthenticationBuilder.java | 121 +++++++++++++ .../support/JwtAuthenticationBuilder.java | 56 ++++++ .../test/oauth2/support/JwtSupport.java | 66 +++++++ .../main/resources/META-INF/spring.factories | 3 + .../support/AuthoritiesAndScopesTest.java | 146 +++++++++++++++ .../support/CollectionsSupportTest.java | 93 ++++++++++ .../test/oauth2/support/JwtSupportTest.java | 149 ++++++++++++++++ oauth2-test/template.mf | 19 ++ .../OAuth2IntrospectionClaimNames.java | 11 +- ...es-boot-oauth2resourceserver-opaque.gradle | 1 + ...y-samples-boot-oauth2resourceserver.gradle | 1 + .../SecurityMockMvcRequestPostProcessors.java | 28 ++- 16 files changed, 1036 insertions(+), 21 deletions(-) create mode 100644 oauth2-test/README.MD create mode 100644 oauth2-test/spring-security-oauth2-resource-server-test.gradle create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopes.java create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/CollectionsSupport.java create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/DefaultOAuth2AuthenticationBuilder.java create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilder.java create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtSupport.java create mode 100644 oauth2-test/src/main/resources/META-INF/spring.factories create mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopesTest.java create mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/CollectionsSupportTest.java create mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtSupportTest.java create mode 100644 oauth2-test/template.mf diff --git a/oauth2-test/README.MD b/oauth2-test/README.MD new file mode 100644 index 00000000000..175073c083e --- /dev/null +++ b/oauth2-test/README.MD @@ -0,0 +1,166 @@ += Unit-testing OAuth2 secured `@Component`s +The aim here is offering the ability to configure unit tests security-context with OAuth2 authentication. + +Three different authentication implementations are supported: JWT, access-token and OidcId token. + +Each of those authentication type can be setup three different ways: + * request post-processors. Concise and easy to aggregate in functions of your own for most common scenari, but limited to servlets `@Controller`s testing + * annotations. Familiar for those used to `@WithMockUser` and allow testing any kind of `@Component` (`@Controller`s of course but also services) + * mutators (implement `WebTestClientConfigurer` and `MockServerConfigurer`). Provide with a flow API for reactive apps + +== Authorities related processing +Quite a few tricks are applied to merge authorities, roles and scopes: + * authorities with "SCOPE_" prefix are added to scope claim + * scopes from scope claim (with either "scope" or "scp" name) are added to authorities after being pre-pended with "SCOPE_" + * scope claim value can either be a single string with roles separated by spaces or a `Collection` implementation + * roles are pre-pended with "ROLE_" before being added to authorities + +== Request post-processors +MockMvc `.with(...)` method is an extension point for further request processing. +This feature is used here to setup SecurityContext with highly configurable authentications. + +=== `JwtRequestPostProcessor` +`org.springframework.security.test.oauth2.request.OAuth2MockMvcRequestPostProcessors.jwt()` +returns a post-processor which populates SecurityContext with a `JwtAuthenticationToken` you can tune +setting roles, scopes, authorities, authentication name, etc. + +Sample usage: +``` java +@RunWith(SpringRunner.class) +@WebMvcTest(OAuth2ResourceServerController.class) +public class OAuth2ResourceServerControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + JwtDecoder jwtDecoder; + + @Test + public void testRequestPostProcessor() throws Exception { + // No post-processor => no authorization => unauthorized + mockMvc.perform(get("/message")).andDo(print()).andExpect(status().isUnauthorized()); + + //Run with default Authentication: subject is "user" and sole authority is "USER" role + mockMvc.perform(get("/").with(jwt())) + .andExpect(status().isOk()) + .andExpect(content().string(is("Hello, user!"))); + + //Customize Authentication name + mockMvc.perform(get("/").with(jwt().name("ch4mpy"))) + .andExpect(status().isOk()) + .andExpect(content().string(is("Hello, ch4mpy!"))); + + //Customize roles, scopes and authorities + mockMvc.perform(get("/message").with(jwt().scope("message:read"))) + .andExpect(status().isOk()) + .andExpect(content().string(is("secret message"))); + + //Add custom claims + mockMvc.perform(get("/message") + .with(jwt() + .name("ch4mpy") + .authority("SCOPE_message:read") + .claim("iat", Instant.parse("2019-03-28T16:36:00Z")))) + .andExpect(status().isOk()) + .andExpect(content().string(is("secret message"))); + } +} +``` + + === `AccessTokenRequestPostProcessor` +`org.springframework.security.test.oauth2.request.OAuth2MockMvcRequestPostProcessors.accessToken()` +returns a post-processor which populates SecurityContext with a `OAuth2IntrospectionAuthenticationToken` you can tune +at wish. + +Usage is pretty similar to `JwtRequestPostProcessor`. + +=== `OidcIdTokenRequestPostProcessor` +Just as `AccessTokenRequestPostProcessor` or `JwtRequestPostProcessor`, call +`org.springframework.security.test.oauth2.request.OAuth2MockMvcRequestPostProcessors.oidcIdToken()` to configure an `OAuth2LoginAuthenticationToken`. + +== Annotations +With `@WithMockUser` as model, allow OAuth2 applications components unit testing + +=== `@WithMockJwt` +Offers the same SecurityContext configuration options as `JwtRequestPostProcessor`. + +Complete sample usage on a service: +``` java +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = MessageServiceTest.SecurityConfiguration.class) +public class MessageServiceTest { + + @Autowired + private MessageService messageService; + + @Test(expected = AuthenticationCredentialsNotFoundException.class) + public void greetWitoutMockJwt() { + messageService.getGreeting(); + } + + @Test + @WithMockJwt(name = "ch4mpy") + public void greetWithMockJwt() { + assertThat(messageService.getGreeting()).isEqualTo("Hello, ch4mpy!"); + } + + @Test(expected = AccessDeniedException.class) + @WithMockJwt + public void secretWithoutMessageReadScope() { + assertThat(messageService.getSecret()).isEqualTo("Secret message"); + } + + @Test + @WithMockJwt("SCOPE_message:read") // same as: + // @WithMockJwt(scopes = "message:read") + // @WithMockJwt(claims = @Attribute(name = "scope", value = "message:read", parser = StringSetParser.class)) + public void secretWithScopeMessageReadAuthority() { + assertThat(messageService.getSecret()).isEqualTo("Secret message"); + } + + interface MessageService { + + @PreAuthorize("authenticated") + String getGreeting(); + + @PreAuthorize("hasAuthority('SCOPE_message:read')") + String getSecret(); + } + + @Component + static final class MessageServiceImpl implements MessageService { + + @Override + public String getGreeting() { + return String.format("Hello, %s!", SecurityContextHolder.getContext().getAuthentication().getName()); + } + + @Override + public String getSecret() { + return "Secret message"; + } + + } + + @EnableGlobalMethodSecurity(prePostEnabled = true) + @ComponentScan(basePackageClasses = MessageService.class) + static class SecurityConfiguration { + + @Bean + JwtDecoder jwtDecoder() { + return null; + } + } +} +``` + +=== `@WithMockAccessToken` +Offers the same SecurityContext configuration options as `AccessTokenRequestPostProcessor`. + +=== `@WithMockOidcIdToken` +Offers the same SecurityContext configuration options as `OidcIdTokenRequestPostProcessor`. + +== Reactive API +Not much more to say here as usage is pretty similar to servlet request post-processors. +Just `import static org.springframework.security.test.oauth2.reactive.server.OAuth2SecurityMockServerConfigurers.*;` and you're all set. diff --git a/oauth2-test/spring-security-oauth2-resource-server-test.gradle b/oauth2-test/spring-security-oauth2-resource-server-test.gradle new file mode 100644 index 00000000000..aa8252c0bca --- /dev/null +++ b/oauth2-test/spring-security-oauth2-resource-server-test.gradle @@ -0,0 +1,16 @@ +apply plugin: 'io.spring.convention.spring-module' + +dependencies { + compile project(':spring-security-test') + compile project(':spring-security-oauth2-resource-server') + compile project(':spring-security-oauth2-jose') + compile project(':spring-security-oauth2-client') + + compile 'org.springframework:spring-test' + + testCompile project(':spring-security-config') + + optional 'org.springframework:spring-webflux' + + provided 'javax.servlet:javax.servlet-api' +} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopes.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopes.java new file mode 100644 index 00000000000..33bd997eb9e --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopes.java @@ -0,0 +1,119 @@ +/* + * 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.oauth2.support; + +import static org.springframework.security.test.oauth2.support.CollectionsSupport.putIfNotEmpty; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +/** + * Helps merging {@code authorities} and {@code scope}. + * + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + * + */ +final class AuthoritiesAndScopes { + public final Set authorities; + public final Set scopes; + public final Optional scopeAttributeName; + + private AuthoritiesAndScopes( + final Set authorities, + final Set scopes, + final Optional scopeAttributeName) { + this.authorities = Collections.unmodifiableSet(authorities); + this.scopes = Collections.unmodifiableSet(scopes); + this.scopeAttributeName = scopeAttributeName; + } + + /** + * Merges {@code authorities} and {@code scope}. Scopes are scanned for in: + *
    + *
  • scopes of course
  • + *
  • authorities ("SCOPE_" prefix)
  • + *
  • claims with keys "scope", "scp" and "scopes", first entry found being used and + * others ignored
  • + *
+ *

+ * All scopes are merged and set in claims, authorities and allScopes + *

+ * + * @param authorities authorities array (probably from an annotation + * {@code authorities()}) + * @param scopes scopes array (probably from an annotation {@code scopes()}) + * @param attributes attributes /!\ mutable /!\ map (probably from an + * annotation {@code attributes()} or {@code claims()}) + * @return a structure containing merged granted authorities and scopes + */ + public static AuthoritiesAndScopes get( + final Collection authorities, + final Collection scopes, + final Map attributes) { + + final Optional scopeAttributeName = attributes.keySet() + .stream() + .filter(k -> "scope".equals(k) || "scp".equals(k) || "scopes".equals(k)) + .sorted() + .findFirst(); + + final Optional scopeAttribute = scopeAttributeName.map(attributes::get); + + final boolean scopeIsString = scopeAttribute.map(s -> s instanceof String).orElse(false); + + final Stream attributesScopes = scopeAttribute.map(s -> { + if (scopeIsString) { + return Stream.of(scopeAttribute.get().toString().split(" ")); + } + return AuthoritiesAndScopes.asStringStream(scopeAttribute.get()); + }).orElse(Stream.empty()); + + final Stream authoritiesScopes = + authorities.stream().filter(a -> a.startsWith("SCOPE_")).map(a -> a.substring(6)); + + final Set allScopes = Stream.concat(scopes.stream(), Stream.concat(authoritiesScopes, attributesScopes)) + .collect(Collectors.toSet()); + + if (scopeIsString) { + putIfNotEmpty( + scopeAttributeName.orElse("scope"), + allScopes.stream().collect(Collectors.joining(" ")), + attributes); + } else { + putIfNotEmpty(scopeAttributeName.orElse("scope"), allScopes, attributes); + } + + final Set allAuthorities = + Stream.concat(authorities.stream(), allScopes.stream().map(scope -> "SCOPE_" + scope)) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + return new AuthoritiesAndScopes(allAuthorities, allScopes, scopeAttributeName); + } + + @SuppressWarnings("unchecked") + private static Stream asStringStream(final Object col) { + return col == null ? Stream.empty() : ((Collection) col).stream(); + } +} \ No newline at end of file diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/CollectionsSupport.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/CollectionsSupport.java new file mode 100644 index 00000000000..40f1e3ff36d --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/CollectionsSupport.java @@ -0,0 +1,62 @@ +/* + * 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.oauth2.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.util.StringUtils; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class CollectionsSupport { + + public static String nullIfEmpty(final String str) { + return StringUtils.isEmpty(str) ? null : str; + } + + public static final Map + putIfNotEmpty(final String key, final String value, final Map map) { + if (value != null && !value.isEmpty()) { + map.put(key, value); + } + return map; + } + + public static final Map + putIfNotEmpty(final String key, final Collection value, final Map map) { + if (value != null && !value.isEmpty()) { + map.put(key, value); + } + return map; + } + + public static final Set asSet(final String... values) { + return values.length == 0 ? Collections.emptySet() : Stream.of(values).collect(Collectors.toSet()); + } + + public static final List asList(final String... values) { + return values.length == 0 ? Collections.emptyList() : Stream.of(values).collect(Collectors.toList()); + } + +} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/DefaultOAuth2AuthenticationBuilder.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/DefaultOAuth2AuthenticationBuilder.java new file mode 100644 index 00000000000..3249a212932 --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/DefaultOAuth2AuthenticationBuilder.java @@ -0,0 +1,121 @@ +/* + * 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.oauth2.support; + +import static org.springframework.security.test.oauth2.support.CollectionsSupport.asSet; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public abstract class DefaultOAuth2AuthenticationBuilder> { + + public static final String DEFAULT_AUTH_NAME = "user"; + + public static final String[] DEFAULT_AUTHORITIES = { "ROLE_USER" }; + + private static final String ROLE_PREFIX = "ROLE_"; + + private static final String SCOPE_PREFIX = "SCOPE_"; + + protected String name; + + protected final Set authorities; + + private boolean isAuthoritiesSet = false; + + protected final Set scopes = new HashSet<>(); + + protected final Map attributes = new HashMap<>(); + + public DefaultOAuth2AuthenticationBuilder(final String defaultName, final String[] defaultAuthorities) { + this.name = defaultName; + this.authorities = new HashSet<>(asSet(defaultAuthorities)); + } + + public DefaultOAuth2AuthenticationBuilder() { + this(DEFAULT_AUTH_NAME, DEFAULT_AUTHORITIES); + } + + public T name(final String name) { + this.name = name; + return downCast(); + } + + public T authority(final String authority) { + assert (authority != null); + if (!this.isAuthoritiesSet) { + this.authorities.clear(); + this.isAuthoritiesSet = true; + } + this.authorities.add(authority); + if (authority.startsWith(SCOPE_PREFIX)) { + this.scopes.add(authority.substring(SCOPE_PREFIX.length())); + } + return downCast(); + } + + public T authorities(final String... authorities) { + Stream.of(authorities).forEach(this::authority); + return downCast(); + } + + public T role(final String role) { + assert (role != null); + assert (!role.startsWith(ROLE_PREFIX)); + return authority(ROLE_PREFIX + role); + } + + public T roles(final String... roles) { + Stream.of(roles).forEach(this::role); + return downCast(); + } + + public T scope(final String role) { + assert (role != null); + assert (!role.startsWith(SCOPE_PREFIX)); + return authority(SCOPE_PREFIX + role); + } + + public T scopes(final String... scope) { + Stream.of(scope).forEach(this::scope); + return downCast(); + } + + public T attributes(final Map attributes) { + assert (attributes != null); + attributes.entrySet().stream().forEach(e -> this.attribute(e.getKey(), e.getValue())); + return downCast(); + } + + public T attribute(final String name, final Object value) { + assert (name != null); + this.attributes.put(name, value); + return downCast(); + } + + @SuppressWarnings("unchecked") + protected T downCast() { + return (T) this; + } + +} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilder.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilder.java new file mode 100644 index 00000000000..66ff8b65f32 --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilder.java @@ -0,0 +1,56 @@ +/* + * 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.oauth2.support; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class JwtAuthenticationBuilder> + extends + DefaultOAuth2AuthenticationBuilder { + + protected Map headers = JwtSupport.DEFAULT_HEADERS; + + private boolean isHeadersSet = false; + + public T claims(final Map claims) { + return attributes(claims); + } + + public T claim(final String name, final Object value) { + return attribute(name, value); + } + + public T headers(final Map headers) { + headers.entrySet().stream().forEach(e -> this.header(e.getKey(), e.getValue())); + return downCast(); + } + + public T header(final String name, final Object value) { + assert (name != null); + if (this.isHeadersSet == false) { + this.headers = new HashMap<>(); + this.isHeadersSet = true; + } + this.headers.put(name, value); + return downCast(); + } + +} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtSupport.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtSupport.java new file mode 100644 index 00000000000..544c4681bfa --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtSupport.java @@ -0,0 +1,66 @@ +/* + * 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.oauth2.support; + +import static org.springframework.security.test.oauth2.support.CollectionsSupport.putIfNotEmpty; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class JwtSupport { + public static final String DEFAULT_TOKEN_VALUE = "test.jwt.value"; + public static final String DEFAULT_HEADER_NAME = "test-header"; + public static final String DEFAULT_HEADER_VALUE = "abracadabra"; + public static final Map DEFAULT_HEADERS = + Collections.singletonMap(DEFAULT_HEADER_NAME, DEFAULT_HEADER_VALUE); + + public static JwtAuthenticationToken authentication( + final String name, + final Collection authorities, + final Collection scopes, + final Map claims, + final Map headers) { + final Map postrPocessedClaims = new HashMap<>(claims); + if (claims.containsKey(JwtClaimNames.SUB)) { + throw new RuntimeException(JwtClaimNames.SUB + " claim is not configurable (forced to \"name\")"); + } else { + putIfNotEmpty(JwtClaimNames.SUB, name, postrPocessedClaims); + } + + final AuthoritiesAndScopes authoritiesAndScopes = + AuthoritiesAndScopes.get(authorities, scopes, postrPocessedClaims); + + return new JwtAuthenticationToken( + new Jwt( + DEFAULT_TOKEN_VALUE, + (Instant) postrPocessedClaims.get(JwtClaimNames.IAT), + (Instant) postrPocessedClaims.get(JwtClaimNames.EXP), + headers, + postrPocessedClaims), + authoritiesAndScopes.authorities); + } +} diff --git a/oauth2-test/src/main/resources/META-INF/spring.factories b/oauth2-test/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..48be9c54768 --- /dev/null +++ b/oauth2-test/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.test.context.TestExecutionListener = \ + org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener,\ + org.springframework.security.test.context.support.ReactorContextTestExecutionListener diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopesTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopesTest.java new file mode 100644 index 00000000000..73919006450 --- /dev/null +++ b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopesTest.java @@ -0,0 +1,146 @@ +/* + * 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.oauth2.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.oauth2.support.CollectionsSupport.asSet; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.junit.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class AuthoritiesAndScopesTest { + + @Test + public void testScopeCollectionAttribute() { + final Map attributes = new HashMap<>(); + attributes.put("scope", Collections.singleton("c")); + final AuthoritiesAndScopes actual = + AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); + + assertThat(actual.authorities).hasSize(4); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_c")); + + assertThat(actual.scopes).hasSize(3); + assertThat(actual.scopes).contains("a"); + assertThat(actual.scopes).contains("b"); + assertThat(actual.scopes).contains("c"); + + assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scope")); + } + + @Test + public void testScopeStringAttribute() { + final Map attributes = new HashMap<>(); + attributes.put("scope", "c"); + final AuthoritiesAndScopes actual = + AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); + + assertThat(actual.authorities).hasSize(4); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_c")); + + assertThat(actual.scopes).hasSize(3); + assertThat(actual.scopes).contains("a"); + assertThat(actual.scopes).contains("b"); + assertThat(actual.scopes).contains("c"); + + assertThat(attributes.get("scope")).isEqualTo("a b c"); + + assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scope")); + } + + @Test + public void testScpAttribute() { + final Map attributes = new HashMap<>(); + attributes.put("scp", Collections.singleton("c")); + final AuthoritiesAndScopes actual = + AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); + + assertThat(actual.authorities).hasSize(4); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_c")); + + assertThat(actual.scopes).hasSize(3); + assertThat(actual.scopes).contains("a"); + assertThat(actual.scopes).contains("b"); + assertThat(actual.scopes).contains("c"); + + assertThat(attributes.get("scp")).isEqualTo(actual.scopes); + + assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scp")); + } + + @Test + public void testScopesAttribute() { + final Map attributes = new HashMap<>(); + attributes.put("scopes", Collections.singleton("c")); + final AuthoritiesAndScopes actual = + AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); + + assertThat(actual.authorities).hasSize(4); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_c")); + + assertThat(actual.scopes).hasSize(3); + assertThat(actual.scopes).contains("a"); + assertThat(actual.scopes).contains("b"); + assertThat(actual.scopes).contains("c"); + + assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scopes")); + } + + @Test + public void testAttributeCollision() { + final Map attributes = new HashMap<>(3); + attributes.put("scopes", Collections.singleton("c")); + attributes.put("scope", Collections.singleton("d")); + attributes.put("scp", Collections.singleton("e")); + final AuthoritiesAndScopes actual = + AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); + + assertThat(actual.authorities).hasSize(4); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); + assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_d")); + + assertThat(actual.scopes).hasSize(3); + assertThat(actual.scopes).contains("a"); + assertThat(actual.scopes).contains("b"); + assertThat(actual.scopes).contains("d"); + + assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scope")); + } + +} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/CollectionsSupportTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/CollectionsSupportTest.java new file mode 100644 index 00000000000..f8e5219dfe0 --- /dev/null +++ b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/CollectionsSupportTest.java @@ -0,0 +1,93 @@ +/* + * 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.oauth2.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.oauth2.support.CollectionsSupport.nullIfEmpty; +import static org.springframework.security.test.oauth2.support.CollectionsSupport.putIfNotEmpty; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.junit.Test; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class CollectionsSupportTest { + + @Test + public void nullIfEmptyReturnsNullForNullString() { + assertThat(nullIfEmpty(null)).isNull(); + } + + @Test + public void nullIfEmptyReturnsNullForEmptyString() { + assertThat(nullIfEmpty("")).isNull(); + } + + @Test + public void nullIfEmptyReturnsNonNullForSpace() { + assertThat(nullIfEmpty(" ")).isEqualTo(" "); + } + + @Test + public void nullIfEmptyReturnsNonNullForToto() { + assertThat(nullIfEmpty("Toto")).isEqualTo("Toto"); + } + + @Test + public void putIfNotEmptyDoesNothingForNullString() { + assertThat(putIfNotEmpty("foo", (String) null, new HashMap<>())).isEmpty(); + } + + @Test + public void putIfNotEmptyDoesNothingForEmptyString() { + assertThat(putIfNotEmpty("foo", "", new HashMap<>())).isEmpty(); + } + + @Test + public void putIfNotEmptyInsertsSpace() { + assertThat(putIfNotEmpty("foo", " ", new HashMap<>()).get("foo")).isEqualTo(" "); + } + + @Test + public void putIfNotEmptyInsertsToto() { + assertThat(putIfNotEmpty("foo", "Toto", new HashMap<>()).get("foo")).isEqualTo("Toto"); + } + + @Test + public void putIfNotEmptyDoesNothingForNullList() { + assertThat(putIfNotEmpty("foo", (List) null, new HashMap<>())).isEmpty(); + } + + @Test + public void putIfNotEmptyDoesNothingForEmptyList() { + assertThat(putIfNotEmpty("foo", Collections.emptyList(), new HashMap<>())).isEmpty(); + } + + @Test + public void putIfNotEmptyInsertsNonEmptyList() { + @SuppressWarnings("unchecked") + final List actual = + (List) (putIfNotEmpty("foo", Collections.singletonList("Toto"), new HashMap<>()).get("foo")); + assertThat(actual).hasSize(1); + assertThat(actual).contains("Toto"); + } + +} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtSupportTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtSupportTest.java new file mode 100644 index 00000000000..c39e440a1d9 --- /dev/null +++ b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtSupportTest.java @@ -0,0 +1,149 @@ +/* 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.oauth2.support; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class JwtSupportTest { + + @Test + public void authenticationNameAndTokenSubjectClaimAreSet() { + final JwtAuthenticationToken actual = JwtSupport.authentication( + "ch4mpy", + Collections.emptySet(), + Collections.emptySet(), + Collections.emptyMap(), + JwtSupport.DEFAULT_HEADERS); + + assertThat(actual.getName()).isEqualTo("ch4mpy"); + assertThat(actual.getTokenAttributes().get(JwtClaimNames.SUB)).isEqualTo("ch4mpy"); + } + + @Test + public void tokenIatIsSetFromClaims() { + final Map claims = + Collections.singletonMap(JwtClaimNames.IAT, Instant.parse("2019-03-21T13:52:25Z")); + final Jwt actual = + JwtSupport + .authentication( + "ch4mpy", + Collections.emptySet(), + Collections.emptySet(), + claims, + JwtSupport.DEFAULT_HEADERS) + .getToken(); + + assertThat(actual.getIssuedAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + assertThat(actual.getExpiresAt()).isNull(); + assertThat(actual.getClaimAsInstant(JwtClaimNames.IAT)).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + assertThat(actual.getClaimAsInstant(JwtClaimNames.EXP)).isNull(); + } + + @Test + public void tokenExpIsSetFromClaims() { + final Jwt actual = + JwtSupport + .authentication( + "ch4mpy", + Collections.emptySet(), + Collections.emptySet(), + Collections.singletonMap(JwtClaimNames.EXP, Instant.parse("2019-03-21T13:52:25Z")), + JwtSupport.DEFAULT_HEADERS) + .getToken(); + + assertThat(actual.getIssuedAt()).isNull(); + assertThat(actual.getExpiresAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + assertThat(actual.getClaimAsInstant(JwtClaimNames.IAT)).isNull(); + assertThat(actual.getClaimAsInstant(JwtClaimNames.EXP)).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + } + + @Test + public void scopesCollectionAndScopeClaimAreAddedToAuthorities() { + final JwtAuthenticationToken actual = JwtSupport.authentication( + "ch4mpy", + Collections.singleton("TEST_AUTHORITY"), + Collections.singleton("scope:collection"), + Collections.singletonMap("scope", Collections.singleton("scope:claim")), + JwtSupport.DEFAULT_HEADERS); + + assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( + new SimpleGrantedAuthority("TEST_AUTHORITY"), + new SimpleGrantedAuthority("SCOPE_scope:collection"), + new SimpleGrantedAuthority("SCOPE_scope:claim")); + } + + @SuppressWarnings("unchecked") + @Test + public void scopesCollectionAndScopeAuthoritiesAreAddedToScopeClaim() { + final JwtAuthenticationToken actual = JwtSupport.authentication( + "ch4mpy", + Collections.singleton("SCOPE_scope:authority"), + Collections.singleton("scope:collection"), + Collections.singletonMap("scope", Collections.singleton("scope:claim")), + JwtSupport.DEFAULT_HEADERS); + + assertThat((Collection) actual.getToken().getClaims().get("scope")) + .containsExactlyInAnyOrder("scope:authority", "scope:collection", "scope:claim"); + } + + /** + * "scp" is the an usual name for "scope" claim + */ + + @Test + public void scopesCollectionAndScpClaimAreAddedToAuthorities() { + final JwtAuthenticationToken actual = JwtSupport.authentication( + "ch4mpy", + Collections.singleton("TEST_AUTHORITY"), + Collections.singleton("scope:collection"), + Collections.singletonMap("scp", Collections.singleton("scope:claim")), + JwtSupport.DEFAULT_HEADERS); + + assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( + new SimpleGrantedAuthority("TEST_AUTHORITY"), + new SimpleGrantedAuthority("SCOPE_scope:collection"), + new SimpleGrantedAuthority("SCOPE_scope:claim")); + } + + @SuppressWarnings("unchecked") + @Test + public void scopesCollectionAndScopeAuthoritiesAreAddedToScpClaim() { + final JwtAuthenticationToken actual = JwtSupport.authentication( + "ch4mpy", + Collections.singleton("SCOPE_scope:authority"), + Collections.singleton("scope:collection"), + Collections.singletonMap("scp", Collections.singleton("scope:claim")), + JwtSupport.DEFAULT_HEADERS); + + assertThat((Collection) actual.getToken().getClaims().get("scp")) + .containsExactlyInAnyOrder("scope:authority", "scope:collection", "scope:claim"); + } + +} diff --git a/oauth2-test/template.mf b/oauth2-test/template.mf new file mode 100644 index 00000000000..01a76852085 --- /dev/null +++ b/oauth2-test/template.mf @@ -0,0 +1,19 @@ +Implementation-Title: org.springframework.security.oauth2.test +Implementation-Version: ${version} +Bundle-SymbolicName: org.springframework.security.oauth2.test +Bundle-Name: Spring Security OAuth2 Test +Bundle-Vendor: SpringSource +Bundle-Version: ${version} +Bundle-ManifestVersion: 2 +Ignored-Existing-Headers: + Import-Package, + Export-Package +Import-Template: + org.apache.commons.logging.*;version="${cloggingRange}", + org.springframework.security.core.*;version="${secRange}", + org.springframework.security.authentication.*;version="${secRange}", + org.springframework.security.oauth2.server.resource.*;version="${secRange}", + org.springframework.security.web.*;version="${secRange}", + org.springframework.beans.factory;version="${springRange}", + org.springframework.util;version="${springRange}", + javax.servlet.*;version="0" diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionClaimNames.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionClaimNames.java index 178f08f33e4..f5d3095b39a 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionClaimNames.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OAuth2IntrospectionClaimNames.java @@ -17,12 +17,13 @@ /** * The names of the "Introspection Claims" defined by an - * Introspection Response. + * Introspection + * Response. * * @author Josh Cummings * @since 5.2 */ -interface OAuth2IntrospectionClaimNames { +public interface OAuth2IntrospectionClaimNames { /** * {@code active} - Indicator whether or not the token is currently active @@ -40,7 +41,8 @@ interface OAuth2IntrospectionClaimNames { String CLIENT_ID = "client_id"; /** - * {@code username} - A human-readable identifier for the resource owner that authorized the token + * {@code username} - A human-readable identifier for the resource owner that + * authorized the token */ String USERNAME = "username"; @@ -65,7 +67,8 @@ interface OAuth2IntrospectionClaimNames { String NOT_BEFORE = "nbf"; /** - * {@code sub} - Usually a machine-readable identifier of the resource owner who authorized the token + * {@code sub} - Usually a machine-readable identifier of the resource owner who + * authorized the token */ String SUBJECT = "sub"; diff --git a/samples/boot/oauth2resourceserver-opaque/spring-security-samples-boot-oauth2resourceserver-opaque.gradle b/samples/boot/oauth2resourceserver-opaque/spring-security-samples-boot-oauth2resourceserver-opaque.gradle index 9074842b18a..0905d5b64d6 100644 --- a/samples/boot/oauth2resourceserver-opaque/spring-security-samples-boot-oauth2resourceserver-opaque.gradle +++ b/samples/boot/oauth2resourceserver-opaque/spring-security-samples-boot-oauth2resourceserver-opaque.gradle @@ -10,5 +10,6 @@ dependencies { compile 'com.squareup.okhttp3:mockwebserver' testCompile project(':spring-security-test') + testCompile project(':spring-security-oauth2-resource-server-test') testCompile 'org.springframework.boot:spring-boot-starter-test' } diff --git a/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle b/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle index 2135bb0af66..1ba15ac36cf 100644 --- a/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle +++ b/samples/boot/oauth2resourceserver/spring-security-samples-boot-oauth2resourceserver.gradle @@ -9,5 +9,6 @@ dependencies { compile 'com.squareup.okhttp3:mockwebserver' testCompile project(':spring-security-test') + testCompile project(':spring-security-oauth2-resource-server-test') testCompile 'org.springframework.boot:spring-boot-starter-test' } diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java index ee13abffa4b..2b2d2eea639 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java @@ -555,7 +555,7 @@ private static String md5Hex(String a2) { * Support class for {@link RequestPostProcessor}'s that establish a Spring Security * context */ - private static abstract class SecurityContextRequestPostProcessorSupport { + public static class SecurityContextRequestPostProcessorSupport { /** * Saves the specified {@link Authentication} into an empty @@ -564,7 +564,7 @@ private static abstract class SecurityContextRequestPostProcessorSupport { * @param authentication the {@link Authentication} to save * @param request the {@link HttpServletRequest} to use */ - final void save(Authentication authentication, HttpServletRequest request) { + public static final void save(Authentication authentication, HttpServletRequest request) { SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(authentication); save(securityContext, request); @@ -576,7 +576,7 @@ final void save(Authentication authentication, HttpServletRequest request) { * @param securityContext the {@link SecurityContext} to save * @param request the {@link HttpServletRequest} to use */ - final void save(SecurityContext securityContext, HttpServletRequest request) { + public static final void save(SecurityContext securityContext, HttpServletRequest request) { SecurityContextRepository securityContextRepository = WebTestUtils .getSecurityContextRepository(request); boolean isTestRepository = securityContextRepository instanceof TestSecurityContextRepository; @@ -653,14 +653,13 @@ private static SecurityContext getContext(HttpServletRequest request) { * @author Rob Winch * @since 4.0 */ - private final static class TestSecurityContextHolderPostProcessor extends - SecurityContextRequestPostProcessorSupport implements RequestPostProcessor { + private final static class TestSecurityContextHolderPostProcessor implements RequestPostProcessor { private SecurityContext EMPTY = SecurityContextHolder.createEmptyContext(); @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { // TestSecurityContextHolder is only a default value - SecurityContext existingContext = TestSecurityContextRepository + SecurityContext existingContext = SecurityContextRequestPostProcessorSupport.TestSecurityContextRepository .getContext(request); if (existingContext != null) { return request; @@ -668,7 +667,7 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) SecurityContext context = TestSecurityContextHolder.getContext(); if (!this.EMPTY.equals(context)) { - save(context, request); + SecurityContextRequestPostProcessorSupport.save(context, request); } return request; @@ -682,8 +681,7 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) * @author Rob Winch * @since 4.0 */ - private final static class SecurityContextRequestPostProcessor extends - SecurityContextRequestPostProcessorSupport implements RequestPostProcessor { + private final static class SecurityContextRequestPostProcessor implements RequestPostProcessor { private final SecurityContext securityContext; @@ -693,7 +691,7 @@ private SecurityContextRequestPostProcessor(SecurityContext securityContext) { @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - save(this.securityContext, request); + SecurityContextRequestPostProcessorSupport.save(this.securityContext, request); return request; } } @@ -706,8 +704,7 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) * @since 4.0 * */ - private final static class AuthenticationRequestPostProcessor extends - SecurityContextRequestPostProcessorSupport implements RequestPostProcessor { + private final static class AuthenticationRequestPostProcessor implements RequestPostProcessor { private final Authentication authentication; private AuthenticationRequestPostProcessor(Authentication authentication) { @@ -716,9 +713,7 @@ private AuthenticationRequestPostProcessor(Authentication authentication) { @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(this.authentication); - save(this.authentication, request); + SecurityContextRequestPostProcessorSupport.save(this.authentication, request); return request; } } @@ -866,8 +861,7 @@ private User createUser() { } } - private static class AnonymousRequestPostProcessor extends - SecurityContextRequestPostProcessorSupport implements RequestPostProcessor { + private static class AnonymousRequestPostProcessor implements RequestPostProcessor { private AuthenticationRequestPostProcessor delegate = new AuthenticationRequestPostProcessor( new AnonymousAuthenticationToken("key", "anonymous", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"))); From 009d754e2ce889334b5492e42d7aa8b172dad8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Wacongne?= Date: Thu, 4 Apr 2019 12:55:10 +0200 Subject: [PATCH 2/2] mockJwt() request post-processor to configure a JwtAuthenticationToken in MockMvc SecurityContext --- .../request/JwtRequestPostProcessor.java | 37 +++ .../OAuth2MockMvcRequestPostProcessors.java | 34 +++ .../AbstractAuthenticationBuilder.java | 188 ++++++++++++ .../AccessTokenAuthenticationBuilder.java | 91 ++++++ .../oauth2/support/AuthoritiesAndScopes.java | 119 -------- .../DefaultOAuth2AuthenticationBuilder.java | 121 -------- .../support/JwtAuthenticationBuilder.java | 90 +++++- .../test/oauth2/support/JwtSupport.java | 66 ---- .../OidcIdTokenAuthenticationBuilder.java | 286 ++++++++++++++++++ .../AbstractRequestPostProcessorTest.java | 59 ++++ .../request/JwtRequestPostProcessorTest.java | 52 ++++ .../AccessTokenAuthenticationBuilderTest.java | 116 +++++++ .../support/AuthoritiesAndScopesTest.java | 146 --------- .../support/JwtAuthenticationBuilderTest.java | 169 +++++++++++ .../test/oauth2/support/JwtSupportTest.java | 149 --------- .../oauth2/support/OidcIdSupportTest.java | 99 ++++++ 16 files changed, 1209 insertions(+), 613 deletions(-) create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/request/JwtRequestPostProcessor.java create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/request/OAuth2MockMvcRequestPostProcessors.java create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AbstractAuthenticationBuilder.java create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AccessTokenAuthenticationBuilder.java delete mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopes.java delete mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/DefaultOAuth2AuthenticationBuilder.java delete mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtSupport.java create mode 100644 oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/OidcIdTokenAuthenticationBuilder.java create mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/request/AbstractRequestPostProcessorTest.java create mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/request/JwtRequestPostProcessorTest.java create mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AccessTokenAuthenticationBuilderTest.java delete mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopesTest.java create mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilderTest.java delete mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtSupportTest.java create mode 100644 oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/OidcIdSupportTest.java diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/request/JwtRequestPostProcessor.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/request/JwtRequestPostProcessor.java new file mode 100644 index 00000000000..566fdbe087b --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/request/JwtRequestPostProcessor.java @@ -0,0 +1,37 @@ +/* + * 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.oauth2.request; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.test.oauth2.support.JwtAuthenticationBuilder; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.SecurityContextRequestPostProcessorSupport; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class JwtRequestPostProcessor extends JwtAuthenticationBuilder + implements + RequestPostProcessor { + + @Override + public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) { + SecurityContextRequestPostProcessorSupport.save(build(), request); + return request; + } + +} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/request/OAuth2MockMvcRequestPostProcessors.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/request/OAuth2MockMvcRequestPostProcessors.java new file mode 100644 index 00000000000..e59617a5f6c --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/request/OAuth2MockMvcRequestPostProcessors.java @@ -0,0 +1,34 @@ +/* + * 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.oauth2.request; + +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public final class OAuth2MockMvcRequestPostProcessors { + + public static JwtRequestPostProcessor mockJwt() { + return new JwtRequestPostProcessor(); + } + + public static JwtRequestPostProcessor mockJwt(final Jwt jwt) { + return mockJwt().jwt(jwt); + } + +} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AbstractAuthenticationBuilder.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AbstractAuthenticationBuilder.java new file mode 100644 index 00000000000..a215f1e5619 --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AbstractAuthenticationBuilder.java @@ -0,0 +1,188 @@ +/* + * 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.oauth2.support; + +import static org.springframework.security.test.oauth2.support.CollectionsSupport.asSet; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public abstract class AbstractAuthenticationBuilder> { + + public static final String DEFAULT_AUTH_NAME = "user"; + + public static final String[] DEFAULT_AUTHORITIES = { "ROLE_USER" }; + + public static final String DEFAULT_SCOPE_ATTRIBUTE_NAME = "scope"; + + private static final String ROLE_PREFIX = "ROLE_"; + + private static final String SCOPE_PREFIX = "SCOPE_"; + + protected String name; + + protected final Set authorities; + + private boolean isAuthoritiesSet = false; + + protected final Map claims = new HashMap<>(); + + protected String scopeClaimName = DEFAULT_SCOPE_ATTRIBUTE_NAME; + + public AbstractAuthenticationBuilder(final String defaultName, final String[] defaultAuthorities) { + this.name = defaultName; + this.authorities = new HashSet<>(asSet(defaultAuthorities)); + } + + public AbstractAuthenticationBuilder() { + this(DEFAULT_AUTH_NAME, DEFAULT_AUTHORITIES); + } + + public T name(final String name) { + this.name = name; + return downCast(); + } + + public T authority(final String authority) { + assert (authority != null); + if (!this.isAuthoritiesSet) { + this.authorities.clear(); + this.isAuthoritiesSet = true; + } + this.authorities.add(authority); + return downCast(); + } + + public T authorities(final Stream authorities) { + this.authorities.clear(); + authorities.forEach(this::authority); + return downCast(); + } + + public T authorities(final String... authorities) { + return authorities(Stream.of(authorities)); + } + + public T authorities(final Collection authorities) { + return authorities(authorities.stream()); + } + + public T role(final String role) { + assert (role != null); + assert (!role.startsWith(ROLE_PREFIX)); + return authority(ROLE_PREFIX + role); + } + + public T roles(final Stream roles) { + this.authorities.removeIf(a -> a.startsWith(ROLE_PREFIX)); + roles.forEach(this::role); + return downCast(); + } + + public T roles(final String... roles) { + return roles(Stream.of(roles)); + } + + public T roles(final Collection roles) { + return roles(roles.stream()); + } + + public T scope(final String scope) { + assert (scope != null); + assert (!scope.startsWith(SCOPE_PREFIX)); + return authority(SCOPE_PREFIX + scope); + } + + public T scopes(final Stream scopes) { + this.authorities.removeIf(a -> a.startsWith(SCOPE_PREFIX)); + scopes.forEach(this::scope); + return downCast(); + } + + public T scopes(final String... scopes) { + return scopes(Stream.of(scopes)); + } + + public T scopes(final Collection scopes) { + return scopes(scopes.stream()); + } + + public T claim(final String name, final Object value) { + assert (name != null); + this.claims.put(name, value); + return downCast(); + } + + public T claims(final Map attributes) { + assert (attributes != null); + this.claims.clear(); + attributes.entrySet().stream().forEach(e -> this.claim(e.getKey(), e.getValue())); + return downCast(); + } + + public T scopesClaimName(final String name) { + this.scopeClaimName = name; + return downCast(); + } + + public Set getAllAuthorities() { + return Stream.concat(authorities.stream(), getScopeAttributeStream().map(scope -> "SCOPE_" + scope)) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + } + + public Set getAllScopes() { + return Stream + .concat( + getScopeAttributeStream(), + authorities.stream().filter(a -> a.startsWith("SCOPE_")).map(a -> a.substring(6))) + .collect(Collectors.toSet()); + } + + @SuppressWarnings("unchecked") + protected T downCast() { + return (T) this; + } + + private Stream getScopeAttributeStream() { + final Object scopeAttribute = claims.get(scopeClaimName); + if (scopeAttribute == null) { + return Stream.empty(); + } + + if (scopeAttribute instanceof Collection) { + return ((Collection) scopeAttribute).stream().map(Object::toString); + } else if (scopeAttribute instanceof String) { + return Stream.of(scopeAttribute.toString().split(" ")); + } else { + throw new RuntimeException( + "Only Collection or String are supported types for scopes. Was " + + scopeAttribute.getClass().getName()); + } + } + +} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AccessTokenAuthenticationBuilder.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AccessTokenAuthenticationBuilder.java new file mode 100644 index 00000000000..3b68fb45814 --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AccessTokenAuthenticationBuilder.java @@ -0,0 +1,91 @@ +/* + * 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.oauth2.support; + +import static org.springframework.security.test.oauth2.support.CollectionsSupport.nullIfEmpty; +import static org.springframework.security.test.oauth2.support.CollectionsSupport.putIfNotEmpty; + +import java.time.Instant; +import java.util.Map; +import java.util.Set; + +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class AccessTokenAuthenticationBuilder> + extends + AbstractAuthenticationBuilder { + + public static final String DEFAULT_TOKEN_VALUE = "Bearer test"; + + private String tokenValue = DEFAULT_TOKEN_VALUE; + + public T accessToken(final OAuth2AccessToken token) { + assert (TokenType.BEARER.equals(token.getTokenType())); + return tokenValue(token.getTokenValue()).scopes(token.getScopes()) + .claim(OAuth2IntrospectionClaimNames.ISSUED_AT, token.getIssuedAt()) + .claim(OAuth2IntrospectionClaimNames.EXPIRES_AT, token.getExpiresAt()); + } + + public T tokenValue(final String tokenValue) { + this.tokenValue = tokenValue; + return downCast(); + } + + public T attribute(final String name, final Object value) { + return claim(name, value); + } + + public T attributes(final Map attributes) { + return claims(attributes); + } + + public OAuth2IntrospectionAuthenticationToken build() { + if (claims.containsKey(OAuth2IntrospectionClaimNames.TOKEN_TYPE)) { + throw new RuntimeException( + OAuth2IntrospectionClaimNames.TOKEN_TYPE + + " claim is not configurable (forced to TokenType.BEARER)"); + } + if (claims.containsKey(OAuth2IntrospectionClaimNames.USERNAME)) { + throw new RuntimeException( + OAuth2IntrospectionClaimNames.USERNAME + + " claim is not configurable (forced to @WithMockAccessToken.name)"); + } + claims.put(OAuth2IntrospectionClaimNames.TOKEN_TYPE, TokenType.BEARER); + putIfNotEmpty(OAuth2IntrospectionClaimNames.USERNAME, name, claims); + + final Set allScopes = getAllScopes(); + putIfNotEmpty(scopeClaimName, allScopes, claims); + + return new OAuth2IntrospectionAuthenticationToken( + new OAuth2AccessToken( + TokenType.BEARER, + tokenValue, + (Instant) claims.get(OAuth2IntrospectionClaimNames.ISSUED_AT), + (Instant) claims.get(OAuth2IntrospectionClaimNames.EXPIRES_AT), + allScopes), + claims, + getAllAuthorities(), + nullIfEmpty(name)); + } + +} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopes.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopes.java deleted file mode 100644 index 33bd997eb9e..00000000000 --- a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopes.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.oauth2.support; - -import static org.springframework.security.test.oauth2.support.CollectionsSupport.putIfNotEmpty; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -/** - * Helps merging {@code authorities} and {@code scope}. - * - * @author Jérôme Wacongne <ch4mp@c4-soft.com> - * @since 5.2.0 - * - */ -final class AuthoritiesAndScopes { - public final Set authorities; - public final Set scopes; - public final Optional scopeAttributeName; - - private AuthoritiesAndScopes( - final Set authorities, - final Set scopes, - final Optional scopeAttributeName) { - this.authorities = Collections.unmodifiableSet(authorities); - this.scopes = Collections.unmodifiableSet(scopes); - this.scopeAttributeName = scopeAttributeName; - } - - /** - * Merges {@code authorities} and {@code scope}. Scopes are scanned for in: - *
    - *
  • scopes of course
  • - *
  • authorities ("SCOPE_" prefix)
  • - *
  • claims with keys "scope", "scp" and "scopes", first entry found being used and - * others ignored
  • - *
- *

- * All scopes are merged and set in claims, authorities and allScopes - *

- * - * @param authorities authorities array (probably from an annotation - * {@code authorities()}) - * @param scopes scopes array (probably from an annotation {@code scopes()}) - * @param attributes attributes /!\ mutable /!\ map (probably from an - * annotation {@code attributes()} or {@code claims()}) - * @return a structure containing merged granted authorities and scopes - */ - public static AuthoritiesAndScopes get( - final Collection authorities, - final Collection scopes, - final Map attributes) { - - final Optional scopeAttributeName = attributes.keySet() - .stream() - .filter(k -> "scope".equals(k) || "scp".equals(k) || "scopes".equals(k)) - .sorted() - .findFirst(); - - final Optional scopeAttribute = scopeAttributeName.map(attributes::get); - - final boolean scopeIsString = scopeAttribute.map(s -> s instanceof String).orElse(false); - - final Stream attributesScopes = scopeAttribute.map(s -> { - if (scopeIsString) { - return Stream.of(scopeAttribute.get().toString().split(" ")); - } - return AuthoritiesAndScopes.asStringStream(scopeAttribute.get()); - }).orElse(Stream.empty()); - - final Stream authoritiesScopes = - authorities.stream().filter(a -> a.startsWith("SCOPE_")).map(a -> a.substring(6)); - - final Set allScopes = Stream.concat(scopes.stream(), Stream.concat(authoritiesScopes, attributesScopes)) - .collect(Collectors.toSet()); - - if (scopeIsString) { - putIfNotEmpty( - scopeAttributeName.orElse("scope"), - allScopes.stream().collect(Collectors.joining(" ")), - attributes); - } else { - putIfNotEmpty(scopeAttributeName.orElse("scope"), allScopes, attributes); - } - - final Set allAuthorities = - Stream.concat(authorities.stream(), allScopes.stream().map(scope -> "SCOPE_" + scope)) - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toSet()); - - return new AuthoritiesAndScopes(allAuthorities, allScopes, scopeAttributeName); - } - - @SuppressWarnings("unchecked") - private static Stream asStringStream(final Object col) { - return col == null ? Stream.empty() : ((Collection) col).stream(); - } -} \ No newline at end of file diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/DefaultOAuth2AuthenticationBuilder.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/DefaultOAuth2AuthenticationBuilder.java deleted file mode 100644 index 3249a212932..00000000000 --- a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/DefaultOAuth2AuthenticationBuilder.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.oauth2.support; - -import static org.springframework.security.test.oauth2.support.CollectionsSupport.asSet; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -/** - * @author Jérôme Wacongne <ch4mp@c4-soft.com> - * @since 5.2.0 - */ -public abstract class DefaultOAuth2AuthenticationBuilder> { - - public static final String DEFAULT_AUTH_NAME = "user"; - - public static final String[] DEFAULT_AUTHORITIES = { "ROLE_USER" }; - - private static final String ROLE_PREFIX = "ROLE_"; - - private static final String SCOPE_PREFIX = "SCOPE_"; - - protected String name; - - protected final Set authorities; - - private boolean isAuthoritiesSet = false; - - protected final Set scopes = new HashSet<>(); - - protected final Map attributes = new HashMap<>(); - - public DefaultOAuth2AuthenticationBuilder(final String defaultName, final String[] defaultAuthorities) { - this.name = defaultName; - this.authorities = new HashSet<>(asSet(defaultAuthorities)); - } - - public DefaultOAuth2AuthenticationBuilder() { - this(DEFAULT_AUTH_NAME, DEFAULT_AUTHORITIES); - } - - public T name(final String name) { - this.name = name; - return downCast(); - } - - public T authority(final String authority) { - assert (authority != null); - if (!this.isAuthoritiesSet) { - this.authorities.clear(); - this.isAuthoritiesSet = true; - } - this.authorities.add(authority); - if (authority.startsWith(SCOPE_PREFIX)) { - this.scopes.add(authority.substring(SCOPE_PREFIX.length())); - } - return downCast(); - } - - public T authorities(final String... authorities) { - Stream.of(authorities).forEach(this::authority); - return downCast(); - } - - public T role(final String role) { - assert (role != null); - assert (!role.startsWith(ROLE_PREFIX)); - return authority(ROLE_PREFIX + role); - } - - public T roles(final String... roles) { - Stream.of(roles).forEach(this::role); - return downCast(); - } - - public T scope(final String role) { - assert (role != null); - assert (!role.startsWith(SCOPE_PREFIX)); - return authority(SCOPE_PREFIX + role); - } - - public T scopes(final String... scope) { - Stream.of(scope).forEach(this::scope); - return downCast(); - } - - public T attributes(final Map attributes) { - assert (attributes != null); - attributes.entrySet().stream().forEach(e -> this.attribute(e.getKey(), e.getValue())); - return downCast(); - } - - public T attribute(final String name, final Object value) { - assert (name != null); - this.attributes.put(name, value); - return downCast(); - } - - @SuppressWarnings("unchecked") - protected T downCast() { - return (T) this; - } - -} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilder.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilder.java index 66ff8b65f32..38f1c8af5bf 100644 --- a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilder.java +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilder.java @@ -15,31 +15,72 @@ */ package org.springframework.security.test.oauth2.support; +import static org.springframework.security.test.oauth2.support.CollectionsSupport.putIfNotEmpty; + +import java.time.Instant; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + /** * @author Jérôme Wacongne <ch4mp@c4-soft.com> * @since 5.2.0 */ -public class JwtAuthenticationBuilder> - extends - DefaultOAuth2AuthenticationBuilder { +public class JwtAuthenticationBuilder> extends AbstractAuthenticationBuilder { - protected Map headers = JwtSupport.DEFAULT_HEADERS; + public static final String DEFAULT_TOKEN_VALUE = "test.jwt.value"; - private boolean isHeadersSet = false; + public static final String DEFAULT_HEADER_NAME = "test-header"; - public T claims(final Map claims) { - return attributes(claims); - } + public static final String DEFAULT_HEADER_VALUE = "abracadabra"; + + public static final Map DEFAULT_HEADERS = + Collections.singletonMap(DEFAULT_HEADER_NAME, DEFAULT_HEADER_VALUE); + + private String tokenValue = DEFAULT_TOKEN_VALUE; - public T claim(final String name, final Object value) { - return attribute(name, value); + private Map headers = DEFAULT_HEADERS; + + private boolean isHeadersSet = false; + + /** + * Solely claims will be considered at build() time. token properties that can be + * stored as claims (audience, expiresAt, id, issuedAt, issuer, notBefore) are + * ignored. + * @param jwt fully configured JWT + * @return pre-configured builder + */ + public T jwt(final Jwt jwt) { + final Map claims = new HashMap<>(jwt.getClaims()); + if (jwt.getIssuedAt() != null) { + if (jwt.getClaims().containsKey(JwtClaimNames.IAT) + && !jwt.getIssuedAt().equals(jwt.getClaimAsInstant(JwtClaimNames.IAT))) { + throw new RuntimeException( + "Inconsistent issue instants: jwt.getIssuedAt() = " + jwt.getIssuedAt() + + " but jwt.getClaimAsInstant(JwtClaimNames.IAT) = " + + jwt.getClaimAsInstant(JwtClaimNames.IAT)); + } + claims.put(JwtClaimNames.IAT, jwt.getIssuedAt()); + } + if (jwt.getExpiresAt() != null) { + if (jwt.getClaims().containsKey(JwtClaimNames.EXP) + && !jwt.getExpiresAt().equals(jwt.getClaimAsInstant(JwtClaimNames.EXP))) { + throw new RuntimeException( + "Inconsistent expiry instants: jwt.getExpiresAt() = " + jwt.getExpiresAt() + + " but jwt.getClaimAsInstant(JwtClaimNames.EXP) = " + + jwt.getClaimAsInstant(JwtClaimNames.EXP)); + } + claims.put(JwtClaimNames.EXP, jwt.getExpiresAt()); + } + return tokenValue(jwt.getTokenValue()).name(jwt.getSubject()).claims(jwt.getClaims()).headers(jwt.getHeaders()); } - public T headers(final Map headers) { - headers.entrySet().stream().forEach(e -> this.header(e.getKey(), e.getValue())); + public T tokenValue(final String tokenValue) { + this.tokenValue = tokenValue; return downCast(); } @@ -53,4 +94,29 @@ public T header(final String name, final Object value) { return downCast(); } + public T headers(final Map headers) { + assert (headers != null); + headers.entrySet().stream().forEach(e -> this.header(e.getKey(), e.getValue())); + return downCast(); + } + + public JwtAuthenticationToken build() { + if (claims.containsKey(JwtClaimNames.SUB) && !claims.get(JwtClaimNames.SUB).equals(name)) { + throw new RuntimeException(JwtClaimNames.SUB + " claim is not configurable (forced to \"name\")"); + } else { + putIfNotEmpty(JwtClaimNames.SUB, name, claims); + } + + putIfNotEmpty(scopeClaimName, getAllScopes(), claims); + + return new JwtAuthenticationToken( + new Jwt( + tokenValue, + (Instant) claims.get(JwtClaimNames.IAT), + (Instant) claims.get(JwtClaimNames.EXP), + headers, + claims), + getAllAuthorities()); + } + } diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtSupport.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtSupport.java deleted file mode 100644 index 544c4681bfa..00000000000 --- a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/JwtSupport.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.oauth2.support; - -import static org.springframework.security.test.oauth2.support.CollectionsSupport.putIfNotEmpty; - -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; - -/** - * @author Jérôme Wacongne <ch4mp@c4-soft.com> - * @since 5.2.0 - */ -public class JwtSupport { - public static final String DEFAULT_TOKEN_VALUE = "test.jwt.value"; - public static final String DEFAULT_HEADER_NAME = "test-header"; - public static final String DEFAULT_HEADER_VALUE = "abracadabra"; - public static final Map DEFAULT_HEADERS = - Collections.singletonMap(DEFAULT_HEADER_NAME, DEFAULT_HEADER_VALUE); - - public static JwtAuthenticationToken authentication( - final String name, - final Collection authorities, - final Collection scopes, - final Map claims, - final Map headers) { - final Map postrPocessedClaims = new HashMap<>(claims); - if (claims.containsKey(JwtClaimNames.SUB)) { - throw new RuntimeException(JwtClaimNames.SUB + " claim is not configurable (forced to \"name\")"); - } else { - putIfNotEmpty(JwtClaimNames.SUB, name, postrPocessedClaims); - } - - final AuthoritiesAndScopes authoritiesAndScopes = - AuthoritiesAndScopes.get(authorities, scopes, postrPocessedClaims); - - return new JwtAuthenticationToken( - new Jwt( - DEFAULT_TOKEN_VALUE, - (Instant) postrPocessedClaims.get(JwtClaimNames.IAT), - (Instant) postrPocessedClaims.get(JwtClaimNames.EXP), - headers, - postrPocessedClaims), - authoritiesAndScopes.authorities); - } -} diff --git a/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/OidcIdTokenAuthenticationBuilder.java b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/OidcIdTokenAuthenticationBuilder.java new file mode 100644 index 00000000000..a4d0e4ee84d --- /dev/null +++ b/oauth2-test/src/main/java/org/springframework/security/test/oauth2/support/OidcIdTokenAuthenticationBuilder.java @@ -0,0 +1,286 @@ +/* + * 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.oauth2.support; + +import static org.springframework.security.test.oauth2.support.CollectionsSupport.putIfNotEmpty; + +import java.time.Instant; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.util.StringUtils; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class OidcIdTokenAuthenticationBuilder> + extends + AbstractAuthenticationBuilder { + public static final String DEFAULT_TOKEN_VALUE = "Open ID test"; + public static final String DEFAULT_NAME_KEY = "sub"; + public static final String REQUEST_REDIRECT_URI = "https://localhost:8080/"; + public static final String REQUEST_AUTHORIZATION_URI = "https://localhost:8080/authorize"; + public static final String REQUEST_GRANT_TYPE = "authorization_code"; + public static final String CLIENT_TOKEN_URI = "https://localhost:8080/token"; + public static final String CLIENT_ID = "mocked-client"; + public static final String CLIENT_REGISTRATION_ID = "mocked-registration"; + public static final String CLIENT_GRANT_TYPE = "client_credentials"; + + protected String tokenValue = DEFAULT_TOKEN_VALUE; + protected String nameAttributeKey = DEFAULT_NAME_KEY; + protected final ClientRegistration.Builder clientRegistrationBuilder; + protected final OAuth2AuthorizationRequest.Builder authorizationRequestBuilder; + + public OidcIdTokenAuthenticationBuilder( + final ClientRegistration.Builder clientRegistration, + final OAuth2AuthorizationRequest.Builder authorizationRequest) { + super(); + this.clientRegistrationBuilder = clientRegistration; + this.authorizationRequestBuilder = authorizationRequest; + this.authorizationRequestBuilder.attributes(claims); + } + + public OidcIdTokenAuthenticationBuilder(final AuthorizationGrantType requestAuthorizationGrantType) { + this(defaultClientRegistration(), defaultAuthorizationRequest(requestAuthorizationGrantType)); + } + + public T tokenValue(final String tokenValue) { + this.tokenValue = tokenValue; + return downCast(); + } + + public T nameAttributeKey(final String nameAttributeKey) { + this.nameAttributeKey = nameAttributeKey; + return downCast(); + } + + public static ClientRegistration.Builder defaultClientRegistration() { + return ClientRegistration.withRegistrationId(CLIENT_REGISTRATION_ID) + .authorizationGrantType(new AuthorizationGrantType(CLIENT_GRANT_TYPE)) + .clientId(CLIENT_ID) + .tokenUri(CLIENT_TOKEN_URI); + } + + public static OAuth2AuthorizationRequest.Builder + defaultAuthorizationRequest(final AuthorizationGrantType authorizationGrantType) { + return authorizationRequestBuilder(authorizationGrantType).authorizationUri(REQUEST_AUTHORIZATION_URI) + .clientId(CLIENT_ID) + .redirectUri(REQUEST_REDIRECT_URI); + } + + public static OAuth2AuthorizationRequest.Builder + authorizationRequestBuilder(final AuthorizationGrantType authorizationGrantType) { + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) { + return OAuth2AuthorizationRequest.authorizationCode(); + } + if (AuthorizationGrantType.IMPLICIT.equals(authorizationGrantType)) { + return OAuth2AuthorizationRequest.implicit(); + } + throw new UnsupportedOperationException( + "Only authorization_code and implicit grant types are supported for MockOAuth2AuthorizationRequest"); + } + + public OAuth2LoginAuthenticationToken build() { + assert (!StringUtils.isEmpty(nameAttributeKey)); + assert (!claims.containsKey(nameAttributeKey) || claims.get(nameAttributeKey).equals(name)); + + putIfNotEmpty(nameAttributeKey, name, claims); + + final Set scopes = getAllScopes(); + putIfNotEmpty(scopeClaimName, scopes, claims); + + final OidcIdToken token = new OidcIdToken( + tokenValue, + (Instant) claims.get(IdTokenClaimNames.IAT), + (Instant) claims.get(IdTokenClaimNames.EXP), + claims); + + final ClientRegistration clientRegistration = + clientRegistrationBuilder.scope(scopes).userNameAttributeName(nameAttributeKey).build(); + + final OAuth2AuthorizationRequest authorizationRequest = + authorizationRequestBuilder.attributes(claims).scopes(scopes).build(); + + final String redirectUri = + StringUtils.isEmpty(authorizationRequest.getRedirectUri()) ? clientRegistration.getRedirectUriTemplate() + : authorizationRequest.getRedirectUri(); + + final OAuth2AuthorizationExchange authorizationExchange = + new OAuth2AuthorizationExchange(authorizationRequest, auth2AuthorizationResponse(redirectUri)); + + final Collection authorities = getAllAuthorities(); + final DefaultOidcUser principal = new DefaultOidcUser(authorities, token, nameAttributeKey); + + final OAuth2AccessToken accessToken = new OAuth2AccessToken( + TokenType.BEARER, + tokenValue, + (Instant) claims.get(IdTokenClaimNames.IAT), + (Instant) claims.get(IdTokenClaimNames.EXP), + authorizationExchange.getAuthorizationRequest().getScopes()); + + return new OAuth2LoginAuthenticationToken( + clientRegistration, + authorizationExchange, + principal, + authorities, + accessToken); + } + + private static OAuth2AuthorizationResponse auth2AuthorizationResponse(final String redirectUri) { + final OAuth2AuthorizationResponse.Builder builder = + OAuth2AuthorizationResponse.success("test-authorization-success-code"); + builder.redirectUri(redirectUri); + return builder.build(); + } + + public static class ClientRegistrationBuilder> { + private final ClientRegistration.Builder delegate; + + public ClientRegistrationBuilder(final ClientRegistration.Builder delegate) { + this.delegate = delegate; + } + + public T authorizationGrantType(final AuthorizationGrantType authorizationGrantType) { + delegate.authorizationGrantType(authorizationGrantType); + return downcast(); + } + + public T authorizationUri(final String authorizationUri) { + delegate.authorizationUri(authorizationUri); + return downcast(); + } + + public T clientAuthenticationMethod(final ClientAuthenticationMethod clientAuthenticationMethod) { + delegate.clientAuthenticationMethod(clientAuthenticationMethod); + return downcast(); + } + + public T clientId(final String clientId) { + delegate.clientId(clientId); + return downcast(); + } + + public T clientName(final String clientName) { + delegate.clientName(clientName); + return downcast(); + } + + public T clientSecret(final String clientSecret) { + delegate.clientSecret(clientSecret); + return downcast(); + } + + public T jwkSetUri(final String jwkSetUri) { + delegate.jwkSetUri(jwkSetUri); + return downcast(); + } + + public T providerConfigurationMetadata(final Map configurationMetadata) { + delegate.providerConfigurationMetadata(configurationMetadata); + return downcast(); + } + + public T redirectUriTemplate(final String redirectUriTemplate) { + delegate.redirectUriTemplate(redirectUriTemplate); + return downcast(); + } + + public T registrationId(final String registrationId) { + delegate.registrationId(registrationId); + return downcast(); + } + + public T tokenUri(final String tokenUri) { + delegate.tokenUri(tokenUri); + return downcast(); + } + + public T userInfoAuthenticationMethod(final AuthenticationMethod userInfoAuthenticationMethod) { + delegate.userInfoAuthenticationMethod(userInfoAuthenticationMethod); + return downcast(); + } + + @SuppressWarnings("unchecked") + protected T downcast() { + return (T) this; + } + } + + public static class AuthorizationRequestBuilder> { + private final OAuth2AuthorizationRequest.Builder delegate; + private final Map additionalParameters; + + public AuthorizationRequestBuilder( + final OAuth2AuthorizationRequest.Builder builder, + final Map additionalParameters) { + this.additionalParameters = additionalParameters; + this.delegate = builder; + this.delegate.additionalParameters(additionalParameters); + } + + public T additionalParameter(final String name, final Object value) { + additionalParameters.put(name, value); + return downcast(); + } + + public T authorizationRequestUri(final String authorizationRequestUri) { + delegate.authorizationRequestUri(authorizationRequestUri); + return downcast(); + } + + public T authorizationUri(final String authorizationUri) { + delegate.authorizationUri(authorizationUri); + return downcast(); + } + + public T clientId(final String clientId) { + delegate.clientId(clientId); + return downcast(); + } + + public T redirectUri(final String redirectUri) { + delegate.redirectUri(redirectUri); + return downcast(); + } + + public T state(final String state) { + delegate.state(state); + return downcast(); + } + + @SuppressWarnings("unchecked") + protected T downcast() { + return (T) this; + } + } + +} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/request/AbstractRequestPostProcessorTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/request/AbstractRequestPostProcessorTest.java new file mode 100644 index 00000000000..ba456a4d670 --- /dev/null +++ b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/request/AbstractRequestPostProcessorTest.java @@ -0,0 +1,59 @@ +/* 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.oauth2.request; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; + +import org.junit.Before; +import org.mockito.Mock; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public abstract class AbstractRequestPostProcessorTest { + + @Mock + MockHttpServletRequest request; + + final static String TEST_NAME = "ch4mpy"; + final static String[] TEST_AUTHORITIES = new String[] { "TEST_AUTHORITY" }; + final static String[] TEST_SCOPES = new String[] { "test:collection" }; + final static String SCOPE_CLAIM_NAME = "scp"; + final static Map TEST_CLAIMS = + Collections.singletonMap(SCOPE_CLAIM_NAME, Collections.singleton("test:claim")); + + @Before + public void setup() throws Exception { + request = new MockHttpServletRequest(); + } + + static Authentication authentication(final MockHttpServletRequest req) { + for (final Enumeration names = req.getAttributeNames(); names.hasMoreElements();) { + final String name = names.nextElement(); + if (name.contains("SecurityContext")) { + final SecurityContext securityContext = (SecurityContext) req.getAttribute(name); + return securityContext.getAuthentication(); + } + } + return null; + } + +} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/request/JwtRequestPostProcessorTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/request/JwtRequestPostProcessorTest.java new file mode 100644 index 00000000000..4650c98da84 --- /dev/null +++ b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/request/JwtRequestPostProcessorTest.java @@ -0,0 +1,52 @@ +/* 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.oauth2.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.oauth2.request.OAuth2MockMvcRequestPostProcessors.mockJwt; + +import java.util.Collection; + +import org.junit.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class JwtRequestPostProcessorTest extends AbstractRequestPostProcessorTest { + + @Test + @SuppressWarnings("unchecked") + public void test() { + final JwtRequestPostProcessor rpp = mockJwt().name(TEST_NAME) + .authorities(TEST_AUTHORITIES) + .scopes(TEST_SCOPES) + .claims(TEST_CLAIMS) + .scopesClaimName(SCOPE_CLAIM_NAME); + + final JwtAuthenticationToken actual = (JwtAuthenticationToken) authentication(rpp.postProcessRequest(request)); + + assertThat(actual.getName()).isEqualTo(TEST_NAME); + assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( + new SimpleGrantedAuthority("TEST_AUTHORITY"), + new SimpleGrantedAuthority("SCOPE_test:collection"), + new SimpleGrantedAuthority("SCOPE_test:claim")); + assertThat((Collection) actual.getTokenAttributes().get("scp")) + .containsExactlyInAnyOrder("test:collection", "test:claim"); + } + +} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AccessTokenAuthenticationBuilderTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AccessTokenAuthenticationBuilderTest.java new file mode 100644 index 00000000000..15629ab6b3a --- /dev/null +++ b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AccessTokenAuthenticationBuilderTest.java @@ -0,0 +1,116 @@ +/* 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.oauth2.support; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class AccessTokenAuthenticationBuilderTest { + static class TestAccessTokenAuthenticationBuilder + extends + AccessTokenAuthenticationBuilder { + } + + @Test + public void authenticationNameAndTokenSubjectClaimAreSet() { + final OAuth2IntrospectionAuthenticationToken actual = + new TestAccessTokenAuthenticationBuilder().name("ch4mpy").build(); + + assertThat(actual.getName()).isEqualTo("ch4mpy"); + assertThat(actual.getTokenAttributes().get(OAuth2IntrospectionClaimNames.USERNAME)).isEqualTo("ch4mpy"); + } + + @Test + public void tokenIatIsSetFromClaims() { + final OAuth2AccessToken actual = new TestAccessTokenAuthenticationBuilder().name("ch4mpy") + .claim(OAuth2IntrospectionClaimNames.ISSUED_AT, Instant.parse("2019-03-21T13:52:25Z")) + .build() + .getToken(); + + assertThat(actual.getIssuedAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + assertThat(actual.getExpiresAt()).isNull(); + } + + @Test + public void tokenExpIsSetFromClaims() { + final OAuth2AccessToken actual = new TestAccessTokenAuthenticationBuilder().name("ch4mpy") + .claim(OAuth2IntrospectionClaimNames.EXPIRES_AT, Instant.parse("2019-03-21T13:52:25Z")) + .build() + .getToken(); + + assertThat(actual.getIssuedAt()).isNull(); + assertThat(actual.getExpiresAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + } + + @Test + public void scopesCollectionAndScopeClaimAreAddedToAuthorities() { + final OAuth2IntrospectionAuthenticationToken actual = new TestAccessTokenAuthenticationBuilder().name("ch4mpy") + .authorities("TEST_AUTHORITY") + .scopes("scope:collection") + .claim("scope", Collections.singleton("scope:claim")) + .build(); + + assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( + new SimpleGrantedAuthority("TEST_AUTHORITY"), + new SimpleGrantedAuthority("SCOPE_scope:collection"), + new SimpleGrantedAuthority("SCOPE_scope:claim")); + } + + @SuppressWarnings("unchecked") + @Test + public void scopesCollectionAndScopeAuthoritiesAreAddedToScopeClaim() { + final OAuth2IntrospectionAuthenticationToken actual = new TestAccessTokenAuthenticationBuilder().name("ch4mpy") + .authorities("SCOPE_scope:authority") + .scope("scope:collection") + .claim("scope", Collections.singleton("scope:claim")) + .build(); + + assertThat((Collection) actual.getTokenAttributes().get("scope")) + .containsExactlyInAnyOrder("scope:authority", "scope:collection", "scope:claim"); + } + + /** + * "scp" is the an usual name for "scope" claim + */ + + @Test + public void scopesCollectionAndScpClaimAreAddedToAuthorities() { + final OAuth2IntrospectionAuthenticationToken actual = new TestAccessTokenAuthenticationBuilder().name("ch4mpy") + .authorities("TEST_AUTHORITY") + .scopes("scope:collection") + .claim("scp", Collections.singleton("scope:claim")) + .scopesClaimName("scp") + .build(); + + assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( + new SimpleGrantedAuthority("TEST_AUTHORITY"), + new SimpleGrantedAuthority("SCOPE_scope:collection"), + new SimpleGrantedAuthority("SCOPE_scope:claim")); + } + +} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopesTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopesTest.java deleted file mode 100644 index 73919006450..00000000000 --- a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/AuthoritiesAndScopesTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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.oauth2.support; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.security.test.oauth2.support.CollectionsSupport.asSet; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import org.junit.Test; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -/** - * @author Jérôme Wacongne <ch4mp@c4-soft.com> - * @since 5.2.0 - */ -public class AuthoritiesAndScopesTest { - - @Test - public void testScopeCollectionAttribute() { - final Map attributes = new HashMap<>(); - attributes.put("scope", Collections.singleton("c")); - final AuthoritiesAndScopes actual = - AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); - - assertThat(actual.authorities).hasSize(4); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_c")); - - assertThat(actual.scopes).hasSize(3); - assertThat(actual.scopes).contains("a"); - assertThat(actual.scopes).contains("b"); - assertThat(actual.scopes).contains("c"); - - assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scope")); - } - - @Test - public void testScopeStringAttribute() { - final Map attributes = new HashMap<>(); - attributes.put("scope", "c"); - final AuthoritiesAndScopes actual = - AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); - - assertThat(actual.authorities).hasSize(4); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_c")); - - assertThat(actual.scopes).hasSize(3); - assertThat(actual.scopes).contains("a"); - assertThat(actual.scopes).contains("b"); - assertThat(actual.scopes).contains("c"); - - assertThat(attributes.get("scope")).isEqualTo("a b c"); - - assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scope")); - } - - @Test - public void testScpAttribute() { - final Map attributes = new HashMap<>(); - attributes.put("scp", Collections.singleton("c")); - final AuthoritiesAndScopes actual = - AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); - - assertThat(actual.authorities).hasSize(4); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_c")); - - assertThat(actual.scopes).hasSize(3); - assertThat(actual.scopes).contains("a"); - assertThat(actual.scopes).contains("b"); - assertThat(actual.scopes).contains("c"); - - assertThat(attributes.get("scp")).isEqualTo(actual.scopes); - - assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scp")); - } - - @Test - public void testScopesAttribute() { - final Map attributes = new HashMap<>(); - attributes.put("scopes", Collections.singleton("c")); - final AuthoritiesAndScopes actual = - AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); - - assertThat(actual.authorities).hasSize(4); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_c")); - - assertThat(actual.scopes).hasSize(3); - assertThat(actual.scopes).contains("a"); - assertThat(actual.scopes).contains("b"); - assertThat(actual.scopes).contains("c"); - - assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scopes")); - } - - @Test - public void testAttributeCollision() { - final Map attributes = new HashMap<>(3); - attributes.put("scopes", Collections.singleton("c")); - attributes.put("scope", Collections.singleton("d")); - attributes.put("scp", Collections.singleton("e")); - final AuthoritiesAndScopes actual = - AuthoritiesAndScopes.get(asSet("AUTHORITY_A", "SCOPE_a"), asSet("b"), attributes); - - assertThat(actual.authorities).hasSize(4); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("AUTHORITY_A")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_a")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_b")); - assertThat(actual.authorities).contains(new SimpleGrantedAuthority("SCOPE_d")); - - assertThat(actual.scopes).hasSize(3); - assertThat(actual.scopes).contains("a"); - assertThat(actual.scopes).contains("b"); - assertThat(actual.scopes).contains("d"); - - assertThat(actual.scopeAttributeName).isEqualTo(Optional.of("scope")); - } - -} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilderTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilderTest.java new file mode 100644 index 00000000000..49fa92c2465 --- /dev/null +++ b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtAuthenticationBuilderTest.java @@ -0,0 +1,169 @@ +/* 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.oauth2.support; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class JwtAuthenticationBuilderTest { + static class TestJwtAuthenticationBuilder extends JwtAuthenticationBuilder { + } + + @Test + public void defaultNameAndAuthority() { + final JwtAuthenticationToken actual = new TestJwtAuthenticationBuilder().build(); + + assertThat(actual.getName()).isEqualTo("user"); + assertThat(actual.getAuthorities()).containsExactly(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Test + public void defaultNameAndRoleOverides() { + assertThat(new TestJwtAuthenticationBuilder().name("ch4mpy").build().getName()).isEqualTo("ch4mpy"); + assertThat(new TestJwtAuthenticationBuilder().authority("TEST").build().getAuthorities()) + .containsExactly(new SimpleGrantedAuthority("TEST")); + assertThat(new TestJwtAuthenticationBuilder().role("TEST").build().getAuthorities()) + .containsExactly(new SimpleGrantedAuthority("ROLE_TEST")); + assertThat(new TestJwtAuthenticationBuilder().scope("TEST").build().getAuthorities()) + .containsExactly(new SimpleGrantedAuthority("SCOPE_TEST")); + } + + @Test + public void authenticationNameAndTokenSubjectClaimAreSet() { + final JwtAuthenticationToken actual = new TestJwtAuthenticationBuilder().name("ch4mpy").build(); + + assertThat(actual.getName()).isEqualTo("ch4mpy"); + assertThat(actual.getTokenAttributes().get(JwtClaimNames.SUB)).isEqualTo("ch4mpy"); + } + + @Test + public void tokenIatIsSetFromClaims() { + final Jwt actual = new TestJwtAuthenticationBuilder().name("ch4mpy") + .claim(JwtClaimNames.IAT, Instant.parse("2019-03-21T13:52:25Z")) + .build() + .getToken(); + + assertThat(actual.getIssuedAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + assertThat(actual.getExpiresAt()).isNull(); + assertThat(actual.getClaimAsInstant(JwtClaimNames.IAT)).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + assertThat(actual.getClaimAsInstant(JwtClaimNames.EXP)).isNull(); + } + + @Test + public void tokenExpIsSetFromClaims() { + final Jwt actual = new TestJwtAuthenticationBuilder().name("ch4mpy") + .claim(JwtClaimNames.EXP, Instant.parse("2019-03-21T13:52:25Z")) + .build() + .getToken(); + + assertThat(actual.getIssuedAt()).isNull(); + assertThat(actual.getExpiresAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + assertThat(actual.getClaimAsInstant(JwtClaimNames.IAT)).isNull(); + assertThat(actual.getClaimAsInstant(JwtClaimNames.EXP)).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + } + + @Test + public void scopesCollectionAndScopeClaimAreAddedToAuthorities() { + final JwtAuthenticationToken actual = new TestJwtAuthenticationBuilder().name("ch4mpy") + .authority("TEST_AUTHORITY") + .scope("scope:collection") + .claim("scope", Collections.singleton("scope:claim")) + .build(); + + assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( + new SimpleGrantedAuthority("TEST_AUTHORITY"), + new SimpleGrantedAuthority("SCOPE_scope:collection"), + new SimpleGrantedAuthority("SCOPE_scope:claim")); + } + + @SuppressWarnings("unchecked") + @Test + public void scopesCollectionAndScopeAuthoritiesAreAddedToScopeClaim() { + final JwtAuthenticationToken actual = new TestJwtAuthenticationBuilder().name("ch4mpy") + .authorities("SCOPE_scope:authority") + .scope("scope:collection") + .claim("scope", Collections.singleton("scope:claim")) + .build(); + + assertThat((Collection) actual.getToken().getClaims().get("scope")) + .containsExactlyInAnyOrder("scope:authority", "scope:collection", "scope:claim"); + } + + /** + * "scp" is the an usual name for "scope" claim + */ + + @Test + public void scopesCollectionAndScpClaimAreAddedToAuthorities() { + final JwtAuthenticationToken actual = new TestJwtAuthenticationBuilder().name("ch4mpy") + .authorities("TEST_AUTHORITY") + .scopes("scope:collection") + .claim("scp", Collections.singleton("scope:claim")) + .scopesClaimName("scp") + .build(); + + assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( + new SimpleGrantedAuthority("TEST_AUTHORITY"), + new SimpleGrantedAuthority("SCOPE_scope:collection"), + new SimpleGrantedAuthority("SCOPE_scope:claim")); + } + + @Test + public void fromJwt() { + final Jwt jwt = new Jwt( + "test-token", + null, + null, + Collections.singletonMap("test-header", "test"), + Collections.singletonMap(JwtClaimNames.SUB, "ch4mpy")); + final JwtAuthenticationToken actual = new TestJwtAuthenticationBuilder().jwt(jwt).build(); + assertThat(actual.getAuthorities()).containsExactly(new SimpleGrantedAuthority("ROLE_USER")); + assertThat(actual.getName()).isEqualTo("ch4mpy"); + assertThat(actual.getTokenAttributes()).hasSize(1); + assertThat(actual.getTokenAttributes().get(JwtClaimNames.SUB)).isEqualTo("ch4mpy"); + } + + @Test(expected = RuntimeException.class) + public void fromInconsistentJwtInstants() { + final Map claims = new HashMap<>(); + claims.put(JwtClaimNames.SUB, "ch4mpy"); + claims.put(JwtClaimNames.IAT, Instant.parse("2018-01-01T01:01:01Z")); + claims.put(JwtClaimNames.EXP, Instant.parse("2018-02-02T02:02:02Z")); + final Jwt jwt = new Jwt( + "test-token", + Instant.parse("2019-01-01T01:01:01Z"), + Instant.parse("2019-02-02T02:02:02Z"), + Collections.singletonMap("test-header", "test"), + claims); + + final JwtAuthenticationToken actual = new TestJwtAuthenticationBuilder().jwt(jwt).build(); + } + +} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtSupportTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtSupportTest.java deleted file mode 100644 index c39e440a1d9..00000000000 --- a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/JwtSupportTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* 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.oauth2.support; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import org.junit.Test; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.jwt.JwtClaimNames; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; - -/** - * @author Jérôme Wacongne <ch4mp@c4-soft.com> - * @since 5.2.0 - */ -public class JwtSupportTest { - - @Test - public void authenticationNameAndTokenSubjectClaimAreSet() { - final JwtAuthenticationToken actual = JwtSupport.authentication( - "ch4mpy", - Collections.emptySet(), - Collections.emptySet(), - Collections.emptyMap(), - JwtSupport.DEFAULT_HEADERS); - - assertThat(actual.getName()).isEqualTo("ch4mpy"); - assertThat(actual.getTokenAttributes().get(JwtClaimNames.SUB)).isEqualTo("ch4mpy"); - } - - @Test - public void tokenIatIsSetFromClaims() { - final Map claims = - Collections.singletonMap(JwtClaimNames.IAT, Instant.parse("2019-03-21T13:52:25Z")); - final Jwt actual = - JwtSupport - .authentication( - "ch4mpy", - Collections.emptySet(), - Collections.emptySet(), - claims, - JwtSupport.DEFAULT_HEADERS) - .getToken(); - - assertThat(actual.getIssuedAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); - assertThat(actual.getExpiresAt()).isNull(); - assertThat(actual.getClaimAsInstant(JwtClaimNames.IAT)).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); - assertThat(actual.getClaimAsInstant(JwtClaimNames.EXP)).isNull(); - } - - @Test - public void tokenExpIsSetFromClaims() { - final Jwt actual = - JwtSupport - .authentication( - "ch4mpy", - Collections.emptySet(), - Collections.emptySet(), - Collections.singletonMap(JwtClaimNames.EXP, Instant.parse("2019-03-21T13:52:25Z")), - JwtSupport.DEFAULT_HEADERS) - .getToken(); - - assertThat(actual.getIssuedAt()).isNull(); - assertThat(actual.getExpiresAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); - assertThat(actual.getClaimAsInstant(JwtClaimNames.IAT)).isNull(); - assertThat(actual.getClaimAsInstant(JwtClaimNames.EXP)).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); - } - - @Test - public void scopesCollectionAndScopeClaimAreAddedToAuthorities() { - final JwtAuthenticationToken actual = JwtSupport.authentication( - "ch4mpy", - Collections.singleton("TEST_AUTHORITY"), - Collections.singleton("scope:collection"), - Collections.singletonMap("scope", Collections.singleton("scope:claim")), - JwtSupport.DEFAULT_HEADERS); - - assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( - new SimpleGrantedAuthority("TEST_AUTHORITY"), - new SimpleGrantedAuthority("SCOPE_scope:collection"), - new SimpleGrantedAuthority("SCOPE_scope:claim")); - } - - @SuppressWarnings("unchecked") - @Test - public void scopesCollectionAndScopeAuthoritiesAreAddedToScopeClaim() { - final JwtAuthenticationToken actual = JwtSupport.authentication( - "ch4mpy", - Collections.singleton("SCOPE_scope:authority"), - Collections.singleton("scope:collection"), - Collections.singletonMap("scope", Collections.singleton("scope:claim")), - JwtSupport.DEFAULT_HEADERS); - - assertThat((Collection) actual.getToken().getClaims().get("scope")) - .containsExactlyInAnyOrder("scope:authority", "scope:collection", "scope:claim"); - } - - /** - * "scp" is the an usual name for "scope" claim - */ - - @Test - public void scopesCollectionAndScpClaimAreAddedToAuthorities() { - final JwtAuthenticationToken actual = JwtSupport.authentication( - "ch4mpy", - Collections.singleton("TEST_AUTHORITY"), - Collections.singleton("scope:collection"), - Collections.singletonMap("scp", Collections.singleton("scope:claim")), - JwtSupport.DEFAULT_HEADERS); - - assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( - new SimpleGrantedAuthority("TEST_AUTHORITY"), - new SimpleGrantedAuthority("SCOPE_scope:collection"), - new SimpleGrantedAuthority("SCOPE_scope:claim")); - } - - @SuppressWarnings("unchecked") - @Test - public void scopesCollectionAndScopeAuthoritiesAreAddedToScpClaim() { - final JwtAuthenticationToken actual = JwtSupport.authentication( - "ch4mpy", - Collections.singleton("SCOPE_scope:authority"), - Collections.singleton("scope:collection"), - Collections.singletonMap("scp", Collections.singleton("scope:claim")), - JwtSupport.DEFAULT_HEADERS); - - assertThat((Collection) actual.getToken().getClaims().get("scp")) - .containsExactlyInAnyOrder("scope:authority", "scope:collection", "scope:claim"); - } - -} diff --git a/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/OidcIdSupportTest.java b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/OidcIdSupportTest.java new file mode 100644 index 00000000000..774aefa4a97 --- /dev/null +++ b/oauth2-test/src/test/java/org/springframework/security/test/oauth2/support/OidcIdSupportTest.java @@ -0,0 +1,99 @@ +/* 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.oauth2.support; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionClaimNames; + +/** + * @author Jérôme Wacongne <ch4mp@c4-soft.com> + * @since 5.2.0 + */ +public class OidcIdSupportTest { + private static final String CLIENT_ID = "test-client"; + TestOidcIdAuthenticationBuilder builder; + + static class TestOidcIdAuthenticationBuilder + extends + OidcIdTokenAuthenticationBuilder { + public TestOidcIdAuthenticationBuilder() { + super(AuthorizationGrantType.AUTHORIZATION_CODE); + } + } + + @Before + public void setUp() { + builder = new TestOidcIdAuthenticationBuilder().name("ch4mpy").nameAttributeKey("userName").role("USER"); + builder.clientRegistrationBuilder.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .clientId(CLIENT_ID) + .tokenUri("https://stub"); + builder.authorizationRequestBuilder.authorizationUri("https://stub") + .clientId(CLIENT_ID) + .redirectUri("https://stub"); + } + + @Test + public void authenticationNameIsSet() { + final OAuth2LoginAuthenticationToken actual = builder.build(); + + assertThat(actual.getName()).isEqualTo("ch4mpy"); + } + + @Test + public void tokenIatIsSetFromClaims() { + final OAuth2AccessToken actual = + builder.claim(OAuth2IntrospectionClaimNames.ISSUED_AT, Instant.parse("2019-03-21T13:52:25Z")) + .build() + .getAccessToken(); + + assertThat(actual.getIssuedAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + assertThat(actual.getExpiresAt()).isNull(); + } + + @Test + public void tokenExpIsSetFromClaims() { + final OAuth2AccessToken actual = + builder.claim(OAuth2IntrospectionClaimNames.EXPIRES_AT, Instant.parse("2019-03-21T13:52:25Z")) + .build() + .getAccessToken(); + + assertThat(actual.getIssuedAt()).isNull(); + assertThat(actual.getExpiresAt()).isEqualTo(Instant.parse("2019-03-21T13:52:25Z")); + } + + @Test + public void scopesCollectionAndScopeClaimAreAddedToAuthorities() { + final OAuth2LoginAuthenticationToken actual = builder.authorities("TEST_AUTHORITY") + .scopes("scope:collection") + .claim("scope", Collections.singleton("scope:claim")) + .build(); + + assertThat(actual.getAuthorities()).containsExactlyInAnyOrder( + new SimpleGrantedAuthority("TEST_AUTHORITY"), + new SimpleGrantedAuthority("SCOPE_scope:collection"), + new SimpleGrantedAuthority("SCOPE_scope:claim")); + } + +}