1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
317
318
319
320
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 }