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 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
67
68
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
100
101
102
103
104
105
106
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
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
156
157
158
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