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 lombok.extern.slf4j.Slf4j;
21  import org.bremersee.security.authentication.AuthProperties;
22  import org.bremersee.security.authentication.AutoSecurityMode;
23  import org.bremersee.security.authentication.InMemoryUserDetailsAutoConfiguration;
24  import org.bremersee.security.authentication.JsonPathJwtConverter;
25  import org.bremersee.security.authentication.PasswordFlowAuthenticationManager;
26  import org.bremersee.security.authentication.PasswordFlowProperties;
27  import org.bremersee.security.authentication.RestTemplateAccessTokenRetriever;
28  import org.springframework.beans.factory.ObjectProvider;
29  import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
30  import org.springframework.boot.actuate.info.Info;
31  import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
32  import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
33  import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
34  import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
35  import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
36  import org.springframework.boot.autoconfigure.security.SecurityProperties;
37  import org.springframework.boot.context.event.ApplicationReadyEvent;
38  import org.springframework.boot.context.properties.EnableConfigurationProperties;
39  import org.springframework.context.annotation.Bean;
40  import org.springframework.context.annotation.Conditional;
41  import org.springframework.context.annotation.Configuration;
42  import org.springframework.context.event.EventListener;
43  import org.springframework.core.Ordered;
44  import org.springframework.http.HttpMethod;
45  import org.springframework.security.config.annotation.web.builders.HttpSecurity;
46  import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
47  import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
48  import org.springframework.security.config.http.SessionCreationPolicy;
49  import org.springframework.security.core.userdetails.UserDetailsService;
50  import org.springframework.security.crypto.password.PasswordEncoder;
51  import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
52  import org.springframework.security.oauth2.jwt.JwtDecoder;
53  import org.springframework.security.oauth2.jwt.JwtValidators;
54  import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
55  import org.springframework.security.web.util.matcher.AndRequestMatcher;
56  import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
57  import org.springframework.util.Assert;
58  import org.springframework.util.ClassUtils;
59  import org.springframework.util.StringUtils;
60  import org.springframework.web.client.RestTemplate;
61  
62  /**
63   * The actuator security auto configuration.
64   *
65   * @author Christian Bremer
66   */
67  @ConditionalOnWebApplication(type = Type.SERVLET)
68  @Conditional({ActuatorAutoSecurityCondition.class})
69  @ConditionalOnClass({
70      HttpSecurity.class,
71      PasswordFlowProperties.class,
72      Info.class
73  })
74  @EnableConfigurationProperties({
75      SecurityProperties.class,
76      AuthProperties.class,
77      ActuatorAuthProperties.class})
78  @Configuration
79  @Slf4j
80  public class ActuatorSecurityAutoConfiguration extends WebSecurityConfigurerAdapter
81      implements Ordered {
82  
83    private final SecurityProperties securityProperties;
84  
85    private final AuthProperties authProperties;
86  
87    private final ActuatorAuthProperties actuatorAuthProperties;
88  
89    private final ObjectProvider<JsonPathJwtConverter> jsonPathJwtConverterProvider;
90  
91    private final ObjectProvider<RestTemplateAccessTokenRetriever> tokenRetrieverProvider;
92  
93    private final ObjectProvider<PasswordEncoder> passwordEncoderProvider;
94  
95    /**
96     * Instantiates a new actuator security auto configuration.
97     *
98     * @param securityProperties the security properties
99     * @param authProperties the security properties
100    * @param actuatorAuthProperties the actuator security properties
101    * @param jsonPathJwtConverterProvider the json path jwt converter provider
102    * @param tokenRetrieverProvider the token retriever provider
103    * @param passwordEncoderProvider the password encoder provider
104    */
105   public ActuatorSecurityAutoConfiguration(
106       SecurityProperties securityProperties,
107       AuthProperties authProperties,
108       ActuatorAuthProperties actuatorAuthProperties,
109       ObjectProvider<JsonPathJwtConverter> jsonPathJwtConverterProvider,
110       ObjectProvider<RestTemplateAccessTokenRetriever> tokenRetrieverProvider,
111       ObjectProvider<PasswordEncoder> passwordEncoderProvider) {
112 
113     this.securityProperties = securityProperties;
114     this.authProperties = authProperties;
115     this.actuatorAuthProperties = actuatorAuthProperties;
116     this.jsonPathJwtConverterProvider = jsonPathJwtConverterProvider;
117     this.tokenRetrieverProvider = tokenRetrieverProvider;
118     this.passwordEncoderProvider = passwordEncoderProvider;
119   }
120 
121   /**
122    * Init.
123    */
124   @EventListener(ApplicationReadyEvent.class)
125   @SuppressWarnings("DuplicatedCode")
126   public void init() {
127     final boolean hasJwkUriSet = StringUtils.hasText(actuatorAuthProperties.getJwkSetUri());
128     log.info("\n"
129             + "*********************************************************************************\n"
130             + "* {}\n"
131             + "*********************************************************************************\n"
132             + "* enable = {}\n"
133             + "* order = {}\n"
134             + "* jwt = {}\n"
135             + "* cors = {}\n"
136             + "*********************************************************************************",
137         ClassUtils.getUserClass(getClass()).getSimpleName(),
138         actuatorAuthProperties.getEnable().name(),
139         actuatorAuthProperties.getOrder(),
140         hasJwkUriSet,
141         actuatorAuthProperties.isEnableCors());
142     if (hasJwkUriSet) {
143       Assert.hasText(actuatorAuthProperties.getPasswordFlow().getTokenEndpoint(),
144           "Token endpoint of actuator password flow must be present.");
145       Assert.hasText(actuatorAuthProperties.getPasswordFlow().getClientId(),
146           "Client ID of actuator password flow must be present.");
147       Assert.notNull(actuatorAuthProperties.getPasswordFlow().getClientSecret(),
148           "Client secret of actuator password flow must be present.");
149     }
150   }
151 
152   @Override
153   public int getOrder() {
154     return actuatorAuthProperties.getOrder();
155   }
156 
157   private EndpointRequest.EndpointRequestMatcher[] unauthenticatedEndpointMatchers() {
158     return actuatorAuthProperties.unauthenticatedEndpointsOrDefaults().stream()
159         .map(EndpointRequest::to)
160         .toArray(EndpointRequest.EndpointRequestMatcher[]::new);
161   }
162 
163   @Override
164   protected void configure(HttpSecurity httpSecurity) throws Exception {
165     log.info("Securing requests to /actuator/**");
166     HttpSecurity http = httpSecurity;
167     ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry reg = http
168         .requestMatcher(EndpointRequest.toAnyEndpoint())
169         .authorizeRequests();
170     if (actuatorAuthProperties.getEnable() == AutoSecurityMode.NONE) {
171       http = reg
172           .anyRequest().permitAll()
173           .and()
174           .httpBasic().disable();
175     } else {
176       if (actuatorAuthProperties.isEnableCors()) {
177         reg = reg.antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
178       }
179       http = reg
180           .requestMatchers(unauthenticatedEndpointMatchers()).permitAll()
181           .requestMatchers(new AndRequestMatcher(
182               EndpointRequest.toAnyEndpoint(),
183               new AntPathRequestMatcher("/**", "GET")))
184           .access(actuatorAuthProperties.buildAccessExpression())
185           .anyRequest()
186           .access(actuatorAuthProperties.buildAdminAccessExpression())
187           .and();
188       if (StringUtils.hasText(actuatorAuthProperties.getJwkSetUri())) {
189         http.authenticationProvider(passwordFlowAuthenticationManager());
190       }
191       http = http
192           .formLogin().disable()
193           .httpBasic().realmName("actuator")
194           .and();
195     }
196     http
197         .csrf().disable()
198         .cors(customizer -> {
199           if (!actuatorAuthProperties.isEnableCors()) {
200             customizer.disable();
201           }
202         })
203         .sessionManagement()
204         .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
205   }
206 
207   @ConditionalOnExpression("'${bremersee.actuator.auth.jwk-set-uri:}'.empty")
208   @ConditionalOnMissingBean
209   @Bean
210   @Override
211   public UserDetailsService userDetailsServiceBean() {
212     return new InMemoryUserDetailsAutoConfiguration().inMemoryUserDetailsManager(
213         securityProperties,
214         authProperties,
215         passwordEncoderProvider);
216   }
217 
218   private PasswordFlowAuthenticationManager passwordFlowAuthenticationManager() {
219     RestTemplateAccessTokenRetriever tokenRetriever = tokenRetrieverProvider.getIfAvailable();
220     log.info("Creating actuator {} with token retriever {} ...",
221         PasswordFlowAuthenticationManager.class.getSimpleName(), tokenRetriever);
222     return new PasswordFlowAuthenticationManager(
223         actuatorAuthProperties.getPasswordFlow(),
224         jwtDecoder(),
225         jwtConverter(),
226         Objects.requireNonNullElseGet(
227             tokenRetriever,
228             () -> new RestTemplateAccessTokenRetriever(new RestTemplate())));
229   }
230 
231   private JwtDecoder jwtDecoder() {
232     NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder
233         .withJwkSetUri(actuatorAuthProperties.getJwkSetUri())
234         .jwsAlgorithm(SignatureAlgorithm.from(actuatorAuthProperties.getJwsAlgorithm()))
235         .build();
236     if (StringUtils.hasText(actuatorAuthProperties.getIssuerUri())) {
237       nimbusJwtDecoder.setJwtValidator(
238           JwtValidators.createDefaultWithIssuer(actuatorAuthProperties.getIssuerUri()));
239     }
240     return nimbusJwtDecoder;
241   }
242 
243   private JsonPathJwtConverter jwtConverter() {
244     JsonPathJwtConverter internalJwtConverter = new JsonPathJwtConverter();
245     internalJwtConverter.setNameJsonPath(actuatorAuthProperties.getNameJsonPath());
246     internalJwtConverter.setRolePrefix(actuatorAuthProperties.getRolePrefix());
247     internalJwtConverter.setRolesJsonPath(actuatorAuthProperties.getRolesJsonPath());
248     internalJwtConverter.setRolesValueList(actuatorAuthProperties.isRolesValueList());
249     internalJwtConverter.setRolesValueSeparator(
250         actuatorAuthProperties.getRolesValueSeparator());
251     JsonPathJwtConverter externalJwtConverter = jsonPathJwtConverterProvider.getIfAvailable();
252     JsonPathJwtConverter jwtConverter;
253     if (internalJwtConverter.equals(externalJwtConverter)) {
254       jwtConverter = externalJwtConverter;
255     } else {
256       jwtConverter = internalJwtConverter;
257     }
258     return jwtConverter;
259   }
260 
261 }