Skip to content

Added support for Anonymous Authentication #6198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public enum SecurityWebFiltersOrder {
*/
FORM_LOGIN,
AUTHENTICATION,
/**
* Instance of AnonymousAuthenticationWebFilter
*/
ANONYMOUS_AUTHENTICATION,
OAUTH2_AUTHORIZATION_CODE,
LOGIN_PAGE_GENERATING,
LOGOUT_PAGE_GENERATING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.UUID;

import reactor.core.publisher.Mono;
import reactor.util.context.Context;
Expand Down Expand Up @@ -158,6 +159,9 @@
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;

/**
* A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux.
Expand Down Expand Up @@ -264,6 +268,8 @@ public class ServerHttpSecurity {

private Throwable built;

private AnonymousSpec anonymous;

/**
* The ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance.
*
Expand Down Expand Up @@ -425,6 +431,29 @@ public CorsSpec cors() {
return this.cors;
}

/**
* @since 5.2.0
* @author Ankur Pathak
* Enables and Configures annonymous authentication. Anonymous Authentication is disabled by default.
*
* <pre class="code">
* &#064;Bean
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
* http
* // ...
* .anonymous().key("key")
* .authorities("ROLE_ANONYMOUS");
* return http.build();
* }
* </pre>
*/
public AnonymousSpec anonymous(){
if (this.anonymous == null) {
this.anonymous = new AnonymousSpec();
}
return this.anonymous;
}

/**
* Configures CORS support within Spring Security. This ensures that the {@link CorsWebFilter} is place in the
* correct order.
Expand Down Expand Up @@ -1356,6 +1385,9 @@ public SecurityWebFilterChain build() {
if (this.client != null) {
this.client.configure(this);
}
if (this.anonymous != null) {
this.anonymous.configure(this);
}
this.loginPage.configure(this);
if (this.logout != null) {
this.logout.configure(this);
Expand Down Expand Up @@ -2589,4 +2621,124 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
.subscriberContext(Context.of(ServerWebExchange.class, exchange));
}
}

/**
* Configures annonymous authentication
* @author Ankur Pathak
* @since 5.2.0
*/
public final class AnonymousSpec {
private String key;
private AnonymousAuthenticationWebFilter authenticationFilter;
private Object principal = "anonymousUser";
private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS");

/**
* Sets the key to identify tokens created for anonymous authentication. Default is a
* secure randomly generated key.
*
* @param key the key to identify tokens created for anonymous authentication. Default
* is a secure randomly generated key.
* @return the {@link AnonymousSpec} for further customization of anonymous
* authentication
*/
public AnonymousSpec key(String key) {
this.key = key;
return this;
}

/**
* Sets the principal for {@link Authentication} objects of anonymous users
*
* @param principal used for the {@link Authentication} object of anonymous users
* @return the {@link AnonymousSpec} for further customization of anonymous
* authentication
*/
public AnonymousSpec principal(Object principal) {
this.principal = principal;
return this;
}

/**
* Sets the {@link org.springframework.security.core.Authentication#getAuthorities()}
* for anonymous users
*
* @param authorities Sets the
* {@link org.springframework.security.core.Authentication#getAuthorities()} for
* anonymous users
* @return the {@link AnonymousSpec} for further customization of anonymous
* authentication
*/
public AnonymousSpec authorities(List<GrantedAuthority> authorities) {
this.authorities = authorities;
return this;
}

/**
* Sets the {@link org.springframework.security.core.Authentication#getAuthorities()}
* for anonymous users
*
* @param authorities Sets the
* {@link org.springframework.security.core.Authentication#getAuthorities()} for
* anonymous users (i.e. "ROLE_ANONYMOUS")
* @return the {@link AnonymousSpec} for further customization of anonymous
* authentication
*/
public AnonymousSpec authorities(String... authorities) {
return authorities(AuthorityUtils.createAuthorityList(authorities));
}

/**
* Sets the {@link AnonymousAuthenticationWebFilter} used to populate an anonymous user.
* If this is set, no attributes on the {@link AnonymousSpec} will be set on the
* {@link AnonymousAuthenticationWebFilter}.
*
* @param authenticationFilter the {@link AnonymousAuthenticationWebFilter} used to
* populate an anonymous user.
*
* @return the {@link AnonymousSpec} for further customization of anonymous
* authentication
*/
public AnonymousSpec authenticationFilter(
AnonymousAuthenticationWebFilter authenticationFilter) {
this.authenticationFilter = authenticationFilter;
return this;
}

/**
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
* @return the {@link ServerHttpSecurity} to continue configuring
*/
public ServerHttpSecurity and() {
return ServerHttpSecurity.this;
}

/**
* Disables anonymous authentication.
* @return the {@link ServerHttpSecurity} to continue configuring
*/
public ServerHttpSecurity disable() {
ServerHttpSecurity.this.anonymous = null;
return ServerHttpSecurity.this;
}

protected void configure(ServerHttpSecurity http) {
if (authenticationFilter == null) {
authenticationFilter = new AnonymousAuthenticationWebFilter(getKey(), principal,
authorities);
}
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.ANONYMOUS_AUTHENTICATION);
}

