From a0f4fff90ea9d550a7b3568f4430ce07cba9f032 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Mon, 22 Jul 2019 09:31:10 -0400 Subject: [PATCH] Support nested builder in DSL for reactive apps Fixes: gh-7107 --- .../config/web/server/ServerHttpSecurity.java | 678 ++++++++++++++++++ .../server/AuthorizeExchangeSpecTests.java | 45 +- .../config/web/server/CorsSpecTests.java | 10 +- .../server/ExceptionHandlingSpecTests.java | 103 ++- .../config/web/server/FormLoginTests.java | 78 ++ .../config/web/server/HeaderSpecTests.java | 219 +++++- .../web/server/HttpsRedirectSpecTests.java | 99 ++- .../config/web/server/LogoutSpecTests.java | 49 +- .../web/server/OAuth2ClientSpecTests.java | 44 ++ .../config/web/server/OAuth2LoginTests.java | 81 +++ .../server/OAuth2ResourceServerSpecTests.java | 179 +++++ .../config/web/server/RequestCacheTests.java | 35 + .../web/server/ServerHttpSecurityTests.java | 123 ++++ .../asciidoc/_includes/reactive/cors.adoc | 4 +- .../asciidoc/_includes/reactive/headers.adoc | 120 ++-- .../asciidoc/_includes/reactive/method.adoc | 9 +- .../reactive/oauth2/access-token.adoc | 2 +- .../_includes/reactive/oauth2/login.adoc | 18 +- .../reactive/oauth2/resource-server.adoc | 124 ++-- .../_includes/reactive/redirect-https.adoc | 12 +- .../asciidoc/_includes/reactive/webflux.adoc | 13 +- .../asciidoc/_includes/reactive/x509.adoc | 60 +- .../src/main/java/sample/SecurityConfig.java | 13 +- .../src/main/java/sample/SecurityConfig.java | 19 +- .../java/sample/config/SecurityConfig.java | 21 +- .../sample/WebfluxFormSecurityConfig.java | 23 +- .../java/sample/WebfluxX509Application.java | 15 +- 27 files changed, 1991 insertions(+), 205 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 6d89dadc24d..a8b21aaa8d5 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -32,6 +32,7 @@ import java.util.function.Function; import java.util.function.Supplier; +import org.springframework.security.config.Customizer; import reactor.core.publisher.Mono; import reactor.util.context.Context; @@ -394,6 +395,48 @@ public HttpsRedirectSpec redirectToHttps() { return this.httpsRedirectSpec; } + /** + * Configures HTTPS redirection rules. If the default is used: + * + *
+	 *  @Bean
+	 * 	public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	 * 	    http
+	 * 	        // ...
+	 * 	        .redirectToHttps(withDefaults());
+	 * 	    return http.build();
+	 * 	}
+	 * 
+ * + * Then all non-HTTPS requests will be redirected to HTTPS. + * + * Typically, all requests should be HTTPS; however, the focus for redirection can also be narrowed: + * + *
+	 *  @Bean
+	 * 	public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 * 	    http
+	 * 	        // ...
+	 * 	        .redirectToHttps(redirectToHttps ->
+	 * 	        	redirectToHttps
+	 * 	            	.httpsRedirectWhen(serverWebExchange ->
+	 * 	            		serverWebExchange.getRequest().getHeaders().containsKey("X-Requires-Https"))
+	 * 	            );
+	 * 	    return http.build();
+	 * 	}
+	 * 
+ * + * @param httpsRedirectCustomizer the {@link Customizer} to provide more options for + * the {@link HttpsRedirectSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity redirectToHttps(Customizer httpsRedirectCustomizer) throws Exception { + this.httpsRedirectSpec = new HttpsRedirectSpec(); + httpsRedirectCustomizer.customize(this.httpsRedirectSpec); + return this; + } + /** * Configures CSRF Protection * which is enabled by default. You can disable it using: @@ -436,6 +479,56 @@ public CsrfSpec csrf() { return this.csrf; } + /** + * Configures CSRF Protection + * which is enabled by default. You can disable it using: + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .csrf(csrf ->
+	 *              csrf.disabled()
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * Additional configuration options can be seen below: + * + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .csrf(csrf ->
+	 *              csrf
+	 *                  // Handle CSRF failures
+	 *                  .accessDeniedHandler(accessDeniedHandler)
+	 *                  // Custom persistence of CSRF Token
+	 *                  .csrfTokenRepository(csrfTokenRepository)
+	 *                  // custom matching when CSRF protection is enabled
+	 *                  .requireCsrfProtectionMatcher(matcher)
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param csrfCustomizer the {@link Customizer} to provide more options for + * the {@link CsrfSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity csrf(Customizer csrfCustomizer) throws Exception { + if (this.csrf == null) { + this.csrf = new CsrfSpec(); + } + csrfCustomizer.customize(this.csrf); + return this; + } + /** * Configures CORS headers. By default if a {@link CorsConfigurationSource} Bean is found, it will be used * to create a {@link CorsWebFilter}. If {@link CorsSpec#configurationSource(CorsConfigurationSource)} is invoked @@ -449,6 +542,24 @@ public CorsSpec cors() { return this.cors; } + /** + * Configures CORS headers. By default if a {@link CorsConfigurationSource} Bean is found, it will be used + * to create a {@link CorsWebFilter}. If {@link CorsSpec#configurationSource(CorsConfigurationSource)} is invoked + * it will be used instead. If neither has been configured, the Cors configuration will do nothing. + * + * @param corsCustomizer the {@link Customizer} to provide more options for + * the {@link CorsSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity cors(Customizer corsCustomizer) throws Exception { + if (this.cors == null) { + this.cors = new CorsSpec(); + } + corsCustomizer.customize(this.cors); + return this; + } + /** * Enables and Configures anonymous authentication. Anonymous Authentication is disabled by default. * @@ -473,6 +584,36 @@ public AnonymousSpec anonymous(){ return this.anonymous; } + /** + * Enables and Configures anonymous authentication. Anonymous Authentication is disabled by default. + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .anonymous(anonymous ->
+	 *              anonymous
+	 *                  .key("key")
+	 *                  .authorities("ROLE_ANONYMOUS")
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param anonymousCustomizer the {@link Customizer} to provide more options for + * the {@link AnonymousSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity anonymous(Customizer anonymousCustomizer) throws Exception { + if (this.anonymous == null) { + this.anonymous = new AnonymousSpec(); + } + anonymousCustomizer.customize(this.anonymous); + return this; + } + /** * Configures CORS support within Spring Security. This ensures that the {@link CorsWebFilter} is place in the * correct order. @@ -560,6 +701,38 @@ public HttpBasicSpec httpBasic() { return this.httpBasic; } + /** + * Configures HTTP Basic authentication. An example configuration is provided below: + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .httpBasic(httpBasic ->
+	 *              httpBasic
+	 *                  // used for authenticating the credentials
+	 *                  .authenticationManager(authenticationManager)
+	 *                  // Custom persistence of the authentication
+	 *                  .securityContextRepository(securityContextRepository)
+	 *              );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param httpBasicCustomizer the {@link Customizer} to provide more options for + * the {@link HttpBasicSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity httpBasic(Customizer httpBasicCustomizer) throws Exception { + if (this.httpBasic == null) { + this.httpBasic = new HttpBasicSpec(); + } + httpBasicCustomizer.customize(this.httpBasic); + return this; + } + /** * Configures form based authentication. An example configuration is provided below: * @@ -590,6 +763,42 @@ public FormLoginSpec formLogin() { return this.formLogin; } + /** + * Configures form based authentication. An example configuration is provided below: + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .formLogin(formLogin ->
+	 *              formLogin
+	 *              	// used for authenticating the credentials
+	 *              	.authenticationManager(authenticationManager)
+	 *              	// Custom persistence of the authentication
+	 *              	.securityContextRepository(securityContextRepository)
+	 *              	// expect a log in page at "/authenticate"
+	 *              	// a POST "/authenticate" is where authentication occurs
+	 *              	// error page at "/authenticate?error"
+	 *              	.loginPage("/authenticate")
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param formLoginCustomizer the {@link Customizer} to provide more options for + * the {@link FormLoginSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity formLogin(Customizer formLoginCustomizer) throws Exception { + if (this.formLogin == null) { + this.formLogin = new FormLoginSpec(); + } + formLoginCustomizer.customize(this.formLogin); + return this; + } + /** * Configures x509 authentication using a certificate provided by a client. * @@ -619,6 +828,39 @@ public X509Spec x509() { return this.x509; } + /** + * Configures x509 authentication using a certificate provided by a client. + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          .x509(x509 ->
+	 *              x509
+	 *          	    .authenticationManager(authenticationManager)
+	 *                  .principalExtractor(principalExtractor)
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * Note that if extractor is not specified, {@link SubjectDnX509PrincipalExtractor} will be used. + * If authenticationManager is not specified, {@link ReactivePreAuthenticatedAuthenticationManager} will be used. + * + * @since 5.2 + * @param x509Customizer the {@link Customizer} to provide more options for + * the {@link X509Spec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity x509(Customizer x509Customizer) throws Exception { + if (this.x509 == null) { + this.x509 = new X509Spec(); + } + x509Customizer.customize(this.x509); + return this; + } + /** * Configures X509 authentication * @@ -702,6 +944,36 @@ public OAuth2LoginSpec oauth2Login() { return this.oauth2Login; } + /** + * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .oauth2Login(oauth2Login ->
+	 *              oauth2Login
+	 *                  .authenticationConverter(authenticationConverter)
+	 *                  .authenticationManager(manager)
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param oauth2LoginCustomizer the {@link Customizer} to provide more options for + * the {@link OAuth2LoginSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity oauth2Login(Customizer oauth2LoginCustomizer) throws Exception { + if (this.oauth2Login == null) { + this.oauth2Login = new OAuth2LoginSpec(); + } + oauth2LoginCustomizer.customize(this.oauth2Login); + return this; + } + public class OAuth2LoginSpec { private ReactiveClientRegistrationRepository clientRegistrationRepository; @@ -995,6 +1267,36 @@ public OAuth2ClientSpec oauth2Client() { return this.client; } + /** + * Configures the OAuth2 client. + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .oauth2Client(oauth2Client ->
+	 *              oauth2Client
+	 *                  .clientRegistrationRepository(clientRegistrationRepository)
+	 *                  .authorizedClientRepository(authorizedClientRepository)
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param oauth2ClientCustomizer the {@link Customizer} to provide more options for + * the {@link OAuth2ClientSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity oauth2Client(Customizer oauth2ClientCustomizer) throws Exception { + if (this.client == null) { + this.client = new OAuth2ClientSpec(); + } + oauth2ClientCustomizer.customize(this.client); + return this; + } + public class OAuth2ClientSpec { private ReactiveClientRegistrationRepository clientRegistrationRepository; @@ -1145,6 +1447,39 @@ public OAuth2ResourceServerSpec oauth2ResourceServer() { return this.resourceServer; } + /** + * Configures OAuth 2.0 Resource Server support. + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .oauth2ResourceServer(oauth2ResourceServer ->
+	 *              oauth2ResourceServer
+	 *                  .jwt(jwt ->
+	 *                      jwt
+	 *                          .publicKey(publicKey())
+	 *                  )
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param oauth2ResourceServerCustomizer the {@link Customizer} to provide more options for + * the {@link OAuth2ResourceServerSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity oauth2ResourceServer(Customizer oauth2ResourceServerCustomizer) + throws Exception { + if (this.resourceServer == null) { + this.resourceServer = new OAuth2ResourceServerSpec(); + } + oauth2ResourceServerCustomizer.customize(this.resourceServer); + return this; + } + /** * Configures OAuth2 Resource Server Support */ @@ -1228,6 +1563,22 @@ public JwtSpec jwt() { return this.jwt; } + /** + * Enables JWT Resource Server support. + * + * @param jwtCustomizer the {@link Customizer} to provide more options for + * the {@link JwtSpec} + * @return the {@link OAuth2ResourceServerSpec} to customize + * @throws Exception + */ + public OAuth2ResourceServerSpec jwt(Customizer jwtCustomizer) throws Exception { + if (this.jwt == null) { + this.jwt = new JwtSpec(); + } + jwtCustomizer.customize(this.jwt); + return this; + } + /** * Enables Opaque Token Resource Server support. * @@ -1240,6 +1591,22 @@ public OpaqueTokenSpec opaqueToken() { return this.opaqueToken; } + /** + * Enables Opaque Token Resource Server support. + * + * @param opaqueTokenCustomizer the {@link Customizer} to provide more options for + * the {@link OpaqueTokenSpec} + * @return the {@link OAuth2ResourceServerSpec} to customize + * @throws Exception + */ + public OAuth2ResourceServerSpec opaqueToken(Customizer opaqueTokenCustomizer) throws Exception { + if (this.opaqueToken == null) { + this.opaqueToken = new OpaqueTokenSpec(); + } + opaqueTokenCustomizer.customize(this.opaqueToken); + return this; + } + protected void configure(ServerHttpSecurity http) { this.bearerTokenServerWebExchangeMatcher .setBearerTokenConverter(this.bearerTokenConverter); @@ -1561,6 +1928,58 @@ public HeaderSpec headers() { return this.headers; } + /** + * Configures HTTP Response Headers. The default headers are: + * + *
+	 * Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+	 * Pragma: no-cache
+	 * Expires: 0
+	 * X-Content-Type-Options: nosniff
+	 * Strict-Transport-Security: max-age=31536000 ; includeSubDomains
+	 * X-Frame-Options: DENY
+	 * X-XSS-Protection: 1; mode=block
+	 * 
+ * + * such that "Strict-Transport-Security" is only added on secure requests. + * + * An example configuration is provided below: + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .headers(headers ->
+	 *              headers
+	 *                  // customize frame options to be same origin
+	 *                  .frameOptions(frameOptions ->
+	 *                      frameOptions
+	 *                          .mode(XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN)
+	 *                   )
+	 *                  // disable cache control
+	 *                  .cache(cache ->
+	 *                      cache
+	 *                          .disable()
+	 *                  )
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param headerCustomizer the {@link Customizer} to provide more options for + * the {@link HeaderSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity headers(Customizer headerCustomizer) throws Exception { + if (this.headers == null) { + this.headers = new HeaderSpec(); + } + headerCustomizer.customize(this.headers); + return this; + } + /** * Configures exception handling (i.e. handles when authentication is requested). An example configuration can * be found below: @@ -1586,6 +2005,38 @@ public ExceptionHandlingSpec exceptionHandling() { return this.exceptionHandling; } + /** + * Configures exception handling (i.e. handles when authentication is requested). An example configuration can + * be found below: + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .exceptionHandling(exceptionHandling ->
+	 *              exceptionHandling
+	 *                  // customize how to request for authentication
+	 *                  .authenticationEntryPoint(entryPoint)
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param exceptionHandlingCustomizer the {@link Customizer} to provide more options for + * the {@link ExceptionHandlingSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity exceptionHandling(Customizer exceptionHandlingCustomizer) + throws Exception { + if (this.exceptionHandling == null) { + this.exceptionHandling = new ExceptionHandlingSpec(); + } + exceptionHandlingCustomizer.customize(this.exceptionHandling); + return this; + } + /** * Configures authorization. An example configuration can be found below: * @@ -1624,6 +2075,51 @@ public AuthorizeExchangeSpec authorizeExchange() { return this.authorizeExchange; } + /** + * Configures authorization. An example configuration can be found below: + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .authorizeExchange(exchanges ->
+	 *              exchanges
+	 *                  // any URL that starts with /admin/ requires the role "ROLE_ADMIN"
+	 *                  .pathMatchers("/admin/**").hasRole("ADMIN")
+	 *                  // a POST to /users requires the role "USER_POST"
+	 *                  .pathMatchers(HttpMethod.POST, "/users").hasAuthority("USER_POST")
+	 *                  // a request to /users/{username} requires the current authentication's username
+	 *                  // to be equal to the {username}
+	 *                  .pathMatchers("/users/{username}").access((authentication, context) ->
+	 *                      authentication
+	 *                          .map(Authentication::getName)
+	 *                          .map(username -> username.equals(context.getVariables().get("username")))
+	 *                          .map(AuthorizationDecision::new)
+	 *                  )
+	 *                  // allows providing a custom matching strategy that requires the role "ROLE_CUSTOM"
+	 *                  .matchers(customMatcher).hasRole("CUSTOM")
+	 *                  // any other request requires the user to be authenticated
+	 *                  .anyExchange().authenticated()
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param authorizeExchangeCustomizer the {@link Customizer} to provide more options for + * the {@link AuthorizeExchangeSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity authorizeExchange(Customizer authorizeExchangeCustomizer) + throws Exception { + if (this.authorizeExchange == null) { + this.authorizeExchange = new AuthorizeExchangeSpec(); + } + authorizeExchangeCustomizer.customize(this.authorizeExchange); + return this; + } + /** * Configures log out. An example configuration can be found below: * @@ -1651,6 +2147,40 @@ public LogoutSpec logout() { return this.logout; } + /** + * Configures log out. An example configuration can be found below: + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .logout(logout ->
+	 *              logout
+	 *                  // configures how log out is done
+	 *                  .logoutHandler(logoutHandler)
+	 *                  // log out will be performed on POST /signout
+	 *                  .logoutUrl("/signout")
+	 *                  // configure what is done on logout success
+	 *                  .logoutSuccessHandler(successHandler)
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param logoutCustomizer the {@link Customizer} to provide more options for + * the {@link LogoutSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity logout(Customizer logoutCustomizer) throws Exception { + if (this.logout == null) { + this.logout = new LogoutSpec(); + } + logoutCustomizer.customize(this.logout); + return this; + } + /** * Configures the request cache which is used when a flow is interrupted (i.e. due to requesting credentials) so * that the request can be replayed after authentication. An example configuration can be found below: @@ -1673,6 +2203,34 @@ public RequestCacheSpec requestCache() { return this.requestCache; } + /** + * Configures the request cache which is used when a flow is interrupted (i.e. due to requesting credentials) so + * that the request can be replayed after authentication. An example configuration can be found below: + * + *
+	 *  @Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
+	 *      http
+	 *          // ...
+	 *          .requestCache(requestCache ->
+	 *              requestCache
+	 *                  // configures how the request is cached
+	 *                  .requestCache(customRequestCache)
+	 *          );
+	 *      return http.build();
+	 *  }
+	 * 
+ * + * @param requestCacheCustomizer the {@link Customizer} to provide more options for + * the {@link RequestCacheSpec} + * @return the {@link ServerHttpSecurity} to customize + * @throws Exception + */ + public ServerHttpSecurity requestCache(Customizer requestCacheCustomizer) throws Exception { + requestCacheCustomizer.customize(this.requestCache); + return this; + } + /** * Configure the default authentication manager. * @param manager the authentication manager to use @@ -2549,6 +3107,19 @@ public CacheSpec cache() { return new CacheSpec(); } + /** + * Configures cache control headers + * + * @param cacheCustomizer the {@link Customizer} to provide more options for + * the {@link CacheSpec} + * @return the {@link HeaderSpec} to customize + * @throws Exception + */ + public HeaderSpec cache(Customizer cacheCustomizer) throws Exception { + cacheCustomizer.customize(new CacheSpec()); + return this; + } + /** * Configures content type response headers * @return the {@link ContentTypeOptionsSpec} to configure @@ -2557,6 +3128,20 @@ public ContentTypeOptionsSpec contentTypeOptions() { return new ContentTypeOptionsSpec(); } + /** + * Configures content type response headers + * + * @param contentTypeOptionsCustomizer the {@link Customizer} to provide more options for + * the {@link ContentTypeOptionsSpec} + * @return the {@link HeaderSpec} to customize + * @throws Exception + */ + public HeaderSpec contentTypeOptions(Customizer contentTypeOptionsCustomizer) + throws Exception { + contentTypeOptionsCustomizer.customize(new ContentTypeOptionsSpec()); + return this; + } + /** * Configures frame options response headers * @return the {@link FrameOptionsSpec} to configure @@ -2565,6 +3150,19 @@ public FrameOptionsSpec frameOptions() { return new FrameOptionsSpec(); } + /** + * Configures frame options response headers + * + * @param frameOptionsCustomizer the {@link Customizer} to provide more options for + * the {@link FrameOptionsSpec} + * @return the {@link HeaderSpec} to customize + * @throws Exception + */ + public HeaderSpec frameOptions(Customizer frameOptionsCustomizer) throws Exception { + frameOptionsCustomizer.customize(new FrameOptionsSpec()); + return this; + } + /** * Configures the Strict Transport Security response headers * @return the {@link HstsSpec} to configure @@ -2573,6 +3171,19 @@ public HstsSpec hsts() { return new HstsSpec(); } + /** + * Configures the Strict Transport Security response headers + * + * @param hstsCustomizer the {@link Customizer} to provide more options for + * the {@link HstsSpec} + * @return the {@link HeaderSpec} to customize + * @throws Exception + */ + public HeaderSpec hsts(Customizer hstsCustomizer) throws Exception { + hstsCustomizer.customize(new HstsSpec()); + return this; + } + protected void configure(ServerHttpSecurity http) { ServerHttpHeadersWriter writer = new CompositeServerHttpHeadersWriter(this.writers); HttpHeaderWriterWebFilter result = new HttpHeaderWriterWebFilter(writer); @@ -2587,6 +3198,19 @@ public XssProtectionSpec xssProtection() { return new XssProtectionSpec(); } + /** + * Configures x-xss-protection response header. + * + * @param xssProtectionCustomizer the {@link Customizer} to provide more options for + * the {@link XssProtectionSpec} + * @return the {@link HeaderSpec} to customize + * @throws Exception + */ + public HeaderSpec xssProtection(Customizer xssProtectionCustomizer) throws Exception { + xssProtectionCustomizer.customize(new XssProtectionSpec()); + return this; + } + /** * Configures {@code Content-Security-Policy} response header. * @param policyDirectives the policy directive(s) @@ -2596,6 +3220,20 @@ public ContentSecurityPolicySpec contentSecurityPolicy(String policyDirectives) return new ContentSecurityPolicySpec(policyDirectives); } + /** + * Configures {@code Content-Security-Policy} response header. + * + * @param contentSecurityPolicyCustomizer the {@link Customizer} to provide more options for + * the {@link ContentSecurityPolicySpec} + * @return the {@link HeaderSpec} to customize + * @throws Exception + */ + public HeaderSpec contentSecurityPolicy(Customizer contentSecurityPolicyCustomizer) + throws Exception { + contentSecurityPolicyCustomizer.customize(new ContentSecurityPolicySpec()); + return this; + } + /** * Configures {@code Feature-Policy} response header. * @param policyDirectives the policy directive(s) @@ -2622,6 +3260,20 @@ public ReferrerPolicySpec referrerPolicy() { return new ReferrerPolicySpec(); } + /** + * Configures {@code Referrer-Policy} response header. + * + * @param referrerPolicyCustomizer the {@link Customizer} to provide more options for + * the {@link ReferrerPolicySpec} + * @return the {@link HeaderSpec} to customize + * @throws Exception + */ + public HeaderSpec referrerPolicy(Customizer referrerPolicyCustomizer) + throws Exception { + referrerPolicyCustomizer.customize(new ReferrerPolicySpec()); + return this; + } + /** * Configures cache control headers * @see #cache() @@ -2781,6 +3433,7 @@ private XssProtectionSpec() {} * @since 5.1 */ public class ContentSecurityPolicySpec { + private static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'"; /** * Whether to include the {@code Content-Security-Policy-Report-Only} header in @@ -2793,6 +3446,17 @@ public HeaderSpec reportOnly(boolean reportOnly) { return HeaderSpec.this; } + /** + * Sets the security policy directive(s) to be used in the response header. + * + * @param policyDirectives the security policy directive(s) + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec policyDirectives(String policyDirectives) { + HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives); + return HeaderSpec.this; + } + /** * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. @@ -2806,6 +3470,9 @@ private ContentSecurityPolicySpec(String policyDirectives) { HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives); } + private ContentSecurityPolicySpec() { + HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(DEFAULT_SRC_SELF_POLICY); + } } /** @@ -2840,6 +3507,17 @@ private FeaturePolicySpec(String policyDirectives) { */ public class ReferrerPolicySpec { + /** + * Sets the policy to be used in the response header. + * + * @param referrerPolicy a referrer policy + * @return the {@link ReferrerPolicySpec} to continue configuring + */ + public ReferrerPolicySpec policy(ReferrerPolicy referrerPolicy) { + HeaderSpec.this.referrerPolicy.setPolicy(referrerPolicy); + return this; + } + /** * Allows method chaining to continue configuring the * {@link ServerHttpSecurity}. diff --git a/config/src/test/java/org/springframework/security/config/web/server/AuthorizeExchangeSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/AuthorizeExchangeSpecTests.java index a645d2b70e6..bfc9b3a0777 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/AuthorizeExchangeSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/AuthorizeExchangeSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -92,6 +92,39 @@ public void antMatchersWhenPatternsThenAnyMethod() { .expectStatus().isUnauthorized(); } + @Test + public void antMatchersWhenPatternsInLambdaThenAnyMethod() throws Exception { + this.http + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .authorizeExchange(exchanges -> + exchanges + .pathMatchers("/a", "/b").denyAll() + .anyExchange().permitAll() + ); + + WebTestClient client = buildClient(); + + client.get() + .uri("/a") + .exchange() + .expectStatus().isUnauthorized(); + + client.get() + .uri("/b") + .exchange() + .expectStatus().isUnauthorized(); + + client.post() + .uri("/a") + .exchange() + .expectStatus().isUnauthorized(); + + client.post() + .uri("/b") + .exchange() + .expectStatus().isUnauthorized(); + } + @Test(expected = IllegalStateException.class) public void antMatchersWhenNoAccessAndAnotherMatcherThenThrowsException() { this.http @@ -117,6 +150,16 @@ public void buildWhenMatcherDefinedWithNoAccessThenThrowsException() { this.http.build(); } + @Test(expected = IllegalStateException.class) + public void buildWhenMatcherDefinedWithNoAccessInLambdaThenThrowsException() throws Exception { + this.http + .authorizeExchange(exchanges -> + exchanges + .pathMatchers("/incomplete") + ); + this.http.build(); + } + private WebTestClient buildClient() { return WebTestClientBuilder.bindToWebFilters(this.http.build()).build(); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java index 45dc46c29b8..7689f539cbe 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/CorsSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -74,6 +74,14 @@ public void corsWhenEnabledThenAccessControlAllowOriginAndSecurityHeaders() { assertHeaders(); } + @Test + public void corsWhenEnabledInLambdaThenAccessControlAllowOriginAndSecurityHeaders() throws Exception { + this.http.cors(cors -> cors.configurationSource(this.source)); + this.expectedHeaders.set("Access-Control-Allow-Origin", "*"); + this.expectedHeaders.set("X-Frame-Options", "DENY"); + assertHeaders(); + } + @Test public void corsWhenCorsConfigurationSourceBeanThenAccessControlAllowOriginAndSecurityHeaders() { when(this.context.getBeanNamesForType(any(ResolvableType.class))).thenReturn(new String[] {"source"}, new String[0]); diff --git a/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java index 8421504cbb3..1b0dcce0e16 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/ExceptionHandlingSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -26,6 +26,8 @@ import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.test.web.reactive.server.WebTestClient; +import static org.springframework.security.config.Customizer.withDefaults; + /** * @author Denys Ivano * @since 5.0.5 @@ -56,6 +58,29 @@ public void defaultAuthenticationEntryPoint() { .expectHeader().valueMatches("WWW-Authenticate", "Basic.*"); } + @Test + public void requestWhenExceptionHandlingWithDefaultsInLambdaThenDefaultAuthenticationEntryPointUsed() + throws Exception { + SecurityWebFilterChain securityWebFilter = this.http + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ) + .exceptionHandling(withDefaults()) + .build(); + + WebTestClient client = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + client + .get() + .uri("/test") + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().valueMatches("WWW-Authenticate", "Basic.*"); + } + @Test public void customAuthenticationEntryPoint() { SecurityWebFilterChain securityWebFilter = this.http @@ -80,6 +105,31 @@ public void customAuthenticationEntryPoint() { .expectHeader().valueMatches("Location", ".*"); } + @Test + public void requestWhenCustomAuthenticationEntryPointInLambdaThenCustomAuthenticationEntryPointUsed() throws Exception { + SecurityWebFilterChain securityWebFilter = this.http + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ) + .exceptionHandling(exceptionHandling -> + exceptionHandling + .authenticationEntryPoint(redirectServerAuthenticationEntryPoint("/auth")) + ) + .build(); + + WebTestClient client = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + client + .get() + .uri("/test") + .exchange() + .expectStatus().isFound() + .expectHeader().valueMatches("Location", ".*"); + } + @Test public void defaultAccessDeniedHandler() { SecurityWebFilterChain securityWebFilter = this.http @@ -104,6 +154,30 @@ public void defaultAccessDeniedHandler() { .expectStatus().isForbidden(); } + @Test + public void requestWhenExceptionHandlingWithDefaultsInLambdaThenDefaultAccessDeniedHandlerUsed() + throws Exception { + SecurityWebFilterChain securityWebFilter = this.http + .httpBasic(withDefaults()) + .authorizeExchange(exchanges -> + exchanges + .anyExchange().hasRole("ADMIN") + ) + .exceptionHandling(withDefaults()) + .build(); + + WebTestClient client = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + client + .get() + .uri("/admin") + .headers(headers -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus().isForbidden(); + } + @Test public void customAccessDeniedHandler() { SecurityWebFilterChain securityWebFilter = this.http @@ -129,6 +203,33 @@ public void customAccessDeniedHandler() { .expectStatus().isBadRequest(); } + @Test + public void requestWhenCustomAccessDeniedHandlerInLambdaThenCustomAccessDeniedHandlerUsed() + throws Exception { + SecurityWebFilterChain securityWebFilter = this.http + .httpBasic(withDefaults()) + .authorizeExchange(exchanges -> + exchanges + .anyExchange().hasRole("ADMIN") + ) + .exceptionHandling(exceptionHandling -> + exceptionHandling + .accessDeniedHandler(httpStatusServerAccessDeniedHandler(HttpStatus.BAD_REQUEST)) + ) + .build(); + + WebTestClient client = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + client + .get() + .uri("/admin") + .headers(headers -> headers.setBasicAuth("user", "password")) + .exchange() + .expectStatus().isBadRequest(); + } + private ServerAuthenticationEntryPoint redirectServerAuthenticationEntryPoint(String location) { return new RedirectServerAuthenticationEntryPoint(location); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java index 96a174f3b69..ac85c3d6914 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/FormLoginTests.java @@ -46,6 +46,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.springframework.security.config.Customizer.withDefaults; /** * @author Rob Winch @@ -96,6 +97,49 @@ public void defaultLoginPage() { .assertLogout(); } + @Test + public void formLoginWhenDefaultsInLambdaThenCreatesDefaultLoginPage() throws Exception { + SecurityWebFilterChain securityWebFilter = this.http + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ) + .formLogin(withDefaults()) + .build(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + DefaultLoginPage loginPage = HomePage.to(driver, DefaultLoginPage.class) + .assertAt(); + + loginPage = loginPage.loginForm() + .username("user") + .password("invalid") + .submit(DefaultLoginPage.class) + .assertError(); + + HomePage homePage = loginPage.loginForm() + .username("user") + .password("password") + .submit(HomePage.class); + + homePage.assertAt(); + + loginPage = DefaultLogoutPage.to(driver) + .assertAt() + .logout(); + + loginPage + .assertAt() + .assertLogout(); + } + @Test public void customLoginPage() { SecurityWebFilterChain securityWebFilter = this.http @@ -128,6 +172,40 @@ public void customLoginPage() { homePage.assertAt(); } + @Test + public void formLoginWhenCustomLoginPageInLambdaThenUsed() throws Exception { + SecurityWebFilterChain securityWebFilter = this.http + .authorizeExchange(exchanges -> + exchanges + .pathMatchers("/login").permitAll() + .anyExchange().authenticated() + ) + .formLogin(formLogin -> + formLogin + .loginPage("/login") + ) + .build(); + + WebTestClient webTestClient = WebTestClient + .bindToController(new CustomLoginPageController(), new WebTestClientBuilder.Http200RestController()) + .webFilter(new WebFilterChainProxy(securityWebFilter)) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + CustomLoginPage loginPage = HomePage.to(driver, CustomLoginPage.class) + .assertAt(); + + HomePage homePage = loginPage.loginForm() + .username("user") + .password("password") + .submit(HomePage.class); + + homePage.assertAt(); + } + @Test public void authenticationSuccess() { SecurityWebFilterChain securityWebFilter = this.http diff --git a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java index 98027922f1d..22cd6082171 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -39,6 +39,7 @@ import org.springframework.test.web.reactive.server.WebTestClient; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.springframework.security.config.Customizer.withDefaults; /** * Tests for {@link ServerHttpSecurity.HeaderSpec}. @@ -49,7 +50,7 @@ */ public class HeaderSpecTests { - private ServerHttpSecurity.HeaderSpec headers = ServerHttpSecurity.http().headers(); + private ServerHttpSecurity http = ServerHttpSecurity.http(); private HttpHeaders expectedHeaders = new HttpHeaders(); @@ -72,14 +73,23 @@ public void setup() { public void headersWhenDisableThenNoSecurityHeaders() { new HashSet<>(this.expectedHeaders.keySet()).forEach(this::expectHeaderNamesNotPresent); - this.headers.disable(); + this.http.headers().disable(); + + assertHeaders(); + } + + @Test + public void headersWhenDisableInLambdaThenNoSecurityHeaders() throws Exception { + new HashSet<>(this.expectedHeaders.keySet()).forEach(this::expectHeaderNamesNotPresent); + + this.http.headers(headers -> headers.disable()); assertHeaders(); } @Test public void headersWhenDisableAndInvokedExplicitlyThenDefautsUsed() { - this.headers.disable() + this.http.headers().disable() .headers(); assertHeaders(); @@ -87,13 +97,34 @@ public void headersWhenDisableAndInvokedExplicitlyThenDefautsUsed() { @Test public void headersWhenDefaultsThenAllDefaultsWritten() { + this.http.headers(); + + assertHeaders(); + } + + @Test + public void headersWhenDefaultsInLambdaThenAllDefaultsWritten() throws Exception { + this.http.headers(withDefaults()); + assertHeaders(); } @Test public void headersWhenCacheDisableThenCacheNotWritten() { expectHeaderNamesNotPresent(HttpHeaders.CACHE_CONTROL, HttpHeaders.PRAGMA, HttpHeaders.EXPIRES); - this.headers.cache().disable(); + this.http.headers().cache().disable(); + + assertHeaders(); + } + + + @Test + public void headersWhenCacheDisableInLambdaThenCacheNotWritten() throws Exception { + expectHeaderNamesNotPresent(HttpHeaders.CACHE_CONTROL, HttpHeaders.PRAGMA, HttpHeaders.EXPIRES); + this.http + .headers(headers -> + headers.cache(cache -> cache.disable()) + ); assertHeaders(); } @@ -101,7 +132,18 @@ public void headersWhenCacheDisableThenCacheNotWritten() { @Test public void headersWhenContentOptionsDisableThenContentTypeOptionsNotWritten() { expectHeaderNamesNotPresent(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS); - this.headers.contentTypeOptions().disable(); + this.http.headers().contentTypeOptions().disable(); + + assertHeaders(); + } + + @Test + public void headersWhenContentOptionsDisableInLambdaThenContentTypeOptionsNotWritten() throws Exception { + expectHeaderNamesNotPresent(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS); + this.http + .headers(headers -> + headers.contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable()) + ); assertHeaders(); } @@ -109,7 +151,18 @@ public void headersWhenContentOptionsDisableThenContentTypeOptionsNotWritten() { @Test public void headersWhenHstsDisableThenHstsNotWritten() { expectHeaderNamesNotPresent(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY); - this.headers.hsts().disable(); + this.http.headers().hsts().disable(); + + assertHeaders(); + } + + @Test + public void headersWhenHstsDisableInLambdaThenHstsNotWritten() throws Exception { + expectHeaderNamesNotPresent(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY); + this.http + .headers(headers -> + headers.hsts(hsts -> hsts.disable()) + ); assertHeaders(); } @@ -118,28 +171,73 @@ public void headersWhenHstsDisableThenHstsNotWritten() { public void headersWhenHstsCustomThenCustomHstsWritten() { this.expectedHeaders.remove(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY); this.expectedHeaders.add(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=60"); - this.headers.hsts() + this.http.headers().hsts() .maxAge(Duration.ofSeconds(60)) .includeSubdomains(false); assertHeaders(); } + @Test + public void headersWhenHstsCustomInLambdaThenCustomHstsWritten() throws Exception { + this.expectedHeaders.remove(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY); + this.expectedHeaders.add(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=60"); + this.http + .headers(headers -> + headers + .hsts(hsts -> + hsts + .maxAge(Duration.ofSeconds(60)) + .includeSubdomains(false) + ) + ); + + assertHeaders(); + } + @Test public void headersWhenHstsCustomWithPreloadThenCustomHstsWritten() { this.expectedHeaders.remove(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY); this.expectedHeaders.add(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=60 ; includeSubDomains ; preload"); - this.headers.hsts() + this.http.headers().hsts() .maxAge(Duration.ofSeconds(60)) .preload(true); assertHeaders(); } + @Test + public void headersWhenHstsCustomWithPreloadInLambdaThenCustomHstsWritten() throws Exception { + this.expectedHeaders.remove(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY); + this.expectedHeaders.add(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=60 ; includeSubDomains ; preload"); + this.http + .headers(headers -> + headers + .hsts(hsts -> + hsts + .maxAge(Duration.ofSeconds(60)) + .preload(true) + ) + ); + + assertHeaders(); + } + @Test public void headersWhenFrameOptionsDisableThenFrameOptionsNotWritten() { expectHeaderNamesNotPresent(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS); - this.headers.frameOptions().disable(); + this.http.headers().frameOptions().disable(); + + assertHeaders(); + } + + @Test + public void headersWhenFrameOptionsDisableInLambdaThenFrameOptionsNotWritten() throws Exception { + expectHeaderNamesNotPresent(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS); + this.http + .headers(headers -> + headers.frameOptions(frameOptions -> frameOptions.disable()) + ); assertHeaders(); } @@ -147,17 +245,43 @@ public void headersWhenFrameOptionsDisableThenFrameOptionsNotWritten() { @Test public void headersWhenFrameOptionsModeThenFrameOptionsCustomMode() { this.expectedHeaders.set(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, "SAMEORIGIN"); - this.headers + this.http.headers() .frameOptions() .mode(XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN); assertHeaders(); } + @Test + public void headersWhenFrameOptionsModeInLambdaThenFrameOptionsCustomMode() throws Exception { + this.expectedHeaders.set(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, "SAMEORIGIN"); + this.http + .headers(headers -> + headers + .frameOptions(frameOptions -> + frameOptions + .mode(XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN) + ) + ); + + assertHeaders(); + } + @Test public void headersWhenXssProtectionDisableThenXssProtectionNotWritten() { expectHeaderNamesNotPresent("X-Xss-Protection"); - this.headers.xssProtection().disable(); + this.http.headers().xssProtection().disable(); + + assertHeaders(); + } + + @Test + public void headersWhenXssProtectionDisableInLambdaThenXssProtectionNotWritten() throws Exception { + expectHeaderNamesNotPresent("X-Xss-Protection"); + this.http + .headers(headers -> + headers.xssProtection(xssProtection -> xssProtection.disable()) + ); assertHeaders(); } @@ -168,7 +292,7 @@ public void headersWhenFeaturePolicyEnabledThenFeaturePolicyWritten() { this.expectedHeaders.add(FeaturePolicyServerHttpHeadersWriter.FEATURE_POLICY, policyDirectives); - this.headers.featurePolicy(policyDirectives); + this.http.headers().featurePolicy(policyDirectives); assertHeaders(); } @@ -179,7 +303,39 @@ public void headersWhenContentSecurityPolicyEnabledThenFeaturePolicyWritten() { this.expectedHeaders.add(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, policyDirectives); - this.headers.contentSecurityPolicy(policyDirectives); + this.http.headers().contentSecurityPolicy(policyDirectives); + + assertHeaders(); + } + + @Test + public void headersWhenContentSecurityPolicyEnabledWithDefaultsInLambdaThenDefaultPolicyWritten() throws Exception { + String expectedPolicyDirectives = "default-src 'self'"; + this.expectedHeaders.add(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, + expectedPolicyDirectives); + + this.http + .headers(headers -> + headers.contentSecurityPolicy(withDefaults()) + ); + + assertHeaders(); + } + + @Test + public void headersWhenContentSecurityPolicyEnabledInLambdaThenContentSecurityPolicyWritten() throws Exception { + String policyDirectives = "default-src 'self' *.trusted.com"; + this.expectedHeaders.add(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, + policyDirectives); + + this.http + .headers(headers -> + headers + .contentSecurityPolicy(contentSecurityPolicy -> + contentSecurityPolicy + .policyDirectives(policyDirectives) + ) + ); assertHeaders(); } @@ -188,7 +344,20 @@ public void headersWhenContentSecurityPolicyEnabledThenFeaturePolicyWritten() { public void headersWhenReferrerPolicyEnabledThenFeaturePolicyWritten() { this.expectedHeaders.add(ReferrerPolicyServerHttpHeadersWriter.REFERRER_POLICY, ReferrerPolicy.NO_REFERRER.getPolicy()); - this.headers.referrerPolicy(); + this.http.headers().referrerPolicy(); + + assertHeaders(); + } + + @Test + public void headersWhenReferrerPolicyEnabledInLambdaThenReferrerPolicyWritten() throws Exception { + this.expectedHeaders.add(ReferrerPolicyServerHttpHeadersWriter.REFERRER_POLICY, + ReferrerPolicy.NO_REFERRER.getPolicy()); + this.http + .headers(headers -> + headers + .referrerPolicy(withDefaults()) + ); assertHeaders(); } @@ -197,7 +366,23 @@ public void headersWhenReferrerPolicyEnabledThenFeaturePolicyWritten() { public void headersWhenReferrerPolicyCustomEnabledThenFeaturePolicyCustomWritten() { this.expectedHeaders.add(ReferrerPolicyServerHttpHeadersWriter.REFERRER_POLICY, ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE.getPolicy()); - this.headers.referrerPolicy(ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE); + this.http.headers().referrerPolicy(ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE); + + assertHeaders(); + } + + @Test + public void headersWhenReferrerPolicyCustomEnabledInLambdaThenCustomReferrerPolicyWritten() throws Exception { + this.expectedHeaders.add(ReferrerPolicyServerHttpHeadersWriter.REFERRER_POLICY, + ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE.getPolicy()); + this.http + .headers(headers -> + headers + .referrerPolicy(referrerPolicy -> + referrerPolicy + .policy(ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE) + ) + ); assertHeaders(); } @@ -228,6 +413,6 @@ private void assertHeaders() { } private WebTestClient buildClient() { - return WebTestClientBuilder.bindToWebFilters(this.headers.and().build()).build(); + return WebTestClientBuilder.bindToWebFilters(this.http.build()).build(); } } diff --git a/config/src/test/java/org/springframework/security/config/web/server/HttpsRedirectSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HttpsRedirectSpecTests.java index 1413bd61e3c..5e2597e2f89 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HttpsRedirectSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/HttpsRedirectSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -33,6 +33,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.springframework.security.config.Customizer.withDefaults; /** * Tests for {@link HttpsRedirectSpecTests} @@ -71,6 +72,17 @@ public void getWhenInsecureThenRespondsWithRedirectToSecure() { .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost"); } + @Test + public void getWhenInsecureAndRedirectConfiguredInLambdaThenRespondsWithRedirectToSecure() { + this.spring.register(RedirectToHttpsInLambdaConfig.class).autowire(); + + this.client.get() + .uri("http://localhost") + .exchange() + .expectStatus().isFound() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost"); + } + @Test public void getWhenInsecureAndPathRequiresTransportSecurityThenRedirects() { this.spring.register(SometimesRedirectToHttpsConfig.class).autowire(); @@ -87,6 +99,22 @@ public void getWhenInsecureAndPathRequiresTransportSecurityThenRedirects() { .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost:8443/secure"); } + @Test + public void getWhenInsecureAndPathRequiresTransportSecurityInLambdaThenRedirects() { + this.spring.register(SometimesRedirectToHttpsInLambdaConfig.class).autowire(); + + this.client.get() + .uri("http://localhost:8080") + .exchange() + .expectStatus().isNotFound(); + + this.client.get() + .uri("http://localhost:8080/secure") + .exchange() + .expectStatus().isFound() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost:8443/secure"); + } + @Test public void getWhenInsecureAndUsingCustomPortMapperThenRespondsWithRedirectToSecurePort() { this.spring.register(RedirectToHttpsViaCustomPortsConfig.class).autowire(); @@ -101,6 +129,20 @@ public void getWhenInsecureAndUsingCustomPortMapperThenRespondsWithRedirectToSec .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost:4443"); } + @Test + public void getWhenInsecureAndUsingCustomPortMapperInLambdaThenRespondsWithRedirectToSecurePort() { + this.spring.register(RedirectToHttpsViaCustomPortsInLambdaConfig.class).autowire(); + + PortMapper portMapper = this.spring.getContext().getBean(PortMapper.class); + when(portMapper.lookupHttpsPort(4080)).thenReturn(4443); + + this.client.get() + .uri("http://localhost:4080") + .exchange() + .expectStatus().isFound() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost:4443"); + } + @EnableWebFlux @EnableWebFluxSecurity static class RedirectToHttpConfig { @@ -115,6 +157,21 @@ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { } } + + @EnableWebFlux + @EnableWebFluxSecurity + static class RedirectToHttpsInLambdaConfig { + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .redirectToHttps(withDefaults()); + // @formatter:on + + return http.build(); + } + } + @EnableWebFlux @EnableWebFluxSecurity static class SometimesRedirectToHttpsConfig { @@ -130,6 +187,24 @@ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { } } + + @EnableWebFlux + @EnableWebFluxSecurity + static class SometimesRedirectToHttpsInLambdaConfig { + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .redirectToHttps(redirectToHttps -> + redirectToHttps + .httpsRedirectWhen(new PathPatternParserServerWebExchangeMatcher("/secure")) + ); + // @formatter:on + + return http.build(); + } + } + @EnableWebFlux @EnableWebFluxSecurity static class RedirectToHttpsViaCustomPortsConfig { @@ -149,4 +224,26 @@ public PortMapper portMapper() { return mock(PortMapper.class); } } + + @EnableWebFlux + @EnableWebFluxSecurity + static class RedirectToHttpsViaCustomPortsInLambdaConfig { + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .redirectToHttps(redirectToHttps -> + redirectToHttps + .portMapper(portMapper()) + ); + // @formatter:on + + return http.build(); + } + + @Bean + public PortMapper portMapper() { + return mock(PortMapper.class); + } + } } diff --git a/config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java index a8dbc2ac264..9a01fba23f0 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -25,6 +25,8 @@ import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import static org.springframework.security.config.Customizer.withDefaults; + /** * @author Shazin Sadakath * @since 5.0 @@ -117,4 +119,49 @@ public void customLogout() { .assertAt() .assertLogout(); } + + @Test + public void logoutWhenCustomLogoutInLambdaThenCustomLogoutUsed() throws Exception { + SecurityWebFilterChain securityWebFilter = this.http + .authorizeExchange(authorizeExchange -> + authorizeExchange + .anyExchange().authenticated() + ) + .formLogin(withDefaults()) + .logout(logout -> + logout + .requiresLogout(ServerWebExchangeMatchers.pathMatchers("/custom-logout")) + ) + .build(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class) + .assertAt(); + + loginPage = loginPage.loginForm() + .username("user") + .password("invalid") + .submit(FormLoginTests.DefaultLoginPage.class) + .assertError(); + + FormLoginTests.HomePage homePage = loginPage.loginForm() + .username("user") + .password("password") + .submit(FormLoginTests.HomePage.class); + + homePage.assertAt(); + + driver.get("http://localhost/custom-logout"); + + FormLoginTests.DefaultLoginPage.create(driver) + .assertAt() + .assertLogout(); + } } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java index 9982ca608a0..4fa2585095e 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java @@ -185,4 +185,48 @@ public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { return http.build(); } } + + @Test + public void oauth2ClientWhenCustomObjectsInLambdaThenUsed() { + this.spring.register(ClientRegistrationConfig.class, OAuth2ClientInLambdaCustomConfig.class, AuthorizedClientController.class).autowire(); + + OAuth2ClientInLambdaCustomConfig config = this.spring.getContext().getBean(OAuth2ClientInLambdaCustomConfig.class); + + ServerAuthenticationConverter converter = config.authenticationConverter; + ReactiveAuthenticationManager manager = config.manager; + + OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success(); + OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes(); + + OAuth2AuthorizationCodeAuthenticationToken result = new OAuth2AuthorizationCodeAuthenticationToken(this.registration, exchange, accessToken); + + when(converter.convert(any())).thenReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c"))); + when(manager.authenticate(any())).thenReturn(Mono.just(result)); + + this.client.get() + .uri("/authorize/oauth2/code/registration-id") + .exchange() + .expectStatus().is3xxRedirection(); + + verify(converter).convert(any()); + verify(manager).authenticate(any()); + } + + @Configuration + static class OAuth2ClientInLambdaCustomConfig { + ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class); + + ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class); + + @Bean + public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) throws Exception { + http + .oauth2Client(oauth2Client -> + oauth2Client + .authenticationConverter(this.authenticationConverter) + .authenticationManager(this.manager) + ); + return http.build(); + } + } } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java index 44bb47bd3e3..c7cbec05a8c 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -261,6 +261,87 @@ public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { } } + @Test + public void oauth2LoginWhenCustomObjectsInLambdaThenUsed() { + this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, + OAuth2LoginMockAuthenticationManagerInLambdaConfig.class).autowire(); + + String redirectLocation = "/custom-redirect-location"; + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(this.springSecurity) + .build(); + + OAuth2LoginMockAuthenticationManagerInLambdaConfig config = this.spring.getContext() + .getBean(OAuth2LoginMockAuthenticationManagerInLambdaConfig.class); + ServerAuthenticationConverter converter = config.authenticationConverter; + ReactiveAuthenticationManager manager = config.manager; + ServerWebExchangeMatcher matcher = config.matcher; + ServerOAuth2AuthorizationRequestResolver resolver = config.resolver; + ServerAuthenticationSuccessHandler successHandler = config.successHandler; + + OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success(); + OAuth2User user = TestOAuth2Users.create(); + OAuth2AccessToken accessToken = TestOAuth2AccessTokens.noScopes(); + + OAuth2LoginAuthenticationToken result = new OAuth2LoginAuthenticationToken(github, exchange, user, user.getAuthorities(), accessToken); + + when(converter.convert(any())).thenReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c"))); + when(manager.authenticate(any())).thenReturn(Mono.just(result)); + when(matcher.matches(any())).thenReturn(ServerWebExchangeMatcher.MatchResult.match()); + when(resolver.resolve(any())).thenReturn(Mono.empty()); + when(successHandler.onAuthenticationSuccess(any(), any())).thenAnswer((Answer>) invocation -> { + WebFilterExchange webFilterExchange = invocation.getArgument(0); + Authentication authentication = invocation.getArgument(1); + + return new RedirectServerAuthenticationSuccessHandler(redirectLocation) + .onAuthenticationSuccess(webFilterExchange, authentication); + }); + + webTestClient.get() + .uri("/login/oauth2/code/github") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals("Location", redirectLocation); + + verify(converter).convert(any()); + verify(manager).authenticate(any()); + verify(matcher).matches(any()); + verify(resolver).resolve(any()); + verify(successHandler).onAuthenticationSuccess(any(), any()); + } + + @Configuration + static class OAuth2LoginMockAuthenticationManagerInLambdaConfig { + ReactiveAuthenticationManager manager = mock(ReactiveAuthenticationManager.class); + + ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class); + + ServerWebExchangeMatcher matcher = mock(ServerWebExchangeMatcher.class); + + ServerOAuth2AuthorizationRequestResolver resolver = mock(ServerOAuth2AuthorizationRequestResolver.class); + + ServerAuthenticationSuccessHandler successHandler = mock(ServerAuthenticationSuccessHandler.class); + + @Bean + public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) throws Exception { + http + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ) + .oauth2Login(oauth2Login -> + oauth2Login + .authenticationConverter(authenticationConverter) + .authenticationManager(manager) + .authenticationMatcher(matcher) + .authorizationRequestResolver(resolver) + .authenticationSuccessHandler(successHandler) + ); + return http.build(); + } + } + @Test public void oauth2LoginWhenCustomBeansThenUsed() { this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class, diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index a9427cb36a6..fa8b937d6cb 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java @@ -175,6 +175,27 @@ public void getWhenUnsignedThenReturnsInvalidToken() { .expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\"")); } + @Test + public void getWhenValidTokenAndPublicKeyInLambdaThenReturnsOk() { + this.spring.register(PublicKeyInLambdaConfig.class, RootController.class).autowire(); + + this.client.get() + .headers(headers -> headers.setBearerAuth(this.messageReadToken)) + .exchange() + .expectStatus().isOk(); + } + + @Test + public void getWhenExpiredTokenAndPublicKeyInLambdaThenReturnsInvalidToken() { + this.spring.register(PublicKeyInLambdaConfig.class).autowire(); + + this.client.get() + .headers(headers -> headers.setBearerAuth(this.expired)) + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"invalid_token\"")); + } + @Test public void getWhenValidUsingPlaceholderThenReturnsOk() { this.spring.register(PlaceholderConfig.class, RootController.class).autowire(); @@ -213,6 +234,18 @@ public void getWhenUsingJwkSetUriThenConsultsAccordingly() { .expectStatus().isOk(); } + @Test + public void getWhenUsingJwkSetUriInLambdaThenConsultsAccordingly() { + this.spring.register(JwkSetUriInLambdaConfig.class, RootController.class).autowire(); + + MockWebServer mockWebServer = this.spring.getContext().getBean(MockWebServer.class); + mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet)); + + this.client.get() + .headers(headers -> headers.setBearerAuth(this.messageReadTokenWithKid)) + .exchange() + .expectStatus().isOk(); + } @Test public void getWhenUsingCustomAuthenticationManagerThenUsesItAccordingly() { @@ -230,6 +263,22 @@ public void getWhenUsingCustomAuthenticationManagerThenUsesItAccordingly() { .expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"mock-failure\"")); } + @Test + public void getWhenUsingCustomAuthenticationManagerInLambdaThenUsesItAccordingly() { + this.spring.register(CustomAuthenticationManagerInLambdaConfig.class).autowire(); + + ReactiveAuthenticationManager authenticationManager = this.spring.getContext().getBean( + ReactiveAuthenticationManager.class); + when(authenticationManager.authenticate(any(Authentication.class))) + .thenReturn(Mono.error(new OAuth2AuthenticationException(new OAuth2Error("mock-failure")))); + + this.client.get() + .headers(headers -> headers.setBearerAuth(this.messageReadToken)) + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"mock-failure\"")); + } + @Test public void getWhenUsingCustomAuthenticationManagerResolverThenUsesItAccordingly() { this.spring.register(CustomAuthenticationManagerResolverConfig.class).autowire(); @@ -396,6 +445,18 @@ public void introspectWhenValidThenReturnsOk() { .expectStatus().isOk(); } + @Test + public void introspectWhenValidAndIntrospectionInLambdaThenReturnsOk() { + this.spring.register(IntrospectionInLambdaConfig.class, RootController.class).autowire(); + this.spring.getContext().getBean(MockWebServer.class) + .setDispatcher(requiresAuth(clientId, clientSecret, active)); + + this.client.get() + .headers(headers -> headers.setBearerAuth(this.messageReadToken)) + .exchange() + .expectStatus().isOk(); + } + @EnableWebFlux @EnableWebFluxSecurity static class PublicKeyConfig { @@ -416,6 +477,30 @@ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception } } + @EnableWebFlux + @EnableWebFluxSecurity + static class PublicKeyInLambdaConfig { + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeExchange(exchanges -> + exchanges + .anyExchange().hasAuthority("SCOPE_message:read") + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(jwt -> + jwt + .publicKey(publicKey()) + ) + ); + // @formatter:on + + return http.build(); + } + } + @EnableWebFlux @EnableWebFluxSecurity static class PlaceholderConfig { @@ -469,6 +554,40 @@ void shutdown() throws IOException { } } + @EnableWebFlux + @EnableWebFluxSecurity + static class JwkSetUriInLambdaConfig { + private MockWebServer mockWebServer = new MockWebServer(); + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { + String jwkSetUri = mockWebServer().url("/.well-known/jwks.json").toString(); + + // @formatter:off + http + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(jwt -> + jwt + .jwkSetUri(jwkSetUri) + ) + ); + // @formatter:on + + return http.build(); + } + + @Bean + MockWebServer mockWebServer() { + return this.mockWebServer; + } + + @PreDestroy + void shutdown() throws IOException { + this.mockWebServer.shutdown(); + } + } + @EnableWebFlux @EnableWebFluxSecurity static class CustomDecoderConfig { @@ -531,6 +650,31 @@ ReactiveAuthenticationManager authenticationManager() { } } + @EnableWebFlux + @EnableWebFluxSecurity + static class CustomAuthenticationManagerInLambdaConfig { + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { + // @formatter:off + http + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(jwt -> + jwt + .authenticationManager(authenticationManager()) + ) + ); + // @formatter:on + + return http.build(); + } + + @Bean + ReactiveAuthenticationManager authenticationManager() { + return mock(ReactiveAuthenticationManager.class); + } + } + @EnableWebFlux @EnableWebFluxSecurity static class CustomAuthenticationManagerResolverConfig { @@ -670,6 +814,41 @@ void shutdown() throws IOException { } } + @EnableWebFlux + @EnableWebFluxSecurity + static class IntrospectionInLambdaConfig { + private MockWebServer mockWebServer = new MockWebServer(); + + @Bean + SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception { + String introspectionUri = mockWebServer().url("/introspect").toString(); + + // @formatter:off + http + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .opaqueToken(opaqueToken -> + opaqueToken + .introspectionUri(introspectionUri) + .introspectionClientCredentials("client", "secret") + ) + ); + // @formatter:on + + return http.build(); + } + + @Bean + MockWebServer mockWebServer() { + return this.mockWebServer; + } + + @PreDestroy + void shutdown() throws IOException { + this.mockWebServer.shutdown(); + } + } + @RestController static class RootController { @GetMapping diff --git a/config/src/test/java/org/springframework/security/config/web/server/RequestCacheTests.java b/config/src/test/java/org/springframework/security/config/web/server/RequestCacheTests.java index f1b862f28b5..3a26d328569 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/RequestCacheTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/RequestCacheTests.java @@ -34,6 +34,7 @@ import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.config.Customizer.withDefaults; /** * @author Rob Winch @@ -103,6 +104,40 @@ public void requestCacheNoOp() { securedPage.assertAt(); } + @Test + public void requestWhenCustomRequestCacheInLambdaThenCustomCacheUsed() throws Exception { + SecurityWebFilterChain securityWebFilter = this.http + .authorizeExchange(authorizeExchange -> + authorizeExchange + .anyExchange().authenticated() + ) + .formLogin(withDefaults()) + .requestCache(requestCache -> + requestCache + .requestCache(NoOpServerRequestCache.getInstance()) + ) + .build(); + + WebTestClient webTestClient = WebTestClient + .bindToController(new SecuredPageController(), new WebTestClientBuilder.Http200RestController()) + .webFilter(new WebFilterChainProxy(securityWebFilter)) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + DefaultLoginPage loginPage = SecuredPage.to(driver, DefaultLoginPage.class) + .assertAt(); + + HomePage securedPage = loginPage.loginForm() + .username("user") + .password("password") + .submit(HomePage.class); + + securedPage.assertAt(); + } + public static class SecuredPage { private WebDriver driver; diff --git a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java index daedfb9a6a4..37514bffc3a 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java @@ -20,8 +20,10 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import static org.springframework.security.config.Customizer.withDefaults; import java.util.Arrays; import java.util.List; @@ -36,6 +38,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter; import reactor.core.publisher.Mono; @@ -236,6 +239,19 @@ public void anonymous(){ } + @Test + public void getWhenAnonymousConfiguredThenAuthenticationIsAnonymous() throws Exception { + SecurityWebFilterChain securityFilterChain = this.http.anonymous(withDefaults()).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"))); @@ -283,6 +299,31 @@ public void basicWithCustomRealmName() { assertThat(result.getResponseCookies().getFirst("SESSION")).isNull(); } + @Test + public void requestWhenBasicWithRealmNameInLambdaThenRealmNameUsed() throws Exception { + this.http.securityContextRepository(new WebSessionServerSecurityContextRepository()); + HttpBasicServerAuthenticationEntryPoint authenticationEntryPoint = new HttpBasicServerAuthenticationEntryPoint(); + authenticationEntryPoint.setRealm("myrealm"); + this.http.httpBasic(httpBasic -> + httpBasic.authenticationEntryPoint(authenticationEntryPoint) + ); + this.http.authenticationManager(this.authenticationManager); + ServerHttpSecurity.AuthorizeExchangeSpec authorize = this.http.authorizeExchange(); + authorize.anyExchange().authenticated(); + + WebTestClient client = buildClient(); + + EntityExchangeResult result = client.get() + .uri("/") + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, value -> assertThat(value).contains("myrealm")) + .expectBody(String.class) + .returnResult(); + + assertThat(result.getResponseCookies().getFirst("SESSION")).isNull(); + } + @Test public void basicWithCustomAuthenticationManager() { ReactiveAuthenticationManager customAuthenticationManager = mock(ReactiveAuthenticationManager.class); @@ -302,6 +343,31 @@ public void basicWithCustomAuthenticationManager() { verifyZeroInteractions(this.authenticationManager); } + @Test + public void requestWhenBasicWithAuthenticationManagerInLambdaThenAuthenticationManagerUsed() throws Exception { + ReactiveAuthenticationManager customAuthenticationManager = mock(ReactiveAuthenticationManager.class); + given(customAuthenticationManager.authenticate(any())) + .willReturn(Mono.just(new TestingAuthenticationToken("rob", "rob", "ROLE_USER", "ROLE_ADMIN"))); + + SecurityWebFilterChain securityFilterChain = this.http + .httpBasic(httpBasic -> + httpBasic.authenticationManager(customAuthenticationManager) + ) + .build(); + WebFilterChainProxy springSecurityFilterChain = new WebFilterChainProxy(securityFilterChain); + WebTestClient client = WebTestClientBuilder.bindToWebFilters(springSecurityFilterChain).build(); + + client.get() + .uri("/") + .headers(headers -> headers.setBasicAuth("rob", "rob")) + .exchange() + .expectStatus().isOk() + .expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok")); + + verifyZeroInteractions(this.authenticationManager); + verify(customAuthenticationManager).authenticate(any(Authentication.class)); + } + @Test @SuppressWarnings("unchecked") public void addsX509FilterWhenX509AuthenticationIsConfigured() { @@ -319,6 +385,23 @@ public void addsX509FilterWhenX509AuthenticationIsConfigured() { assertThat(x509WebFilter).isNotNull(); } + @Test + public void x509WhenCustomizedThenAddsX509Filter() throws Exception { + X509PrincipalExtractor mockExtractor = mock(X509PrincipalExtractor.class); + ReactiveAuthenticationManager mockAuthenticationManager = mock(ReactiveAuthenticationManager.class); + + this.http.x509(x509 -> + x509 + .principalExtractor(mockExtractor) + .authenticationManager(mockAuthenticationManager) + ); + + SecurityWebFilterChain securityWebFilterChain = this.http.build(); + WebFilter x509WebFilter = securityWebFilterChain.getWebFilters().filter(this::isX509Filter).blockFirst(); + + assertThat(x509WebFilter).isNotNull(); + } + @Test public void addsX509FilterWhenX509AuthenticationIsConfiguredWithDefaults() { this.http.x509(); @@ -329,6 +412,46 @@ public void addsX509FilterWhenX509AuthenticationIsConfiguredWithDefaults() { assertThat(x509WebFilter).isNotNull(); } + @Test + public void x509WhenDefaultsThenAddsX509Filter() throws Exception { + this.http.x509(withDefaults()); + + SecurityWebFilterChain securityWebFilterChain = this.http.build(); + WebFilter x509WebFilter = securityWebFilterChain.getWebFilters().filter(this::isX509Filter).blockFirst(); + + assertThat(x509WebFilter).isNotNull(); + } + + @Test + public void postWhenCsrfDisabledThenPermitted() throws Exception { + SecurityWebFilterChain securityFilterChain = this.http.csrf(csrf -> csrf.disable()).build(); + WebFilterChainProxy springSecurityFilterChain = new WebFilterChainProxy(securityFilterChain); + WebTestClient client = WebTestClientBuilder.bindToWebFilters(springSecurityFilterChain).build(); + + client.post() + .uri("/") + .exchange() + .expectStatus().isOk(); + } + + @Test + public void postWhenCustomCsrfTokenRepositoryThenUsed() throws Exception { + ServerCsrfTokenRepository customServerCsrfTokenRepository = mock(ServerCsrfTokenRepository.class); + when(customServerCsrfTokenRepository.loadToken(any(ServerWebExchange.class))).thenReturn(Mono.empty()); + SecurityWebFilterChain securityFilterChain = this.http + .csrf(csrf -> csrf.csrfTokenRepository(customServerCsrfTokenRepository)) + .build(); + WebFilterChainProxy springSecurityFilterChain = new WebFilterChainProxy(securityFilterChain); + WebTestClient client = WebTestClientBuilder.bindToWebFilters(springSecurityFilterChain).build(); + + client.post() + .uri("/") + .exchange() + .expectStatus().isForbidden(); + + verify(customServerCsrfTokenRepository).loadToken(any()); + } + private boolean isX509Filter(WebFilter filter) { try { Object converter = ReflectionTestUtils.getField(filter, "authenticationConverter"); diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/cors.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/cors.adoc index 4e62b55a77a..968db86d14d 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/cors.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/cors.adoc @@ -28,10 +28,10 @@ The following will disable the CORS integration within Spring Security: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .cors().disable(); + .cors(cors -> cors.disable()); return http.build(); } ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/headers.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/headers.adoc index 43907c2834f..22b886482a7 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/headers.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/headers.adoc @@ -53,12 +53,20 @@ You can easily do this with the following Java Configuration: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .hsts().disable() - .frameOptions().mode(Mode.SAMEORIGIN); + .headers(headers -> + headers + .hsts(hsts -> + hsts + .disable() + ) + .frameOptions(frameOptions -> + frameOptions + .mode(Mode.SAMEORIGIN) + ) + ); return http.build(); } ---- @@ -72,11 +80,13 @@ If necessary, you can disable all of the HTTP Security response headers with the [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .disable(); + .headers(headers -> + headers + .disable() + ); return http.build(); } ---- @@ -104,11 +114,13 @@ You can also disable cache control using the following Java Configuration: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .cache().disable(); + .headers(headers -> + headers + .cache(cache -> cache.disable()) + ); return http.build(); } ---- @@ -143,11 +155,13 @@ However, if need to disable the header, the following may be used: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .contentTypeOptions().disable(); + .headers(headers -> + headers + .contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable()) + ); return http.build(); } ---- @@ -188,14 +202,18 @@ You can customize HSTS headers with Java Configuration: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .hsts() - .includeSubdomains(true) - .preload(true) - .maxAge(Duration.ofDays(365)); + .headers(headers -> + headers + .hsts(hsts -> + hsts + .includeSubdomains(true) + .preload(true) + .maxAge(Duration.ofDays(365)) + ) + ); return http.build(); } ---- @@ -232,12 +250,16 @@ You can customize X-Frame-Options with Java Configuration using the following: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .frameOptions() - .mode(SAMEORIGIN); + .headers(headers -> + headers + .frameOptions(frameOptions -> + frameOptions + .mode(SAMEORIGIN) + ) + ); return http.build(); } ---- @@ -264,12 +286,13 @@ However, we can customize with Java Configuration with the following: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .xssProtection() - .disable(); + .headers(headers -> + headers + .xssProtection(xssProtection -> xssProtection.disable()) + ); return http.build(); } ---- @@ -345,11 +368,16 @@ You can enable the CSP header using Java configuration as shown below: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"); + .headers(headers -> + headers + .contentSecurityPolicy(contentSecurityPolicy -> + contentSecurityPolicy + .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") + ) + ); return http.build(); } ---- @@ -359,12 +387,17 @@ To enable the CSP _'report-only'_ header, provide the following Java configurati [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") - .reportOnly(); + .headers(headers -> + headers + .contentSecurityPolicy(contentSecurityPolicy -> + contentSecurityPolicy + .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") + .reportOnly() + ) + ); return http.build(); } ---- @@ -405,11 +438,16 @@ You can enable the Referrer-Policy header using Java configuration as shown belo [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .referrerPolicy(ReferrerPolicy.SAME_ORIGIN); + .headers(headers -> + headers + .referrerPolicy(referrerPolicy -> + referrerPolicy + .policy(ReferrerPolicy.SAME_ORIGIN) + ) + ); return http.build(); } ---- @@ -438,11 +476,13 @@ You can enable the Feature-Policy header using Java configuration as shown below [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .headers() - .featurePolicy("geolocation 'self'"); + .headers(headers -> + headers + .featurePolicy("geolocation 'self'") + ); return http.build(); } ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc index 531a0d6ce85..fb32f0e14ef 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/method.adoc @@ -88,10 +88,11 @@ public class SecurityConfig { return http // Demonstrate that method security works // Best practice to use both for defense in depth - .authorizeExchange() - .anyExchange().permitAll() - .and() - .httpBasic().and() + .authorizeExchange(exchanges -> + exchanges + .anyExchange().permitAll() + ) + .httpBasic(withDefaults()) .build(); } diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/access-token.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/access-token.adoc index a58f08c2829..cdefccc337f 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/access-token.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/access-token.adoc @@ -27,7 +27,7 @@ The next step is to instruct Spring Security that you wish to act as an OAuth2 C SecurityWebFilterChain configure(ServerHttpSecurity http) throws Exception { http // ... - .oauth2Client(); + .oauth2Client(withDefaults()); return http.build(); } ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/login.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/login.adoc index 719011752f5..fea6fa26294 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/login.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/login.adoc @@ -128,10 +128,10 @@ ReactiveClientRegistrationRepository clientRegistrations() { } @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .oauth2Login(); + .oauth2Login(withDefaults()); return http.build(); } ---- @@ -141,14 +141,16 @@ Additional configuration options can be seen below: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .oauth2Login() - .authenticationConverter(converter) - .authenticationManager(manager) - .authorizedClientRepository(authorizedClients) - .clientRegistrationRepository(clientRegistrations); + .oauth2Login(oauth2Login -> + oauth2Login + .authenticationConverter(converter) + .authenticationManager(manager) + .authorizedClientRepository(authorizedClients) + .clientRegistrationRepository(clientRegistrations) + ); return http.build(); } ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc index 6e7a4667e20..17886682491 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc @@ -121,14 +121,17 @@ The first is a `SecurityWebFilterChain` that configures the app as a resource se [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - .authorizeExchange() - .anyExchange().authenticated() - .and() - .oauth2ResourceServer() - .jwt(); - return http.build(); +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { + http + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(withDefaults()) + ); + return http.build(); } ---- @@ -139,14 +142,17 @@ Replacing this is as simple as exposing the bean within the application: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http - .authorizeExchange() - .pathMatchers("/message/**").hasAuthority("SCOPE_message:read") - .anyExchange().authenticated() - .and() - .oauth2ResourceServer() - .jwt(); + .authorizeExchange(exchanges -> + exchanges + .pathMatchers("/message/**").hasAuthority("SCOPE_message:read") + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(withDefaults()) + ); return http.build(); } ---- @@ -177,15 +183,20 @@ An authorization server's JWK Set Uri can be configured < + exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(jwt -> + jwt + .jwkSetUri("https://idp.example.com/.well-known/jwks.json") + ) + ); + return http.build(); } ---- @@ -199,14 +210,19 @@ More powerful than `jwkSetUri()` is `decoder()`, which will completely replace a [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - .authorizeExchange() - .anyExchange().authenticated() - .and() - .oauth2ResourceServer() - .jwt() - .decoder(myCustomDecoder()); +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { + http + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(jwt -> + jwt + .decoder(myCustomDecoder()) + ) + ); return http.build(); } ---- @@ -240,15 +256,18 @@ This means that to protect an endpoint or method with a scope derived from a JWT [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - .authorizeExchange() - .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") - .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") - .anyExchange().authenticated() - .and() - .oauth2ResourceServer() - .jwt(); +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { + http + .authorizeExchange(exchanges -> + exchanges + .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") + .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(withDefaults()) + ); return http.build(); } ---- @@ -273,15 +292,20 @@ To this end, the DSL exposes `jwtAuthenticationConverter()`: [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - .authorizeExchange() - .anyExchange().authenticated() - .and() - .oauth2ResourceServer() - .jwt() - .jwtAuthenticationConverter(grantedAuthoritiesExtractor()); - return http.build(); +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { + http + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(jwt -> + jwt + .jwtAuthenticationConverter(grantedAuthoritiesExtractor()) + ) + ); + return http.build(); } Converter> grantedAuthoritiesExtractor() { diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/redirect-https.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/redirect-https.adoc index 5c142b6209a..9e9020d1e56 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/redirect-https.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/redirect-https.adoc @@ -7,10 +7,10 @@ Spring Security can be configured to perform a redirect to https using the follo [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .redirectToHttps(); + .redirectToHttps(withDefaults()); return http.build(); } ---- @@ -22,11 +22,13 @@ For example, if the production environment adds a header named `X-Forwarded-Prot [source,java] ---- @Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http // ... - .redirectToHttps() - .httpsRedirectWhen(e -> e.getRequest().getHeaders().containsKey("X-Forwarded-Proto")); + .redirectToHttps(redirectToHttps -> + redirectToHttps + .httpsRedirectWhen(e -> e.getRequest().getHeaders().containsKey("X-Forwarded-Proto")) + ); return http.build(); } ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc index 8752b8311b8..03cd665fe8f 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc @@ -52,13 +52,14 @@ public class HelloWebfluxSecurityConfig { } @Bean - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http - .authorizeExchange() - .anyExchange().authenticated() - .and() - .httpBasic().and() - .formLogin(); + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ) + .httpBasic(withDefaults()) + .formLogin(withDefaults()); return http.build(); } } diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/x509.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/x509.adoc index 7caee9091aa..211cf3ace7c 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/x509.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/x509.adoc @@ -7,14 +7,14 @@ Below is an example of a reactive x509 security configuration: [source,java] ---- @Bean -public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - http - .x509() - .and() - .authorizeExchange() - .anyExchange().permitAll(); - - return http.build(); +public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) throws Exception { + http + .x509(withDefaults()) + .authorizeExchange(exchanges -> + exchanges + .anyExchange().permitAll() + ); + return http.build(); } ---- @@ -25,28 +25,28 @@ The next example demonstrates how these defaults can be overridden. [source,java] ---- @Bean -public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - SubjectDnX509PrincipalExtractor principalExtractor = - new SubjectDnX509PrincipalExtractor(); - - principalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)"); - - ReactiveAuthenticationManager authenticationManager = authentication -> { - authentication.setAuthenticated("Trusted Org Unit".equals(authentication.getName())); - return Mono.just(authentication); - }; - - // @formatter:off - http - .x509() - .principalExtractor(principalExtractor) - .authenticationManager(authenticationManager) - .and() - .authorizeExchange() - .anyExchange().authenticated(); - // @formatter:on - - return http.build(); +public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) throws Exception { + SubjectDnX509PrincipalExtractor principalExtractor = + new SubjectDnX509PrincipalExtractor(); + + principalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)"); + + ReactiveAuthenticationManager authenticationManager = authentication -> { + authentication.setAuthenticated("Trusted Org Unit".equals(authentication.getName())); + return Mono.just(authentication); + }; + + http + .x509(x509 -> + x509 + .principalExtractor(principalExtractor) + .authenticationManager(authenticationManager) + ) + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ); + return http.build(); } ---- diff --git a/samples/boot/hellowebflux-method/src/main/java/sample/SecurityConfig.java b/samples/boot/hellowebflux-method/src/main/java/sample/SecurityConfig.java index e3eb5ae3418..bb7349c7dc3 100644 --- a/samples/boot/hellowebflux-method/src/main/java/sample/SecurityConfig.java +++ b/samples/boot/hellowebflux-method/src/main/java/sample/SecurityConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -25,6 +25,8 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.server.SecurityWebFilterChain; +import static org.springframework.security.config.Customizer.withDefaults; + /** * @author Rob Winch * @since 5.0 @@ -38,10 +40,11 @@ SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exce return http // Demonstrate that method security works // Best practice to use both for defense in depth - .authorizeExchange() - .anyExchange().permitAll() - .and() - .httpBasic().and() + .authorizeExchange(exchanges -> + exchanges + .anyExchange().permitAll() + ) + .httpBasic(withDefaults()) .build(); } diff --git a/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java index ba7a373fd00..9c2e54afce4 100644 --- a/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java +++ b/samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -21,6 +21,8 @@ import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; +import static org.springframework.security.config.Customizer.withDefaults; + /** * @author Rob Winch * @since 5.1 @@ -31,12 +33,15 @@ public class SecurityConfig { @Bean SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http - .authorizeExchange() - .pathMatchers("/message/**").hasAuthority("SCOPE_message:read") - .anyExchange().authenticated() - .and() - .oauth2ResourceServer() - .jwt(); + .authorizeExchange(exchanges -> + exchanges + .pathMatchers("/message/**").hasAuthority("SCOPE_message:read") + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(withDefaults()) + ); return http.build(); } } diff --git a/samples/boot/oauth2webclient-webflux/src/main/java/sample/config/SecurityConfig.java b/samples/boot/oauth2webclient-webflux/src/main/java/sample/config/SecurityConfig.java index aaf2796fea3..d2b73b4824e 100644 --- a/samples/boot/oauth2webclient-webflux/src/main/java/sample/config/SecurityConfig.java +++ b/samples/boot/oauth2webclient-webflux/src/main/java/sample/config/SecurityConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -23,6 +23,8 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.server.SecurityWebFilterChain; +import static org.springframework.security.config.Customizer.withDefaults; + /** * @author Rob Winch */ @@ -32,15 +34,14 @@ public class SecurityConfig { @Bean SecurityWebFilterChain configure(ServerHttpSecurity http) throws Exception { http - .authorizeExchange() - .pathMatchers("/", "/public/**").permitAll() - .anyExchange().authenticated() - .and() - .oauth2Login() - .and() - .formLogin() - .and() - .oauth2Client(); + .authorizeExchange(exchanges -> + exchanges + .pathMatchers("/", "/public/**").permitAll() + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()) + .formLogin(withDefaults()) + .oauth2Client(withDefaults()); return http.build(); } diff --git a/samples/boot/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java b/samples/boot/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java index a133b734ca1..3b291f709c2 100644 --- a/samples/boot/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java +++ b/samples/boot/webflux-form/src/main/java/sample/WebfluxFormSecurityConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -24,6 +24,8 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.server.SecurityWebFilterChain; +import static org.springframework.security.config.Customizer.withDefaults; + /** * @author Rob Winch * @since 5.0 @@ -42,15 +44,18 @@ public MapReactiveUserDetailsService userDetailsService() { } @Bean - SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { http - .authorizeExchange() - .pathMatchers("/login").permitAll() - .anyExchange().authenticated() - .and() - .httpBasic().and() - .formLogin() - .loginPage("/login"); + .authorizeExchange(exchanges -> + exchanges + .pathMatchers("/login").permitAll() + .anyExchange().authenticated() + ) + .httpBasic(withDefaults()) + .formLogin(formLogin -> + formLogin + .loginPage("/login") + ); return http.build(); } } diff --git a/samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java b/samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java index a6871f7c72d..b4ce0c0a9af 100644 --- a/samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java +++ b/samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -25,6 +25,8 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.web.server.SecurityWebFilterChain; +import static org.springframework.security.config.Customizer.withDefaults; + /** * @author Alexey Nesterov * @since 5.2 @@ -40,13 +42,14 @@ public ReactiveUserDetailsService reactiveUserDetailsService() { } @Bean - public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) throws Exception { // @formatter:off http - .x509() - .and() - .authorizeExchange() - .anyExchange().authenticated(); + .x509(withDefaults()) + .authorizeExchange(exchanges -> + exchanges + .anyExchange().authenticated() + ); // @formatter:on return http.build();