View Javadoc
1   /*
2    * Copyright 2020 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.actuator.security.authentication;
18  
19  import java.util.Objects;
20  import java.util.Optional;
21  import lombok.extern.slf4j.Slf4j;
22  import org.bremersee.core.OrderedProxy;
23  import org.bremersee.security.authentication.AuthProperties;
24  import org.bremersee.security.authentication.AutoSecurityMode;
25  import org.bremersee.security.authentication.JsonPathJwtConverter;
26  import org.bremersee.security.authentication.JsonPathReactiveJwtConverter;
27  import org.bremersee.security.authentication.PasswordFlowProperties;
28  import org.bremersee.security.authentication.PasswordFlowReactiveAuthenticationManager;
29  import org.bremersee.security.authentication.RoleBasedAuthorizationManager;
30  import org.bremersee.security.authentication.RoleOrIpBasedAuthorizationManager;
31  import org.bremersee.security.authentication.WebClientAccessTokenRetriever;
32  import org.springframework.beans.factory.ObjectProvider;
33  import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;
34  import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest.EndpointServerWebExchangeMatcher;
35  import org.springframework.boot.actuate.info.Info;
36  import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
37  import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
38  import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
39  import org.springframework.boot.autoconfigure.security.SecurityProperties;
40  import org.springframework.boot.context.event.ApplicationReadyEvent;
41  import org.springframework.boot.context.properties.EnableConfigurationProperties;
42  import org.springframework.context.annotation.Bean;
43  import org.springframework.context.annotation.Conditional;
44  import org.springframework.context.annotation.Configuration;
45  import org.springframework.context.event.EventListener;
46  import org.springframework.http.HttpMethod;
47  import org.springframework.security.authentication.ReactiveAuthenticationManager;
48  import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
49  import org.springframework.security.config.web.server.ServerHttpSecurity;
50  import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec;
51  import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
52  import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
53  import org.springframework.security.crypto.password.PasswordEncoder;
54  import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
55  import org.springframework.security.oauth2.jwt.JwtValidators;
56  import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
57  import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
58  import org.springframework.security.web.server.SecurityWebFilterChain;
59  import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
60  import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
61  import org.springframework.util.Assert;
62  import org.springframework.util.ClassUtils;
63  import org.springframework.util.StringUtils;
64  
65  /**
66   * The reactive actuator security auto configuration.
67   *
68   * @author Christian Bremer
69   */
70  @ConditionalOnWebApplication(type = Type.REACTIVE)
71  @Conditional({ActuatorAutoSecurityCondition.class})
72  @ConditionalOnClass({
73      ServerHttpSecurity.class,
74      ReactiveAuthenticationManager.class,
75      PasswordFlowProperties.class,
76      Info.class
77  })
78  @EnableConfigurationProperties({
79      SecurityProperties.class,
80      AuthProperties.class,
81      ActuatorAuthProperties.class})
82  @Configuration
83  @Slf4j
84  public class ReactiveActuatorSecurityAutoConfiguration {
85  
86    private final AuthProperties authProperties;
87  
88    private final ActuatorAuthProperties actuatorAuthProperties;
89  
90    private final ObjectProvider<JsonPathReactiveJwtConverter> jsonPathJwtConverterProvider;
91  
92    private final ObjectProvider<WebClientAccessTokenRetriever> tokenRetrieverProvider;
93  
94    private final ObjectProvider<ReactiveUserDetailsService> userDetailsServiceProvider;
95  
96    private final ObjectProvider<PasswordEncoder> passwordEncoderProvider;
97  
98    /**
99     * Instantiates a new reactive actuator security auto configuration.
100    *
101    * @param authProperties the security properties
102    * @param actuatorAuthProperties the actuator security properties
103    * @param jsonPathJwtConverterProvider the json path jwt converter provider
104    * @param tokenRetrieverProvider the token retriever provider
105    * @param userDetailsServiceProvider the user details service provider
106    * @param passwordEncoderProvider the password encoder provider
107    */
108   public ReactiveActuatorSecurityAutoConfiguration(
109       AuthProperties authProperties,
110       ActuatorAuthProperties actuatorAuthProperties,
111       ObjectProvider<JsonPathReactiveJwtConverter> jsonPathJwtConverterProvider,
112       ObjectProvider<WebClientAccessTokenRetriever> tokenRetrieverProvider,
113       ObjectProvider<ReactiveUserDetailsService> userDetailsServiceProvider,
114       ObjectProvider<PasswordEncoder> passwordEncoderProvider) {
115     this.authProperties = authProperties;
116     this.actuatorAuthProperties = actuatorAuthProperties;
117     this.jsonPathJwtConverterProvider = jsonPathJwtConverterProvider;
118     this.tokenRetrieverProvider = tokenRetrieverProvider;
119     this.userDetailsServiceProvider = userDetailsServiceProvider;
120     this.passwordEncoderProvider = passwordEncoderProvider;
121   }
122 
123   /**
124    * Init.
125    */
126   @EventListener(ApplicationReadyEvent.class)
127   @SuppressWarnings("DuplicatedCode")
128   public void init() {
129     final boolean hasJwkUriSet = StringUtils.hasText(actuatorAuthProperties.getJwkSetUri());
130     log.info("\n"
131             + "*********************************************************************************\n"
132             + "* {}\n"
133             + "*********************************************************************************\n"
134             + "* enable = {}\n"
135             + "* order = {}\n"
136             + "* jwt = {}\n"
137             + "* cors = {}\n"
138             + "*********************************************************************************",
139         ClassUtils.getUserClass(getClass()).getSimpleName(),
140         actuatorAuthProperties.getEnable().name(),
141         actuatorAuthProperties.getOrder(),
142         hasJwkUriSet,
143         actuatorAuthProperties.isEnableCors());
144     if (hasJwkUriSet) {
145       Assert.hasText(actuatorAuthProperties.getPasswordFlow().getTokenEndpoint(),
146           "Token endpoint of actuator password flow must be present.");
147       Assert.hasText(actuatorAuthProperties.getPasswordFlow().getClientId(),
148           "Client ID of actuator password flow must be present.");
149       Assert.notNull(actuatorAuthProperties.getPasswordFlow().getClientSecret(),
150           "Client secret of actuator password flow must be present.");
151     }
152   }
153 
154   /**
155    * Actuator filter chain security web filter chain.
156    *
157    * @param httpProvider the http provider
158    * @return the security web filter chain
159    */
160   @Bean
161   public SecurityWebFilterChain actuatorFilterChain(
162       ObjectProvider<ServerHttpSecurity> httpProvider) {
163 
164     ServerHttpSecurity http = httpProvider.getIfAvailable();
165     Assert.notNull(http, "Server http security must be present.");
166     log.info("Securing requests to /actuator/**");
167     AuthorizeExchangeSpec spec = http
168         .securityMatcher(EndpointRequest.toAnyEndpoint())
169         .authorizeExchange();
170     if (actuatorAuthProperties.getEnable() == AutoSecurityMode.NONE) {
171       http = spec
172           .anyExchange().permitAll()
173           .and()
174           .httpBasic().disable();
175     } else {
176       if (actuatorAuthProperties.isEnableCors()) {
177         spec = spec.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll();
178       }
179       http = spec
180           .matchers(unauthenticatedEndpointMatchers()).permitAll()
181           .matchers(new AndServerWebExchangeMatcher(
182               EndpointRequest.toAnyEndpoint(),
183               ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**")))
184           .access(new RoleOrIpBasedAuthorizationManager(
185               actuatorAuthProperties.rolesOrDefaults(),
186               actuatorAuthProperties.getIpAddresses()))
187           .anyExchange()
188           .access(new RoleBasedAuthorizationManager(
189               actuatorAuthProperties.adminRolesOrDefaults()))
190           .and()
191           .authenticationManager(authenticationManager())
192           .httpBasic()
193           .and()
194           .formLogin().disable();
195     }
196     http = http
197         .csrf().disable()
198         .cors(customizer -> {
199           if (!actuatorAuthProperties.isEnableCors()) {
200             customizer.disable();
201           }
202         });
203     return OrderedProxy.create(http.build(), actuatorAuthProperties.getOrder());
204   }
205 
206   private EndpointServerWebExchangeMatcher[] unauthenticatedEndpointMatchers() {
207     return actuatorAuthProperties.unauthenticatedEndpointsOrDefaults().stream()
208         .map(EndpointRequest::to)
209         .toArray(EndpointServerWebExchangeMatcher[]::new);
210   }
211 
212   private ReactiveAuthenticationManager authenticationManager() {
213     return StringUtils.hasText(actuatorAuthProperties.getJwkSetUri())
214         ? passwordFlowReactiveAuthenticationManager()
215         : userDetailsAuthenticationManager();
216   }
217 
218   private ReactiveAuthenticationManager userDetailsAuthenticationManager() {
219     return Optional.ofNullable(userDetailsServiceProvider.getIfAvailable())
220         .map(UserDetailsRepositoryReactiveAuthenticationManager::new)
221         .orElseGet(() -> new UserDetailsRepositoryReactiveAuthenticationManager(
222             new MapReactiveUserDetailsService(authProperties
223                 .buildBasicAuthUserDetails(passwordEncoderProvider.getIfAvailable()))));
224   }
225 
226   private PasswordFlowReactiveAuthenticationManager passwordFlowReactiveAuthenticationManager() {
227     WebClientAccessTokenRetriever tokenRetriever = tokenRetrieverProvider.getIfAvailable();
228     log.info("Creating actuator {} with token retriever {} ...",
229         PasswordFlowReactiveAuthenticationManager.class.getSimpleName(), tokenRetriever);
230     return new PasswordFlowReactiveAuthenticationManager(
231         actuatorAuthProperties.getPasswordFlow(),
232         jwtDecoder(),
233         jwtConverter(),
234         Objects.requireNonNullElseGet(tokenRetriever, WebClientAccessTokenRetriever::new));
235   }
236 
237   private ReactiveJwtDecoder jwtDecoder() {
238     NimbusReactiveJwtDecoder nimbusJwtDecoder = NimbusReactiveJwtDecoder
239         .withJwkSetUri(actuatorAuthProperties.getJwkSetUri())
240         .jwsAlgorithm(SignatureAlgorithm.from(actuatorAuthProperties.getJwsAlgorithm()))
241         .build();
242     if (StringUtils.hasText(actuatorAuthProperties.getIssuerUri())) {
243       nimbusJwtDecoder.setJwtValidator(
244           JwtValidators.createDefaultWithIssuer(actuatorAuthProperties.getIssuerUri()));
245     }
246     return nimbusJwtDecoder;
247   }
248 
249   private JsonPathReactiveJwtConverter jwtConverter() {
250     JsonPathJwtConverter tmpJwtConverter = new JsonPathJwtConverter();
251     tmpJwtConverter.setNameJsonPath(actuatorAuthProperties.getNameJsonPath());
252     tmpJwtConverter.setRolePrefix(actuatorAuthProperties.getRolePrefix());
253     tmpJwtConverter.setRolesJsonPath(actuatorAuthProperties.getRolesJsonPath());
254     tmpJwtConverter.setRolesValueList(actuatorAuthProperties.isRolesValueList());
255     tmpJwtConverter.setRolesValueSeparator(actuatorAuthProperties.getRolesValueSeparator());
256     JsonPathReactiveJwtConverter internalJwtConverter = new JsonPathReactiveJwtConverter(
257         tmpJwtConverter);
258     JsonPathReactiveJwtConverter externalJwtConverter = jsonPathJwtConverterProvider
259         .getIfAvailable();
260     JsonPathReactiveJwtConverter jwtConverter;
261     if (internalJwtConverter.equals(externalJwtConverter)) {
262       log.info("Actuator security is using jwt converter from main application.");
263       jwtConverter = externalJwtConverter;
264     } else {
265       log.info("Actuator security is using it's own jwt converter.");
266       jwtConverter = internalJwtConverter;
267     }
268     return jwtConverter;
269   }
270 
271 }
272