diff --git a/README.md b/README.md index c476c3bb..29be4e93 100644 --- a/README.md +++ b/README.md @@ -35,4 +35,4 @@ See [https://docs.swedenconnect.se/saml-identity-provider](https://docs.swedenco ----- -Copyright © 2022-2023, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file +Copyright © 2022-2024, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file diff --git a/autoconfigure/pom.xml b/autoconfigure/pom.xml index eda50dc3..965e2f35 100644 --- a/autoconfigure/pom.xml +++ b/autoconfigure/pom.xml @@ -10,7 +10,7 @@ se.swedenconnect.spring.saml.idp spring-saml-idp-parent - 2.0.2 + 2.1.0-SNAPSHOT Sweden Connect :: Spring SAML Identity Provider :: Spring Boot Autoconfigure module @@ -77,7 +77,27 @@ spring-saml-idp ${project.version} + + + + org.springframework.data + spring-data-redis + true + + + org.springframework.session + spring-session-data-redis + true + + + + org.redisson + redisson-spring-boot-starter + 3.25.2 + true + + diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditEventRepositoryFactory.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditEventRepositoryFactory.java new file mode 100644 index 00000000..3ce18493 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditEventRepositoryFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.audit; + +import java.util.function.Predicate; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; + +import se.swedenconnect.spring.saml.idp.audit.repository.AuditEventMapper; + +/** + * For creating Redis {@link AuditEventRepository} beans. + */ +@FunctionalInterface +public interface AuditEventRepositoryFactory { + + /** + * Creates an {@link AuditEventRepository}. + * + * @param name the Redis name for the list/timeseries + * @param auditEventMapper the event mapper + * @param filter the filter predicate + * @return an {@link AuditEventRepository} + */ + AuditEventRepository create( + final String name, final AuditEventMapper auditEventMapper, Predicate filter); + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditRepositoryAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditRepositoryAutoConfiguration.java new file mode 100644 index 00000000..d9cf5ba5 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditRepositoryAutoConfiguration.java @@ -0,0 +1,152 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.audit; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import se.swedenconnect.spring.saml.idp.audit.repository.AuditEventMapper; +import se.swedenconnect.spring.saml.idp.audit.repository.DelegatingAuditEventRepository; +import se.swedenconnect.spring.saml.idp.audit.repository.FileBasedAuditEventRepository; +import se.swedenconnect.spring.saml.idp.audit.repository.FilteringAuditEventRepository; +import se.swedenconnect.spring.saml.idp.audit.repository.JsonAuditEventMapper; +import se.swedenconnect.spring.saml.idp.audit.repository.MemoryBasedAuditEventRepository; + +/** + * Auto configuration for auditing support where an {@link AuditEventRepository} is created. + * + * @author Martin Lindström + */ +@ConditionalOnMissingBean(AuditEventRepository.class) +@AutoConfiguration(before = AuditAutoConfiguration.class) +@EnableConfigurationProperties(AuditRepositoryConfigurationProperties.class) +public class AuditRepositoryAutoConfiguration { + + /** The audit properties. */ + private final AuditRepositoryConfigurationProperties properties; + + /** The JSON object mapper needed. */ + private final ObjectMapper objectMapper; + + /** + * Constructor. + * + * @param properties the audit properties + * @param objectMapper the JSON object mapper + */ + public AuditRepositoryAutoConfiguration(final AuditRepositoryConfigurationProperties properties, final ObjectMapper objectMapper) { + this.properties = Objects.requireNonNull(properties, "properties must not be null"); + this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null"); + } + + /** + * Creates an {@link AuditEventMapper} bean. + * + * @return the {@link AuditEventMapper} bean + */ + @ConditionalOnMissingBean + @Bean + AuditEventMapper auditEventMapper() { + return new JsonAuditEventMapper(this.objectMapper); + } + + /** + * Sets up an {@link AuditEventRepository} bean according to the configuration properties (unless such a bean has + * already been provided). + * + * @param auditEventMapper the event mapper + * @param redisFactory optional factory bean for creating Redis repositories + * @return an {@link AuditEventRepository} bean + * @throws IOException for errors setting up the file repository + */ + @Bean + AuditEventRepository auditEventRepository(final AuditEventMapper auditEventMapper, + @Autowired(required = false) final AuditEventRepositoryFactory redisFactory) throws IOException { + + final List repositories = new ArrayList<>(); + final Predicate filter = FilteringAuditEventRepository.inclusionExclusionPredicate( + this.properties.getIncludeEvents(), this.properties.getExcludeEvents()); + + if (this.properties.getFile() != null) { + repositories + .add(new FileBasedAuditEventRepository(this.properties.getFile().getLogFile(), auditEventMapper, filter)); + } + if (redisFactory != null) { + repositories.add(redisFactory.create(this.properties.getRedis().getName(), auditEventMapper, filter)); + } + if (this.properties.getInMemory() != null) { + repositories.add(new MemoryBasedAuditEventRepository(filter, this.properties.getInMemory().getCapacity())); + } + + // The file repository does not support reads, so if this is the only repository, install an in-memory + // repository as well. + // + if (repositories.size() == 1 && this.properties.getFile() != null) { + repositories.add(0, new MemoryBasedAuditEventRepository(filter)); + } + + // Make sure we have at least one repository ... + // + if (repositories.isEmpty()) { + repositories.add(new MemoryBasedAuditEventRepository(filter)); + } + + return repositories.size() == 1 ? repositories.get(0) : new DelegatingAuditEventRepository(repositories); + } + + /** + * Throws a {@link BeanCreationException} when the type is "timeseries" and Redisson is not available. + * + * @return never returns anything + * @throws BeanCreationException to signal that Redisson is required + */ + @ConditionalOnProperty(value = "saml.idp.audit.redis.type", havingValue = "timeseries", matchIfMissing = false) + @ConditionalOnMissingBean(type = "org.redisson.api.RedissonClient") + @Bean + AuditEventRepositoryFactory noRedisTimeseriesRepository() { + throw new BeanCreationException("saml.idp.audit.redis.type is set to 'timeseries', but Redisson is not available"); + } + + /** + * Throws a {@link BeanCreationException} when the type is "list" and Redis is not available. + * + * @return never returns anything + * @throws BeanCreationException to signal that Redis is required + */ + @ConditionalOnProperty(value = "saml.idp.audit.redis.type", havingValue = "list", matchIfMissing = false) + @ConditionalOnMissingBean(type = "org.springframework.data.redis.core.StringRedisTemplate") + @Bean + AuditEventRepositoryFactory noRedisListRepository() { + throw new BeanCreationException("saml.idp.audit.redis.type is set to 'list', but Redis is not available"); + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditRepositoryConfigurationProperties.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditRepositoryConfigurationProperties.java new file mode 100644 index 00000000..ef95e988 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditRepositoryConfigurationProperties.java @@ -0,0 +1,189 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.audit; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import se.swedenconnect.spring.saml.idp.audit.repository.MemoryBasedAuditEventRepository; + +/** + * Configuration properties for auditing. + * + * @author Martin Lindström + */ +@ConfigurationProperties("saml.idp.audit") +@Slf4j +public class AuditRepositoryConfigurationProperties implements InitializingBean { + + /** + * For audit logging to a file. + */ + @Getter + @Setter + private FileRepository file; + + /** + * For in-memory audit logging. + */ + @Getter + @Setter + private InMemoryRepository inMemory; + + /** + * For using Redis to store audit events. Note that a Redis client must also be configured in order for this setting + * to be effective. + */ + @Getter + @Setter + private RedisRepository redis; + + /** + * A list of event ID:s for the events that will be logged to the repository. If not set, all events will + * be logged (except to excluded by the "exclude-events"). + */ + @Getter + private List includeEvents = new ArrayList<>(); + + /** + * A list of event ID:s to exclude from being logged to the repository. See also the "include-events" + * setting. + */ + @Getter + private List excludeEvents = new ArrayList<>(); + + /** {@inheritDoc} */ + @Override + public void afterPropertiesSet() throws Exception { + if (this.file != null) { + this.file.afterPropertiesSet(); + } + if (this.inMemory != null) { + this.inMemory.afterPropertiesSet(); + } + if (this.redis != null) { + this.redis.afterPropertiesSet(); + } + + // We need at least one repository + if (this.file == null && this.inMemory == null && this.redis == null) { + this.inMemory = new InMemoryRepository(); + log.info("No repository was configured for saml.idp.audit - using inMemory"); + } + } + + /** + * For audit logging to a file. + */ + public static class FileRepository implements InitializingBean { + + /** + * The complete path to the log file where to write audit events. + */ + @Getter + @Setter + private String logFile; + + /** {@inheritDoc} */ + @Override + public void afterPropertiesSet() throws Exception { + Assert.hasText(this.logFile, "saml.idp.audit.file.log-file must be assigned"); + } + + } + + /** + * For in-memory audit logging. + */ + public static class InMemoryRepository implements InitializingBean { + + /** + * The number of events that the repository should hold. + */ + @Getter + @Setter + private Integer capacity = MemoryBasedAuditEventRepository.DEFAULT_CAPACITY; + + /** {@inheritDoc} */ + @Override + public void afterPropertiesSet() throws Exception { + if (this.capacity == null) { + this.capacity = MemoryBasedAuditEventRepository.DEFAULT_CAPACITY; + } + } + + } + + /** + * For Redis storage of audit entries. + */ + public static class RedisRepository implements InitializingBean { + + /** + * The name of the Redis list/time series object that will hold the audit events. + */ + @Getter + @Setter + private String name; + + /** + * The type of Redis storage - "list" or "timeseries". Note that Redisson is required for Redis Timeseries. + */ + @Getter + @Setter + private String type; + + /** {@inheritDoc} */ + @Override + public void afterPropertiesSet() throws Exception { + if (this.type == null) { + this.type = "list"; + log.info("saml.idp.audit.redis.type not set, defaulting to {}", this.type); + } + else { + if (this.type.equalsIgnoreCase("list")) { + this.type = "list"; + } + else if (this.type.equalsIgnoreCase("timeseries")) { + this.type = "timeseries"; + } + else { + throw new IllegalArgumentException( + "Invalid value for saml.idp.audit.redis.type - expected 'list' or 'timeseries'"); + } + } + if (!StringUtils.hasText(this.name)) { + if ("list".equals(this.type)) { + this.name = "audit:list"; + } + else { + this.name = "audit:ts"; + } + log.info("saml.idp.audit.redis.name not set, defaulting to '{}'", this.name); + } + } + + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/RedisAuditRepositoryAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/RedisAuditRepositoryAutoConfiguration.java new file mode 100644 index 00000000..3409c7b3 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/RedisAuditRepositoryAutoConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.audit; + +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import se.swedenconnect.spring.saml.idp.audit.repository.RedisListAuditEventRepository; + +/** + * Auto configuration for auditing support where a Redis {@link AuditEventRepository} is created. + * + * @author Martin Lindström + */ +@ConditionalOnProperty(value = "saml.idp.audit.redis.type", havingValue = "list", matchIfMissing = false) +@ConditionalOnMissingBean(AuditEventRepository.class) +@ConditionalOnBean(StringRedisTemplate.class) +@AutoConfiguration(before = AuditRepositoryAutoConfiguration.class) +public class RedisAuditRepositoryAutoConfiguration { + + /** + * Creates an {@link AuditEventRepositoryFactory} that creates a {@link RedisListAuditEventRepository} bean. + * + * @param redisTemplate the Redis template + * @return an {@link AuditEventRepositoryFactory} + */ + @Bean + AuditEventRepositoryFactory redisListRepository(final StringRedisTemplate redisTemplate) { + return (name, mapper, filter) -> new RedisListAuditEventRepository(redisTemplate, name, mapper, filter); + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/RedissonAuditRepositoryAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/RedissonAuditRepositoryAutoConfiguration.java new file mode 100644 index 00000000..e4a67af1 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/RedissonAuditRepositoryAutoConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.audit; + +import org.redisson.api.RedissonClient; +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; + +import se.swedenconnect.spring.saml.idp.audit.repository.RedissonTimeSeriesAuditEventRepository; + +/** + * Auto configuration for auditing support where a Redis {@link AuditEventRepository} is created. + * + * @author Martin Lindström + */ +@ConditionalOnProperty(value = "saml.idp.audit.redis.type", havingValue = "timeseries", matchIfMissing = false) +@ConditionalOnMissingBean(AuditEventRepository.class) +@ConditionalOnBean(RedissonClient.class) +@AutoConfiguration(before = AuditRepositoryAutoConfiguration.class) +public class RedissonAuditRepositoryAutoConfiguration { + + /** + * Creates an {@link AuditEventRepositoryFactory} that creates a {@link RedissonTimeSeriesAuditEventRepository} bean. + * + * @param redissonClient the Redisson client bean + * @return an {@link AuditEventRepositoryFactory} + */ + @Bean + AuditEventRepositoryFactory redisTimeseriesRepository(final RedissonClient redissonClient) { + return (name, mapper, filter) -> new RedissonTimeSeriesAuditEventRepository(redissonClient, name, mapper, filter); + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/package-info.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/package-info.java new file mode 100644 index 00000000..4976ea46 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/package-info.java @@ -0,0 +1,4 @@ +/** + * Audit settings. + */ +package se.swedenconnect.spring.saml.idp.autoconfigure.audit; \ No newline at end of file diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/base/ConvertersConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/base/ConvertersConfiguration.java index 650addc6..e69371a8 100644 --- a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/base/ConvertersConfiguration.java +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/base/ConvertersConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Sweden Connect + * Copyright 2023-2024 Sweden Connect * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.DependsOn; import org.springframework.core.convert.converter.Converter; import se.swedenconnect.opensaml.common.utils.LocalizedString; @@ -35,7 +34,7 @@ * Configuration class that registers converters for Spring converters needed to applying properties to SignService * configuration properties classes. */ -@AutoConfiguration +@AutoConfiguration(after = OpenSAMLConfiguration.class) public class ConvertersConfiguration { /** @@ -60,7 +59,6 @@ PropertyToX509CertificateConverter propertyToX509CertificateConverter() { @ConditionalOnMissingBean @Bean @ConfigurationPropertiesBinding - @DependsOn("openSAML") PropertyToEntityDescriptorConverter propertyToEntityDescriptorConverter() { return new PropertyToEntityDescriptorConverter(); } diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/base/OpenSAMLConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/base/OpenSAMLConfiguration.java index 11c19c05..33fc43d2 100644 --- a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/base/OpenSAMLConfiguration.java +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/base/OpenSAMLConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Sweden Connect + * Copyright 2023-2024 Sweden Connect * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisExtensionsAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisExtensionsAutoConfiguration.java new file mode 100644 index 00000000..1cda1378 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisExtensionsAutoConfiguration.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.redis; + +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.data.redis.JedisClientConfigurationBuilderCustomizer; +import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisOperations; + +import se.swedenconnect.spring.saml.idp.autoconfigure.redis.RedisTlsExtensionsConfiguration.SslBundleRegistrationBean; + +/** + * Auto configuration for Redis extensions. + * + * @author Martin Lindström + */ +@AutoConfiguration(before = RedisAutoConfiguration.class ) +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties({ RedisProperties.class, RedisTlsProperties.class }) +@Import(RedisTlsExtensionsConfiguration.class) +public class RedisExtensionsAutoConfiguration { + + /** To ensure that the TLS extensions have been processed. */ + @Autowired + SslBundleRegistrationBean _dummy; + + /** Spring Data Redis properties. */ + @Autowired + private RedisProperties redisProperties; + + /** The Redis properties. */ + @Autowired + private RedisTlsProperties redisTlsProperties; + + /** + * If Jedis is available, a {@link JedisClientConfigurationBuilderCustomizer} is created that configures the Jedis + * client according to our extended Redis properties. + * + * @return a {@link JedisClientConfigurationBuilderCustomizer} bean + */ + @ConditionalOnClass + @Bean + JedisClientConfigurationBuilderCustomizer jedisCustomizer() { + return c -> { + if (this.redisProperties.getSsl().isEnabled()) { + if (!this.redisTlsProperties.isEnableHostnameVerification()) { + c.useSsl().hostnameVerifier(NoopHostnameVerifier.INSTANCE); + } + } + }; + } + + /** + * If Lettuce is available, a {@link LettuceClientConfigurationBuilderCustomizer} is created that configures the + * Lettuce client according to our extended Redis properties. + * + * @return a {@link LettuceClientConfigurationBuilderCustomizer} bean + */ + @ConditionalOnClass + @Bean + LettuceClientConfigurationBuilderCustomizer lettuceCustomizer() { + return c -> { + if (this.redisProperties.getSsl().isEnabled()) { + if (!this.redisTlsProperties.isEnableHostnameVerification()) { + c.useSsl().disablePeerVerification(); + } + } + }; + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisTlsExtensionsConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisTlsExtensionsConfiguration.java new file mode 100644 index 00000000..945d5f9a --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisTlsExtensionsConfiguration.java @@ -0,0 +1,186 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.redis; + +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.UUID; +import java.util.stream.StreamSupport; + +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslBundleRegistry; +import org.springframework.boot.ssl.SslManagerBundle; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.ssl.SslStoreBundle; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.extern.slf4j.Slf4j; + +/** + * Configuration class that transforms the deprecated {@link RedisTlsProperties} to a {@link SslBundle}. + * + * @author Martin Lindström + */ +@Configuration +@EnableConfigurationProperties({ RedisProperties.class, RedisTlsProperties.class }) +@Slf4j +public class RedisTlsExtensionsConfiguration { + + /** For registering SslBundles (needed to support the old way of handling Redis TLs properties). */ + private final SslBundleRegistry sslBundleRegistry; + + /** The Redis properties. */ + private final RedisProperties redisProperties; + + /** The extended Redis TLS properties. */ + private final RedisTlsProperties tlsProperties; + + /** + * Constructor. + * + * @param redisProperties the Redis properties + * @param tlsProperties the extended Redis TLS properties + * @param sslBundleRegistry for registering SslBundles + */ + public RedisTlsExtensionsConfiguration(final RedisProperties redisProperties, + final RedisTlsProperties tlsProperties, final SslBundleRegistry sslBundleRegistry) { + this.redisProperties = redisProperties; + this.tlsProperties = tlsProperties; + this.sslBundleRegistry = sslBundleRegistry; + } + + /** + * Creates a {@link SslBundleRegistrationBean} that registers a {@link SslBundle} and updates the + * {@link RedisProperties} if the settings of {@link RedisTlsProperties} are assigned. + * + * @return a SslBundleRegistrationBean + * @throws Exception for KeyStore errors + */ + @Bean + SslBundleRegistrationBean sslBundleRegistrationBean() throws Exception { + return new SslBundleRegistrationBean(this.redisProperties, this.tlsProperties, this.sslBundleRegistry); + } + + /** + * For registering a SslBunde based on TLS extension properties. + */ + public static class SslBundleRegistrationBean { + + public SslBundleRegistrationBean(final RedisProperties redisProperties, + final RedisTlsProperties tlsProperties, final SslBundleRegistry sslBundleRegistry) throws Exception { + + // If a bundle is configured, we use that ... + // + if (redisProperties.getSsl().getBundle() != null) { + return; + } + + if (tlsProperties.getCredential() == null && tlsProperties.getTrust() == null) { + return; + } + + final SslBundleKey sslBundleKey; + final KeyStore keyStore; + final String keyStorePassword; + final KeyStore trustStore; + + if (tlsProperties.getCredential() != null) { + + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStorePassword = tlsProperties.getCredential().getPassword(); + + try (final InputStream is = tlsProperties.getCredential().getResource().getInputStream()) { + keyStore.load(is, keyStorePassword.toCharArray()); + } + final String alias = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(keyStore.aliases().asIterator(), Spliterator.ORDERED), false) + .filter(a -> { + try { + return keyStore.isKeyEntry(a); + } + catch (KeyStoreException e) { + return false; + } + }) + .findFirst() + .orElseThrow(() -> new SecurityException("No valid alias found")); + + sslBundleKey = SslBundleKey.of(tlsProperties.getCredential().getPassword(), alias); + } + else { + sslBundleKey = SslBundleKey.NONE; + keyStore = null; + keyStorePassword = null; + } + if (tlsProperties.getTrust() != null) { + trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + try (final InputStream is = tlsProperties.getTrust().getResource().getInputStream()) { + final char[] password = Optional.ofNullable(tlsProperties.getTrust().getPassword()) + .map(String::toCharArray) + .orElseGet(() -> new char[0]); + trustStore.load(is, password); + } + } + else { + trustStore = null; + } + + final SslBundle sslBundle = new SslBundle() { + + @Override + public SslStoreBundle getStores() { + return SslStoreBundle.of(keyStore, keyStorePassword, trustStore); + } + + @Override + public String getProtocol() { + return SslBundle.DEFAULT_PROTOCOL; + } + + @Override + public SslOptions getOptions() { + return SslOptions.NONE; + } + + @Override + public SslManagerBundle getManagers() { + return SslManagerBundle.from(this.getStores(), this.getKey()); + } + + @Override + public SslBundleKey getKey() { + return sslBundleKey; + } + }; + + final String bundleName = UUID.randomUUID().toString(); + + log.info("Registering SslBunde '{}' to hold settings from spring.data.redis.ssl-ext.*", bundleName); + sslBundleRegistry.registerBundle(bundleName, sslBundle); + + redisProperties.getSsl().setBundle(bundleName); + } + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisTlsProperties.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisTlsProperties.java new file mode 100644 index 00000000..a3630234 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedisTlsProperties.java @@ -0,0 +1,109 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.redis; + +import java.security.KeyStore; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +import lombok.Getter; +import lombok.Setter; + +/** + * Spring Boot's Redis support does not enable us to configure SSL/TLS against the Redis server in a good way. + * Therefore, we extend Spring's Redis configuration with this configuration properties class. + * + * @author Martin Lindström + * @author Felix Hellman + */ +@ConfigurationProperties(prefix = "spring.data.redis.ssl-ext") +public class RedisTlsProperties implements InitializingBean { + + /** + * Configuration for the KeyStore holding the Redis client SSL/TLS credential. + */ + @Setter + private KeyStoreConfiguration credential; + + /** + * Should we verify the the peer's hostname as part of the SSL/TLS handshake? + */ + @Setter + @Getter + private boolean enableHostnameVerification = true; + + /** + * To configure a specific trust for SSL/TLS we can supply a trust KeyStore. + */ + @Setter + private KeyStoreConfiguration trust; + + /** + * Configuration for the KeyStore holding the Redis client SSL/TLS credential. + * + * @return keystore configuration + */ + @DeprecatedConfigurationProperty(reason = "Use SslBundles instead") + public KeyStoreConfiguration getCredential() { + return this.credential; + } + + /** + * To configure a specific trust for SSL/TLS we can supply a trust KeyStore. + * @return keystore configuration + */ + @DeprecatedConfigurationProperty(reason = "Use SslBundles instead") + public KeyStoreConfiguration getTrust() { + return this.trust; + } + + /** {@inheritDoc} */ + @Override + public void afterPropertiesSet() throws Exception { + if (this.credential != null) { + Assert.notNull(this.credential.getResource(), "spring.redis.ssl-ext.credential.resource must be set"); + Assert.hasText(this.credential.getPassword(), "spring.redis.ssl-ext.credential.password must be set"); + } + if (this.trust != null) { + Assert.notNull(this.trust.getResource(), "spring.redis.ssl-ext.trust.resource must be set"); + } + } + + /** + * Configuration for a {@link KeyStore}. + */ + public static class KeyStoreConfiguration { + + /** + * The KeyStore resource. + */ + @Getter + @Setter + private Resource resource; + + /** + * The KeyStore password. + */ + @Getter + @Setter + private String password; + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedissonClusterProperties.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedissonClusterProperties.java new file mode 100644 index 00000000..5def7b36 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedissonClusterProperties.java @@ -0,0 +1,116 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.redis; + +import java.util.List; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.Assert; + +import lombok.Getter; +import lombok.Setter; + +/** + * Class for containing additional Redis cluster properties. + *

+ * Note: Has only effect if Redisson is being used as Redis client. + *

+ * + * @author Martin Lindström + * @author Felix Hellman + */ +@ConfigurationProperties(prefix = "spring.data.redis.cluster-ext") +public class RedissonClusterProperties implements InitializingBean { + + /** + * A list of NAT translation entries. + */ + @Getter + @Setter + private List natTranslation; + + /** + * Node type used for read operation. The default value is MASTER. Available values: SLAVE - Read from slave nodes, + * uses MASTER if no SLAVES are available, MASTER - Read from master node, MASTER_SLAVE - Read from master and slave + * nodes. + */ + @Getter + @Setter + private String readMode = "MASTER"; + + /** {@inheritDoc} */ + @Override + public void afterPropertiesSet() throws Exception { + if (this.natTranslation != null) { + for (final NatTranslationEntry entry : this.natTranslation) { + Assert.hasText(entry.getTo(), "Invalid NAT translation configuration - 'to' is required"); + Assert.hasText(entry.getFrom(), "Invalid NAT translation configuration - 'from' is required"); + } + } + if (this.readMode == null) { + this.readMode = "MASTER"; + } + try { + ReadMode.valueOf(this.readMode); + } + catch (final Exception e) { + throw new IllegalArgumentException("Invalid value for read-mode"); + } + } + + /** + * An entry for NAT translation. + */ + public static class NatTranslationEntry { + + /** + * Address to translate from. + */ + @Getter + @Setter + private String from; + + /** + * Address to translate to. + */ + @Getter + @Setter + private String to; + } + + /** + * Read mode from Redis cluster. + */ + public static enum ReadMode { + + /** + * Read from slave nodes. + */ + SLAVE, + + /** + * Read from master node. + */ + MASTER, + + /** + * Read from master and slave nodes. + */ + MASTER_SLAVE, + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedissonExtensionsAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedissonExtensionsAutoConfiguration.java new file mode 100644 index 00000000..cebddd60 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/RedissonExtensionsAutoConfiguration.java @@ -0,0 +1,155 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.redis; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.redisson.api.HostPortNatMapper; +import org.redisson.config.BaseConfig; +import org.redisson.config.ClusterServersConfig; +import org.redisson.config.Config; +import org.redisson.config.ReadMode; +import org.redisson.config.SingleServerConfig; +import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer; +import org.redisson.spring.starter.RedissonAutoConfigurationV2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +import se.swedenconnect.spring.saml.idp.autoconfigure.redis.RedisTlsExtensionsConfiguration.SslBundleRegistrationBean; +import se.swedenconnect.spring.saml.idp.autoconfigure.redis.RedissonClusterProperties.NatTranslationEntry; + +/** + * For configuring Redisson extensions. + * + * @author Martin Lindström + */ +@AutoConfiguration(before = RedissonAutoConfigurationV2.class) +@ConditionalOnClass(RedissonAutoConfigurationV2.class) +@EnableConfigurationProperties({ RedisProperties.class, RedissonClusterProperties.class, RedisTlsProperties.class }) +@Import(RedisTlsExtensionsConfiguration.class) +public class RedissonExtensionsAutoConfiguration { + + /** To ensure that the TLS extensions have been processed. */ + @Autowired + SslBundleRegistrationBean _dummy; + + /** For accessing SslBundles. */ + @Autowired + private SslBundles sslBundles; + + /** Spring Data Redis properties. */ + @Autowired + private RedisProperties redisProperties; + + /** Redis TLS extensions. */ + @Autowired + private RedisTlsProperties redisTlsProperties; + + /** Redis cluster properties. */ + @Autowired + private RedissonClusterProperties clusterProperties; + + /** + * If Redisson is used, a {@link RedissonAutoConfigurationCustomizer} is created that configures the Redisson client + * according to our extended Redis properties. + * + * @return a {@link RedissonAutoConfigurationCustomizer} bean + */ + @Bean + RedissonAutoConfigurationCustomizer redissonCustomizer() { + return c -> { + final BaseConfig config = this.getRedissonConfiguration(c); + if (this.redisProperties.getSsl().isEnabled()) { + config.setSslEnableEndpointIdentification(this.redisTlsProperties.isEnableHostnameVerification()); + final String bundle = this.redisProperties.getSsl().getBundle(); + if (bundle != null) { + final SslBundle sslBundle = this.sslBundles.getBundle(bundle); + config.setSslKeyManagerFactory(sslBundle.getManagers().getKeyManagerFactory()); + config.setSslTrustManagerFactory(sslBundle.getManagers().getTrustManagerFactory()); + if (sslBundle.getOptions().getCiphers() != null) { + config.setSslCiphers(sslBundle.getOptions().getCiphers()); + } + } + } + }; + } + + private BaseConfig getRedissonConfiguration(final Config config) { + if (config.isSingleConfig()) { + return RedissonAddressCustomizers.singleServerSslCustomizer.apply(config.useSingleServer()); + } + if (config.isClusterConfig()) { + return RedissonAddressCustomizers.clusterServerCustomizer.apply( + config.useClusterServers(), this.clusterProperties); + } + if (config.isSentinelConfig()) { + throw new IllegalArgumentException("Sentinel Configuration is not implementend"); + } + throw new IllegalStateException("Could not determine configuration type"); + } + + /** + * Customizers to handle a bug where the protocol section of the address becomes non-TLS when TLS is enabled. + * + * @author Martin Lindström + * @author Felix Hellman + */ + private static class RedissonAddressCustomizers { + + public static BiFunction clusterServerCustomizer = + (config, clusterProperties) -> { + final List addresses = new ArrayList<>(); + config.getNodeAddresses().forEach(address -> { + String addr = address; + if (address.contains("redis://")) { + addr = address.replace("redis://", "rediss://"); + } + addresses.add(addr); + }); + config.setNodeAddresses(addresses); + if (clusterProperties.getNatTranslation() != null) { + final HostPortNatMapper mapper = new HostPortNatMapper(); + mapper.setHostsPortMap(clusterProperties.getNatTranslation().stream() + .collect(Collectors.toMap(NatTranslationEntry::getFrom, NatTranslationEntry::getTo))); + config.setNatMapper(mapper); + } + config.setReadMode(ReadMode.valueOf(clusterProperties.getReadMode())); + return config; + }; + + public static Function singleServerSslCustomizer = (s) -> { + final String redisAddress = s.getAddress(); + if (redisAddress.contains("redis://")) { + // The protocol part has not been configured by Spring even though we have enabled ssl + s.setAddress(redisAddress.replace("redis://", "rediss://")); + } + return s; + }; + + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/package-info.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/package-info.java new file mode 100644 index 00000000..faa53c61 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * Auto configuration for Redis extensions. + */ +package se.swedenconnect.spring.saml.idp.autoconfigure.redis; \ No newline at end of file diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/MemorySessionAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/MemorySessionAutoConfiguration.java new file mode 100644 index 00000000..b75cd540 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/MemorySessionAutoConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.session; + +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration; +import org.springframework.boot.autoconfigure.session.SessionProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.session.MapSessionRepository; +import org.springframework.session.Session; +import org.springframework.session.SessionRepository; +import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; + +/** + * Configuration class for setting up Spring Session to use an in-memory map for storing sessions. + * + * @author Martin Lindström + */ +@ConditionalOnProperty(value = "saml.idp.session.module", havingValue = "memory", matchIfMissing = true) +@ConditionalOnClass(Session.class) +@ConditionalOnMissingBean(SessionRepository.class) +@ConditionalOnWebApplication +@AutoConfiguration(before = SessionAutoConfiguration.class, after = RedisSessionAutoConfiguration.class) +@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class }) +@EnableSpringHttpSession +@EnableScheduling +public class MemorySessionAutoConfiguration { + + /** Server properties. */ + private final ServerProperties serverProperties; + + /** Session properties. */ + private final SessionProperties sessionProperties; + + /** + * Constructor. + * + * @param serverProperties the server properties + * @param sessionProperties the session properties + */ + public MemorySessionAutoConfiguration( + final ServerProperties serverProperties, final SessionProperties sessionProperties) { + this.serverProperties = serverProperties; + this.sessionProperties = sessionProperties; + } + + /** + * Creates an in-memory session repository. + * + * @param sessionMap the map to hold the session objects + * @return a {@link MapSessionRepository} bean + */ + @Bean + MapSessionRepository sessionRepository(final PurgeableMap sessionMap) { + + final Duration timeout = this.sessionProperties.determineTimeout( + () -> this.serverProperties.getServlet().getSession().getTimeout()); + + final MapSessionRepository sessionRepository = new MapSessionRepository(sessionMap); + sessionRepository.setDefaultMaxInactiveInterval(timeout); + + return sessionRepository; + } + + /** + * Creates the map holding the sessions. + * + * @return a {@code PurgeableMap} + */ + @Bean + PurgeableMap sessionMap() { + return new PurgeableMap(); + } + + /** + * A {@link ConcurrentHashMap} that has support for purging expired sessions. + */ + private static class PurgeableMap extends ConcurrentHashMap { + + private static final long serialVersionUID = 3055404662826427441L; + + /** + * Purges expired sessions. + */ + @Scheduled(fixedDelay = 600000L) + public void purgeExpired() { + this.entrySet().removeIf(e -> e.getValue().isExpired()); + } + + } + +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/RedisSessionAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/RedisSessionAutoConfiguration.java new file mode 100644 index 00000000..3d5efcdc --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/RedisSessionAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.session; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.session.SessionRepository; +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; +import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration; + +import se.swedenconnect.spring.saml.idp.autoconfigure.redis.RedisExtensionsAutoConfiguration; +import se.swedenconnect.spring.saml.idp.autoconfigure.redis.RedissonExtensionsAutoConfiguration; + +/** + * For setting up Spring Session using Redis. + * + * @author Martin Lindström + */ +@ConditionalOnProperty(value = "saml.idp.session.module", havingValue = "redis", matchIfMissing = true) +@ConditionalOnClass(RedisHttpSessionConfiguration.class) +@ConditionalOnMissingBean(SessionRepository.class) +@ConditionalOnWebApplication +@AutoConfiguration( + after = { RedissonExtensionsAutoConfiguration.class, RedisExtensionsAutoConfiguration.class }, + before = MemorySessionAutoConfiguration.class) +@EnableRedisHttpSession +public class RedisSessionAutoConfiguration { +} diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/package-info.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/package-info.java new file mode 100644 index 00000000..ef910a1e --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/session/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring session configuration. + */ +package se.swedenconnect.spring.saml.idp.autoconfigure.session; \ No newline at end of file diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderAutoConfiguration.java index 35bfbd70..423ec092 100644 --- a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderAutoConfiguration.java +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderAutoConfiguration.java @@ -20,7 +20,6 @@ import java.util.stream.Collectors; import org.opensaml.saml.metadata.resolver.MetadataResolver; -import org.opensaml.storage.ReplayCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -32,9 +31,6 @@ import org.springframework.context.annotation.Import; import lombok.Setter; -import se.swedenconnect.opensaml.saml2.response.replay.InMemoryReplayChecker; -import se.swedenconnect.opensaml.saml2.response.replay.MessageReplayChecker; -import se.swedenconnect.opensaml.saml2.response.replay.MessageReplayCheckerImpl; import se.swedenconnect.security.credential.PkiCredential; import se.swedenconnect.spring.saml.idp.config.configurers.Saml2IdpConfigurer; import se.swedenconnect.spring.saml.idp.events.Saml2IdpEventPublisher; @@ -223,15 +219,4 @@ Saml2IdpEventPublisher saml2IdpEventPublisher(final ApplicationEventPublisher ap return new Saml2IdpEventPublisher(applicationEventPublisher); } - @ConditionalOnMissingBean - @Bean - MessageReplayChecker messageReplayChecker(@Autowired(required = false) final ReplayCache replayCache) { - if (replayCache == null) { - return new InMemoryReplayChecker(); - } - else { - return new MessageReplayCheckerImpl(replayCache, "idp-replay-checker"); - } - } - } diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderConfigurationProperties.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderConfigurationProperties.java index 43a6c4ae..03fed482 100644 --- a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderConfigurationProperties.java +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderConfigurationProperties.java @@ -20,9 +20,12 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** @@ -30,7 +33,6 @@ * * @author Martin Lindström */ -@Data @ConfigurationProperties("saml.idp") @Slf4j public class IdentityProviderConfigurationProperties implements InitializingBean { @@ -38,11 +40,15 @@ public class IdentityProviderConfigurationProperties implements InitializingBean /** * The Identity Provider SAML entityID. */ + @Getter + @Setter private String entityId; /** * The Identity Provider base URL, i.e., the protocol, domain and context path. Must not end with an '/'. */ + @Getter + @Setter private String baseUrl; /** @@ -53,53 +59,87 @@ public class IdentityProviderConfigurationProperties implements InitializingBean * this setting represents this base URL. *

*/ + @Getter + @Setter private String hokBaseUrl; /** * Whether the IdP requires signed authentication requests. */ + @Getter + @Setter private Boolean requiresSignedRequests; - + /** * Clock skew adjustment (in both directions) to consider for accepting messages based on their age. */ + @Getter + @Setter private Duration clockSkewAdjustment; - + /** * Maximum allowed age of received messages. */ + @Getter + @Setter private Duration maxMessageAge; - + /** * Based on a previous authentication, for how long may this authentication be re-used? */ + @Getter + @Setter private Duration ssoDurationLimit; /** * The Identity Provider credentials. */ + @Getter + @Setter private CredentialConfigurationProperties credentials; /** * The SAML IdP endpoints. */ + @Getter + @Setter private EndpointsConfigurationProperties endpoints; - + /** * Assertion settings. */ + @Getter + @Setter private AssertionSettingsConfigurationProperties assertions; /** * The IdP metadata. */ + @Getter + @Setter private MetadataConfigurationProperties metadata; /** * The IdP metadata provider(s). */ + @Getter + @Setter private List metadataProviders; + /** + * Configuration for replay checking. + */ + @Getter + @NestedConfigurationProperty + private ReplayCheckerConfigurationProperties replay = new ReplayCheckerConfigurationProperties(); + + /** + * Session configuration. + */ + @Getter + @NestedConfigurationProperty + private SessionConfiguration session = new SessionConfiguration(); + /** {@inheritDoc} */ @Override public void afterPropertiesSet() throws Exception { @@ -116,6 +156,73 @@ public void afterPropertiesSet() throws Exception { if (this.assertions == null) { log.debug("saml.idp.assertions.* is not assigned, will apply default values"); } + this.replay.afterPropertiesSet(); + this.session.afterPropertiesSet(); + } + + /** + * Session handling configuration. + */ + public static class SessionConfiguration implements InitializingBean { + + /** + * The session module to use. Supported values are "memory" and "redis". Set to other value if you extend the IdP + * with your own session handling. + */ + @Getter + @Setter + private String module; + + /** {@inheritDoc} */ + @Override + public void afterPropertiesSet() throws Exception { + } + + } + + /** + * For configuring the message replay checker. + */ + public static class ReplayCheckerConfigurationProperties implements InitializingBean { + + /** The default expiration time for entries added to the cache. */ + public static final Duration DEFAULT_EXPIRATION = Duration.ofMinutes(5); + + /** The default context name to use for storing the cache. */ + public static final String DEFAULT_CONTEXT_NAME = "idp-replay-checker"; + + /** + * The type of replay checker. Supported values are "memory" and "redis". + */ + @Getter + @Setter + private String type; + + /** + * For how long should authentication request ID:s be stored in the cache before they expire? + */ + @Getter + @Setter + private Duration expiration; + + /** + * Under which context should the cache be stored? Applies to repositories that persist/distribute the cache. + */ + @Getter + @Setter + private String context; + + /** {@inheritDoc} */ + @Override + public void afterPropertiesSet() throws Exception { + if (this.expiration == null) { + this.expiration = DEFAULT_EXPIRATION; + } + if (!StringUtils.hasText(this.context)) { + this.context = DEFAULT_CONTEXT_NAME; + } + } + } } diff --git a/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/MessageReplayCheckerAutoConfiguration.java b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/MessageReplayCheckerAutoConfiguration.java new file mode 100644 index 00000000..7568ed90 --- /dev/null +++ b/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/MessageReplayCheckerAutoConfiguration.java @@ -0,0 +1,105 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.autoconfigure.settings; + +import org.opensaml.storage.ReplayCache; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.StringRedisTemplate; + +import se.swedenconnect.opensaml.saml2.response.replay.MessageReplayChecker; +import se.swedenconnect.opensaml.saml2.response.replay.MessageReplayCheckerImpl; +import se.swedenconnect.spring.saml.idp.authnrequest.validation.replay.InMemoryReplayCache; +import se.swedenconnect.spring.saml.idp.authnrequest.validation.replay.RedisReplayCache; + +/** + * Auto configuration for setting up a {@link MessageReplayChecker} bean. + * + * @author Martin Lindström + */ +@ConditionalOnMissingBean(MessageReplayChecker.class) +@AutoConfiguration(before = IdentityProviderAutoConfiguration.class) +@EnableConfigurationProperties(IdentityProviderConfigurationProperties.class) +@Import(MessageReplayCheckerAutoConfiguration.RedisMessageReplayCheckerConfiguration.class) +public class MessageReplayCheckerAutoConfiguration { + + /** The configuration properties. */ + private final IdentityProviderConfigurationProperties properties; + + /** + * Constructor. + * + * @param properties the configuration properties + */ + public MessageReplayCheckerAutoConfiguration(final IdentityProviderConfigurationProperties properties) { + this.properties = properties; + } + + /** + * Creates an in-memory {@link ReplayCache} bean. + * + * @return a {@link ReplayCache} + */ + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "saml.idp.replay.type", havingValue = "memory", matchIfMissing = true) + @Bean + ReplayCache inMemoryReplayCache() { + return new InMemoryReplayCache(); + } + + /** + * Creates a {@link MessageReplayChecker} bean. + * + * @param replayCache the {@link ReplayCache} + * @return a {@link MessageReplayChecker} bean + */ + @Bean + MessageReplayChecker messageReplayChecker(final ReplayCache replayCache) { + final MessageReplayCheckerImpl checker = + new MessageReplayCheckerImpl(replayCache, this.properties.getReplay().getContext()); + checker.setReplayCacheExpiration(this.properties.getReplay().getExpiration().toMillis()); + return checker; + } + + /** + * For configuration of a {@link RedisReplayCache} bean. + */ + @ConditionalOnProperty(value = "saml.idp.replay.type", havingValue = "redis", matchIfMissing = true) + @ConditionalOnBean(StringRedisTemplate.class) + @Configuration + public static class RedisMessageReplayCheckerConfiguration { + + /** + * If we are using Redis, we create a {@link RedisReplayCache} + * + * @param connectionFactory the Redis connection factory + * @return a {@link RedisReplayCache} + */ + @ConditionalOnMissingBean + @Bean + ReplayCache redisReplayCache(final StringRedisTemplate redisTemplate) { + return new RedisReplayCache(redisTemplate); + } + + } + +} diff --git a/autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index b9ff2b77..3e7c80fe 100644 --- a/autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,5 +1,13 @@ se.swedenconnect.spring.saml.idp.autoconfigure.base.OpenSAMLConfiguration se.swedenconnect.spring.saml.idp.autoconfigure.base.ConvertersConfiguration +se.swedenconnect.spring.saml.idp.autoconfigure.audit.RedisAuditRepositoryAutoConfiguration +se.swedenconnect.spring.saml.idp.autoconfigure.audit.RedissonAuditRepositoryAutoConfiguration +se.swedenconnect.spring.saml.idp.autoconfigure.audit.AuditRepositoryAutoConfiguration se.swedenconnect.spring.saml.idp.autoconfigure.settings.IdentityProviderAutoConfiguration +se.swedenconnect.spring.saml.idp.autoconfigure.settings.MessageReplayCheckerAutoConfiguration se.swedenconnect.spring.saml.idp.autoconfigure.web.security.IdentityProviderSecurityFilterChainAutoConfiguration se.swedenconnect.spring.saml.idp.autoconfigure.error.Saml2IdpErrorAutoConfiguration +se.swedenconnect.spring.saml.idp.autoconfigure.redis.RedissonExtensionsAutoConfiguration +se.swedenconnect.spring.saml.idp.autoconfigure.redis.RedisExtensionsAutoConfiguration +se.swedenconnect.spring.saml.idp.autoconfigure.session.RedisSessionAutoConfiguration +se.swedenconnect.spring.saml.idp.autoconfigure.session.MemorySessionAutoConfiguration \ No newline at end of file diff --git a/docs/audit.md b/docs/audit.md index c88762f8..4d317dd6 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -16,7 +16,7 @@ If you want to be able to obtain audit logs via Spring Boot Actuator you need to - Include the string `auditevents` among the list specified by the setting `management.endpoints.web.exposure.include`. -- Make sure a `org.springframework.boot.actuate.audit.AuditEventRepository` bean exists. +- Make sure a `org.springframework.boot.actuate.audit.AuditEventRepository` bean exists. See [Audit Configuration](configuration.html#audit-configuration). ## Audit Events @@ -164,4 +164,4 @@ a SAML error response back, this error is displayed in the user interface. In th --- -Copyright © 2022-2023, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). +Copyright © 2022-2024, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). diff --git a/docs/configuration.md b/docs/configuration.md index a7548e17..658804ca 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -42,6 +42,9 @@ This section documents all properties that can be provided to configure the IdP. | `saml.idp.assertions.*` | Configuration for IdP Assertion issuance, see [Assertion Settings Configuration](#assertion-settings-configuration) below. | [AssertionSettingsConfigurationProperties](https://github.com/swedenconnect/saml-identity-provider/blob/main/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/AssertionSettingsConfigurationProperties.java) | See below. | | `saml.idp.metadata.*` | Configuration for the SAML metadata produced (and published) by the IdP, see [MetadataConfiguration](#metadata-configuration) below. | [MetadataConfigurationProperties](https://github.com/swedenconnect/saml-identity-provider/blob/main/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/MetadataConfigurationProperties.java) | See below. | | `saml.idp-metadata-providers[].*` | A list of "metadata providers" that tells how the IdP downloads federation metadata. See [Metadata Provider Configuration](#metadata-provider-configuration) below. | [MetadataProviderConfigurationProperties](https://github.com/swedenconnect/saml-identity-provider/blob/main/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/MetadataProviderConfigurationProperties.java) | See below. | +| `saml.idp.audit.*` | Audit logging configuration. See [Audit Configuration](#audit-configuration) below. | [AuditRepositoryConfigurationProperties](https://github.com/swedenconnect/saml-identity-provider/blob/main/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/audit/AuditRepositoryConfigurationProperties.java) | See below. | +| `saml.idp.replay.*` | Configuration for message replay checking. See [Replay Checker Configuration](#replay-checker-configuration) below. | [ReplayCheckerConfigurationProperties](https://github.com/swedenconnect/saml-identity-provider/blob/main/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/IdentityProviderConfigurationProperties.java) | See below. | +| `saml.idp.session.module` | The session module to use. Supported values are "memory" and "redis". Set to other value if you extend the IdP with your own session handling. | String | If Redis and Spring Session are available `redis` is the default, otherwise `memory`. | #### Credentials Configuration @@ -117,7 +120,117 @@ See https://github.com/swedenconnect/credentials-support for details about the [ | `validation-certificate` | The certificate used to validate the metadata. | [Resource](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/Resource.html) pointing at the certificate resource. | - | | `http-proxy.*` | If the `location` setting is an URL and a HTTP proxy is required this setting configures this proxy.

**Note:** This setting is only needed if you require another HTTP proxy that what is configured for the system, or if the system HTTP proxy settings are not set. If Java's HTTP proxy settings are set (see [Java Networking and Proxies](https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html)), these settings will be used by the metadata provider. | [MetadataProviderConfigurationProperties.HttpProxy](https://github.com/swedenconnect/saml-identity-provider/blob/main/autoconfigure/src/main/java/se/swedenconnect/spring/saml/idp/autoconfigure/settings/MetadataProviderConfigurationProperties.java) | - | + +#### Audit Configuration + +The SAML IdP Spring Boot starter offers automatic support for setting up a [AuditEventRepository](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/actuate/audit/AuditEventRepository.html) bean +based on the below settings. Also see the [Identity Provider Auditing](audit.html) page. + +| Property | Description | Type | Default value | +| :--- | :--- | :--- | :--- | +| `file.log-file` | For audit logging to a file. | String | - | +| `im-memory.capacity` | For audit logging to an in-memory repository. Sets the capacity (number of stored events) of this repository. | Integer | - | +| `redis.name` | For logging to Redis. The name of the Redis list/time series object that will hold the audit events. | String | - | +| `redis.type` | For logging to Redis. The type of Redis storage - "list" or "timeseries". Note that Redisson is required for Redis Timeseries. | String | - | +| `include-events[]` | A list of event ID:s for the events that will be logged to the repository. If not set, all events will be logged (except to excluded by the `exclude-events`). | List of strings | Empty list | +| `exclude-events[]` | A list of event ID:s to exclude from being logged to the repository. See also the `include-events` setting. | List of strings | Empty list | + +If no repository is configured and no [AuditEventRepository](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/actuate/audit/AuditEventRepository.html) bean exists, an in-memory +repository with the `capacity` set to `1000` will be created. + + +#### Replay Checker Configuration + +The SAML IdP makes use of a [MessageReplayChecker](https://docs.swedenconnect.se/opensaml-addons/apidoc/se/swedenconnect/opensaml/saml2/response/replay/MessageReplayChecker.html) to protect against replay +attacks (i.e., that an authentication request is "replayed"). + +If no [MessageReplayChecker](https://docs.swedenconnect.se/opensaml-addons/apidoc/se/swedenconnect/opensaml/saml2/response/replay/MessageReplayChecker.html) bean is provided by the application the +IdP Spring Boot starter will create this bean (using the configuration settings below). + +| Property | Description | Type | Default value | +| :--- | :--- | :--- | :--- | +| `type` | The type of replay checker. Supported values are "memory" and "redis". If set to "redis", Redis must be available and configured. | String | If Redis is available, `redis` is the default, otherwise `memory` | +| `expiration` | For how long should authentication request ID:s be stored in the cache before they expire? | [Duration](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html) | 5 minutes | +| `context` | Under which context should the cache be stored? Applies to repositories that persist/distribute the cache. | String | `idp-replay-checker` | + + +#### Redis Configuration + +Redis may be used for session handling and/or replay checking. + +How Redis is configured and setup for Spring Boot is described here: + +- [Spring Boot Reference Documentation - Common Application Properties](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.application-properties) +- [Spring Session Redis](https://docs.spring.io/spring-session/reference/configuration/redis.html) +- [Spring Data Redis](https://spring.io/projects/spring-data-redis/) + +The SAML IdP Spring Boot Starter defines a few extensions to the core Spring Redis configuration: + +The setting `spring.data.redis.ssl-ext.enable-hostname-verification` may be set to `false` in +order to turn off hostname verification when SSL/TLS is configured (using [SslBundles](https://spring.io/blog/2023/06/07/securing-spring-boot-applications-with-ssl/)) for the Redis connection. This can +be useful during testing. + +Example: + +``` +spring: + ... + data: + redis: + ... + ssl: + enabled: true + bundle: redis-tls-bundle + ssl-ext: + enable-hostname-verification: false +``` + +It Redisson is used for the Redis client, the starter also adds extended support to configure +Redis clusters: + +In order to configure Redis Clusters NAT translation for addresses have been added. This is done +so that the application knows how to reach the Redis cluster if it is not located on the same network. +This can be done under the key `spring.data.redis.cluster-ext`. This property key is a list of +entries as described below: + +| Property | Description | Type | +| :--- | :--- | :--- | +| `nat-translation[].from` | Address to translate from. e.g. "172.20.0.31:2001". | String | +| `nat-translation[].to`| Address to translate to, e.g., "redis1.local.dev.swedenconnect.se:2001". | String | +| `read-mode`| Set cluster read mode to either `SLAVE`, `MASTER` or `MASTER_SLAVE`. The default value is `MASTER` since read/write is highly coupled in Spring Session, selecting `SLAVE` can result in race-conditions leading to the session not being synchronized to the slave in time causing errors. | String | + +**Example:** + +The three Redis nodes are exposed via NAT to the application on redis(1-3).local.dev.swedenconnect.se. +But internally they refer to eachother as 172.20.0.3(1-3). +When the application connects to the first node, it will reconfigure itself by reading the configuration +from redis1. + +Since the application is not located on the same network the connection will fail since those addresses are not located on the same network. + +This solution is to add the configuration below that will re-map outgoing connections to the correct node. + +```yaml +spring: + ... + data: + redis: + cluster: + nodes: + - redis1.local.dev.swedenconnect.se:2001 + - redis2.local.dev.swedenconnect.se:2002 + - redis3.local.dev.swedenconnect.se:2003 + cluster-ext: + nat-translation: + - from: "172.20.0.31:2001" + to: "redis1.local.dev.swedenconnect.se:2001" + - from: "172.20.0.32:2002" + to: "redis2.local.dev.swedenconnect.se:2002" + - from: "172.20.0.33:2003" + to: "redis3.local.dev.swedenconnect.se:2003" +``` + --- -Copyright © 2022-2023, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). +Copyright © 2022-2024, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). diff --git a/docs/example.md b/docs/example.md index 03fd39a5..bb55dc8a 100644 --- a/docs/example.md +++ b/docs/example.md @@ -17,4 +17,4 @@ Open your web browser and go to the test client: `https://localhost:8445/client/ ----- -Copyright © 2022-2023, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file +Copyright © 2022-2024, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 9c38f464..9678cc99 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,4 +19,4 @@ https://docs.swedenconnect.se/technical-framework). ----- -Copyright © 2022-2023, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file +Copyright © 2022-2024, [Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG)](http://www.digg.se). Licensed under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file diff --git a/pom.xml b/pom.xml index c2560523..6f5d03f4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ se.swedenconnect.spring.saml.idp spring-saml-idp-parent pom - 2.0.2 + 2.1.0-SNAPSHOT Sweden Connect :: Spring SAML Identity Provider :: Parent POM Parent POM for Spring SAML Identity Provider libraries @@ -44,9 +44,9 @@ UTF-8 17 - 3.2.3 + 3.2.4 6.1.5 - 1.76 + 1.77 @@ -134,6 +134,12 @@ + + se.swedenconnect.opensaml + opensaml-security-ext + 4.0.1 + + se.swedenconnect.opensaml opensaml-addons @@ -149,7 +155,7 @@ se.swedenconnect.opensaml opensaml-eidas - 3.0.0 + 3.0.1 diff --git a/saml-identity-provider/pom.xml b/saml-identity-provider/pom.xml index b9aa502f..58a1168e 100644 --- a/saml-identity-provider/pom.xml +++ b/saml-identity-provider/pom.xml @@ -9,7 +9,7 @@ se.swedenconnect.spring.saml.idp spring-saml-idp-parent - 2.0.2 + 2.1.0-SNAPSHOT Sweden Connect :: Spring SAML Identity Provider :: Core Library @@ -119,7 +119,27 @@ org.projectlombok lombok + + + commons-io + commons-io + 2.15.1 + + + + + org.springframework.data + spring-data-redis + true + + + org.redisson + redisson + 3.25.2 + true + + org.slf4j @@ -150,7 +170,7 @@ - + org.springframework.boot spring-boot-starter-web @@ -186,7 +206,6 @@ jacoco-maven-plugin - **/*Exception.class diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/Saml2IdentityProviderVersion.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/Saml2IdentityProviderVersion.java index 96d5177b..2d5a21a7 100644 --- a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/Saml2IdentityProviderVersion.java +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/Saml2IdentityProviderVersion.java @@ -23,8 +23,8 @@ public final class Saml2IdentityProviderVersion { private static final int MAJOR = 2; - private static final int MINOR = 0; - private static final int PATCH = 2; + private static final int MINOR = 1; + private static final int PATCH = 0; /** * Global serialization value for SAML Identity Provider classes. diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/AuditEventMapper.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/AuditEventMapper.java new file mode 100644 index 00000000..ebac1226 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/AuditEventMapper.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import org.springframework.boot.actuate.audit.AuditEvent; + +/** + * An interface that defines how an {@link AuditEvent} is written to a string, and read from a string. + * + * @author Martin Lindström + */ +public interface AuditEventMapper { + + /** + * Serializes the AuditEvent to a {@link String}. + * + * @param event to serialize + * @return the string + */ + String write(final AuditEvent event); + + /** + * Deserializes AuditEvent from its string representation. + * + * @param event to deserialize + * @return an {@link AuditEvent} + */ + + AuditEvent read(final String event); + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/DateRollingFileHandler.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/DateRollingFileHandler.java new file mode 100644 index 00000000..67489ff7 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/DateRollingFileHandler.java @@ -0,0 +1,168 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.apache.commons.io.FilenameUtils; +import org.springframework.util.StringUtils; + +/** + * A wrapper class to Java Util Logging's {@link FileHandler} that supports "rolling files" per date. + * + * @author Martin Lindström + * @author Felix Hellman + */ +class DateRollingFileHandler extends Handler { + + /** The log file. */ + private final Path logFile; + + /** Holds the last-modified time of the log file. */ + private Instant lastModified = null; + + /** The actual log handler. */ + private FileHandler handler; + + /** Formatter for backup file names. */ + private static final DateTimeFormatter dateFormatter = + DateTimeFormatter.ofPattern("yyyyMMdd").withZone(ZoneId.of("UTC")); + + /** + * Constructor setting up the file handler. + * + * @param logFile the log file (including the path) + * @throws IOException for file errors + */ + public DateRollingFileHandler(final String logFile) throws IOException { + this.logFile = Path.of(Objects.requireNonNull(logFile, "logFile must not be null")); + if (Files.exists(this.logFile)) { + if (Files.isDirectory(this.logFile)) { + throw new IOException("Given logFile points to a directory and not a file"); + } + if (!Files.isWritable(this.logFile)) { + throw new IOException("Given logFile is not writable"); + } + // Get last modified date ... + final BasicFileAttributes attr = Files.readAttributes(this.logFile, BasicFileAttributes.class); + this.lastModified = attr.lastModifiedTime().toInstant(); + } + else { + final Path parent = this.logFile.getParent(); + if (parent != null && !Files.exists(parent)) { + Files.createDirectories(parent); + } + } + this.initializeHandler(); + } + + /** + * Initializes the underlying handler. + * + * @throws IOException if the {@link FileHandler} cannot be created + */ + private void initializeHandler() throws IOException { + this.handler = new FileHandler(this.logFile.toString(), true); + this.handler.setLevel(Level.INFO); + this.handler.setFormatter(new AuditLoggerFormatter()); + } + + /** {@inheritDoc} */ + @Override + public synchronized void publish(final LogRecord record) { + if (record != null && this.isLoggable(record)) { + + // Check if the current log file is too old to write to ... + // + if (this.lastModified != null + && Instant.now().truncatedTo(ChronoUnit.DAYS).isAfter(this.lastModified.truncatedTo(ChronoUnit.DAYS))) { + // Time to save the current log file to -.log + this.backupFile(); + } + + this.handler.publish(record); + this.lastModified = Instant.now(); + } + } + + /** + * Performs a backup of the current log file to -. and re-initializes the underlying + * handler. + * + * @throws UncheckedIOException if the backup operation fails + */ + private void backupFile() throws UncheckedIOException { + try { + this.flush(); + this.close(); + + final String dateString = dateFormatter.format(this.lastModified); + final String path = this.logFile.toString(); + final String extension = FilenameUtils.getExtension(path); + final String backupPath = StringUtils.hasText(extension) + ? String.format("%s-%s.%s", + path.substring(0, path.length() - extension.length() - 1), dateString, extension) + : String.format("%s-%s", path, dateString); + + Files.move(this.logFile, Path.of(backupPath), StandardCopyOption.REPLACE_EXISTING); + this.lastModified = null; + this.initializeHandler(); + } + catch (final IOException e) { + throw new UncheckedIOException(e.getMessage(), e); + } + } + + /** {@inheritDoc} */ + @Override + public synchronized void flush() { + this.handler.flush(); + } + + /** {@inheritDoc} */ + @Override + public synchronized void close() throws SecurityException { + this.handler.close(); + } + + /** + * A simple {@link Formatter} that only outputs the actual message. + */ + private static class AuditLoggerFormatter extends Formatter { + + /** {@inheritDoc} */ + @Override + public String format(final LogRecord record) { + return record.getMessage() + System.lineSeparator(); + } + + } +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/DelegatingAuditEventRepository.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/DelegatingAuditEventRepository.java new file mode 100644 index 00000000..970ec816 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/DelegatingAuditEventRepository.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; + +import lombok.extern.slf4j.Slf4j; + +/** + * A delegating {@link AuditEventRepository} that can be used to support multiple {@link AuditEventRepository} + * instances. + *

+ * Note that when invoking {@link #find(String, Instant, String)}, the first installed repository will be tried, and if + * that repository returns an empty list, the next repository will be tried. + *

+ * + * @author Martin Lindström + */ +@Slf4j +public class DelegatingAuditEventRepository implements AuditEventRepository { + + /** The underlying {@link AuditEventRepository} instances. */ + private final List repositories; + + /** + * Constructor. + * + * @param repositories the underlying {@link AuditEventRepository} instances. + */ + public DelegatingAuditEventRepository(final List repositories) { + this.repositories = Objects.requireNonNull(repositories, "repositories must not be null"); + } + + /** + * Adds the event to all installed repositories. + */ + @Override + public void add(final AuditEvent event) { + this.repositories.forEach(r -> { + try { + r.add(event); + } + catch (final Exception e) { + log.error("Failed to add event to {}", r.getClass().getSimpleName(), e); + } + }); + } + + /** + * The first installed repository will be tried, and if that repository returns an empty list, the next repository + * will be tried, and so on. + */ + @Override + public List find(final String principal, final Instant after, final String type) { + for (final AuditEventRepository r : this.repositories) { + final List events = r.find(principal, after, type); + if (events != null && !events.isEmpty()) { + return events; + } + } + return Collections.emptyList(); + } + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/FileBasedAuditEventRepository.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/FileBasedAuditEventRepository.java new file mode 100644 index 00000000..95b8c1f8 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/FileBasedAuditEventRepository.java @@ -0,0 +1,103 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; + +import lombok.extern.slf4j.Slf4j; + +/** + * A write-only {@link AuditEventRepository} that writes audit events to a file. + * + * @author Martin Lindström + */ +@Slf4j +public class FileBasedAuditEventRepository extends FilteringAuditEventRepository { + + /** The audit logger (Java Util Logging logger). */ + private final java.util.logging.Logger auditLogger; + + /** The underlying JUL handler. */ + private final DateRollingFileHandler handler; + + /** For mapping events to strings. */ + private final AuditEventMapper eventMapper; + + /** + * Constructor mapping to {@link #FileBasedAuditEventRepository(String, AuditEventMapper, Predicate)} where the filter + * allows all events. + * + * @param logFile the log file including its path + * @param eventMapper the event mapper used to map events to strings + * @throws IOException if the logfile is invalid + */ + public FileBasedAuditEventRepository(final String logFile, final AuditEventMapper eventMapper) throws IOException { + this(logFile, eventMapper, null); + } + + /** + * Constructor. + * + * @param logFile the log file including its path + * @param eventMapper the event mapper used to map events to strings + * @param filter filter for determining which events to log + * @throws IOException if the logfile is invalid + */ + public FileBasedAuditEventRepository( + final String logFile, final AuditEventMapper eventMapper, final Predicate filter) + throws IOException { + super(filter); + this.eventMapper = Objects.requireNonNull(eventMapper, "eventMapper must not be null"); + + this.handler = new DateRollingFileHandler(logFile); + // Build the logger name based on the log file name ... + final String loggerName = Path.of(logFile).toAbsolutePath().toString(); + + this.auditLogger = Logger.getLogger(loggerName); + this.auditLogger.setLevel(Level.INFO); + this.auditLogger.addHandler(this.handler); + this.auditLogger.setUseParentHandlers(false); + } + + /** {@inheritDoc} */ + @Override + public void addEvent(final AuditEvent event) { + try { + this.auditLogger.log(Level.INFO, this.eventMapper.write(event)); + } + catch (final Throwable e) { + log.error("Failed to audit log to file - {}", e.getMessage(), e); + } + } + + /** {@inheritDoc} */ + @Override + public List find(final String principal, final Instant after, final String type) { + return Collections.emptyList(); + } + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/FilteringAuditEventRepository.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/FilteringAuditEventRepository.java new file mode 100644 index 00000000..67978ff8 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/FilteringAuditEventRepository.java @@ -0,0 +1,125 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; + +import lombok.extern.slf4j.Slf4j; + +/** + * Abstract {@link AuditEventRepository} that supports filtering of events. + * + * @author Martin Lindström + */ +@Slf4j +public abstract class FilteringAuditEventRepository implements AuditEventRepository { + + /** The filter. */ + private final Predicate filter; + + /** + * Constructor setting up a filter that accepts all events. + */ + public FilteringAuditEventRepository() { + this(null); + } + + /** + * Constructor. + * + * @param filter the filter + */ + public FilteringAuditEventRepository(final Predicate filter) { + this.filter = Optional.ofNullable(filter).orElseGet(() -> inclusionPredicate(Collections.emptyList())); + } + + /** {@inheritDoc} */ + @Override + public final void add(final AuditEvent event) { + if (event != null) { + if (this.filter.test(event)) { + log.debug("Audit logging event '{}' for principal '{}' ...", event.getType(), event.getPrincipal()); + this.addEvent(event); + } + else { + log.debug("Audit event {} not logged - filter rules excludes it", event.getType()); + } + } + } + + /** + * Logs an event. + * + * @param event the audit event to log + */ + protected abstract void addEvent(final AuditEvent event); + + /** + * Returns an audit event filter that accepts a list of event types that are accepted. + *

+ * If the {@code types} parameter is {@code null} or an empty list, all events are accepted. + *

+ * + * @param types the types that are accepted + * @return a {@link Predicate} that returns {@code true} if an event should be audited + */ + public static Predicate inclusionPredicate(final List types) { + return event -> { + if (types == null || types.isEmpty()) { + return true; + } + return types.contains(event.getType()); + }; + } + + /** + * Returns an audit event filter that excludes the given event types from being audited. + *

+ * If the {@code types} parameter is {@code null} or an empty list, no events are excluded. + *

+ * + * @param types the types to exclude + * @return a {@link Predicate} that returns {@code true} if an event should be audited + */ + public static Predicate exclusionPredicate(final List types) { + return event -> { + if (types == null) { + return true; + } + return !types.contains(event.getType()); + }; + } + + /** + * Returns an audit event filter that combines {@link #inclusionExclusionPredicate(List, List)} and + * {@link #exclusionPredicate(List)}. + * + * @param includeTypes the types to include (if {@code null} or empty, all events are accepted) + * @param dontIncludeTypes the types to exclude (if {@code null} or empty, no events are excluded) + * @return a {@link Predicate} that returns {@code true} if an event should be audited + */ + public static Predicate inclusionExclusionPredicate( + final List includeTypes, final List dontIncludeTypes) { + return inclusionPredicate(includeTypes).and(exclusionPredicate(dontIncludeTypes)); + } + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/JsonAuditEventMapper.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/JsonAuditEventMapper.java new file mode 100644 index 00000000..3c5e7ea6 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/JsonAuditEventMapper.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.springframework.boot.actuate.audit.AuditEvent; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; + +import se.swedenconnect.spring.saml.idp.Saml2IdentityProviderVersion; + +/** + * A JSON {@link AuditEventMapper}. + * + * @author Martin Lindström + * @author Felix Hellman + */ +public class JsonAuditEventMapper implements AuditEventMapper { + + /** The underlying {@link ObjectMapper}. */ + private final ObjectMapper mapper; + + /** + * Constructor. + * + * @param mapper the {@link ObjectMapper} + */ + public JsonAuditEventMapper(final ObjectMapper mapper) { + this.mapper = Objects.requireNonNull(mapper, "mapper must not be null"); + } + + /** {@inheritDoc} */ + @Override + public String write(final AuditEvent event) { + try { + return this.mapper.writerFor(AuditEvent.class).writeValueAsString(event); + } + catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + /** {@inheritDoc} */ + @Override + public AuditEvent read(final String event) { + try { + final JsonAuditEvent auditEvent = this.mapper.readerFor(JsonAuditEvent.class).readValue(event); + return auditEvent; + } + catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Helper class for reading events. + */ + private static class JsonAuditEvent extends AuditEvent { + + private static final long serialVersionUID = Saml2IdentityProviderVersion.SERIAL_VERSION_UID; + + /** + * Adds a JsonCreator for Jackson to be able to serialize AuditEvents. + * + * @param principal to deserialize + * @param type to deserialize + * @param data to deserialize + */ + @JsonCreator + public JsonAuditEvent( + @JsonProperty("principal") final String principal, + @JsonProperty("type") final String type, + @JsonProperty("data") final Map data) { + super(principal, type, Optional.ofNullable(data).orElse(Map.of())); + } + } + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/MemoryBasedAuditEventRepository.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/MemoryBasedAuditEventRepository.java new file mode 100644 index 00000000..0786e56f --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/MemoryBasedAuditEventRepository.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import java.time.Instant; +import java.util.List; +import java.util.function.Predicate; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository; +import org.springframework.util.Assert; + +/** + * An in-memory {@link AuditEventRepository} that adds filtering support (compared to + * {@link InMemoryAuditEventRepository}). + * + * @author Martin Lindström + */ +public class MemoryBasedAuditEventRepository extends FilteringAuditEventRepository { + + public static final int DEFAULT_CAPACITY = 1000; + + private final InMemoryAuditEventRepository repository; + + /** + * Constructor setting up a memory based {@link AuditEventRepository} that logs all events and has a capacity of + * {@value #DEFAULT_CAPACITY}. + */ + public MemoryBasedAuditEventRepository() { + this(null, DEFAULT_CAPACITY); + } + + /** + * Constructor setting up a memory based {@link AuditEventRepository} that logs events determined by the supplied + * filter and has a capacity of {@value #DEFAULT_CAPACITY}. + * + * @param filter for determining which events to log + */ + public MemoryBasedAuditEventRepository(final Predicate filter) { + this(filter, DEFAULT_CAPACITY); + } + + /** + * Constructor setting up a memory based {@link AuditEventRepository} that logs events determined by the supplied + * filter and has a capacity given by {@code capacity}. + * + * @param filter for determining which events to log + * @param capacity the capacity for the number of events that should be saved + */ + public MemoryBasedAuditEventRepository(final Predicate filter, final int capacity) { + super(filter); + Assert.isTrue(capacity > 0, "Invalid capacity - must be greater than 0"); + this.repository = new InMemoryAuditEventRepository(capacity); + } + + /** {@inheritDoc} */ + @Override + protected void addEvent(final AuditEvent event) { + this.repository.add(event); + } + + /** {@inheritDoc} */ + @Override + public List find(final String principal, final Instant after, final String type) { + return this.repository.find(principal, after, type); + } + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/RedisListAuditEventRepository.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/RedisListAuditEventRepository.java new file mode 100644 index 00000000..78433894 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/RedisListAuditEventRepository.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.StringRedisTemplate; + +import lombok.extern.slf4j.Slf4j; + +/** + * An implementation of the {@link AuditEventRepository} that uses Redis lists to store the events. + * + * @author Martin Lindström + * @author Felix Hellman + */ +@Slf4j +public class RedisListAuditEventRepository extends FilteringAuditEventRepository { + + /** The Redis list operations. */ + private final ListOperations listOps; + + /** The name of the Redis key holding the audit event list. */ + private final String keyName; + + /** The audit event mapper. */ + private final AuditEventMapper eventMapper; + + /** + * Constructor setting up the repository to log all events. + * + * @param redisTemplate the Redis template + * @param keyName the name of the Redis key holding the audit event list + * @param mapper mapper for creating/reading JSON events + */ + public RedisListAuditEventRepository(final StringRedisTemplate redisTemplate, final String keyName, + final AuditEventMapper mapper) { + this(redisTemplate, keyName, mapper, null); + } + + /** + * Constructor setting up the repository to log events according to the supplied filter. + * + * @param redisTemplate the Redis template + * @param keyName the name of the Redis key holding the audit event list + * @param mapper mapper for creating/reading JSON events + * @param filter filter for determining which events to log + */ + public RedisListAuditEventRepository(final StringRedisTemplate redisTemplate, final String keyName, + final AuditEventMapper mapper, final Predicate filter) { + super(filter); + this.listOps = Objects.requireNonNull(redisTemplate, "redisTemplate must not be null").opsForList(); + this.keyName = Objects.requireNonNull(keyName, "keyName must not be null"); + this.eventMapper = Objects.requireNonNull(mapper, "mapper must not be null"); + } + + /** {@inheritDoc} */ + @Override + protected void addEvent(final AuditEvent event) { + try { + this.listOps.rightPush(this.keyName, this.eventMapper.write(event)); + } + catch (final Throwable e) { + log.error("Failed to write event '{}' to Redis list {}", event.getType(), this.keyName, e); + } + } + + /** {@inheritDoc} */ + @Override + public List find(final String principal, final Instant after, final String type) { + final long size = Optional.ofNullable(this.listOps.size(this.keyName)).orElse(0L); + final List list = this.listOps.range(this.keyName, 0, size); + if (list == null) { + return Collections.emptyList(); + } + return list.stream() + .map(e -> this.eventMapper.read(e)) + .filter(e -> type != null ? type.equals(e.getType()) : true) + .filter(e -> principal != null ? principal.equals(e.getPrincipal()) : true) + .filter(e -> after != null ? after.isBefore(e.getTimestamp()) : true) + .toList(); + } +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/RedissonTimeSeriesAuditEventRepository.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/RedissonTimeSeriesAuditEventRepository.java new file mode 100644 index 00000000..8d9268c2 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/RedissonTimeSeriesAuditEventRepository.java @@ -0,0 +1,108 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.audit.repository; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + +import org.redisson.api.RedissonClient; +import org.redisson.api.TimeSeriesEntry; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.AuditEventRepository; + +import lombok.extern.slf4j.Slf4j; + +/** + * An {@link AuditEventRepository} implementation that uses Redis time series to store events. + * + * @author Martin Lindström + * @author Felix Hellman + */ +@Slf4j +public class RedissonTimeSeriesAuditEventRepository extends FilteringAuditEventRepository { + + /** The Redis client. */ + private final RedissonClient client; + + /** The Redis timeseries name holding the audit events. */ + private final String tsName; + + /** The audit event mapper. */ + private final AuditEventMapper eventMapper; + + /** + * Constructor setting up the repository to log all events. + * + * @param client the Redis client + * @param tsName the Redis timeseries name holding the audit events + * @param mapper mapper for creating/reading JSON events + */ + public RedissonTimeSeriesAuditEventRepository(final RedissonClient client, final String tsName, + final AuditEventMapper mapper) { + this(client, tsName, mapper, null); + } + + /** + * Constructor setting up the repository to log events according to the supplied filter. + * + * @param client the Redis client + * @param tsName the Redis timeseries name holding the audit events + * @param mapper mapper for creating/reading JSON events + * @param filter filter for determining which events to log + */ + public RedissonTimeSeriesAuditEventRepository(final RedissonClient client, final String tsName, + final AuditEventMapper mapper, final Predicate filter) { + super(filter); + this.client = Objects.requireNonNull(client, "client must not be null"); + this.tsName = Objects.requireNonNull(tsName, "tsName must not be null"); + this.eventMapper = Objects.requireNonNull(mapper, "mapper must not be null"); + } + + /** {@inheritDoc} */ + @Override + protected void addEvent(final AuditEvent event) { + try { + this.client.getTimeSeries(this.tsName) + .add(event.getTimestamp().toEpochMilli(), this.eventMapper.write(event)); + } + catch (final Throwable e) { + log.error("Failed to write event '{}' to Redis timeseries {}", event.getType(), this.tsName, e); + } + } + + /** {@inheritDoc} */ + @Override + public List find(final String principal, final Instant after, final String type) { + final Collection> timeSeries = + this.client.getTimeSeries(this.tsName).entryRange( + Optional.ofNullable(after) + .orElseGet(() -> Instant.EPOCH) + .toEpochMilli(), + Instant.now().plus(1, ChronoUnit.MINUTES).toEpochMilli()); + + return timeSeries.stream() + .map(e -> this.eventMapper.read((String) e.getValue())) + .filter(e -> type != null ? type.equals(e.getType()) : true) + .filter(e -> principal != null ? principal.equals(e.getPrincipal()) : true) + .toList(); + } + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/package-info.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/package-info.java new file mode 100644 index 00000000..7cec9397 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/audit/repository/package-info.java @@ -0,0 +1,4 @@ +/** + * Audit logging repositories. + */ +package se.swedenconnect.spring.saml.idp.audit.repository; \ No newline at end of file diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/InMemoryReplayCache.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/InMemoryReplayCache.java new file mode 100644 index 00000000..c606b69b --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/InMemoryReplayCache.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.authnrequest.validation.replay; + +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.opensaml.storage.ReplayCache; + +import lombok.extern.slf4j.Slf4j; + +/** + * An in-memory implementation of the {@link ReplayCache} interface. + * + * @author Martin Lindström + */ +@Slf4j +public class InMemoryReplayCache implements ReplayCache { + + /** The cache. */ + private final ConcurrentMap cache = new ConcurrentHashMap<>(); + + /** + * Constructor. + */ + public InMemoryReplayCache() { + log.warn("{} is used, consider using a distributed cache for production", this.getClass().getSimpleName()); + } + + /** {@inheritDoc} */ + @Override + public boolean check(final String context, final String key, final Instant expires) { + + // Remove expired entries ... + // + final long now = Instant.now().getEpochSecond(); + this.cache.entrySet().removeIf(e -> e.getValue() < now); + + if (this.cache.containsKey(key)) { + log.debug("Key '{}' was present in in-memory replay cache, returning false", key); + return false; + } + else { + this.cache.put(key, expires.getEpochSecond()); + log.trace("Key '{}' was not present in in-memory replay cache, adding it and returning true", key); + return true; + } + } + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/RedisReplayCache.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/RedisReplayCache.java new file mode 100644 index 00000000..b34926cf --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/RedisReplayCache.java @@ -0,0 +1,70 @@ +/* + * Copyright 2023-2024 Sweden Connect + * + * Licensed 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 se.swedenconnect.spring.saml.idp.authnrequest.validation.replay; + +import java.time.Instant; +import java.util.Objects; + +import org.opensaml.storage.ReplayCache; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; + +import lombok.extern.slf4j.Slf4j; + +/** + * A generic Redis {@link ReplayCache} implementation. + * + * @author Martin Lindström + */ +@Slf4j +public class RedisReplayCache implements ReplayCache { + + /** The Redis set. */ + private final ZSetOperations redisSet; + + /** + * Constructor. + * + * @param redisTemplate the Redis template + */ + public RedisReplayCache(final StringRedisTemplate redisTemplate) { + this.redisSet = Objects.requireNonNull(redisTemplate, "redisTemplate must not be null").opsForZSet(); + } + + /** {@inheritDoc} */ + @Override + public boolean check(final String context, final String key, final Instant expires) { + + // Remove expired entries ... + // + final Long noRemoved = this.redisSet.removeRangeByScore(context, 0, Instant.now().getEpochSecond()); + log.trace("Removed {} expired entries in Redis replay cache", noRemoved); + + // If the key is present, we return false, otherwise we add the key to the set and return true. + // + if (this.redisSet.rank(context, key) != null) { + log.debug("Key '{}' was present in Redis replay cache ({}), returning false", key, context); + return false; + } + else { + this.redisSet.add(context, key, expires.getEpochSecond()); + log.trace("Key '{}' was not present in Redis replay cache ({}), adding it and returning true", key, context); + return true; + } + + } + +} diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/package-info.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/package-info.java new file mode 100644 index 00000000..d9fec773 --- /dev/null +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/authnrequest/validation/replay/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for replay checking. + */ +package se.swedenconnect.spring.saml.idp.authnrequest.validation.replay; \ No newline at end of file diff --git a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/config/Saml2IdpConfiguration.java b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/config/Saml2IdpConfiguration.java index 0690f07e..8807b32e 100644 --- a/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/config/Saml2IdpConfiguration.java +++ b/saml-identity-provider/src/main/java/se/swedenconnect/spring/saml/idp/config/Saml2IdpConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -111,7 +112,7 @@ public static void applyDefaultSecurity(final HttpSecurity http, .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .securityContext((sc) -> sc.requireExplicitSave(false)) - .apply(idpConfigurer); + .with(idpConfigurer, Customizer.withDefaults()); } /** diff --git a/samples/client/pom.xml b/samples/client/pom.xml index 3a350c7f..0395dc7c 100644 --- a/samples/client/pom.xml +++ b/samples/client/pom.xml @@ -9,7 +9,7 @@ se.swedenconnect.spring.saml.idp spring-saml-idp-samples-parent - 2.0.2 + 2.1.0-SNAPSHOT Sweden Connect :: Spring SAML Identity Provider :: Samples :: Client Application @@ -74,6 +74,12 @@ 1.2.8
+ + org.apache.santuario + xmlsec + 2.3.4 + + diff --git a/samples/demo-boot-idp/pom.xml b/samples/demo-boot-idp/pom.xml index b515cf01..f80a4258 100644 --- a/samples/demo-boot-idp/pom.xml +++ b/samples/demo-boot-idp/pom.xml @@ -9,7 +9,7 @@ se.swedenconnect.spring.saml.idp spring-saml-idp-samples-parent - 2.0.2 + 2.1.0-SNAPSHOT Sweden Connect :: Spring SAML Identity Provider :: Samples :: Spring Boot Starter Demo Application @@ -91,12 +91,15 @@ test
- org.webjars diff --git a/samples/demo-boot-idp/src/main/java/se/swedenconnect/spring/saml/idp/demo/IdpConfiguration.java b/samples/demo-boot-idp/src/main/java/se/swedenconnect/spring/saml/idp/demo/IdpConfiguration.java index 3e1b1b5c..4134d964 100644 --- a/samples/demo-boot-idp/src/main/java/se/swedenconnect/spring/saml/idp/demo/IdpConfiguration.java +++ b/samples/demo-boot-idp/src/main/java/se/swedenconnect/spring/saml/idp/demo/IdpConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Sweden Connect + * Copyright 2023-2024 Sweden Connect * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ import org.opensaml.saml.saml2.core.NameID; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -121,9 +120,4 @@ SecurityFilterChain defaultSecurityFilterChain(final HttpSecurity http) throws E return http.build(); } - @Bean - InMemoryAuditEventRepository repository() { - return new InMemoryAuditEventRepository(); - } - } diff --git a/samples/demo-boot-idp/src/main/resources/application.yml b/samples/demo-boot-idp/src/main/resources/application.yml index 25c4f553..8e10d4e6 100644 --- a/samples/demo-boot-idp/src/main/resources/application.yml +++ b/samples/demo-boot-idp/src/main/resources/application.yml @@ -1,32 +1,42 @@ + +spring: + messages: + basename: messages,idp-errors/idp-error-messages + ssl: + bundle: + jks: + server: + key: + alias: local + password: secret + keystore: + location: classpath:ssl.jks + password: secret + type: JKS + server: port: 8443 servlet: - context-path: /idp + context-path: /idp + session: + timeout: 30m ssl: enabled: true - key-store: classpath:ssl.jks - key-store-type: JKS - key-alias: local - key-store-password: secret - key-password: secret + bundle: server error: include-message: always include-exception: true include-stacktrace: always - + management: server: port: 8444 endpoints: web: exposure: - include: health, auditevents + include: info, health, auditevents auditevents: - enabled: true - -spring: - messages: - basename: messages,idp-errors/idp-error-messages + enabled: true demo: users: @@ -48,6 +58,8 @@ demo: saml: idp: + session: + module: memory entity-id: https://demo.swedenconnect.se/idp base-url: https://local.dev.swedenconnect.se:8443/idp credentials: @@ -72,6 +84,16 @@ saml: - location: https://eid.svelegtest.se/metadata/mdx/role/sp.xml backup-location: target/metadata-backup.xml validation-certificate: classpath:sandbox-metadata.crt + replay: + type: memory + expiration: 10m + context: "idp-replay-cache" + audit: + file: + log-file: target/audit.log + in-memory: + capacity: 10000 + logging: level: @@ -79,5 +101,7 @@ logging: swedenconnect: spring: saml: TRACE + + diff --git a/samples/pom.xml b/samples/pom.xml index c2c163c0..340eb04f 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -9,7 +9,7 @@ se.swedenconnect.spring.saml.idp spring-saml-idp-parent - 2.0.2 + 2.1.0-SNAPSHOT Sweden Connect :: Spring SAML Identity Provider :: Samples :: Parent POM diff --git a/starter/pom.xml b/starter/pom.xml index 6e6b9050..11850682 100644 --- a/starter/pom.xml +++ b/starter/pom.xml @@ -10,7 +10,7 @@ se.swedenconnect.spring.saml.idp spring-saml-idp-parent - 2.0.2 + 2.1.0-SNAPSHOT Sweden Connect :: Spring SAML Identity Provider :: Spring Boot Starter