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.wa.starter.config;
20  
21  import com.github.benmanes.caffeine.cache.Cache;
22  import com.github.benmanes.caffeine.cache.Caffeine;
23  import com.github.benmanes.caffeine.cache.LoadingCache;
24  import com.warrenstrange.googleauth.IGoogleAuthenticator;
25  import io.swagger.v3.oas.models.OpenAPI;
26  import io.swagger.v3.oas.models.info.Contact;
27  import io.swagger.v3.oas.models.info.Info;
28  import io.swagger.v3.oas.models.security.SecurityScheme;
29  import java.io.Serializable;
30  import java.time.OffsetDateTime;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.Optional;
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
36  import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStart;
37  import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStop;
38  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
39  import org.apache.syncope.wa.bootstrap.WAProperties;
40  import org.apache.syncope.wa.bootstrap.WARestClient;
41  import org.apache.syncope.wa.bootstrap.mapping.AttrReleaseMapper;
42  import org.apache.syncope.wa.starter.actuate.SyncopeCoreHealthIndicator;
43  import org.apache.syncope.wa.starter.actuate.SyncopeWAInfoContributor;
44  import org.apache.syncope.wa.starter.audit.WAAuditTrailManager;
45  import org.apache.syncope.wa.starter.events.WAEventRepository;
46  import org.apache.syncope.wa.starter.gauth.WAGoogleMfaAuthCredentialRepository;
47  import org.apache.syncope.wa.starter.gauth.WAGoogleMfaAuthTokenRepository;
48  import org.apache.syncope.wa.starter.mapping.AccessMapper;
49  import org.apache.syncope.wa.starter.mapping.AuthMapper;
50  import org.apache.syncope.wa.starter.mapping.CASSPClientAppTOMapper;
51  import org.apache.syncope.wa.starter.mapping.ClientAppMapper;
52  import org.apache.syncope.wa.starter.mapping.DefaultAccessMapper;
53  import org.apache.syncope.wa.starter.mapping.DefaultAuthMapper;
54  import org.apache.syncope.wa.starter.mapping.DefaultTicketExpirationMapper;
55  import org.apache.syncope.wa.starter.mapping.HttpRequestAccessMapper;
56  import org.apache.syncope.wa.starter.mapping.OIDCRPClientAppTOMapper;
57  import org.apache.syncope.wa.starter.mapping.RegisteredServiceMapper;
58  import org.apache.syncope.wa.starter.mapping.RemoteEndpointAccessMapper;
59  import org.apache.syncope.wa.starter.mapping.SAML2SPClientAppTOMapper;
60  import org.apache.syncope.wa.starter.mapping.TicketExpirationMapper;
61  import org.apache.syncope.wa.starter.mapping.TimeBasedAccessMapper;
62  import org.apache.syncope.wa.starter.mfa.WAMultifactorAuthenticationTrustStorage;
63  import org.apache.syncope.wa.starter.oidc.WAOIDCJWKSGeneratorService;
64  import org.apache.syncope.wa.starter.pac4j.saml.WASAML2ClientCustomizer;
65  import org.apache.syncope.wa.starter.saml.idp.WASamlIdPCasEventListener;
66  import org.apache.syncope.wa.starter.saml.idp.metadata.WASamlIdPMetadataGenerator;
67  import org.apache.syncope.wa.starter.saml.idp.metadata.WASamlIdPMetadataLocator;
68  import org.apache.syncope.wa.starter.services.WAServiceRegistry;
69  import org.apache.syncope.wa.starter.surrogate.WASurrogateAuthenticationService;
70  import org.apache.syncope.wa.starter.u2f.WAU2FDeviceRepository;
71  import org.apache.syncope.wa.starter.webauthn.WAWebAuthnCredentialRepository;
72  import org.apereo.cas.adaptors.u2f.storage.U2FDeviceRepository;
73  import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
74  import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
75  import org.apereo.cas.authentication.MultifactorAuthenticationProvider;
76  import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService;
77  import org.apereo.cas.configuration.CasConfigurationProperties;
78  import org.apereo.cas.configuration.model.support.mfa.gauth.LdapGoogleAuthenticatorMultifactorProperties;
79  import org.apereo.cas.configuration.model.support.mfa.u2f.U2FCoreMultifactorAuthenticationProperties;
80  import org.apereo.cas.gauth.credential.LdapGoogleAuthenticatorTokenCredentialRepository;
81  import org.apereo.cas.oidc.jwks.generator.OidcJsonWebKeystoreGeneratorService;
82  import org.apereo.cas.otp.repository.credentials.OneTimeTokenCredentialRepository;
83  import org.apereo.cas.services.ServiceRegistryExecutionPlanConfigurer;
84  import org.apereo.cas.services.ServiceRegistryListener;
85  import org.apereo.cas.support.events.CasEventRepository;
86  import org.apereo.cas.support.events.CasEventRepositoryFilter;
87  import org.apereo.cas.support.pac4j.authentication.clients.DelegatedClientFactoryCustomizer;
88  import org.apereo.cas.support.pac4j.authentication.handler.support.DelegatedClientAuthenticationHandler;
89  import org.apereo.cas.support.saml.idp.SamlIdPCasEventListener;
90  import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator;
91  import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
92  import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator;
93  import org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument;
94  import org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustRecordKeyGenerator;
95  import org.apereo.cas.trusted.authentication.api.MultifactorAuthenticationTrustStorage;
96  import org.apereo.cas.util.DateTimeUtils;
97  import org.apereo.cas.util.LdapUtils;
98  import org.apereo.cas.util.crypto.CipherExecutor;
99  import org.apereo.cas.webauthn.storage.WebAuthnCredentialRepository;
100 import org.ldaptive.ConnectionFactory;
101 import org.pac4j.core.client.Client;
102 import org.springframework.beans.factory.ObjectProvider;
103 import org.springframework.beans.factory.annotation.Qualifier;
104 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
105 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
106 import org.springframework.cloud.context.config.annotation.RefreshScope;
107 import org.springframework.context.ConfigurableApplicationContext;
108 import org.springframework.context.annotation.Bean;
109 import org.springframework.context.annotation.Configuration;
110 import org.springframework.context.annotation.Lazy;
111 import org.springframework.context.annotation.ScopedProxyMode;
112 import org.springframework.security.core.userdetails.User;
113 import org.springframework.security.core.userdetails.UserDetails;
114 import org.springframework.security.core.userdetails.UserDetailsService;
115 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
116 
117 @Configuration(proxyBeanMethods = false)
118 public class WAContext {
119 
120     public static final String CUSTOM_GOOGLE_AUTHENTICATOR_ACCOUNT_REGISTRY =
121             "customGoogleAuthenticatorAccountRegistry";
122 
123     private static String version(final ConfigurableApplicationContext ctx) {
124         return ctx.getEnvironment().getProperty("version");
125     }
126 
127     @ConditionalOnMissingBean
128     @Bean
129     public OpenAPI casSwaggerOpenApi(final ConfigurableApplicationContext ctx) {
130         return new OpenAPI().
131                 info(new Info().
132                         title("Apache Syncope").
133                         description("Apache Syncope " + version(ctx)).
134                         contact(new Contact().
135                                 name("The Apache Syncope community").
136                                 email("dev@syncope.apache.org").
137                                 url("https://syncope.apache.org")).
138                         version(version(ctx))).
139                 schemaRequirement("BasicAuthentication",
140                         new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("basic")).
141                 schemaRequirement("Bearer",
142                         new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"));
143     }
144 
145     @ConditionalOnMissingBean
146     @Bean
147     public AccessMapper defaultAccessMapper() {
148         return new DefaultAccessMapper();
149     }
150 
151     @ConditionalOnMissingBean
152     @Bean
153     public HttpRequestAccessMapper httpRequestAccessMapper() {
154         return new HttpRequestAccessMapper();
155     }
156 
157     @ConditionalOnMissingBean
158     @Bean
159     public RemoteEndpointAccessMapper remoteEndpointAccessMapper() {
160         return new RemoteEndpointAccessMapper();
161     }
162 
163     @ConditionalOnMissingBean
164     @Bean
165     public TimeBasedAccessMapper timeBasedAccessMapper() {
166         return new TimeBasedAccessMapper();
167     }
168 
169     @ConditionalOnMissingBean
170     @Bean
171     public AuthMapper authMapper() {
172         return new DefaultAuthMapper();
173     }
174 
175     @ConditionalOnMissingBean
176     @Bean
177     public TicketExpirationMapper ticketExpirationMapper() {
178         return new DefaultTicketExpirationMapper();
179     }
180 
181     @ConditionalOnMissingBean(name = "casSPClientAppTOMapper")
182     @Bean
183     public ClientAppMapper casSPClientAppTOMapper() {
184         return new CASSPClientAppTOMapper();
185     }
186 
187     @ConditionalOnMissingBean(name = "oidcRPClientAppTOMapper")
188     @Bean
189     public ClientAppMapper oidcRPClientAppTOMapper() {
190         return new OIDCRPClientAppTOMapper();
191     }
192 
193     @ConditionalOnMissingBean(name = "saml2SPClientAppTOMapper")
194     @Bean
195     public ClientAppMapper saml2SPClientAppTOMapper() {
196         return new SAML2SPClientAppTOMapper();
197     }
198 
199     @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
200     @ConditionalOnMissingBean
201     @Bean
202     public RegisteredServiceMapper registeredServiceMapper(
203             final CasConfigurationProperties casProperties,
204             final ObjectProvider<AuthenticationEventExecutionPlan> authenticationEventExecutionPlan,
205             final List<MultifactorAuthenticationProvider> multifactorAuthenticationProviders,
206             final List<AuthMapper> authMappers,
207             final List<AccessMapper> accessMappers,
208             final List<AttrReleaseMapper> attrReleaseMappers,
209             final List<TicketExpirationMapper> ticketExpirationMappers,
210             final List<ClientAppMapper> clientAppMappers) {
211 
212         return new RegisteredServiceMapper(
213                 Optional.ofNullable(casProperties.getAuthn().getPac4j().getCore().getName()).
214                         orElse(DelegatedClientAuthenticationHandler.class.getSimpleName()),
215                 authenticationEventExecutionPlan,
216                 multifactorAuthenticationProviders,
217                 authMappers,
218                 accessMappers,
219                 attrReleaseMappers,
220                 ticketExpirationMappers,
221                 clientAppMappers);
222     }
223 
224     @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
225     @ConditionalOnMissingBean
226     @Bean
227     public ServiceRegistryExecutionPlanConfigurer syncopeServiceRegistryConfigurer(
228             final ConfigurableApplicationContext ctx,
229             final WARestClient waRestClient,
230             final RegisteredServiceMapper registeredServiceMapper,
231             @Qualifier("serviceRegistryListeners")
232             final ObjectProvider<List<ServiceRegistryListener>> serviceRegistryListeners) {
233 
234         WAServiceRegistry registry = new WAServiceRegistry(
235                 waRestClient, registeredServiceMapper, ctx,
236                 Optional.ofNullable(serviceRegistryListeners.getIfAvailable()).orElseGet(ArrayList::new));
237         return plan -> plan.registerServiceRegistry(registry);
238     }
239 
240     @Bean
241     @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
242     @Lazy(false)
243     public SamlIdPCasEventListener samlIdPCasEventListener() {
244         return new WASamlIdPCasEventListener();
245     }
246 
247     @Bean
248     public SamlIdPMetadataGenerator samlIdPMetadataGenerator(
249             final WARestClient waRestClient,
250             final SamlIdPMetadataGeneratorConfigurationContext context) {
251 
252         return new WASamlIdPMetadataGenerator(context, waRestClient);
253     }
254 
255     @Bean
256     public SamlIdPMetadataLocator samlIdPMetadataLocator(
257             @Qualifier("samlIdPMetadataGeneratorCipherExecutor")
258             final CipherExecutor<String, String> cipherExecutor,
259             @Qualifier("samlIdPMetadataCache")
260             final Cache<String, SamlIdPMetadataDocument> samlIdPMetadataCache,
261             final WARestClient waRestClient) {
262 
263         return new WASamlIdPMetadataLocator(
264                 cipherExecutor,
265                 samlIdPMetadataCache,
266                 waRestClient);
267     }
268 
269     @Bean
270     public AuditTrailExecutionPlanConfigurer auditConfigurer(final WARestClient waRestClient) {
271         return plan -> plan.registerAuditTrailManager(new WAAuditTrailManager(waRestClient));
272     }
273 
274     @ConditionalOnMissingBean
275     @Bean
276     public CasEventRepositoryFilter syncopeWAEventRepositoryFilter() {
277         return CasEventRepositoryFilter.noOp();
278     }
279 
280     @Bean
281     public CasEventRepository casEventRepository(
282             final WARestClient waRestClient,
283             @Qualifier("syncopeWAEventRepositoryFilter")
284             final CasEventRepositoryFilter syncopeWAEventRepositoryFilter) {
285 
286         return new WAEventRepository(syncopeWAEventRepositoryFilter, waRestClient);
287     }
288 
289     @Bean
290     public DelegatedClientFactoryCustomizer<Client> delegatedClientCustomizer(final WARestClient waRestClient) {
291         return new WASAML2ClientCustomizer(waRestClient);
292     }
293 
294     @Bean
295     public WAGoogleMfaAuthTokenRepository oneTimeTokenAuthenticatorTokenRepository(
296             final CasConfigurationProperties casProperties,
297             final WARestClient waRestClient) {
298 
299         return new WAGoogleMfaAuthTokenRepository(
300                 waRestClient, casProperties.getAuthn().getMfa().getGauth().getCore().getTimeStepSize());
301     }
302 
303     @ConditionalOnMissingBean(name = CUSTOM_GOOGLE_AUTHENTICATOR_ACCOUNT_REGISTRY)
304     @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
305     @Bean
306     public OneTimeTokenCredentialRepository googleAuthenticatorAccountRegistry(
307             final CasConfigurationProperties casProperties,
308             @Qualifier("googleAuthenticatorAccountCipherExecutor")
309             final CipherExecutor<String, String> googleAuthenticatorAccountCipherExecutor,
310             @Qualifier("googleAuthenticatorScratchCodesCipherExecutor")
311             final CipherExecutor<Number, Number> googleAuthenticatorScratchCodesCipherExecutor,
312             final IGoogleAuthenticator googleAuthenticatorInstance,
313             final WARestClient waRestClient) {
314 
315         /*
316          * Declaring the LDAP-based repository as a Spring bean that would be conditionally activated
317          * via properties using annotations is not possible; conditionally-created spring beans cannot be
318          * refreshed, which means the settings ever change and the context is refreshed, the repository
319          * option can not be re-created. This could be revisited later in CAS 6.6.x using the {@code BeanSupplier}
320          * API construct to recreate the same bean in a more conventional way.
321          */
322         LdapGoogleAuthenticatorMultifactorProperties ldap = casProperties.getAuthn().getMfa().getGauth().getLdap();
323         if (StringUtils.isNotBlank(ldap.getBaseDn())
324                 && StringUtils.isNotBlank(ldap.getLdapUrl())
325                 && StringUtils.isNotBlank(ldap.getSearchFilter())) {
326 
327             ConnectionFactory connectionFactory = LdapUtils.newLdaptiveConnectionFactory(ldap);
328             return new LdapGoogleAuthenticatorTokenCredentialRepository(
329                     googleAuthenticatorAccountCipherExecutor,
330                     googleAuthenticatorScratchCodesCipherExecutor,
331                     googleAuthenticatorInstance,
332                     connectionFactory,
333                     ldap);
334         }
335         return new WAGoogleMfaAuthCredentialRepository(waRestClient, googleAuthenticatorInstance);
336     }
337 
338     @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
339     @Bean(name = MultifactorAuthenticationTrustStorage.BEAN_NAME)
340     public MultifactorAuthenticationTrustStorage mfaTrustStorage(
341             final CasConfigurationProperties casProperties,
342             @Qualifier("mfaTrustRecordKeyGenerator")
343             final MultifactorAuthenticationTrustRecordKeyGenerator keyGenerationStrategy,
344             @Qualifier("mfaTrustCipherExecutor")
345             final CipherExecutor<Serializable, String> mfaTrustCipherExecutor,
346             final WARestClient waRestClient) {
347 
348         return new WAMultifactorAuthenticationTrustStorage(
349                 casProperties.getAuthn().getMfa().getTrusted(),
350                 mfaTrustCipherExecutor,
351                 keyGenerationStrategy,
352                 waRestClient);
353     }
354 
355     @Bean
356     public OidcJsonWebKeystoreGeneratorService oidcJsonWebKeystoreGeneratorService(
357             final CasConfigurationProperties casProperties,
358             final WARestClient waRestClient) {
359 
360         return new WAOIDCJWKSGeneratorService(
361                 waRestClient,
362                 casProperties.getAuthn().getOidc().getJwks().getCore().getJwksKeyId(),
363                 casProperties.getAuthn().getOidc().getJwks().getCore().getJwksType(),
364                 casProperties.getAuthn().getOidc().getJwks().getCore().getJwksKeySize());
365     }
366 
367     @Bean
368     public WebAuthnCredentialRepository webAuthnCredentialRepository(
369             final CasConfigurationProperties casProperties,
370             final WARestClient waRestClient) {
371 
372         return new WAWebAuthnCredentialRepository(casProperties, waRestClient);
373     }
374 
375     @Bean
376     public U2FDeviceRepository u2fDeviceRepository(
377             final CasConfigurationProperties casProperties,
378             final WARestClient waRestClient) {
379 
380         U2FCoreMultifactorAuthenticationProperties u2f = casProperties.getAuthn().getMfa().getU2f().getCore();
381         OffsetDateTime expirationDate = OffsetDateTime.now().
382                 minus(u2f.getExpireDevices(), DateTimeUtils.toChronoUnit(u2f.getExpireDevicesTimeUnit()));
383         LoadingCache<String, String> requestStorage = Caffeine.newBuilder().
384                 expireAfterWrite(u2f.getExpireRegistrations(), u2f.getExpireRegistrationsTimeUnit()).
385                 build(key -> StringUtils.EMPTY);
386         return new WAU2FDeviceRepository(casProperties, requestStorage, waRestClient, expirationDate);
387     }
388 
389     @Bean
390     public SurrogateAuthenticationService surrogateAuthenticationService(final WARestClient waRestClient) {
391         return new WASurrogateAuthenticationService(waRestClient);
392     }
393 
394     @ConditionalOnMissingBean
395     @Bean
396     public SyncopeCoreHealthIndicator syncopeCoreHealthIndicator(final WARestClient waRestClient) {
397         return new SyncopeCoreHealthIndicator(waRestClient);
398     }
399 
400     @ConditionalOnMissingBean
401     @Bean
402     public SyncopeWAInfoContributor syncopeWAInfoContributor(final WAProperties waProperties) {
403         return new SyncopeWAInfoContributor(waProperties);
404     }
405 
406     @ConditionalOnMissingBean
407     @Bean
408     public UserDetailsService actuatorUserDetailsService(final WAProperties waProperties) {
409         UserDetails user = User.withUsername(waProperties.getAnonymousUser()).
410                 password("{noop}" + waProperties.getAnonymousKey()).
411                 roles(IdRepoEntitlement.ANONYMOUS).
412                 build();
413         return new InMemoryUserDetailsManager(user);
414     }
415 
416     @ConditionalOnProperty(
417             prefix = "keymaster", name = "enableAutoRegistration", havingValue = "true", matchIfMissing = true)
418     @Bean
419     public KeymasterStart keymasterStart() {
420         return new KeymasterStart(NetworkService.Type.WA);
421     }
422 
423     @ConditionalOnProperty(
424             prefix = "keymaster", name = "enableAutoRegistration", havingValue = "true", matchIfMissing = true)
425     @Bean
426     public KeymasterStop keymasterStop() {
427         return new KeymasterStop(NetworkService.Type.WA);
428     }
429 }