private String getKey() {
if (key == null) {
key = UUID.randomUUID().toString();
}
return key;
}


private AnonymousSpec() {}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.test.publisher.TestPublisher;

Expand Down Expand Up @@ -63,6 +61,9 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;

/**
* @author Rob Winch
Expand Down Expand Up @@ -216,6 +217,44 @@ public void addFilterBeforeIsApplied(){

}

@Test
public void anonymous(){
SecurityWebFilterChain securityFilterChain = this.http.anonymous().and().build();
WebTestClient client = WebTestClientBuilder.bindToControllerAndWebFilters(AnonymousAuthenticationWebFilterTests.HttpMeController.class,
securityFilterChain).build();

client.get()
.uri("/me")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("anonymousUser");

}

@Test
public void basicWithAnonymous() {
given(this.authenticationManager.authenticate(any())).willReturn(Mono.just(new TestingAuthenticationToken("rob", "rob", "ROLE_USER", "ROLE_ADMIN")));

this.http.securityContextRepository(new WebSessionServerSecurityContextRepository());
this.http.httpBasic().and().anonymous();
this.http.authenticationManager(this.authenticationManager);
ServerHttpSecurity.AuthorizeExchangeSpec authorize = this.http.authorizeExchange();
authorize.anyExchange().hasAuthority("ROLE_ADMIN");

WebTestClient client = buildClient();

EntityExchangeResult<String> result = client.get()
.uri("/")
.headers(headers -> headers.setBasicAuth("rob", "rob"))
.exchange()
.expectStatus().isOk()
.expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+")
.expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok"))
.returnResult();

assertThat(result.getResponseCookies().getFirst("SESSION")).isNull();
}

private <T extends WebFilter> Optional<T> getWebFilter(SecurityWebFilterChain filterChain, Class<T> filterClass) {
return (Optional<T>) filterChain.getWebFilters()
.filter(Objects::nonNull)
Expand All @@ -242,7 +281,6 @@ Mono<String> pathWithinApplicationFromContext() {
}

private static class TestWebFilter implements WebFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.web.server.authentication;

import java.util.List;

import reactor.core.publisher.Mono;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;

/**
* Detects if there is no {@code Authentication} object in the
* {@code ReactiveSecurityContextHolder}, and populates it with one if needed.
*
* @author Ankur Pathak
* @since 5.2.0
*/
public class AnonymousAuthenticationWebFilter implements WebFilter {
// ~ Instance fields
// ================================================================================================

private String key;
private Object principal;
private List<GrantedAuthority> authorities;

/**
* Creates a filter with a principal named "anonymousUser" and the single authority
* "ROLE_ANONYMOUS".
*
* @param key the key to identify tokens created by this filter
*/
public AnonymousAuthenticationWebFilter(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}

/**
* @param key key the key to identify tokens created by this filter
* @param principal the principal which will be used to represent anonymous users
* @param authorities the authority list for anonymous users
*/
public AnonymousAuthenticationWebFilter(String key, Object principal,
List<GrantedAuthority> authorities) {
Assert.hasLength(key, "key cannot be null or empty");
Assert.notNull(principal, "Anonymous authentication principal must be set");
Assert.notNull(authorities, "Anonymous authorities must be set");
this.key = key;
this.principal = principal;
this.authorities = authorities;
}


@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.switchIfEmpty(Mono.defer(() -> {
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(createAuthentication(exchange));
return chain.filter(exchange)
.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
.then(Mono.empty());
})).flatMap(securityContext -> chain.filter(exchange));

}

protected Authentication createAuthentication(ServerWebExchange exchange) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
return auth;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ public static Builder bindToWebFilters(SecurityWebFilterChain securityWebFilterC
return bindToWebFilters(new WebFilterChainProxy(securityWebFilterChain));
}

public static Builder bindToControllerAndWebFilters(Class<?> controller, WebFilter... webFilters) {
return WebTestClient.bindToController(controller).webFilter(webFilters).configureClient();
}

public static Builder bindToControllerAndWebFilters(Class<?> controller, SecurityWebFilterChain securityWebFilterChain) {
return bindToControllerAndWebFilters(controller, new WebFilterChainProxy(securityWebFilterChain));
}

@RestController
public static class Http200RestController {
@RequestMapping("/**")
Expand All @@ -51,4 +59,5 @@ public String ok() {
return "ok";
}
}

}
Loading