parent
9d66f2ccce
commit
bd593a3431
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.security.test.web.reactive.server;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -26,7 +27,9 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.util.StringUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
@ -47,7 +50,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
|||
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
|
@ -58,8 +63,10 @@ import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
|||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
|
||||
import org.springframework.security.web.server.csrf.CsrfWebFilter;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
|
@ -153,6 +160,20 @@ public class SecurityMockServerConfigurers {
|
|||
return new JwtMutator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
|
||||
* {@link BearerTokenAuthentication} for the
|
||||
* {@link Authentication} and an {@link OAuth2AuthenticatedPrincipal} for the
|
||||
* {@link Authentication#getPrincipal()}. All details are
|
||||
* declarative and do not require the token to be valid.
|
||||
*
|
||||
* @return the {@link OpaqueTokenMutator} to further configure or use
|
||||
* @since 5.3
|
||||
*/
|
||||
public static OpaqueTokenMutator mockOpaqueToken() {
|
||||
return new OpaqueTokenMutator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
|
||||
* {@link OAuth2AuthenticationToken} for the
|
||||
|
@ -516,6 +537,165 @@ public class SecurityMockServerConfigurers {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
* @since 5.3
|
||||
*/
|
||||
public final static class OpaqueTokenMutator implements WebTestClientConfigurer, MockServerConfigurer {
|
||||
private Supplier<Map<String, Object>> attributes = this::defaultAttributes;
|
||||
private Supplier<Collection<GrantedAuthority>> authorities = this::defaultAuthorities;
|
||||
|
||||
private Supplier<OAuth2AuthenticatedPrincipal> principal = this::defaultPrincipal;
|
||||
|
||||
private OpaqueTokenMutator() { }
|
||||
|
||||
/**
|
||||
* Mutate the attributes using the given {@link Consumer}
|
||||
*
|
||||
* @param attributesConsumer The {@link Consumer} for mutating the {@Map} of attributes
|
||||
* @return the {@link OpaqueTokenMutator} for further configuration
|
||||
*/
|
||||
public OpaqueTokenMutator attributes(Consumer<Map<String, Object>> attributesConsumer) {
|
||||
Assert.notNull(attributesConsumer, "attributesConsumer cannot be null");
|
||||
this.attributes = () -> {
|
||||
Map<String, Object> attributes = defaultAttributes();
|
||||
attributesConsumer.accept(attributes);
|
||||
return attributes;
|
||||
};
|
||||
this.principal = this::defaultPrincipal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the provided authorities in the resulting principal
|
||||
* @param authorities the authorities to use
|
||||
* @return the {@link OpaqueTokenMutator} for further configuration
|
||||
*/
|
||||
public OpaqueTokenMutator authorities(Collection<GrantedAuthority> authorities) {
|
||||
Assert.notNull(authorities, "authorities cannot be null");
|
||||
this.authorities = () -> authorities;
|
||||
this.principal = this::defaultPrincipal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the provided authorities in the resulting principal
|
||||
* @param authorities the authorities to use
|
||||
* @return the {@link OpaqueTokenMutator} for further configuration
|
||||
*/
|
||||
public OpaqueTokenMutator authorities(GrantedAuthority... authorities) {
|
||||
Assert.notNull(authorities, "authorities cannot be null");
|
||||
this.authorities = () -> Arrays.asList(authorities);
|
||||
this.principal = this::defaultPrincipal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the provided scopes as the authorities in the resulting principal
|
||||
* @param scopes the scopes to use
|
||||
* @return the {@link OpaqueTokenMutator} for further configuration
|
||||
*/
|
||||
public OpaqueTokenMutator scopes(String... scopes) {
|
||||
Assert.notNull(scopes, "scopes cannot be null");
|
||||
this.authorities = () -> getAuthorities(Arrays.asList(scopes));
|
||||
this.principal = this::defaultPrincipal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the provided principal
|
||||
* @param principal the principal to use
|
||||
* @return the {@link OpaqueTokenMutator} for further configuration
|
||||
*/
|
||||
public OpaqueTokenMutator principal(OAuth2AuthenticatedPrincipal principal) {
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
this.principal = () -> principal;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
|
||||
configurer().beforeServerCreated(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
|
||||
configurer().afterConfigureAdded(serverSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConfigurerAdded(
|
||||
WebTestClient.Builder builder,
|
||||
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
|
||||
@Nullable ClientHttpConnector connector) {
|
||||
httpHandlerBuilder.filter((exchange, chain) -> {
|
||||
CsrfWebFilter.skipExchange(exchange);
|
||||
return chain.filter(exchange);
|
||||
});
|
||||
configurer().afterConfigurerAdded(builder, httpHandlerBuilder, connector);
|
||||
}
|
||||
|
||||
private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer() {
|
||||
OAuth2AuthenticatedPrincipal principal = this.principal.get();
|
||||
OAuth2AccessToken accessToken = getOAuth2AccessToken(principal);
|
||||
BearerTokenAuthentication token = new BearerTokenAuthentication
|
||||
(principal, accessToken, principal.getAuthorities());
|
||||
return mockAuthentication(token);
|
||||
}
|
||||
|
||||
private Map<String, Object> defaultAttributes() {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put(OAuth2IntrospectionClaimNames.SUBJECT, "user");
|
||||
attributes.put(OAuth2IntrospectionClaimNames.SCOPE, "read");
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private Collection<GrantedAuthority> defaultAuthorities() {
|
||||
Map<String, Object> attributes = this.attributes.get();
|
||||
Object scope = attributes.get(OAuth2IntrospectionClaimNames.SCOPE);
|
||||
if (scope == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (scope instanceof Collection) {
|
||||
return getAuthorities((Collection) scope);
|
||||
}
|
||||
String scopes = scope.toString();
|
||||
if (StringUtils.isBlank(scopes)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return getAuthorities(Arrays.asList(scopes.split(" ")));
|
||||
}
|
||||
|
||||
private OAuth2AuthenticatedPrincipal defaultPrincipal() {
|
||||
return new DefaultOAuth2AuthenticatedPrincipal
|
||||
(this.attributes.get(), this.authorities.get());
|
||||
}
|
||||
|
||||
private Collection<GrantedAuthority> getAuthorities(Collection<?> scopes) {
|
||||
return scopes.stream()
|
||||
.map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private OAuth2AccessToken getOAuth2AccessToken(OAuth2AuthenticatedPrincipal principal) {
|
||||
Instant expiresAt = getInstant(principal.getAttributes(), "exp");
|
||||
Instant issuedAt = getInstant(principal.getAttributes(), "iat");
|
||||
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
"token", issuedAt, expiresAt);
|
||||
}
|
||||
|
||||
private Instant getInstant(Map<String, Object> attributes, String name) {
|
||||
Object value = attributes.get(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Instant) {
|
||||
return (Instant) value;
|
||||
}
|
||||
throw new IllegalArgumentException(name + " attribute must be of type Instant");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
* @since 5.3
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.test.web.reactive.server;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
|
||||
import org.springframework.security.web.reactive.result.method.annotation.CurrentSecurityContextArgumentResolver;
|
||||
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals.active;
|
||||
import static org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames.SUBJECT;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOpaqueToken;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
|
||||
|
||||
/**
|
||||
* @author Josh Cummings
|
||||
* @since 5.3
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SecurityMockServerConfigurerOpaqueTokenTests extends AbstractMockServerConfigurersTests {
|
||||
private GrantedAuthority authority1 = new SimpleGrantedAuthority("one");
|
||||
|
||||
private GrantedAuthority authority2 = new SimpleGrantedAuthority("two");
|
||||
|
||||
private WebTestClient client = WebTestClient
|
||||
.bindToController(securityContextController)
|
||||
.webFilter(new SecurityContextServerWebExchangeWebFilter())
|
||||
.argumentResolvers(resolvers -> resolvers.addCustomResolver(
|
||||
new CurrentSecurityContextArgumentResolver(new ReactiveAdapterRegistry())))
|
||||
.apply(springSecurity())
|
||||
.configureClient()
|
||||
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void mockOpaqueTokenWhenUsingDefaultsThenBearerTokenAuthentication() {
|
||||
this.client
|
||||
.mutateWith(mockOpaqueToken())
|
||||
.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
SecurityContext context = securityContextController.removeSecurityContext();
|
||||
assertThat(context.getAuthentication()).isInstanceOf(
|
||||
BearerTokenAuthentication.class);
|
||||
BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
|
||||
assertThat(token.getAuthorities()).isNotEmpty();
|
||||
assertThat(token.getToken()).isNotNull();
|
||||
assertThat(token.getTokenAttributes().get(SUBJECT)).isEqualTo("user");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mockOpaqueTokenWhenAuthoritiesThenBearerTokenAuthentication() {
|
||||
this.client
|
||||
.mutateWith(mockOpaqueToken()
|
||||
.authorities(this.authority1, this.authority2))
|
||||
.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
SecurityContext context = securityContextController.removeSecurityContext();
|
||||
assertThat((List<GrantedAuthority>) context.getAuthentication().getAuthorities())
|
||||
.containsOnly(this.authority1, this.authority2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mockOpaqueTokenWhenScopesThenBearerTokenAuthentication() {
|
||||
this.client
|
||||
.mutateWith(mockOpaqueToken().scopes("scoped", "authorities"))
|
||||
.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
SecurityContext context = securityContextController.removeSecurityContext();
|
||||
assertThat((List<GrantedAuthority>) context.getAuthentication().getAuthorities())
|
||||
.containsOnly(new SimpleGrantedAuthority("SCOPE_scoped"),
|
||||
new SimpleGrantedAuthority("SCOPE_authorities"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mockOpaqueTokenWhenAttributesThenBearerTokenAuthentication() {
|
||||
String sub = new String("my-subject");
|
||||
this.client
|
||||
.mutateWith(mockOpaqueToken()
|
||||
.attributes(attributes -> attributes.put(SUBJECT, sub)))
|
||||
.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
SecurityContext context = securityContextController.removeSecurityContext();
|
||||
assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
|
||||
BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
|
||||
assertThat(token.getTokenAttributes().get(SUBJECT)).isSameAs(sub);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mockOpaqueTokenWhenPrincipalThenBearerTokenAuthentication() {
|
||||
OAuth2AuthenticatedPrincipal principal = active();
|
||||
this.client
|
||||
.mutateWith(mockOpaqueToken()
|
||||
.principal(principal))
|
||||
.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
SecurityContext context = securityContextController.removeSecurityContext();
|
||||
assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
|
||||
BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
|
||||
assertThat(token.getPrincipal()).isSameAs(principal);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mockOpaqueTokenWhenPrincipalSpecifiedThenLastCalledTakesPrecedence() {
|
||||
OAuth2AuthenticatedPrincipal principal = active(a -> a.put("scope", "user"));
|
||||
|
||||
this.client
|
||||
.mutateWith(mockOpaqueToken()
|
||||
.attributes(a -> a.put(SUBJECT, "foo"))
|
||||
.principal(principal))
|
||||
.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
SecurityContext context = securityContextController.removeSecurityContext();
|
||||
assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
|
||||
BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
|
||||
assertThat((String) ((OAuth2AuthenticatedPrincipal) token.getPrincipal()).getAttribute(SUBJECT))
|
||||
.isEqualTo(principal.getAttribute(SUBJECT));
|
||||
|
||||
this.client
|
||||
.mutateWith(mockOpaqueToken()
|
||||
.principal(principal)
|
||||
.attributes(a -> a.put(SUBJECT, "bar")))
|
||||
.get()
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
context = securityContextController.removeSecurityContext();
|
||||
assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
|
||||
token = (BearerTokenAuthentication) context.getAuthentication();
|
||||
assertThat((String) ((OAuth2AuthenticatedPrincipal) token.getPrincipal()).getAttribute(SUBJECT))
|
||||
.isEqualTo("bar");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue