| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.syncope.sra; |
| |
| import java.io.InputStream; |
| import java.security.KeyStore; |
| import java.security.PrivateKey; |
| import java.security.cert.X509Certificate; |
| import java.text.ParseException; |
| import java.util.Map; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.syncope.common.lib.types.IdRepoEntitlement; |
| import org.apache.syncope.common.lib.types.SAML2BindingType; |
| import org.apache.syncope.sra.security.CsrfRouteMatcher; |
| import org.apache.syncope.sra.security.LogoutRouteMatcher; |
| import org.apache.syncope.sra.security.oauth2.OAuth2SecurityConfigUtils; |
| import org.apache.syncope.sra.security.PublicRouteMatcher; |
| import org.apache.syncope.sra.security.cas.CASSecurityConfigUtils; |
| import org.apache.syncope.sra.security.pac4j.NoOpLogoutHandler; |
| import org.apache.syncope.sra.security.saml2.SAML2MetadataEndpoint; |
| import org.apache.syncope.sra.security.saml2.SAML2SecurityConfigUtils; |
| import org.apache.syncope.sra.security.saml2.SAML2WebSsoAuthenticationWebFilter; |
| import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver; |
| import org.pac4j.saml.client.SAML2Client; |
| import org.pac4j.saml.config.SAML2Configuration; |
| import org.pac4j.saml.metadata.keystore.BaseSAML2KeystoreGenerator; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest; |
| import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| import org.springframework.cache.CacheManager; |
| import org.springframework.context.ConfigurableApplicationContext; |
| import org.springframework.context.annotation.Bean; |
| import org.springframework.context.annotation.Configuration; |
| import org.springframework.core.annotation.Order; |
| import org.springframework.core.convert.converter.Converter; |
| import org.springframework.core.io.FileUrlResource; |
| import org.springframework.core.io.support.ResourcePatternResolver; |
| import org.springframework.http.HttpMethod; |
| import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; |
| import org.springframework.security.config.web.server.ServerHttpSecurity; |
| import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; |
| import org.springframework.security.core.userdetails.User; |
| import org.springframework.security.core.userdetails.UserDetails; |
| import org.springframework.security.oauth2.client.registration.ClientRegistration; |
| import org.springframework.security.oauth2.client.registration.ClientRegistrations; |
| import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; |
| import org.springframework.security.oauth2.core.AuthorizationGrantType; |
| import org.springframework.security.oauth2.core.OAuth2TokenValidator; |
| import org.springframework.security.oauth2.jwt.Jwt; |
| import org.springframework.security.oauth2.jwt.JwtValidators; |
| import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter; |
| import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; |
| import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; |
| import org.springframework.security.web.server.SecurityWebFilterChain; |
| import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher; |
| import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; |
| import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; |
| import reactor.core.publisher.Mono; |
| |
| @EnableWebFluxSecurity |
| @Configuration |
| public class SecurityConfig { |
| |
| @Autowired |
| private ResourcePatternResolver resourceResolver; |
| |
| @Autowired |
| private SRAProperties props; |
| |
| @Bean |
| @Order(0) |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "SAML2") |
| public SecurityWebFilterChain saml2SecurityFilterChain(final ServerHttpSecurity http) { |
| ServerWebExchangeMatcher metadataMatcher = |
| ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, SAML2MetadataEndpoint.METADATA_URL); |
| return http.securityMatcher(metadataMatcher). |
| authorizeExchange().anyExchange().permitAll(). |
| and().csrf().requireCsrfProtectionMatcher(new NegatedServerWebExchangeMatcher(metadataMatcher)). |
| and().build(); |
| } |
| |
| @Bean |
| @Order(1) |
| public SecurityWebFilterChain actuatorSecurityFilterChain(final ServerHttpSecurity http) { |
| ServerWebExchangeMatcher actuatorMatcher = EndpointRequest.toAnyEndpoint(); |
| return http.securityMatcher(actuatorMatcher). |
| authorizeExchange().anyExchange().authenticated(). |
| and().httpBasic(). |
| and().csrf().requireCsrfProtectionMatcher(new NegatedServerWebExchangeMatcher(actuatorMatcher)). |
| and().build(); |
| } |
| |
| @Bean |
| public MapReactiveUserDetailsService userDetailsService() { |
| UserDetails user = User.builder(). |
| username(props.getAnonymousUser()). |
| password("{noop}" + props.getAnonymousKey()). |
| roles(IdRepoEntitlement.ANONYMOUS). |
| build(); |
| return new MapReactiveUserDetailsService(user); |
| } |
| |
| @Bean |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OIDC") |
| public InMemoryReactiveClientRegistrationRepository oidcClientRegistrationRepository() { |
| return new InMemoryReactiveClientRegistrationRepository( |
| ClientRegistrations.fromOidcIssuerLocation(props.getOidc().getConfiguration()). |
| registrationId(SRAProperties.AMType.OIDC.name()). |
| clientId(props.getOidc().getClientId()). |
| clientSecret(props.getOidc().getClientSecret()). |
| scope(props.getOidc().getScopes().toArray(new String[0])). |
| build()); |
| } |
| |
| @Bean |
| @ConditionalOnMissingBean |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OIDC") |
| public OAuth2TokenValidator<Jwt> oidcJWTValidator() { |
| return JwtValidators.createDefaultWithIssuer(props.getOidc().getConfiguration()); |
| } |
| |
| @Bean |
| @ConditionalOnMissingBean |
| public Converter<Map<String, Object>, Map<String, Object>> jwtClaimSetConverter() { |
| return MappedJwtClaimSetConverter.withDefaults(Map.of()); |
| } |
| |
| @Bean |
| @ConditionalOnMissingBean |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OIDC") |
| public ReactiveJwtDecoder oidcJWTDecoder() { |
| NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSetUri( |
| oidcClientRegistrationRepository().iterator().next().getProviderDetails().getJwkSetUri()).build(); |
| jwtDecoder.setJwtValidator(oidcJWTValidator()); |
| jwtDecoder.setClaimSetConverter(jwtClaimSetConverter()); |
| return jwtDecoder; |
| } |
| |
| @Bean |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OAUTH2") |
| public InMemoryReactiveClientRegistrationRepository oauth2ClientRegistrationRepository() { |
| return new InMemoryReactiveClientRegistrationRepository( |
| ClientRegistration.withRegistrationId(SRAProperties.AMType.OAUTH2.name()). |
| redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}"). |
| tokenUri(props.getOauth2().getTokenUri()). |
| authorizationUri(props.getOauth2().getAuthorizationUri()). |
| userInfoUri(props.getOauth2().getUserInfoUri()). |
| userNameAttributeName(props.getOauth2().getUserNameAttributeName()). |
| clientId(props.getOauth2().getClientId()). |
| clientSecret(props.getOauth2().getClientSecret()). |
| scope(props.getOauth2().getScopes().toArray(new String[0])). |
| authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE). |
| jwkSetUri(props.getOauth2().getJwkSetUri()). |
| build()); |
| } |
| |
| @Bean |
| @ConditionalOnMissingBean |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OAUTH2") |
| public OAuth2TokenValidator<Jwt> oauth2JWTValidator() { |
| return props.getOauth2().getIssuer() == null |
| ? JwtValidators.createDefault() |
| : JwtValidators.createDefaultWithIssuer(props.getOauth2().getIssuer()); |
| } |
| |
| @Bean |
| @ConditionalOnMissingBean |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "OAUTH2") |
| public ReactiveJwtDecoder oauth2JWTDecoder() { |
| String jwkSetUri = oauth2ClientRegistrationRepository().iterator().next().getProviderDetails().getJwkSetUri(); |
| NimbusReactiveJwtDecoder jwtDecoder; |
| if (StringUtils.isBlank(jwkSetUri)) { |
| jwtDecoder = new NimbusReactiveJwtDecoder(jwt -> { |
| try { |
| return Mono.just(jwt.getJWTClaimsSet()); |
| } catch (ParseException e) { |
| return Mono.error(e); |
| } |
| }); |
| } else { |
| jwtDecoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); |
| } |
| jwtDecoder.setJwtValidator(oauth2JWTValidator()); |
| jwtDecoder.setClaimSetConverter(jwtClaimSetConverter()); |
| return jwtDecoder; |
| } |
| |
| @Bean |
| @ConditionalOnMissingBean |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE, havingValue = "SAML2") |
| public SAML2Client saml2Client() { |
| SAML2Configuration cfg = new SAML2Configuration( |
| resourceResolver.getResource(props.getSaml2().getKeystore()), |
| props.getSaml2().getKeystoreStorePass(), |
| props.getSaml2().getKeystoreKeypass(), |
| resourceResolver.getResource(props.getSaml2().getIdpMetadata())); |
| |
| cfg.setKeystoreType(props.getSaml2().getKeystoreType()); |
| if (cfg.getKeystoreResource() instanceof FileUrlResource) { |
| cfg.setKeystoreGenerator(new BaseSAML2KeystoreGenerator(cfg) { |
| |
| @Override |
| protected void store( |
| final KeyStore ks, |
| final X509Certificate certificate, |
| final PrivateKey privateKey) throws Exception { |
| |
| // nothing to do |
| } |
| |
| @Override |
| public InputStream retrieve() throws Exception { |
| return cfg.getKeystoreResource().getInputStream(); |
| } |
| }); |
| } |
| |
| cfg.setAuthnRequestBindingType(props.getSaml2().getAuthnRequestBinding().getUri()); |
| cfg.setResponseBindingType(SAML2BindingType.POST.getUri()); |
| cfg.setSpLogoutRequestBindingType(props.getSaml2().getLogoutRequestBinding().getUri()); |
| cfg.setSpLogoutResponseBindingType(props.getSaml2().getLogoutResponseBinding().getUri()); |
| |
| cfg.setServiceProviderEntityId(props.getSaml2().getEntityId()); |
| |
| cfg.setWantsAssertionsSigned(true); |
| cfg.setAuthnRequestSigned(true); |
| cfg.setSpLogoutRequestSigned(true); |
| cfg.setServiceProviderMetadataResourceFilepath(props.getSaml2().getSpMetadataFilePath()); |
| cfg.setAcceptedSkew(props.getSaml2().getSkew()); |
| |
| cfg.setLogoutHandler(new NoOpLogoutHandler()); |
| |
| SAML2Client saml2Client = new SAML2Client(cfg); |
| saml2Client.setName(SRAProperties.AMType.SAML2.name()); |
| saml2Client.setCallbackUrl(props.getSaml2().getEntityId() |
| + SAML2WebSsoAuthenticationWebFilter.FILTER_PROCESSES_URI); |
| saml2Client.setCallbackUrlResolver(new NoParameterCallbackUrlResolver()); |
| saml2Client.init(); |
| |
| return saml2Client; |
| } |
| |
| @Bean |
| @Order(2) |
| @ConditionalOnProperty(prefix = SRAProperties.PREFIX, name = SRAProperties.AM_TYPE) |
| public SecurityWebFilterChain routesSecurityFilterChain( |
| final ServerHttpSecurity http, |
| final CacheManager cacheManager, |
| final LogoutRouteMatcher logoutRouteMatcher, |
| final PublicRouteMatcher publicRouteMatcher, |
| final CsrfRouteMatcher csrfRouteMatcher, |
| final ConfigurableApplicationContext ctx) { |
| |
| ServerHttpSecurity.AuthorizeExchangeSpec builder = http.authorizeExchange(). |
| matchers(publicRouteMatcher).permitAll(). |
| anyExchange().authenticated(); |
| |
| switch (props.getAmType()) { |
| case OIDC: |
| case OAUTH2: |
| OAuth2SecurityConfigUtils.forLogin(http, props.getAmType(), ctx); |
| OAuth2SecurityConfigUtils.forLogout(builder, props.getAmType(), cacheManager, logoutRouteMatcher, ctx); |
| http.oauth2ResourceServer().jwt().jwtDecoder(ctx.getBean(ReactiveJwtDecoder.class)); |
| break; |
| |
| case SAML2: |
| SAML2Client saml2Client = saml2Client(); |
| SAML2SecurityConfigUtils.forLogin(http, saml2Client, publicRouteMatcher); |
| SAML2SecurityConfigUtils.forLogout(builder, saml2Client, cacheManager, logoutRouteMatcher, ctx); |
| break; |
| |
| case CAS: |
| CASSecurityConfigUtils.forLogin( |
| http, |
| props.getCas().getServerName(), |
| props.getCas().getProtocol(), |
| props.getCas().getServerPrefix(), |
| publicRouteMatcher); |
| CASSecurityConfigUtils.forLogout( |
| builder, |
| cacheManager, |
| props.getCas().getServerPrefix(), |
| logoutRouteMatcher, |
| ctx); |
| break; |
| |
| default: |
| } |
| |
| return builder.and().csrf().requireCsrfProtectionMatcher(csrfRouteMatcher).and().build(); |
| } |
| } |