From 2442685b856548a5a30f6e8bc2c54343b3904e2e Mon Sep 17 00:00:00 2001 From: Jens Kutzsche Date: Tue, 21 Jun 2022 15:45:08 +0200 Subject: [PATCH] feat: The authentication tokens (JWT) now retain their validity beyond the restart of the IRIS client. This means that, ideally, users notice only little of a restart of the application. If no secrets for JWT have been set via external properties, then secure ones are automatically generated and stored in the database. This means that the secrets and the JWTs are retained beyond the restart. After a configurable duration (default: 3 months) they are renewed. PR #804 --- .../client_bff/auth/db/jwt/JWTService.java | 59 ++++++++++++++++++- .../client_bff/core/settings/Setting.java | 40 +++++++++++++ .../core/settings/SettingsRepository.java | 5 ++ .../src/main/resources/application.properties | 7 ++- .../db/migration/V1014__add_settings.sql | 5 ++ .../migration_mssql/V1014__add_settings.sql | 5 ++ .../migration_mysql/V1014__add_settings.sql | 5 ++ 7 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 iris-client-bff/src/main/java/iris/client_bff/core/settings/Setting.java create mode 100644 iris-client-bff/src/main/java/iris/client_bff/core/settings/SettingsRepository.java create mode 100644 iris-client-bff/src/main/resources/db/migration/V1014__add_settings.sql create mode 100644 iris-client-bff/src/main/resources/db/migration_mssql/V1014__add_settings.sql create mode 100644 iris-client-bff/src/main/resources/db/migration_mysql/V1014__add_settings.sql diff --git a/iris-client-bff/src/main/java/iris/client_bff/auth/db/jwt/JWTService.java b/iris-client-bff/src/main/java/iris/client_bff/auth/db/jwt/JWTService.java index b75294ca6..62bc98ccb 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/auth/db/jwt/JWTService.java +++ b/iris-client-bff/src/main/java/iris/client_bff/auth/db/jwt/JWTService.java @@ -6,16 +6,26 @@ import io.vavr.control.Option; import io.vavr.control.Try; +import iris.client_bff.core.settings.Setting; +import iris.client_bff.core.settings.Setting.Name; +import iris.client_bff.core.settings.SettingsRepository; import lombok.Value; +import lombok.experimental.NonFinal; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; +import java.time.Period; import java.util.Date; import java.util.List; +import java.util.Random; import java.util.function.Function; +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -24,7 +34,9 @@ import javax.validation.constraints.NotNull; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.hibernate.validator.constraints.time.DurationMin; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; @@ -247,9 +259,18 @@ private String hashToken(String jwt) { @Value static class Properties { - @NotBlank + private static final Random random = new SecureRandom(); + + @Autowired + SettingsRepository settings; + + @Nullable + @NonFinal String sharedSecret; + @NotNull + Period saveSecretFor; + @NotNull @DurationMin(seconds = 1) Duration expirationTime; @@ -268,14 +289,48 @@ String getSameSiteStr() { @NotNull @Valid RefreshProperties refresh; + + @PostConstruct + void initSecrets() { + + if (isBlank(sharedSecret)) { + + var setting = settings.findById(Name.JWT_SECRET) + .filter(it -> it.getSavedAt().plus(saveSecretFor).isAfter(LocalDate.now())) + .orElseGet(() -> createNewRandomSecret(Name.JWT_SECRET)); + + sharedSecret = setting.getStoredValue(); + } + + if (isBlank(refresh.getSharedSecret())) { + + var setting = settings.findById(Name.REFRESH_SECRET) + .filter(it -> it.getSavedAt().plus(refresh.getSaveSecretFor()).isAfter(LocalDate.now())) + .orElseGet(() -> createNewRandomSecret(Name.REFRESH_SECRET)); + + refresh.sharedSecret = setting.getStoredValue(); + } + } + + private Setting createNewRandomSecret(Name name) { + + // 64 Bytes of printable Ascii characters + var secret = RandomStringUtils.random(64, 32, 127, false, false, null, random); + + return settings.save(new Setting(name, secret, LocalDate.now())); + } } @Value static class RefreshProperties { - @NotBlank + @Nullable + @NonFinal String sharedSecret; + @NotNull + Period saveSecretFor; + @NotNull @DurationMin(seconds = 1) Duration expirationTime; diff --git a/iris-client-bff/src/main/java/iris/client_bff/core/settings/Setting.java b/iris-client-bff/src/main/java/iris/client_bff/core/settings/Setting.java new file mode 100644 index 000000000..fdef7141f --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/core/settings/Setting.java @@ -0,0 +1,40 @@ +package iris.client_bff.core.settings; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.validation.constraints.Size; + +/** + * @author Jens Kutzsche + */ +@Entity +@Table(name = "settings") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Setting { + + @Id + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Name name; + + @Size(max = 1000) + private String storedValue; // value is a keyword for DBMS + + private LocalDate savedAt = LocalDate.now(); + + public enum Name { + JWT_SECRET, REFRESH_SECRET; + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/core/settings/SettingsRepository.java b/iris-client-bff/src/main/java/iris/client_bff/core/settings/SettingsRepository.java new file mode 100644 index 000000000..d70330c5a --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/core/settings/SettingsRepository.java @@ -0,0 +1,5 @@ +package iris.client_bff.core.settings; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SettingsRepository extends JpaRepository {} diff --git a/iris-client-bff/src/main/resources/application.properties b/iris-client-bff/src/main/resources/application.properties index cbf4b6dee..db55502ea 100644 --- a/iris-client-bff/src/main/resources/application.properties +++ b/iris-client-bff/src/main/resources/application.properties @@ -69,12 +69,15 @@ spring.jackson.default-property-inclusion=NON_ABSENT springdoc.api-docs.enabled=false -security.jwt.shared-secret=${random.value}${random.value} +# A random value generated at runtime if nothing is set here. +security.jwt.shared-secret= +security.jwt.save-secret-for=3m security.jwt.expiration-time=1h security.jwt.cookie-name=IRIS_JWT security.jwt.set-secure=true security.jwt.same-site=Strict -security.jwt.refresh.shared-secret=${random.value}${random.value} +security.jwt.refresh.shared-secret= +security.jwt.refresh.save-secret-for=3m security.jwt.refresh.expiration-time=24h security.jwt.refresh.cookie-name=IRIS_REFRESH_JWT # Durations >0 like descripted in https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties.conversion diff --git a/iris-client-bff/src/main/resources/db/migration/V1014__add_settings.sql b/iris-client-bff/src/main/resources/db/migration/V1014__add_settings.sql new file mode 100644 index 000000000..f0be4ef71 --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration/V1014__add_settings.sql @@ -0,0 +1,5 @@ +CREATE TABLE settings ( + name varchar(50) primary key, + stored_value varchar(1000) NOT NULL, + saved_at date NOT NULL +); diff --git a/iris-client-bff/src/main/resources/db/migration_mssql/V1014__add_settings.sql b/iris-client-bff/src/main/resources/db/migration_mssql/V1014__add_settings.sql new file mode 100644 index 000000000..f0be4ef71 --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration_mssql/V1014__add_settings.sql @@ -0,0 +1,5 @@ +CREATE TABLE settings ( + name varchar(50) primary key, + stored_value varchar(1000) NOT NULL, + saved_at date NOT NULL +); diff --git a/iris-client-bff/src/main/resources/db/migration_mysql/V1014__add_settings.sql b/iris-client-bff/src/main/resources/db/migration_mysql/V1014__add_settings.sql new file mode 100644 index 000000000..f0be4ef71 --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration_mysql/V1014__add_settings.sql @@ -0,0 +1,5 @@ +CREATE TABLE settings ( + name varchar(50) primary key, + stored_value varchar(1000) NOT NULL, + saved_at date NOT NULL +);