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