1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
64
65
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
97
98
99
100
101
102
103
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
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 }