diff --git a/README.fork.ip6li.md b/README.fork.ip6li.md
new file mode 100644
index 00000000..14c757fa
--- /dev/null
+++ b/README.fork.ip6li.md
@@ -0,0 +1,90 @@
+# Generation vapi Keypair
+
+Alovoa need for class *PushAsyncService* used in *NotificationService* a key pair.
+
+```
+$ git clone --depth 1 https://github.com/web-push-libs/webpush-java.git
+$ cd webpush-java
+$ ./gradlew run --args="generate-key"
+> Task :run
+PublicKey:
+BBGKmyzGuw53tFpqxy-ol7v1FF5932sRV6iKPdtGtcFPTSC76QmevQw89pVSSzvPQCN9f_v2JbsF7n4ieAG2nhQ=
+PrivateKey:
+JIEdQkCnF-YFAI-kOExRzPIDO4qxSFeHT-RGdxmTuFs=
+```
+
+Save generated private and public key in vault as *app.vapid.public* and *app.vapid.private*.
+You should consider to save at least private key in vault.
+
+# Vault
+
+It is a really bad idea to store credentials or private keys in source code or
+property files. Spring Boot support vault solutions like Hashicorp Vault.
+
+Before starting Alovoa the first time you should set up Hashicorp Vault.
+
+## Config
+
+Following config is able to store mostly everything which may be configurable
+in *application.properties*. Alovoa fork (ab)uses vault as an alternative
+properties store.
+
+Generate an individual vapi keypair as shown above.
+
+```
+vault secrets enable -path=alovoa kv
+vault kv put -mount=alovoa creds app.text.key=32charKey app.text.salt=16charSalt app.admin.key=InitialAdmiPassword app.admin.email=AdminEmail spring.mail.username=SmtpUsername spring.mail.password=SmtpPassword app.vapid.private=vapidPrivateKey app.vapid.public=vapidPublicKey
+```
+
+Feel free to add more properties you would not like to see in application.properties.
+
+## Database Credentials
+
+Add a MySQL user which is allowed to set passwords. Configure that user in Hashicorp Vault:
+
+```
+vault write database/config/alovoa \
+ plugin_name=mysql-database-plugin \
+ connection_url="{{username}}:{{password}}@tcp(mysql)/" \
+ allowed_roles="mysqlrole" \
+ username="vault" \
+ password="PasswordForMysqlUserVault"
+```
+
+Tell vault how to create a temp user. This user is retrieved and used by Alovoa:
+
+```
+vault write database/roles/mysqlrole \
+ db_name=alovoa \
+ creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT ALL PRIVILEGES ON alovoa.* TO {{name}}'@'%';" \
+ default_ttl="1h" \
+ max_ttl="24h"
+```
+
+## Vault Policy
+
+Alovoa should not use root vault token, it should use a dedicated token with specific entitelments:
+
+```
+vault policy write alovoa-policy - << EOF
+path "database/creds/mysqlrole" {
+ capabilities = ["read"]
+}
+path "oauth-idp/keycloak" {
+ capabilities = ["read"]
+}
+path "alovoa/creds" {
+ capabilities = ["read"]
+}
+EOF
+```
+
+## Vault Token
+
+Now generate a dedicated token for Alovoa.
+
+```
+vault token create -policy=alovoa-policy
+```
+
+Save output of that command.
diff --git a/pom.xml b/pom.xml
index 6a66732c..4eddd560 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.0.9
+ 3.1.2
com.alovoa
@@ -16,7 +16,7 @@
Alovoa
- 1.8
+ 17
@@ -25,6 +25,16 @@
org.springframework.boot
spring-boot-starter-data-jpa
+
+ org.springframework.vault
+ spring-vault-core
+ 3.0.4
+
+
+ org.springframework.cloud
+ spring-cloud-vault-config-databases
+ 4.0.1
+
org.springframework.boot
spring-boot-starter-jdbc
diff --git a/src/main/java/com/nonononoki/alovoa/AlovoaApplication.java b/src/main/java/com/nonononoki/alovoa/AlovoaApplication.java
index f666eec6..76568895 100644
--- a/src/main/java/com/nonononoki/alovoa/AlovoaApplication.java
+++ b/src/main/java/com/nonononoki/alovoa/AlovoaApplication.java
@@ -1,15 +1,17 @@
package com.nonononoki.alovoa;
-import java.security.Security;
-
+import com.nonononoki.alovoa.config.VaultPropertySource;
+import com.nonononoki.alovoa.model.AlovoaException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
+import java.security.Security;
+
@SpringBootApplication
@EnableJpaRepositories("com.nonononoki.alovoa.repo")
@@ -18,9 +20,25 @@
@EnableScheduling
public class AlovoaApplication {
- public static void main(String[] args) {
-
- Security.addProvider(new BouncyCastleProvider());
- SpringApplication.run(AlovoaApplication.class, args);
- }
+ public static void main(String[] args) {
+ System.setProperty("javax.net.ssl.trustStore", "classpath:truststore.jks");
+ System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
+
+ Security.addProvider(new BouncyCastleProvider());
+
+ new SpringApplicationBuilder()
+ .sources(AlovoaApplication.class)
+ .initializers(context -> {
+ try {
+ context
+ .getEnvironment() // preserve default sources
+ .getPropertySources() // preserve default sources
+ .addFirst(VaultPropertySource.getInstance(context));
+ } catch (AlovoaException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ )
+ .run(args);
+ }
}
diff --git a/src/main/java/com/nonononoki/alovoa/config/DatasourceConfig.java b/src/main/java/com/nonononoki/alovoa/config/DatasourceConfig.java
new file mode 100644
index 00000000..31b13f9d
--- /dev/null
+++ b/src/main/java/com/nonononoki/alovoa/config/DatasourceConfig.java
@@ -0,0 +1,63 @@
+package com.nonononoki.alovoa.config;
+
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Properties;
+
+@ConfigurationProperties(prefix = "spring.datasource")
+@Configuration
+public class DatasourceConfig {
+ private static final Logger logger = LoggerFactory.getLogger(DatasourceConfig.class);
+
+ @Value("${spring.datasource.url}")
+ private String dsUrl;
+
+ @Value("${spring.datasource.driver-class-name}")
+ private String dsDriver;
+
+ private URI vaultUri;
+ @Value("${spring.cloud.vault.uri}")
+ private void setVaultUri(String newVaultUri) throws URISyntaxException {
+ vaultUri = new URI(newVaultUri);
+ };
+
+ @Value("${spring.cloud.vault.token}")
+ private String vaultToken;
+
+ @Value("${spring.cloud.vault.templatepath}")
+ private String vaultTemplatePath;
+
+ @Bean
+ public VaultHikariDataSource getHikariDataSource() {
+ Properties properties = new Properties();
+ properties.setProperty("cachePrepStmts" , "true");
+ properties.setProperty("prepStmtCacheSize" , "250" );
+ properties.setProperty("prepStmtCacheSqlLimit" , "2048" );
+
+ VaultHikariDataSource vaultHikariDataSource = new VaultHikariDataSource();
+ vaultHikariDataSource.setDriverClassName(dsDriver);
+ vaultHikariDataSource.setJdbcUrl(dsUrl);
+
+ vaultHikariDataSource.getMysqlCredentials().setVaultUrl(vaultUri);
+ vaultHikariDataSource.getMysqlCredentials().setVaultToken(vaultToken);
+ vaultHikariDataSource.getMysqlCredentials().setVaultTemplatePath(vaultTemplatePath);
+ vaultHikariDataSource.getMysqlCredentials().update();
+
+ vaultHikariDataSource.setDataSourceProperties(properties);
+ return vaultHikariDataSource;
+ }
+
+ @PostConstruct
+ private void postConstruct() {
+ logger.info("postConstruct called");
+ }
+
+}
diff --git a/src/main/java/com/nonononoki/alovoa/config/DatasourceCredentials.java b/src/main/java/com/nonononoki/alovoa/config/DatasourceCredentials.java
new file mode 100644
index 00000000..f4159023
--- /dev/null
+++ b/src/main/java/com/nonononoki/alovoa/config/DatasourceCredentials.java
@@ -0,0 +1,97 @@
+package com.nonononoki.alovoa.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.vault.authentication.TokenAuthentication;
+import org.springframework.vault.client.VaultEndpoint;
+import org.springframework.vault.core.VaultTemplate;
+import org.springframework.vault.support.VaultResponse;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class DatasourceCredentials {
+ private static final Logger logger = LoggerFactory.getLogger(DatasourceCredentials.class);
+
+ private static DatasourceCredentials datasourceCredentials;
+ private static String username;
+ private static String password;
+
+ private static URI vaultUrl;
+ private static String vaultToken;
+ private static String vaultTemplatePath;
+
+ private DatasourceCredentials() {}
+
+ public static DatasourceCredentials getInstance() {
+
+ if (datasourceCredentials == null) {
+ logger.info(String.format("Created new %s instance", DatasourceCredentials.class));
+ datasourceCredentials = new DatasourceCredentials();
+ } else {
+ logger.info(String.format("Using existing %s instance", DatasourceCredentials.class));
+ }
+ return datasourceCredentials;
+ }
+
+ public void setVaultUrl(URI newVaultUrl) {
+ vaultUrl = newVaultUrl;
+ }
+
+ public void setVaultToken(String newVaultToken) {
+ vaultToken = newVaultToken;
+ }
+
+ public void setVaultTemplatePath(String newVaultTemplatePath) {
+ vaultTemplatePath = newVaultTemplatePath;
+ }
+
+ private VaultTemplate getVaultTemplate() {
+ VaultEndpoint vaultEndpoint = new VaultEndpoint();
+ vaultEndpoint.setHost(vaultUrl.getHost());
+ vaultEndpoint.setScheme(vaultUrl.getScheme());
+ vaultEndpoint.setPort(vaultUrl.getPort());
+ return new VaultTemplate(
+ vaultEndpoint,
+ new TokenAuthentication(vaultToken)
+ );
+ }
+
+ @Scheduled(fixedDelay = 300000)
+ public void update() {
+ VaultTemplate vaultTemplate = getVaultTemplate();
+ VaultResponse vaultResponse = vaultTemplate.read(vaultTemplatePath);
+ assert vaultResponse != null;
+ Map vaultResponseData = vaultResponse.getData();
+ assert vaultResponseData != null;
+ username = vaultResponseData.get("username").toString();
+ password = vaultResponseData.get("password").toString();
+ logger.debug(String.format("Database credentials updated with username %s", username));
+ }
+
+ public Map getVaultCredentials(String vaultPath) {
+ VaultTemplate vaultTemplate = getVaultTemplate();
+ VaultResponse vaultResponse = vaultTemplate.read(vaultPath);
+ assert vaultResponse != null;
+ Map vaultResponseData = vaultResponse.getData();
+ Map dataMap = new HashMap<>();
+ assert vaultResponseData != null;
+ vaultResponseData.forEach((k, v) -> {
+ dataMap.put(k, v.toString());
+ });
+ return dataMap;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+}
diff --git a/src/main/java/com/nonononoki/alovoa/config/SecurityConfig.java b/src/main/java/com/nonononoki/alovoa/config/SecurityConfig.java
index 54385a19..77ccf21a 100644
--- a/src/main/java/com/nonononoki/alovoa/config/SecurityConfig.java
+++ b/src/main/java/com/nonononoki/alovoa/config/SecurityConfig.java
@@ -1,9 +1,11 @@
package com.nonononoki.alovoa.config;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+// See also https://www.baeldung.com/spring-boot-keycloak
+import com.nonononoki.alovoa.component.*;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
@@ -11,14 +13,33 @@
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
+import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
@@ -32,167 +53,265 @@
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
-import com.nonononoki.alovoa.component.AuthFilter;
-import com.nonononoki.alovoa.component.AuthProvider;
-import com.nonononoki.alovoa.component.CustomTokenBasedRememberMeServices;
-import com.nonononoki.alovoa.component.CustomUserDetailsService;
-import com.nonononoki.alovoa.component.AuthFailureHandler;
-import com.nonononoki.alovoa.component.AuthSuccessHandler;
+import java.util.*;
-import lombok.RequiredArgsConstructor;
+import static com.nonononoki.alovoa.rest.Oauth2Controller.OAUTH_ROLE_ADMIN;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
-
- @Value("${app.text.key}")
- private String key;
-
- @Value("${app.login.remember.key}")
- private String rememberKey;
-
- @Autowired
- private Environment env;
-
- @Autowired
- private AuthFailureHandler failureHandler;
-
- @Autowired
- private CustomUserDetailsService customUserDetailsService;
-
- private final AuthenticationConfiguration configuration;
-
- public static final String ROLE_USER = "ROLE_USER";
- public static final String ROLE_ADMIN = "ROLE_ADMIN";
- public static final String COOKIE_SESSION = "JSESSIONID";
- public static final String COOKIE_REMEMBER = "remember-me";
-
- public static String getRoleUser() {
- return ROLE_USER;
- }
-
- public static String getRoleAdmin() {
- return ROLE_ADMIN;
- }
-
- @Bean
- SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
-
- AuthenticationManagerBuilder authenticationManagerBuilder = http
- .getSharedObject(AuthenticationManagerBuilder.class);
- authenticationManagerBuilder.authenticationProvider(authProvider());
-
- http.authorizeHttpRequests().requestMatchers("/admin").hasAnyAuthority(ROLE_ADMIN).requestMatchers("/admin/**")
- .hasAnyAuthority(ROLE_ADMIN).requestMatchers("/css/**").permitAll().requestMatchers("/js/**").permitAll()
- .requestMatchers("/img/**").permitAll().requestMatchers("/font/**").permitAll().requestMatchers("/json/**")
- .permitAll().requestMatchers("/oauth2/**").permitAll().requestMatchers("/").permitAll().requestMatchers("/login/**")
- .permitAll().requestMatchers("/terms-conditions").permitAll().requestMatchers("/imprint").permitAll()
- .requestMatchers("/imprint/*").permitAll().requestMatchers("/privacy").permitAll().requestMatchers("/faq")
- .permitAll().requestMatchers("/tos").permitAll().requestMatchers("/register").permitAll()
- .requestMatchers("/register/**").permitAll().requestMatchers("/captcha/**").permitAll()
- .requestMatchers("/donate-list").permitAll().requestMatchers("/donate/received/**").permitAll()
- .requestMatchers("/password/**").permitAll().requestMatchers("/favicon.ico").permitAll().requestMatchers("/sw.js")
- .permitAll().requestMatchers("/robots.txt").permitAll().requestMatchers("/.well-known/assetlinks.json")
- .permitAll().requestMatchers("/text/*").permitAll().requestMatchers("/manifest/**").permitAll()
- .requestMatchers("/fonts/**").permitAll().requestMatchers("/error").permitAll()
-
- .anyRequest().authenticated().and().formLogin().loginPage("/login").and().logout()
- .deleteCookies(COOKIE_SESSION, COOKIE_REMEMBER).logoutUrl("/logout").logoutSuccessUrl("/?logout").and()
- .oauth2Login().loginPage("/login").defaultSuccessUrl("/login/oauth2/success").and()
- .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class).rememberMe()
- .rememberMeServices(oAuthRememberMeServices()).key(rememberKey);
-
- http.sessionManagement().maximumSessions(10).expiredSessionStrategy(getSessionInformationExpiredStrategy())
- .sessionRegistry(sessionRegistry());
- http.csrf().disable();
-
- if (env.acceptsProfiles(Profiles.of("prod"))) {
- http.requiresChannel().anyRequest().requiresSecure();
- }
-
- http.cors();
- return http.build();
- }
-
- @Bean
- AuthenticationManager authenticationManager() throws Exception {
- return configuration.getAuthenticationManager();
- }
-
- @Bean
- AuthSuccessHandler successHandler() {
- return new AuthSuccessHandler(this);
- }
-
- @Bean
- AuthFilter authenticationFilter() throws Exception {
- AuthFilter filter = new AuthFilter();
- filter.setAuthenticationManager(authenticationManager());
- filter.setAuthenticationSuccessHandler(successHandler());
- filter.setAuthenticationFailureHandler(failureHandler);
- filter.setRememberMeServices(rememberMeServices());
- filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
- return filter;
- }
-
- // https://stackoverflow.com/questions/32463022/sessionregistry-is-empty-when-i-use-concurrentsessioncontrolauthenticationstrate
- public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
- List stratList = new ArrayList<>();
- SessionFixationProtectionStrategy concStrat = new SessionFixationProtectionStrategy();
- stratList.add(concStrat);
- RegisterSessionAuthenticationStrategy regStrat = new RegisterSessionAuthenticationStrategy(sessionRegistry());
- stratList.add(regStrat);
- CompositeSessionAuthenticationStrategy compStrat = new CompositeSessionAuthenticationStrategy(stratList);
- return compStrat;
- }
-
- public SessionInformationExpiredStrategy getSessionInformationExpiredStrategy() {
- SessionInformationExpiredStrategy strat = new SimpleRedirectSessionInformationExpiredStrategy("/logout");
- return strat;
- }
-
- @Bean
- SessionRegistry sessionRegistry() {
- return new SessionRegistryImpl();
- }
-
- @Bean
- TokenBasedRememberMeServices rememberMeServices() {
- return new TokenBasedRememberMeServices(rememberKey, customUserDetailsService);
+ private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
+
+ @Value("${app.login.remember.key}")
+ private String rememberKey;
+
+ @Value("${app.domain}")
+ private String appDomain;
+
+ @Autowired
+ private Environment env;
+
+ @Autowired
+ private AuthFailureHandler failureHandler;
+
+ @Autowired
+ private CustomUserDetailsService customUserDetailsService;
+
+ @Autowired
+ private ClientRegistrationRepository clientRegistrationRepository;
+
+ private final AuthenticationConfiguration configuration;
+
+ public static final String ROLE_USER = "ROLE_USER";
+ public static final String ROLE_ADMIN = "ROLE_ADMIN";
+ public static final String COOKIE_SESSION = "JSESSIONID";
+ public static final String COOKIE_REMEMBER = "remember-me";
+
+ public static String getRoleUser() {
+ return ROLE_USER;
+ }
+
+ public static String getRoleAdmin() {
+ return ROLE_ADMIN;
+ }
+
+ private OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
+ OidcClientInitiatedLogoutSuccessHandler successHandler =
+ new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
+
+ successHandler.setPostLogoutRedirectUri(appDomain);
+
+ return successHandler;
+ }
+
+ @Bean
+ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ AuthenticationManagerBuilder authenticationManagerBuilder = http
+ .getSharedObject(AuthenticationManagerBuilder.class);
+ authenticationManagerBuilder.authenticationProvider(authProvider());
+
+ http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
+ .requestMatchers("/admin").hasAnyAuthority(ROLE_ADMIN)
+ .requestMatchers("/admin/**").hasAnyAuthority(ROLE_ADMIN)
+ .requestMatchers("/static/element-web").permitAll()
+ .requestMatchers("/static/element-web/**").permitAll()
+ .requestMatchers("/css/**").permitAll()
+ .requestMatchers("/js/**").permitAll()
+ .requestMatchers("/img/**").permitAll()
+ .requestMatchers("/font/**").permitAll()
+ .requestMatchers("/json/**").permitAll()
+ .requestMatchers("/oauth2/**").permitAll()
+ .requestMatchers("/").permitAll()
+ .requestMatchers("/login/**").permitAll()
+ .requestMatchers("/terms-conditions").permitAll()
+ .requestMatchers("/imprint").permitAll()
+ .requestMatchers("/imprint/*").permitAll()
+ .requestMatchers("/privacy").permitAll()
+ .requestMatchers("/faq").permitAll()
+ .requestMatchers("/tos").permitAll()
+ .requestMatchers("/register").permitAll()
+ .requestMatchers("/register/**").permitAll()
+ .requestMatchers("/captcha/**").permitAll()
+ .requestMatchers("/donate-list").permitAll()
+ .requestMatchers("/donate/received/**").permitAll()
+ .requestMatchers("/password/**").permitAll()
+ .requestMatchers("/favicon.ico").permitAll()
+ .requestMatchers("/sw.js").permitAll()
+ .requestMatchers("/robots.txt").permitAll()
+ .requestMatchers("/.well-known/assetlinks.json").permitAll()
+ .requestMatchers("/text/*").permitAll()
+ .requestMatchers("/manifest/**").permitAll()
+ .requestMatchers("/fonts/**").permitAll()
+ .requestMatchers("/error").permitAll()
+ .anyRequest().authenticated())
+ .formLogin(formLogin -> formLogin
+ .loginPage("/login")
+ )
+ .logout(logout -> logout
+ .deleteCookies(COOKIE_SESSION, COOKIE_REMEMBER)
+ .logoutUrl("/logout")
+ .logoutSuccessHandler(oidcLogoutSuccessHandler())
+ .invalidateHttpSession(true)
+ .clearAuthentication(true)
+ .logoutSuccessUrl("/?logout")
+ )
+ .oauth2Login(
+ oauth2Login -> oauth2Login
+ .userInfoEndpoint(userInfo -> userInfo
+ .userAuthoritiesMapper(this.userAuthoritiesMapper()))
+ .loginPage("/login")
+ .defaultSuccessUrl("/login/oauth2/success")
+
+ )
+ .addFilterBefore(
+ authenticationFilter(),
+ UsernamePasswordAuthenticationFilter.class
+ )
+ .rememberMe(rememberMe -> rememberMe
+ .rememberMeServices(oAuthRememberMeServices())
+ .key(rememberKey)
+ );
+
+ http.sessionManagement(sessionManagement -> sessionManagement
+ .maximumSessions(10)
+ .expiredSessionStrategy(getSessionInformationExpiredStrategy())
+ .sessionRegistry(sessionRegistry())
+ );
+
+ http.csrf(AbstractHttpConfigurer::disable);
+
+ if (env.acceptsProfiles(Profiles.of("prod"))) {
+ http.requiresChannel(
+ requiresChannel -> requiresChannel
+ .anyRequest()
+ .requiresSecure()
+ );
+ }
+
+ http.cors(Customizer.withDefaults());
+ logger.debug("filterChain configured");
+ return http.build();
+ }
+
+
+
+ private GrantedAuthoritiesMapper userAuthoritiesMapper() {
+ return (authorities) -> {
+ Set mappedAuthorities = new HashSet<>();
+ logger.info(String.format("userAuthoritiesMapper authorities: %s", authorities.getClass().getName()));
+ authorities.forEach(authority -> {
+ if (authority instanceof OidcUserAuthority oidcUserAuthority) {
+ logger.trace("userAuthoritiesMapper oidc path");
+ OidcIdToken idToken = oidcUserAuthority.getIdToken();
+ OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
+ // Map the claims found in idToken and/or userInfo
+ // to one or more GrantedAuthority's and add it to mappedAuthorities
+ getGroupMembership(mappedAuthorities, oidcUserAuthority.getAttributes());
+ } else if (authority instanceof OAuth2UserAuthority oauth2UserAuthority) {
+ logger.trace("userAuthoritiesMapper oauth2 path");
+ getGroupMembership(mappedAuthorities, oauth2UserAuthority.getAttributes());
+ } else {
+ authorities.forEach((v) -> {
+ logger.trace(String.format("userAuthoritiesMapper other: %s", v));
+ });
+ logger.trace("userAuthoritiesMapper other path");
+ }
+ });
+
+ return mappedAuthorities;
+ };
}
- @Bean
- TokenBasedRememberMeServices oAuthRememberMeServices() {
- CustomTokenBasedRememberMeServices rememberMeService = new CustomTokenBasedRememberMeServices(rememberKey,
- customUserDetailsService);
- rememberMeService.setAlwaysRemember(true);
- return rememberMeService;
- }
-
- @Bean
- PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
-
- @Bean
- AuthProvider authProvider() {
- return new AuthProvider();
- }
-
- @Bean
- CorsFilter corsFilter() {
- final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
- final CorsConfiguration config = new CorsConfiguration();
- config.setAllowCredentials(true);
- config.setAllowedOriginPatterns(Arrays.asList("*"));
- config.setAllowedHeaders(Arrays.asList("*"));
- config.setAllowedMethods(Arrays.asList("*"));
- source.registerCorsConfiguration("/**", config);
- return new CorsFilter(source);
- }
-
- public CustomTokenBasedRememberMeServices getOAuthRememberMeServices() {
- return (CustomTokenBasedRememberMeServices) oAuthRememberMeServices();
- }
-}
\ No newline at end of file
+ private void getGroupMembership(Set mappedAuthorities, Map attributes) {
+ ArrayList roles = (ArrayList) attributes.get("groups");
+ if (roles!=null && roles.contains(OAUTH_ROLE_ADMIN)) {
+ logger.info(String.format("User %s is Admin", attributes.get("email")));
+ mappedAuthorities.add(new SimpleGrantedAuthority(ROLE_ADMIN));
+ } else {
+ logger.trace(String.format("User %s is not Admin", attributes.get("email")));
+ }
+ }
+
+ @Bean
+ AuthenticationManager authenticationManager() throws Exception {
+ return configuration.getAuthenticationManager();
+ }
+
+ @Bean
+ AuthSuccessHandler successHandler() {
+ return new AuthSuccessHandler(this);
+ }
+
+ @Bean
+ AuthFilter authenticationFilter() throws Exception {
+ AuthFilter filter = new AuthFilter();
+ filter.setAuthenticationManager(authenticationManager());
+ filter.setAuthenticationSuccessHandler(successHandler());
+ filter.setAuthenticationFailureHandler(failureHandler);
+ filter.setRememberMeServices(rememberMeServices());
+ filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
+ return filter;
+ }
+
+ // https://stackoverflow.com/questions/32463022/sessionregistry-is-empty-when-i-use-concurrentsessioncontrolauthenticationstrate
+ public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
+ List stratList = new ArrayList<>();
+ SessionFixationProtectionStrategy concStrat = new SessionFixationProtectionStrategy();
+ stratList.add(concStrat);
+ RegisterSessionAuthenticationStrategy regStrat = new RegisterSessionAuthenticationStrategy(sessionRegistry());
+ stratList.add(regStrat);
+ CompositeSessionAuthenticationStrategy compStrat = new CompositeSessionAuthenticationStrategy(stratList);
+ return compStrat;
+ }
+
+ public SessionInformationExpiredStrategy getSessionInformationExpiredStrategy() {
+ SessionInformationExpiredStrategy strat = new SimpleRedirectSessionInformationExpiredStrategy("/logout");
+ return strat;
+ }
+
+ @Bean
+ SessionRegistry sessionRegistry() {
+ return new SessionRegistryImpl();
+ }
+
+ @Bean
+ TokenBasedRememberMeServices rememberMeServices() {
+ return new TokenBasedRememberMeServices(rememberKey, customUserDetailsService);
+ }
+
+ @Bean
+ TokenBasedRememberMeServices oAuthRememberMeServices() {
+ CustomTokenBasedRememberMeServices rememberMeService = new CustomTokenBasedRememberMeServices(rememberKey,
+ customUserDetailsService);
+ rememberMeService.setAlwaysRemember(true);
+ return rememberMeService;
+ }
+
+ @Bean
+ PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ AuthProvider authProvider() {
+ return new AuthProvider();
+ }
+
+ @Bean
+ CorsFilter corsFilter() {
+ final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ final CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ config.setAllowedOriginPatterns(List.of("*"));
+ config.setAllowedHeaders(List.of("*"));
+ config.setAllowedMethods(List.of("*"));
+ source.registerCorsConfiguration("/**", config);
+ return new CorsFilter(source);
+ }
+
+ public CustomTokenBasedRememberMeServices getOAuthRememberMeServices() {
+ return (CustomTokenBasedRememberMeServices) oAuthRememberMeServices();
+ }
+
+}
diff --git a/src/main/java/com/nonononoki/alovoa/config/VaultHikariDataSource.java b/src/main/java/com/nonononoki/alovoa/config/VaultHikariDataSource.java
new file mode 100644
index 00000000..2b2d2a5d
--- /dev/null
+++ b/src/main/java/com/nonononoki/alovoa/config/VaultHikariDataSource.java
@@ -0,0 +1,27 @@
+package com.nonononoki.alovoa.config;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class VaultHikariDataSource extends HikariDataSource {
+ private static final Logger logger = LoggerFactory.getLogger(VaultHikariDataSource.class);
+
+ private final DatasourceCredentials datasourceCredentials = DatasourceCredentials.getInstance();
+
+ @Override
+ public String getPassword() {
+ return datasourceCredentials.getPassword();
+ }
+
+ @Override
+ public String getUsername() {
+ String username = datasourceCredentials.getUsername();
+ logger.debug(String.format("Using username %s", username));
+ return username;
+ }
+
+ public DatasourceCredentials getMysqlCredentials() {
+ return datasourceCredentials;
+ }
+}
diff --git a/src/main/java/com/nonononoki/alovoa/config/VaultPropertySource.java b/src/main/java/com/nonononoki/alovoa/config/VaultPropertySource.java
new file mode 100644
index 00000000..48e827a1
--- /dev/null
+++ b/src/main/java/com/nonononoki/alovoa/config/VaultPropertySource.java
@@ -0,0 +1,80 @@
+package com.nonononoki.alovoa.config;
+
+import com.nonononoki.alovoa.model.AlovoaException;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.PropertySource;
+import org.springframework.lang.Nullable;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/*
+Resources
+ https://cloud.spring.io/spring-cloud-vault/reference/html/
+ https://www.baeldung.com/properties-with-spring
+ */
+
+public class VaultPropertySource extends PropertySource {
+ private static DatasourceCredentials datasourceCredentials;
+ private static final Map cachedVaultData = new HashMap<>();
+ private static VaultPropertySource vaultPropertySource;
+ private static String vaultPropertiesPath;
+
+ private VaultPropertySource(String name, ConfigurableApplicationContext context)
+ throws AlovoaException {
+ super(name);
+ datasourceCredentials = DatasourceCredentials.getInstance();
+ ConfigurableEnvironment contextEnvironment = context.getEnvironment();
+ String vaultToken = contextEnvironment.getProperty("spring.cloud.vault.token");
+ vaultPropertiesPath = contextEnvironment.getProperty("app.vault.propertypath");
+ if (vaultPropertiesPath ==null) { vaultPropertiesPath = "alovoa/creds"; }
+ if (vaultToken == null) {
+ throw new AlovoaException(
+ "spring.cloud.vault.token not defined in " +
+ "application.properties or environment variable" +
+ "'vault.token' not set"
+ );
+ }
+
+ try {
+ URI vaultUrl = new URI(Objects.requireNonNull(contextEnvironment.getProperty("spring.cloud.vault.uri")));
+ datasourceCredentials.setVaultUrl(vaultUrl);
+ datasourceCredentials.setVaultToken(vaultToken);
+ } catch (URISyntaxException e) {
+ throw new AlovoaException(String.format("'spring.cloud.vault.uri' contains invalid URI: %s", e.getMessage()));
+ }
+ }
+
+ public static VaultPropertySource getInstance(ConfigurableApplicationContext context)
+ throws AlovoaException {
+ if (vaultPropertySource == null) {
+ vaultPropertySource = new VaultPropertySource("vault", context);
+ }
+ return vaultPropertySource;
+ }
+
+ @Override
+ public String getProperty(@Nullable String propertyName) {
+ logger.trace(String.format("getProperty: %s", propertyName));
+ if (cachedVaultData.containsKey(propertyName)) {
+ logger.trace(String.format("Returned cached value for %s", propertyName));
+ return cachedVaultData.get(propertyName);
+ }
+
+ String vaultValue = datasourceCredentials.getVaultCredentials(vaultPropertiesPath).get(propertyName);
+ cachedVaultData.put(propertyName, vaultValue);
+ if (vaultValue == null) {
+ logger.trace(String.format("Vault does not contain a value for %s", propertyName));
+ }
+ return vaultValue;
+ }
+
+ public void clearCachedVaultProperties() {
+ cachedVaultData.clear();
+ }
+
+}
diff --git a/src/main/java/com/nonononoki/alovoa/rest/Oauth2Controller.java b/src/main/java/com/nonononoki/alovoa/rest/Oauth2Controller.java
index c80c0bfe..6875ae19 100644
--- a/src/main/java/com/nonononoki/alovoa/rest/Oauth2Controller.java
+++ b/src/main/java/com/nonononoki/alovoa/rest/Oauth2Controller.java
@@ -1,21 +1,17 @@
package com.nonononoki.alovoa.rest;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Base64;
-import java.util.Map;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
+import com.nonononoki.alovoa.Tools;
+import com.nonononoki.alovoa.config.SecurityConfig;
+import com.nonononoki.alovoa.entity.User;
+import com.nonononoki.alovoa.html.LoginResource;
+import com.nonononoki.alovoa.html.RegisterResource;
+import com.nonononoki.alovoa.model.AlovoaException;
+import com.nonononoki.alovoa.repo.UserRepository;
+import com.nonononoki.alovoa.service.PublicService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -37,14 +33,10 @@
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
-import com.nonononoki.alovoa.Tools;
-import com.nonononoki.alovoa.config.SecurityConfig;
-import com.nonononoki.alovoa.entity.User;
-import com.nonononoki.alovoa.html.LoginResource;
-import com.nonononoki.alovoa.html.RegisterResource;
-import com.nonononoki.alovoa.model.AlovoaException;
-import com.nonononoki.alovoa.repo.UserRepository;
-import com.nonononoki.alovoa.service.PublicService;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Map;
@RestController
@RequestMapping("/")
@@ -86,23 +78,20 @@ public class Oauth2Controller {
private static final int REDIRECT_DEFAULT = 3;
private static final int HOUR_S = 3600;
- @GetMapping("/oauth2/authorization/google/{redirectUrlEncoded}")
- public ModelAndView oauth2Google(@PathVariable String redirectUrlEncoded) {
- httpSession.setAttribute(REDIRECT_URL, redirectUrlEncoded);
- return new ModelAndView(new RedirectView("/oauth2/authorization/google"));
- }
+ public static final String OAUTH_ROLE_ADMIN = "AlovoaAdmin";
- @GetMapping("/oauth2/authorization/facebook/{redirectUrlEncoded}")
- public ModelAndView oauth2Facebook(@PathVariable String redirectUrlEncoded) {
+ @GetMapping("/oauth2/authorization/{idp}/{redirectUrlEncoded}")
+ public ModelAndView oauth2general(
+ @PathVariable String idp,
+ @PathVariable String redirectUrlEncoded
+ ) {
httpSession.setAttribute(REDIRECT_URL, redirectUrlEncoded);
- return new ModelAndView(new RedirectView("/oauth2/authorization/facebook"));
+ return new ModelAndView(new RedirectView(String.format("/oauth2/authorization/%s", idp)));
}
@SuppressWarnings("rawtypes")
@GetMapping("/login/oauth2/success")
- public ModelAndView oauth2Success()
- throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException,
- NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, AlovoaException {
+ public ModelAndView oauth2Success() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
try {
@@ -155,10 +144,13 @@ public ModelAndView oauth2Success()
// administrator cannot use oauth for security reason e.g. password breach on
// oath provider
+ // ToDo: Not a problem on local IDP solution !!!
+ /*
if (user.isAdmin()) {
SecurityContextHolder.clearContext();
throw new AlovoaException("not_supported_for_admin");
}
+ */
if (!user.isConfirmed()) {
if (httpSession.getAttribute(REDIRECT_URL) != null) {
@@ -211,4 +203,5 @@ public void getRememberMeCookie(@PathVariable String rememberCookieValue, @PathV
private String getOauthParams(String username, String firstName, int page) {
return Tools.getAuthParams(securityConfig, httpSession.getId(), username, firstName, page);
}
+
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 39f8c22a..9467732c 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,8 +1,32 @@
#only used when not configured in run configuration, dev profile always recommended to save in IDE
-spring.profiles.active=test
+spring.profiles.active=test
+
+chat.enabled=true
+chat.type=internal
+#chat.type=matrix
+
+donate.enabled=false
+app.local.login.enabled=true
+
+##############################################################################
+spring.cloud.vault.uri=https://vault.test.felsing.net:8200
+# vault.token must be provided by system environment
+spring.cloud.vault.token=${vault.token}
+spring.cloud.vault.templatepath=database/creds/mysqlrole
+app.vault.propertypath=alovoa/creds
+#spring.cloud.vault.ssl.trust-store=classpath:truststore.jks
+#spring.cloud.vault.ssl.trust-store-password=changeit
+##############################################################################
+# cp /etc/pki/ca-trust/extracted/java/cacerts truststore.jks
+# keytool -keystore truststore.jks -alias vault-ca -import -file ca.crt -storepass changeit -noprompt
+##############################################################################
server.port=${PORT:8080}
+server.forward-headers-strategy=native
+server.tomcat.remote-ip-header=x-forwarded-for
+server.tomcat.protocol-header=x-forwarded-proto
+
#enable to force https for development
#server.ssl.key-store-type=PKCS12
#server.ssl.key-store=alovoa.p12
@@ -13,28 +37,29 @@ spring.main.allow-bean-definition-overriding=true
server.http2.enabled=true
-spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
-spring.datasource.url=jdbc:mariadb://localhost:3306/alovoa?createDatabaseIfNotExist=true&serverTimezone=UTC&useLegacyDatetimeCode=false
+#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
+#spring.datasource.url=jdbc:mariadb://mysql:3306/alovoa?createDatabaseIfNotExist=true&serverTimezone=UTC&useLegacyDatetimeCode=false
#spring.datasource.hikari.maximum-pool-size=10
#spring.datasource.hikari.connectionTimeout=30000
#spring.datasource.hikari.idleTimeout=600000
#spring.datasource.hikari.maxLifetime=1800000
#Insert your oauth2 ids and secrets here
-#spring.security.oauth2.client.registration.google.client-id=
-#spring.security.oauth2.client.registration.google.client-secret=
-#spring.security.oauth2.client.registration.facebook.client-id=
-#spring.security.oauth2.client.registration.facebook.client-secret=
-
-#we keep the default /login/oauth2/code/{registrationId} scheme
-
-spring.web.locale=en_EN
-
spring.security.oauth2.client.registration.google.scope[0]=email
spring.security.oauth2.client.registration.google.scope[1]=profile
+
spring.security.oauth2.client.registration.facebook.scope[0]=email
spring.security.oauth2.client.registration.facebook.scope[1]=public_profile
+spring.security.oauth2.client.provider.ip6li.issuer-uri=https://auth1.ip6.li/realms/matrix
+spring.security.oauth2.authorizationserver.endpoint.oidc.logout-uri=https://auth1.ip6.li/realms/matrix/protocol/openid-connect/logout?redirect_uri=https://alovoa.test.felsing.net
+spring.security.oauth2.client.registration.ip6li.scope[0]=openid
+spring.security.oauth2.client.registration.ip6li.scope[1]=email
+spring.security.oauth2.client.registration.ip6li.scope[2]=profile
+#we keep the default /login/oauth2/code/{registrationId} scheme
+
+spring.web.locale=en_EN
+
server.servlet.session.timeout=1h
server.servlet.session.cookie.max-age=1h
server.servlet.session.cookie.same-site=LAX
@@ -44,6 +69,7 @@ server.compression.min-response-size=10240
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDB103Dialect
spring.jpa.database-platform=org.hibernate.dialect.MariaDB103Dialect
+spring.jpa.open-in-view=true
spring.jpa.show-sql=false
spring.jpa.generate-ddl=true
spring.jpa.properties.hibernate.id.db_structure_naming_strategy=legacy
@@ -54,28 +80,20 @@ spring.jpa.properties.hibernate.plan_cache_max_size=16
spring.jpa.properties.hibernate.plan_parameter_metadata_max_size=16
spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true
-### KEYS AND CREDENTIALS ###
-#spring.datasource.password=
-#spring.datasource.username=
-#keys for transparent database encryption
-#app.text.key= #key must be 16, 24 or 32 bytes long
-#app.text.salt= #salt must be 16 bytes long
-#app.admin.email=
-#app.admin.key=
-#spring.mail.password=
-spring.mail.host=mail.gmx.net
-spring.mail.port=587
-spring.mail.username=nonononoki@gmx.de
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.test-connection=true
# logging
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
-logging.level.org.springframework.web=INFO
+logging.level.org.springframework.web=info
+logging.level.org.springframework.security=info
+logging.level.root=info
logging.level.org.hibernate.SQL=ERROR
logging.level.org.hibernate=ERROR
-logging.file.name=./logs/spring.log
+logging.level.org.springframework.vault=info
+logging.file.name=/logs/spring.log
+logging.level.javax.net.ssl=info
#caching
spring.cache.cache-names=bucket4j
@@ -158,7 +176,7 @@ bucket4j.filters[8].rate-limits[0].bandwidths[0].unit=seconds
bucket4j.filters[9].cache-name=bucket4j
bucket4j.filters[9].url=/user/update/profile-picture
-bucket4j.filters[9].metrics.tags[0].key=IP
+bucket4j.filters[9].metrics.tags[0].key=IPdataSource
bucket4j.filters[9].metrics.tags[0].expression=getRemoteAddr()
bucket4j.filters[9].rate-limits[0].bandwidths[0].capacity=5
bucket4j.filters[9].rate-limits[0].bandwidths[0].time=60
@@ -190,7 +208,7 @@ bucket4j.filters[12].rate-limits[0].bandwidths[0].unit=seconds
bucket4j.filters[13].cache-name=bucket4j
bucket4j.filters[13].url=/profile/view/*
-bucket4j.filters[13].metrics.tags[0].key=IP
+bucket4j.filters[13].metrics.tags[0].key=IPdataSource
bucket4j.filters[13].metrics.tags[0].expression=getRemoteAddr()
bucket4j.filters[13].rate-limits[0].bandwidths[0].capacity=5
bucket4j.filters[13].rate-limits[0].bandwidths[0].time=10
@@ -252,13 +270,13 @@ bucket4j.filters[20].rate-limits[0].bandwidths[0].capacity=10
bucket4j.filters[20].rate-limits[0].bandwidths[0].time=10
bucket4j.filters[20].rate-limits[0].bandwidths[0].unit=seconds
-
# custom values
app.company.name=Alovoa
app.privacy.update-date=2021-06-16
app.tos.update-date=2021-09-22
app.name=Alovoa
-app.domain=http://localhost:8080
+app.domain=https://alovoa.test.felsing.net
+app.captcha.enabled=false
#secret in url to verify that the transaction is legit, change in production!
app.donate.kofi.key=kofi123
@@ -289,11 +307,6 @@ app.donate.users.max=100
app.vapid.max=10
app.referral.max=20
-#test values only, use proper keys for production!
-app.login.remember.key=CWjuiidCW23KnRWUnzVpcpE4pVFcVCC7pi5X9htZVxDPp5ztWDrj2ePahFXQQeTx
-app.vapid.public=BEfuzSVrY3s8fD1MpytGBncV-S_Wv4KdOvomVaiwpomu3sJwIkdI5ivmrs3D4RVTApxytQnY3761XlQGj-jv6XA
-app.vapid.private=QibUiZ7evckF5kC2S6ppG8EJQhWPnyel9DfQEs-k41I
-
app.intention.delay=86400000
app.interest.max=5
app.interest.min-chars=3
@@ -320,3 +333,4 @@ app.schedule.delay.captcha=300000
app.schedule.delay.hide=9720000000
#90 days
app.schedule.delay.contact=9720000000
+
diff --git a/src/main/resources/application.properties.ORIG b/src/main/resources/application.properties.ORIG
new file mode 100644
index 00000000..39f8c22a
--- /dev/null
+++ b/src/main/resources/application.properties.ORIG
@@ -0,0 +1,322 @@
+#only used when not configured in run configuration, dev profile always recommended to save in IDE
+spring.profiles.active=test
+
+server.port=${PORT:8080}
+
+#enable to force https for development
+#server.ssl.key-store-type=PKCS12
+#server.ssl.key-store=alovoa.p12
+#server.ssl.key-store-password=alovoa
+#server.ssl.key-alias=alovoa
+
+spring.main.allow-bean-definition-overriding=true
+
+server.http2.enabled=true
+
+spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
+spring.datasource.url=jdbc:mariadb://localhost:3306/alovoa?createDatabaseIfNotExist=true&serverTimezone=UTC&useLegacyDatetimeCode=false
+#spring.datasource.hikari.maximum-pool-size=10
+#spring.datasource.hikari.connectionTimeout=30000
+#spring.datasource.hikari.idleTimeout=600000
+#spring.datasource.hikari.maxLifetime=1800000
+
+#Insert your oauth2 ids and secrets here
+#spring.security.oauth2.client.registration.google.client-id=
+#spring.security.oauth2.client.registration.google.client-secret=
+#spring.security.oauth2.client.registration.facebook.client-id=
+#spring.security.oauth2.client.registration.facebook.client-secret=
+
+#we keep the default /login/oauth2/code/{registrationId} scheme
+
+spring.web.locale=en_EN
+
+spring.security.oauth2.client.registration.google.scope[0]=email
+spring.security.oauth2.client.registration.google.scope[1]=profile
+spring.security.oauth2.client.registration.facebook.scope[0]=email
+spring.security.oauth2.client.registration.facebook.scope[1]=public_profile
+
+server.servlet.session.timeout=1h
+server.servlet.session.cookie.max-age=1h
+server.servlet.session.cookie.same-site=LAX
+server.compression.enabled=true
+server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml
+server.compression.min-response-size=10240
+
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDB103Dialect
+spring.jpa.database-platform=org.hibernate.dialect.MariaDB103Dialect
+spring.jpa.show-sql=false
+spring.jpa.generate-ddl=true
+spring.jpa.properties.hibernate.id.db_structure_naming_strategy=legacy
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.properties.hibernate.jdbc.time_zone=UTC
+spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
+spring.jpa.properties.hibernate.plan_cache_max_size=16
+spring.jpa.properties.hibernate.plan_parameter_metadata_max_size=16
+spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true
+
+### KEYS AND CREDENTIALS ###
+#spring.datasource.password=
+#spring.datasource.username=
+#keys for transparent database encryption
+#app.text.key= #key must be 16, 24 or 32 bytes long
+#app.text.salt= #salt must be 16 bytes long
+#app.admin.email=
+#app.admin.key=
+#spring.mail.password=
+spring.mail.host=mail.gmx.net
+spring.mail.port=587
+spring.mail.username=nonononoki@gmx.de
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.starttls.enable=true
+spring.mail.test-connection=true
+
+# logging
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
+logging.level.org.springframework.web=INFO
+logging.level.org.hibernate.SQL=ERROR
+logging.level.org.hibernate=ERROR
+logging.file.name=./logs/spring.log
+
+#caching
+spring.cache.cache-names=bucket4j
+spring.cache.caffeine.spec=maximumSize=1000000,expireAfterAccess=1d
+
+#bucket4j
+bucket4j.enabled=true
+
+bucket4j.filters[0].cache-name=bucket4j
+bucket4j.filters[0].url=.*
+bucket4j.filters[0].metrics.tags[0].key=IP
+bucket4j.filters[0].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[0].rate-limits[0].bandwidths[0].capacity=200
+bucket4j.filters[0].rate-limits[0].bandwidths[0].time=6
+bucket4j.filters[0].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[1].cache-name=bucket4j
+bucket4j.filters[1].url=/login
+bucket4j.filters[1].metrics.tags[0].key=IP
+bucket4j.filters[1].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[1].rate-limits[0].bandwidths[0].capacity=20
+bucket4j.filters[1].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[1].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[2].cache-name=bucket4j
+bucket4j.filters[2].url=/delete-account-confirm
+bucket4j.filters[2].metrics.tags[0].key=IP
+bucket4j.filters[2].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[2].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[2].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[2].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[3].cache-name=bucket4j
+bucket4j.filters[3].url=/password/reset
+bucket4j.filters[3].metrics.tags[0].key=IP
+bucket4j.filters[3].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[3].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[3].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[3].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[4].cache-name=bucket4j
+bucket4j.filters[4].url=/password/change
+bucket4j.filters[4].metrics.tags[0].key=IP
+bucket4j.filters[4].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[4].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[4].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[4].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[5].cache-name=bucket4j
+bucket4j.filters[5].url=/user/like/*
+bucket4j.filters[5].metrics.tags[0].key=IP
+bucket4j.filters[5].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[5].rate-limits[0].bandwidths[0].capacity=10
+bucket4j.filters[5].rate-limits[0].bandwidths[0].time=10
+bucket4j.filters[5].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[6].cache-name=bucket4j
+bucket4j.filters[6].url=/search/users/*
+bucket4j.filters[6].metrics.tags[0].key=IP
+bucket4j.filters[6].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[6].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[6].rate-limits[0].bandwidths[0].time=30
+bucket4j.filters[6].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[7].cache-name=bucket4j
+bucket4j.filters[7].url=/register
+bucket4j.filters[7].metrics.tags[0].key=IP
+bucket4j.filters[7].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[7].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[7].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[7].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[8].cache-name=bucket4j
+bucket4j.filters[8].url=/user/delete-account-confirm
+bucket4j.filters[8].metrics.tags[0].key=IP
+bucket4j.filters[8].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[8].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[8].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[8].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[9].cache-name=bucket4j
+bucket4j.filters[9].url=/user/update/profile-picture
+bucket4j.filters[9].metrics.tags[0].key=IP
+bucket4j.filters[9].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[9].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[9].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[9].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[10].cache-name=bucket4j
+bucket4j.filters[10].url=/user/update/audio*
+bucket4j.filters[10].metrics.tags[0].key=IP
+bucket4j.filters[10].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[10].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[10].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[10].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[11].cache-name=bucket4j
+bucket4j.filters[11].url=/user/image/add
+bucket4j.filters[11].metrics.tags[0].key=IP
+bucket4j.filters[11].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[11].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[11].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[11].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[12].cache-name=bucket4j
+bucket4j.filters[12].url=/user/report
+bucket4j.filters[12].metrics.tags[0].key=IP
+bucket4j.filters[12].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[12].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[12].rate-limits[0].bandwidths[0].time=30
+bucket4j.filters[12].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[13].cache-name=bucket4j
+bucket4j.filters[13].url=/profile/view/*
+bucket4j.filters[13].metrics.tags[0].key=IP
+bucket4j.filters[13].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[13].rate-limits[0].bandwidths[0].capacity=5
+bucket4j.filters[13].rate-limits[0].bandwidths[0].time=10
+bucket4j.filters[13].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[14].cache-name=bucket4j
+bucket4j.filters[14].url=/captcha/generate
+bucket4j.filters[14].metrics.tags[0].key=IP
+bucket4j.filters[14].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[14].rate-limits[0].bandwidths[0].capacity=10
+bucket4j.filters[14].rate-limits[0].bandwidths[0].time=60
+bucket4j.filters[14].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[15].cache-name=bucket4j
+bucket4j.filters[15].url=/info
+bucket4j.filters[15].metrics.tags[0].key=IP
+bucket4j.filters[15].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[15].rate-limits[0].bandwidths[0].capacity=2
+bucket4j.filters[15].rate-limits[0].bandwidths[0].time=20
+bucket4j.filters[15].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[16].cache-name=bucket4j
+bucket4j.filters[16].url=/user/*
+bucket4j.filters[16].metrics.tags[0].key=IP
+bucket4j.filters[16].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[16].rate-limits[0].bandwidths[0].capacity=10
+bucket4j.filters[16].rate-limits[0].bandwidths[0].time=20
+bucket4j.filters[16].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[17].cache-name=bucket4j
+bucket4j.filters[17].url=/user/liked-users
+bucket4j.filters[17].metrics.tags[0].key=IP
+bucket4j.filters[17].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[17].rate-limits[0].bandwidths[0].capacity=2
+bucket4j.filters[17].rate-limits[0].bandwidths[0].time=120
+bucket4j.filters[17].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[18].cache-name=bucket4j
+bucket4j.filters[18].url=/user/disliked-users
+bucket4j.filters[18].metrics.tags[0].key=IP
+bucket4j.filters[18].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[18].rate-limits[0].bandwidths[0].capacity=2
+bucket4j.filters[18].rate-limits[0].bandwidths[0].time=120
+bucket4j.filters[18].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[19].cache-name=bucket4j
+bucket4j.filters[19].url=/user/userdata/*
+bucket4j.filters[19].metrics.tags[0].key=IP
+bucket4j.filters[19].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[19].rate-limits[0].bandwidths[0].capacity=2
+bucket4j.filters[19].rate-limits[0].bandwidths[0].time=1200
+bucket4j.filters[19].rate-limits[0].bandwidths[0].unit=seconds
+
+bucket4j.filters[20].cache-name=bucket4j
+bucket4j.filters[20].url=/profile/*
+bucket4j.filters[20].metrics.tags[0].key=IP
+bucket4j.filters[20].metrics.tags[0].expression=getRemoteAddr()
+bucket4j.filters[20].rate-limits[0].bandwidths[0].capacity=10
+bucket4j.filters[20].rate-limits[0].bandwidths[0].time=10
+bucket4j.filters[20].rate-limits[0].bandwidths[0].unit=seconds
+
+
+# custom values
+app.company.name=Alovoa
+app.privacy.update-date=2021-06-16
+app.tos.update-date=2021-09-22
+app.name=Alovoa
+app.domain=http://localhost:8080
+
+#secret in url to verify that the transaction is legit, change in production!
+app.donate.kofi.key=kofi123
+app.donate.bmac.key=bmac123
+
+app.mail.plus-addressing=false
+app.first-name.length-min=2
+app.first-name.length-max=10
+app.audio.max-time=10
+app.media.max-size=2097152
+app.captcha.length=4
+app.password-token.length=32
+app.age.min=16
+app.age.max=99
+app.age.range=10
+#7 days
+app.donation.popup.time=604800000
+app.token.length=30
+app.profile.image.length=600
+app.profile.image.max=4
+app.profile.description.size=255
+app.search.max=20
+app.search.max.distance=100
+app.search.estimate.max=200
+app.message.size=255
+app.conversation.messages-max=50
+app.donate.users.max=100
+app.vapid.max=10
+app.referral.max=20
+
+#test values only, use proper keys for production!
+app.login.remember.key=CWjuiidCW23KnRWUnzVpcpE4pVFcVCC7pi5X9htZVxDPp5ztWDrj2ePahFXQQeTx
+app.vapid.public=BEfuzSVrY3s8fD1MpytGBncV-S_Wv4KdOvomVaiwpomu3sJwIkdI5ivmrs3D4RVTApxytQnY3761XlQGj-jv6XA
+app.vapid.private=QibUiZ7evckF5kC2S6ppG8EJQhWPnyel9DfQEs-k41I
+
+app.intention.delay=86400000
+app.interest.max=5
+app.interest.min-chars=3
+app.interest.max-chars=30
+app.interest.autocomplete.max=5
+app.donation.modulus=10
+app.url.auth.success=/
+app.url.auth.failure=/login?error
+
+#24 hours
+app.user.delete.duration.valid=86400000
+#5 minutes
+app.user.password-reset.duration.valid=300000
+
+#schedules
+app.schedule.enabled=true
+#30 seconds
+app.schedule.short=30000
+#1 hour
+app.schedule.long=3600000
+#5 minutes
+app.schedule.delay.captcha=300000
+#90 days
+app.schedule.delay.hide=9720000000
+#90 days
+app.schedule.delay.contact=9720000000