View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.sra;
20  
21  import java.io.InputStream;
22  import java.security.KeyStore;
23  import java.security.PrivateKey;
24  import java.security.cert.X509Certificate;
25  import java.text.ParseException;
26  import java.util.Map;
27  import org.apache.commons.lang3.StringUtils;
28  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
29  import org.apache.syncope.common.lib.types.SAML2BindingType;
30  import org.apache.syncope.sra.security.CsrfRouteMatcher;
31  import org.apache.syncope.sra.security.LogoutRouteMatcher;
32  import org.apache.syncope.sra.security.PublicRouteMatcher;
33  import org.apache.syncope.sra.security.cas.CASSecurityConfigUtils;
34  import org.apache.syncope.sra.security.oauth2.OAuth2SecurityConfigUtils;
35  import org.apache.syncope.sra.security.pac4j.NoOpLogoutHandler;
36  import org.apache.syncope.sra.security.saml2.SAML2MetadataEndpoint;
37  import org.apache.syncope.sra.security.saml2.SAML2SecurityConfigUtils;
38  import org.apache.syncope.sra.security.saml2.SAML2WebSsoAuthenticationWebFilter;
39  import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver;
40  import org.pac4j.saml.client.SAML2Client;
41  import org.pac4j.saml.config.SAML2Configuration;
42  import org.pac4j.saml.metadata.keystore.BaseSAML2KeystoreGenerator;
43  import org.springframework.beans.factory.ObjectProvider;
44  import org.springframework.beans.factory.annotation.Qualifier;
45  import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;
46  import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
47  import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
48  import org.springframework.cache.CacheManager;
49  import org.springframework.context.ConfigurableApplicationContext;
50  import org.springframework.context.annotation.Bean;
51  import org.springframework.context.annotation.Configuration;
52  import org.springframework.core.annotation.Order;
53  import org.springframework.core.convert.converter.Converter;
54  import org.springframework.core.io.FileUrlResource;
55  import org.springframework.core.io.support.ResourcePatternResolver;
56  import org.springframework.http.HttpMethod;
57  import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
58  import org.springframework.security.config.web.server.ServerHttpSecurity;
59  import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
60  import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
61  import org.springframework.security.core.userdetails.User;
62  import org.springframework.security.core.userdetails.UserDetails;
63  import org.springframework.security.oauth2.client.registration.ClientRegistration;
64  import org.springframework.security.oauth2.client.registration.ClientRegistrations;
65  import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
66  import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
67  import org.springframework.security.oauth2.core.AuthorizationGrantType;
68  import org.springframework.security.oauth2.core.OAuth2TokenValidator;
69  import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
70  import org.springframework.security.oauth2.jwt.Jwt;
71  import org.springframework.security.oauth2.jwt.JwtValidators;
72  import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter;
73  import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
74  import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
75  import org.springframework.security.web.server.SecurityWebFilterChain;
76  import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
77  import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
78  import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
79  import reactor.core.publisher.Mono;
80  
81  @EnableWebFluxSecurity
82  @Configuration(proxyBeanMethods = false)
83  public class SecurityConfig {
84  
85      @Bean
86      @Order(0)
87      @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "SAML2")
88      public SecurityWebFilterChain saml2SecurityFilterChain(final ServerHttpSecurity http) {
89          ServerWebExchangeMatcher metadataMatcher =
90                  ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, SAML2MetadataEndpoint.METADATA_URL);
91          return http.securityMatcher(metadataMatcher).
92                  authorizeExchange().anyExchange().permitAll().
93                  and().csrf().requireCsrfProtectionMatcher(new NegatedServerWebExchangeMatcher(metadataMatcher)).
94                  and().build();
95      }
96  
97      @ConditionalOnMissingBean
98      @Bean
99      @Order(1)
100     public SecurityWebFilterChain actuatorSecurityFilterChain(final ServerHttpSecurity http) {
101         ServerWebExchangeMatcher actuatorMatcher = EndpointRequest.toAnyEndpoint();
102         return http.securityMatcher(actuatorMatcher).
103                 authorizeExchange().anyExchange().authenticated().
104                 and().httpBasic().
105                 and().csrf().requireCsrfProtectionMatcher(new NegatedServerWebExchangeMatcher(actuatorMatcher)).
106                 and().build();
107     }
108 
109     @ConditionalOnMissingBean
110     @Bean
111     public ReactiveUserDetailsService actuatorUserDetailsService(final SRAProperties props) {
112         UserDetails user = User.builder().
113                 username(props.getAnonymousUser()).
114                 password("{noop}" + props.getAnonymousKey()).
115                 roles(IdRepoEntitlement.ANONYMOUS).
116                 build();
117         return new MapReactiveUserDetailsService(user);
118     }
119 
120     @Bean
121     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OIDC")
122     public ClientRegistration oidcClientRegistration(final SRAProperties props) {
123         return ClientRegistrations.fromOidcIssuerLocation(props.getOidc().getConfiguration()).
124                 registrationId(SRAProperties.AMType.OIDC.name()).
125                 clientId(props.getOidc().getClientId()).
126                 clientSecret(props.getOidc().getClientSecret()).
127                 scope(props.getOidc().getScopes().toArray(String[]::new)).
128                 build();
129     }
130 
131     @Bean
132     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OIDC")
133     public ReactiveClientRegistrationRepository oidcClientRegistrationRepository(
134             @Qualifier("oidcClientRegistration") final ClientRegistration oidcClientRegistration) {
135         return new InMemoryReactiveClientRegistrationRepository(oidcClientRegistration);
136     }
137 
138     @Bean
139     @ConditionalOnMissingBean
140     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OIDC")
141     public OAuth2TokenValidator<Jwt> oidcJWTValidator(final SRAProperties props) {
142         return JwtValidators.createDefaultWithIssuer(props.getOidc().getConfiguration());
143     }
144 
145     @Bean
146     @ConditionalOnMissingBean
147     public Converter<Map<String, Object>, Map<String, Object>> jwtClaimSetConverter() {
148         return MappedJwtClaimSetConverter.withDefaults(Map.of());
149     }
150 
151     @Bean
152     @ConditionalOnMissingBean
153     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OIDC")
154     public ReactiveJwtDecoder oidcJWTDecoder(
155             @Qualifier("oidcClientRegistration")
156             final ClientRegistration oidcClientRegistration,
157             @Qualifier("oidcJWTValidator")
158             final OAuth2TokenValidator<Jwt> oidcJWTValidator,
159             @Qualifier("jwtClaimSetConverter")
160             final Converter<Map<String, Object>, Map<String, Object>> jwtClaimSetConverter) {
161         String jwkSetUri = oidcClientRegistration.getProviderDetails().getJwkSetUri();
162         NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri)
163                 .jwsAlgorithm(SignatureAlgorithm.RS256)
164                 .jwsAlgorithm(SignatureAlgorithm.RS512)
165                 .build();
166         jwtDecoder.setJwtValidator(oidcJWTValidator);
167         jwtDecoder.setClaimSetConverter(jwtClaimSetConverter);
168         return jwtDecoder;
169     }
170 
171     @Bean
172     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OAUTH2")
173     public ClientRegistration oauth2ClientRegistration(final SRAProperties props) {
174         return ClientRegistration.withRegistrationId(SRAProperties.AMType.OAUTH2.name()).
175                 redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}").
176                 tokenUri(props.getOauth2().getTokenUri()).
177                 authorizationUri(props.getOauth2().getAuthorizationUri()).
178                 userInfoUri(props.getOauth2().getUserInfoUri()).
179                 userNameAttributeName(props.getOauth2().getUserNameAttributeName()).
180                 clientId(props.getOauth2().getClientId()).
181                 clientSecret(props.getOauth2().getClientSecret()).
182                 scope(props.getOauth2().getScopes().toArray(String[]::new)).
183                 authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).
184                 jwkSetUri(props.getOauth2().getJwkSetUri()).
185                 build();
186     }
187 
188     @Bean
189     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OAUTH2")
190     public ReactiveClientRegistrationRepository oauth2ClientRegistrationRepository(
191             @Qualifier("oauth2ClientRegistration") final ClientRegistration oauth2ClientRegistration) {
192         return new InMemoryReactiveClientRegistrationRepository(oauth2ClientRegistration);
193     }
194 
195     @Bean
196     @ConditionalOnMissingBean
197     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OAUTH2")
198     public OAuth2TokenValidator<Jwt> oauth2JWTValidator(final SRAProperties props) {
199         return props.getOauth2().getIssuer() == null
200                 ? JwtValidators.createDefault()
201                 : JwtValidators.createDefaultWithIssuer(props.getOauth2().getIssuer());
202     }
203 
204     @Bean
205     @ConditionalOnMissingBean
206     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OAUTH2")
207     public ReactiveJwtDecoder oauth2JWTDecoder(
208             @Qualifier("oauth2ClientRegistration")
209             final ClientRegistration oauth2ClientRegistration,
210             @Qualifier("oauth2JWTValidator")
211             final OAuth2TokenValidator<Jwt> oauth2JWTValidator,
212             @Qualifier("jwtClaimSetConverter")
213             final Converter<Map<String, Object>, Map<String, Object>> jwtClaimSetConverter) {
214 
215         String jwkSetUri = oauth2ClientRegistration.getProviderDetails().getJwkSetUri();
216         NimbusReactiveJwtDecoder jwtDecoder;
217         if (StringUtils.isBlank(jwkSetUri)) {
218             jwtDecoder = new NimbusReactiveJwtDecoder(jwt -> {
219                 try {
220                     return Mono.just(jwt.getJWTClaimsSet());
221                 } catch (ParseException e) {
222                     return Mono.error(e);
223                 }
224             });
225         } else {
226             jwtDecoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
227         }
228         jwtDecoder.setJwtValidator(oauth2JWTValidator);
229         jwtDecoder.setClaimSetConverter(jwtClaimSetConverter);
230         return jwtDecoder;
231     }
232 
233     @Bean
234     @ConditionalOnMissingBean
235     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "SAML2")
236     public SAML2Client saml2Client(final ResourcePatternResolver resourceResolver,
237             final SRAProperties props) {
238         SAML2Configuration cfg = new SAML2Configuration(
239                 resourceResolver.getResource(props.getSaml2().getKeystore()),
240                 props.getSaml2().getKeystoreStorePass(),
241                 props.getSaml2().getKeystoreKeypass(),
242                 resourceResolver.getResource(props.getSaml2().getIdpMetadata()));
243 
244         cfg.setKeystoreType(props.getSaml2().getKeystoreType());
245         if (cfg.getKeystoreResource() instanceof FileUrlResource) {
246             cfg.setKeystoreGenerator(new BaseSAML2KeystoreGenerator(cfg) {
247 
248                 @Override
249                 protected void store(
250                         final KeyStore ks,
251                         final X509Certificate certificate,
252                         final PrivateKey privateKey) throws Exception {
253 
254                     // nothing to do
255                 }
256 
257                 @Override
258                 public InputStream retrieve() throws Exception {
259                     return cfg.getKeystoreResource().getInputStream();
260                 }
261             });
262         }
263 
264         cfg.setAuthnRequestBindingType(props.getSaml2().getAuthnRequestBinding().getUri());
265         cfg.setResponseBindingType(SAML2BindingType.POST.getUri());
266         cfg.setSpLogoutRequestBindingType(props.getSaml2().getLogoutRequestBinding().getUri());
267         cfg.setSpLogoutResponseBindingType(props.getSaml2().getLogoutResponseBinding().getUri());
268 
269         cfg.setServiceProviderEntityId(props.getSaml2().getEntityId());
270 
271         cfg.setWantsAssertionsSigned(true);
272         cfg.setAuthnRequestSigned(true);
273         cfg.setSpLogoutRequestSigned(true);
274         cfg.setServiceProviderMetadataResourceFilepath(props.getSaml2().getSpMetadataFilePath());
275         cfg.setMaximumAuthenticationLifetime(props.getSaml2().getMaximumAuthenticationLifetime());
276         cfg.setAcceptedSkew(props.getSaml2().getAcceptedSkew());
277 
278         cfg.setLogoutHandler(new NoOpLogoutHandler());
279 
280         SAML2Client saml2Client = new SAML2Client(cfg);
281         saml2Client.setName(SRAProperties.AMType.SAML2.name());
282         saml2Client.setCallbackUrl(props.getSaml2().getEntityId()
283                 + SAML2WebSsoAuthenticationWebFilter.FILTER_PROCESSES_URI);
284         saml2Client.setCallbackUrlResolver(new NoParameterCallbackUrlResolver());
285         saml2Client.init();
286 
287         return saml2Client;
288     }
289 
290     @Bean
291     @Order(2)
292     @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE)
293     public SecurityWebFilterChain routesSecurityFilterChain(
294             @Qualifier("saml2Client") final ObjectProvider<SAML2Client> saml2Client,
295             final SRAProperties props,
296             final ServerHttpSecurity http,
297             final CacheManager cacheManager,
298             final LogoutRouteMatcher logoutRouteMatcher,
299             final PublicRouteMatcher publicRouteMatcher,
300             final CsrfRouteMatcher csrfRouteMatcher,
301             final ConfigurableApplicationContext ctx) {
302 
303         ServerHttpSecurity.AuthorizeExchangeSpec builder = http.authorizeExchange().
304                 matchers(publicRouteMatcher).permitAll().
305                 anyExchange().authenticated();
306 
307         switch (props.getAmType()) {
308             case OIDC:
309             case OAUTH2:
310                 OAuth2SecurityConfigUtils.forLogin(http, props.getAmType(), ctx);
311                 OAuth2SecurityConfigUtils.forLogout(builder, props.getAmType(), cacheManager, logoutRouteMatcher, ctx);
312                 http.oauth2ResourceServer().jwt().jwtDecoder(ctx.getBean(ReactiveJwtDecoder.class));
313                 break;
314 
315             case SAML2:
316                 saml2Client.ifAvailable(client -> {
317                     SAML2SecurityConfigUtils.forLogin(http, client, publicRouteMatcher);
318                     SAML2SecurityConfigUtils.forLogout(builder, client, cacheManager, logoutRouteMatcher, ctx);
319                 });
320                 break;
321 
322             case CAS:
323                 CASSecurityConfigUtils.forLogin(
324                         http,
325                         props.getCas().getProtocol(),
326                         props.getCas().getServerPrefix(),
327                         publicRouteMatcher);
328                 CASSecurityConfigUtils.forLogout(
329                         builder,
330                         cacheManager,
331                         props.getCas().getServerPrefix(),
332                         logoutRouteMatcher,
333                         ctx);
334                 break;
335 
336             default:
337         }
338 
339         return builder.and().csrf().requireCsrfProtectionMatcher(csrfRouteMatcher).and().build();
340     }
341 }