AbstractReactiveResourceServerAutoConfiguration.java
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bremersee.security.authentication;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.bremersee.core.OrderedProxy;
import org.bremersee.security.FrameOptionsMode;
import org.bremersee.security.authentication.AuthProperties.PathMatcherProperties;
import org.bremersee.web.CorsProperties;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* The abstract reactive resource server security auto configuration.
*
* @author Christian Bremer
*/
@Slf4j
public abstract class AbstractReactiveResourceServerAutoConfiguration {
private final Environment environment;
private final CorsProperties corsProperties;
private final AuthProperties authProperties;
private final ObjectProvider<JsonPathReactiveJwtConverter> jwtConverterProvider;
private final ObjectProvider<ReactiveUserDetailsService> userDetailsServiceProvider;
private final ObjectProvider<PasswordEncoder> passwordEncoderProvider;
/**
* Instantiates a new abstract reactive resource server security auto configuration.
*
* @param environment the environment
* @param corsProperties the cors properties
* @param authProperties the authentication nad authorization properties
* @param jwtConverterProvider the jwt converter provider
* @param userDetailsServiceProvider the user details service provider
* @param passwordEncoderProvider the password encoder provider
*/
protected AbstractReactiveResourceServerAutoConfiguration(
Environment environment,
CorsProperties corsProperties,
AuthProperties authProperties,
ObjectProvider<JsonPathReactiveJwtConverter> jwtConverterProvider,
ObjectProvider<ReactiveUserDetailsService> userDetailsServiceProvider,
ObjectProvider<PasswordEncoder> passwordEncoderProvider) {
this.environment = environment;
this.corsProperties = corsProperties;
this.authProperties = authProperties;
this.jwtConverterProvider = jwtConverterProvider;
this.userDetailsServiceProvider = userDetailsServiceProvider;
this.passwordEncoderProvider = passwordEncoderProvider;
}
/**
* Init.
*/
protected void init() {
final boolean hasJwkUriSet = StringUtils
.hasText(environment.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri"));
log.info("\n"
+ "*********************************************************************************\n"
+ "* {}\n"
+ "*********************************************************************************\n"
+ "* enable = {}\n"
+ "* order = {}\n"
+ "* jwt = {}\n"
+ "* cors = {}\n"
+ "*********************************************************************************",
ClassUtils.getUserClass(getClass()).getSimpleName(),
authProperties.getResourceServer().name(),
authProperties.getResourceServerOrder(),
hasJwkUriSet,
corsProperties.isEnable());
}
/**
* Init authorize exchange.
*
* @param http the http
* @return the authorize exchange spec
*/
protected abstract AuthorizeExchangeSpec init(ServerHttpSecurity http);
/**
* Resource server filter chain.
*
* @param serverHttpSecurity the server http security
* @return the security web filter chain
*/
protected SecurityWebFilterChain resourceServerFilterChain(
ServerHttpSecurity serverHttpSecurity) {
ServerHttpSecurity http = serverHttpSecurity;
http.headers().frameOptions(conf -> conf.mode(Mode.SAMEORIGIN));
Assert.notNull(http, "Server http security must be present.");
AuthorizeExchangeSpec spec = init(http);
if (authProperties.getResourceServer() == AutoSecurityMode.NONE) {
http = spec
.anyExchange().permitAll()
.and()
.httpBasic().disable();
} else {
spec = configurePathMatchers(spec);
http = configureAuthenticationManager(spec.and());
}
http = http
.headers().frameOptions(customizer -> {
if (authProperties.getFrameOptionsMode() == FrameOptionsMode.DISABLE) {
customizer.disable();
} else {
customizer.mode(authProperties.getFrameOptionsMode().getMode());
}
})
.and()
.csrf().disable();
if (corsProperties.isEnable()) {
http = http.cors().and();
} else {
http = http.cors().disable();
}
return OrderedProxy.create(
http.build(),
authProperties.getResourceServerOrder());
}
private AuthorizeExchangeSpec configurePathMatchers(AuthorizeExchangeSpec spec) {
for (PathMatcherProperties props : authProperties.preparePathMatchers(corsProperties)) {
log.info("Securing requests to {}", props);
switch (props.getAccessMode()) {
case DENY_ALL:
spec = spec.matchers(matcher(props)).denyAll();
break;
case PERMIT_ALL:
spec = spec.matchers(matcher(props)).permitAll();
break;
default:
spec = spec.matchers(matcher(props)).access(new RoleOrIpBasedAuthorizationManager(
props.roles(authProperties::ensureRolePrefix),
props.getIpAddresses()));
}
}
return spec;
}
private ServerWebExchangeMatcher matcher(PathMatcherProperties props) {
return Optional.ofNullable(props.httpMethod())
.map(method -> ServerWebExchangeMatchers.pathMatchers(method, props.getAntPattern()))
.orElseGet(() -> ServerWebExchangeMatchers.pathMatchers(props.getAntPattern()));
}
private ServerHttpSecurity configureAuthenticationManager(ServerHttpSecurity http) {
return Optional.ofNullable(jwtConverterProvider.getIfAvailable())
.map(jwtConverter -> http
.oauth2ResourceServer((rs) -> rs
.jwt()
.jwtAuthenticationConverter(jwtConverter)
.and()))
.orElseGet(() -> http
.authenticationManager(userDetailsAuthenticationManager())
.httpBasic()
.and()
.formLogin().disable());
}
private ReactiveAuthenticationManager userDetailsAuthenticationManager() {
ReactiveUserDetailsService userDetailsService = userDetailsServiceProvider
.getIfAvailable(this::defaultReactiveUserDetailsService);
log.info("Creating ReactiveAuthenticationManager with {}",
ClassUtils.getUserClass(userDetailsService).getSimpleName());
UserDetailsRepositoryReactiveAuthenticationManager manager
= new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
passwordEncoderProvider.ifAvailable(passwordEncoder -> {
log.info("Setting {} to ReactiveAuthenticationManager", ClassUtils.getUserClass(passwordEncoder).getSimpleName());
manager.setPasswordEncoder(passwordEncoder);
});
return manager;
}
private ReactiveUserDetailsService defaultReactiveUserDetailsService() {
UserDetails[] userDetails = authProperties.buildBasicAuthUserDetails(passwordEncoderProvider.getIfAvailable());
return new MapReactiveUserDetailsService(userDetails);
}
}