diff --git a/build-all.sh b/build-all.sh index 0009f7d..6fb490c 100644 --- a/build-all.sh +++ b/build-all.sh @@ -52,6 +52,7 @@ build_gradle_module "spring-boot/startup" build_gradle_module "spring-boot/static" build_gradle_module "spring-boot/validation" build_gradle_module "spring-boot/profiles" +build_gradle_module "spring-boot/password-encoding" build_gradle_module "spring-cloud/feign-with-spring-data-rest" build_gradle_module "spring-cloud/sleuth-downstream-service" build_gradle_module "spring-cloud/sleuth-upstream-service" diff --git a/spring-boot/password-encoding/.gitignore b/spring-boot/password-encoding/.gitignore new file mode 100644 index 0000000..6c01878 --- /dev/null +++ b/spring-boot/password-encoding/.gitignore @@ -0,0 +1,32 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/spring-boot/password-encoding/build.gradle b/spring-boot/password-encoding/build.gradle new file mode 100644 index 0000000..3a1e180 --- /dev/null +++ b/spring-boot/password-encoding/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'org.springframework.boot' version '2.2.2.RELEASE' + id 'io.spring.dependency-management' version '1.0.8.RELEASE' + id 'java' +} + +group = 'io.reflectoring' +version = '0.0.1-SNAPSHOT' +description = 'password-encoding-spring-boot' +sourceCompatibility = '11' + + +repositories { + mavenCentral() +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-security:2.2.4.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-web:2.2.4.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.2.4.RELEASE' + implementation 'com.h2database:h2:1.4.200' + implementation 'org.flywaydb:flyway-core:6.0.8' + implementation 'org.bouncycastle:bcprov-jdk15on:1.64' + implementation 'org.projectlombok:lombok:1.18.12' + annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + implementation 'com.google.guava:guava:28.2-jre' + testImplementation 'org.springframework.boot:spring-boot-starter-test:2.2.4.RELEASE' + testImplementation 'org.springframework.security:spring-security-test:5.2.1.RELEASE' +} + + +test { + useJUnitPlatform() +} diff --git a/spring-boot/password-encoding/gradle/wrapper/gradle-wrapper.jar b/spring-boot/password-encoding/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..cc4fdc2 Binary files /dev/null and b/spring-boot/password-encoding/gradle/wrapper/gradle-wrapper.jar differ diff --git a/spring-boot/password-encoding/gradle/wrapper/gradle-wrapper.properties b/spring-boot/password-encoding/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9492014 --- /dev/null +++ b/spring-boot/password-encoding/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/spring-boot/password-encoding/gradlew b/spring-boot/password-encoding/gradlew new file mode 100644 index 0000000..2fe81a7 --- /dev/null +++ b/spring-boot/password-encoding/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/spring-boot/password-encoding/gradlew.bat b/spring-boot/password-encoding/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/spring-boot/password-encoding/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/PasswordEncodingSpringBootApplication.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/PasswordEncodingSpringBootApplication.java new file mode 100644 index 0000000..5a23e82 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/PasswordEncodingSpringBootApplication.java @@ -0,0 +1,13 @@ +package io.reflectoring.passwordencoding; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PasswordEncodingSpringBootApplication { + + public static void main(String[] args) { + SpringApplication.run(PasswordEncodingSpringBootApplication.class, args); + } + +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/JdbcUserDetailPasswordService.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/JdbcUserDetailPasswordService.java new file mode 100644 index 0000000..d150a1f --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/JdbcUserDetailPasswordService.java @@ -0,0 +1,28 @@ +package io.reflectoring.passwordencoding.authentication; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@Service +public class JdbcUserDetailPasswordService implements UserDetailsPasswordService { + + private final UserRepository userRepository; + + private final UserDetailsMapper userDetailsMapper; + + public JdbcUserDetailPasswordService(UserRepository userRepository, UserDetailsMapper userDetailsMapper) { + this.userRepository = userRepository; + this.userDetailsMapper = userDetailsMapper; + } + + + @Override + public UserDetails updatePassword(UserDetails user, String newPassword) { + UserCredentials userCredentials = userRepository.findByUsername(user.getUsername()); + userCredentials.setPassword(newPassword); + return userDetailsMapper.toUserDetails(userCredentials); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/JdbcUserDetailsService.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/JdbcUserDetailsService.java new file mode 100644 index 0000000..a9beaa2 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/JdbcUserDetailsService.java @@ -0,0 +1,28 @@ +package io.reflectoring.passwordencoding.authentication; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class JdbcUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + private final UserDetailsMapper userDetailsMapper; + + public JdbcUserDetailsService(UserRepository userRepository, UserDetailsMapper userDetailsMapper) { + this.userRepository = userRepository; + this.userDetailsMapper = userDetailsMapper; + } + + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserCredentials userCredentials = userRepository.findByUsername(username); + return userDetailsMapper.toUserDetails(userCredentials); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserCredentials.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserCredentials.java new file mode 100644 index 0000000..48b48b1 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserCredentials.java @@ -0,0 +1,31 @@ +package io.reflectoring.passwordencoding.authentication; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.util.Set; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "users") +public class UserCredentials { + + @Id + private String username; + + private String password; + + boolean enabled; + + @ElementCollection + @JoinTable(name = "authorities", joinColumns = {@JoinColumn(name = "username")}) + @Column(name = "authority") + private Set roles; +} + diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserDetailsMapper.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserDetailsMapper.java new file mode 100644 index 0000000..de6085d --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserDetailsMapper.java @@ -0,0 +1,18 @@ +package io.reflectoring.passwordencoding.authentication; + +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +@Component +public class UserDetailsMapper { + + UserDetails toUserDetails(UserCredentials userCredentials) { + + return User + .withUsername(userCredentials.getUsername()) + .password(userCredentials.getPassword()) + .roles(userCredentials.getRoles().toArray(String[]::new)) + .build(); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserRepository.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserRepository.java new file mode 100644 index 0000000..61b67d7 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserRepository.java @@ -0,0 +1,10 @@ +package io.reflectoring.passwordencoding.authentication; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public interface UserRepository extends JpaRepository { + + UserCredentials findByUsername(String username); +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/configuration/SecurityConfiguration.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..f32704a --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/configuration/SecurityConfiguration.java @@ -0,0 +1,101 @@ +package io.reflectoring.passwordencoding.configuration; + +import io.reflectoring.passwordencoding.authentication.JdbcUserDetailPasswordService; +import io.reflectoring.passwordencoding.authentication.JdbcUserDetailsService; +import io.reflectoring.passwordencoding.authentication.UserDetailsMapper; +import io.reflectoring.passwordencoding.authentication.UserRepository; +import io.reflectoring.passwordencoding.workfactor.BcCryptWorkFactorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.DelegatingPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; +import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + + private final UserRepository userRepository; + private final UserDetailsMapper userDetailsMapper; + private final BcCryptWorkFactorService bcCryptWorkFactorService; + + public SecurityConfiguration(UserRepository userRepository, UserDetailsMapper userDetailsMapper, BcCryptWorkFactorService bcCryptWorkFactorService) { + this.userRepository = userRepository; + this.userDetailsMapper = userDetailsMapper; + this.bcCryptWorkFactorService = bcCryptWorkFactorService; + } + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity + .csrf().disable() + .authorizeRequests() + .antMatchers("/registration").permitAll() + .anyRequest().authenticated() + .and() + .httpBasic(); + + httpSecurity.headers().frameOptions().disable(); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .authenticationProvider(daoAuthenticationProvider()) + .eraseCredentials(false); + } + + + @Bean + public PasswordEncoder passwordEncoder() { + // we must user deprecated encoder to support their encoding + String encodingId = "bcrypt"; + Map encoders = new HashMap<>(); + encoders.put(encodingId, new BCryptPasswordEncoder(bcCryptWorkFactorService.calculateStrength())); + encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); + encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); + encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); + encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); + encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); + encoders.put("scrypt", new SCryptPasswordEncoder()); + encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); + encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256")); + encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder()); + encoders.put("argon2", new Argon2PasswordEncoder()); + + return new DelegatingPasswordEncoder(encodingId, encoders); + } + + @Bean + public UserDetailsPasswordService userDetailsPasswordService() { + return new JdbcUserDetailPasswordService(userRepository, userDetailsMapper); + } + + public UserDetailsService userDetailsService() { + return new JdbcUserDetailsService(userRepository, userDetailsMapper); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); + daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); + daoAuthenticationProvider.setUserDetailsPasswordService(userDetailsPasswordService()); + daoAuthenticationProvider.setUserDetailsService(userDetailsService()); + return daoAuthenticationProvider; + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/migration/PasswordMigration.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/migration/PasswordMigration.java new file mode 100644 index 0000000..1ab6f8a --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/migration/PasswordMigration.java @@ -0,0 +1,33 @@ +package io.reflectoring.passwordencoding.migration; + +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsPasswordService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class PasswordMigration { + + @Bean + public ApplicationListener authenticationSuccessListener( + PasswordEncoder encoder, + UserDetailsPasswordService userDetailsPasswordService) { + return (AuthenticationSuccessEvent event) -> { + Authentication authentication = event.getAuthentication(); + User user = (User) authentication.getPrincipal(); + String encodedPassword = user.getPassword(); + if (encodedPassword.startsWith("{SHA-1}")) { + CharSequence clearTextPassword = (CharSequence) authentication.getCredentials(); + String newPassword = encoder.encode(clearTextPassword); + userDetailsPasswordService.updatePassword(user, newPassword); + } + ((UsernamePasswordAuthenticationToken) authentication).eraseCredentials(); + }; + } + +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/Car.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/Car.java new file mode 100644 index 0000000..98eacfd --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/Car.java @@ -0,0 +1,16 @@ +package io.reflectoring.passwordencoding.resources; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +@Builder +@AllArgsConstructor +public class Car { + + private String name; + private String color; +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/CarResources.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/CarResources.java new file mode 100644 index 0000000..db68b8d --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/CarResources.java @@ -0,0 +1,26 @@ +package io.reflectoring.passwordencoding.resources; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Set; + +@RestController +public class CarResources { + + // we use this endpoint as authentication test + @GetMapping("/cars") + public Set cars() { + return Set.of( + Car.builder() + .name("vw") + .color("black") + .build(), + Car.builder() + .name("bmw") + .color("white") + .build() + ); + } +} + diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/UserCredentialsDto.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/UserCredentialsDto.java new file mode 100644 index 0000000..e38695f --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/UserCredentialsDto.java @@ -0,0 +1,16 @@ +package io.reflectoring.passwordencoding.resources; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class UserCredentialsDto { + + private String username; + private String password; +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/UserResources.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/UserResources.java new file mode 100644 index 0000000..62b6a16 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/UserResources.java @@ -0,0 +1,39 @@ +package io.reflectoring.passwordencoding.resources; + +import io.reflectoring.passwordencoding.authentication.UserCredentials; +import io.reflectoring.passwordencoding.authentication.UserRepository; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Set; + +@RestController +@Transactional +public class UserResources { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public UserResources(UserRepository userRepository, PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + + @PostMapping("/registration") + @ResponseStatus(code = HttpStatus.CREATED) + public void register(@RequestBody UserCredentialsDto userCredentialsDto) { + UserCredentials user = UserCredentials.builder() + .enabled(true) + .username(userCredentialsDto.getUsername()) + .password(passwordEncoder.encode(userCredentialsDto.getPassword())) + .roles(Set.of("USER")) + .build(); + userRepository.save(user); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/BcCryptWorkFactorService.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/BcCryptWorkFactorService.java new file mode 100644 index 0000000..688b358 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/BcCryptWorkFactorService.java @@ -0,0 +1,92 @@ +package io.reflectoring.passwordencoding.workfactor; + +import com.google.common.base.Stopwatch; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class BcCryptWorkFactorService { + + private static final String TEST_PASSWORD = "my password"; + private static final int GOAL_MILLISECONDS_PER_PASSWORD = 1000; + private static final int MIN_STRENGTH = 4; + private static final int MAX_STRENGTH = 31; + + + /** + * Calculates the strength (a.k.a. log rounds) for the BCrypt Algorithm, so that password encoding takes about 1s. + * This method iterates over strength from 4 to 31 and calculates the duration of password encoding for every value of strength. + * It returns the first strength, that takes more than 500s + */ + public int calculateStrength() { + for (int strength = MIN_STRENGTH; strength <= MAX_STRENGTH; strength++) { + + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength); + + Stopwatch stopwatch = Stopwatch.createStarted(); + bCryptPasswordEncoder.encode(TEST_PASSWORD); + stopwatch.stop(); + + long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS); + if (duration >= GOAL_MILLISECONDS_PER_PASSWORD) { + return strength; + } + } + throw new RuntimeException(String.format("Could not find suitable round number for bcrypt encoding. The encoding with %d rounds" + + " takes less than %d ms.", MAX_STRENGTH, GOAL_MILLISECONDS_PER_PASSWORD)); + } + + /** + * Calculates the strength (a.k.a. log rounds) for the BCrypt Algorithm, so that password encoding takes about 1s. + * This method iterate over strength from 4 to 31 and calculates the duration of password encoding for every value of strength. + * When the the duration takes more than 1s, it is compared to previous one and the method returns the strength, tha is closer + * to 1s. + */ + public int calculateStrengthClosestToTimeGoal() { + + long previousDuration = Long.MIN_VALUE; + for (int strength = MIN_STRENGTH; strength <= MAX_STRENGTH; strength++) { + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength); + + Stopwatch stopwatch = Stopwatch.createStarted(); + bCryptPasswordEncoder.encode(TEST_PASSWORD); + stopwatch.stop(); + long currentDuration = stopwatch.elapsed(TimeUnit.MILLISECONDS); + + if (isGreaterThanGoal(currentDuration)) { + return getStrength(previousDuration, currentDuration, strength); + } + previousDuration = currentDuration; + } + throw new RuntimeException(String.format("Could not find suitable round number for bcrypt encoding. The encoding with %d rounds" + + " takes less than %d ms.", MAX_STRENGTH, GOAL_MILLISECONDS_PER_PASSWORD)); + } + + /** + * @param previousDuration duration from previous iteration + * @param currentDuration duration of current iteration + * @param strength current strength + * @return return the current strength, if current duration is closer to GOAL_MILLISECONDS_PER_PASSWORD, otherwise + * current strength-1. + */ + int getStrength(long previousDuration, long currentDuration, int strength) { + if (isPreviousDurationCloserToGoal(previousDuration, currentDuration)) { + return strength - 1; + } else { + return strength; + } + } + + private boolean isGreaterThanGoal(long duration) { + return duration > GOAL_MILLISECONDS_PER_PASSWORD; + } + + /** + * return true, if previousDuration is closer to the goal than currentDuration, false otherwise. + */ + boolean isPreviousDurationCloserToGoal(long previousDuration, long currentDuration) { + return Math.abs(GOAL_MILLISECONDS_PER_PASSWORD - previousDuration) < Math.abs(GOAL_MILLISECONDS_PER_PASSWORD - currentDuration); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/Pbkdf2WorkFactorService.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/Pbkdf2WorkFactorService.java new file mode 100644 index 0000000..3bf734f --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/Pbkdf2WorkFactorService.java @@ -0,0 +1,39 @@ +package io.reflectoring.passwordencoding.workfactor; + +import com.google.common.base.Stopwatch; +import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class Pbkdf2WorkFactorService { + + private static final String TEST_PASSWORD = "my password"; + private static final String NO_ADDITIONAL_SECRET = ""; + private static final int GOAL_MILLISECONDS_PER_PASSWORD = 1000; + private static final int HASH_WIDTH = 256; + private static final int ITERATION_STEP = 5000; + + /** + * Finds the number of Iteration for the {@link Pbkdf2PasswordEncoder} to get the duration of password encoding + * close to 1s. The Calculation does not use any secret (pepper) and applies hash algorithm SHA256. + */ + public int calculateIteration() { + + int iterationNumber = 150000; + while (true) { + Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder(NO_ADDITIONAL_SECRET, iterationNumber, HASH_WIDTH); + + Stopwatch stopwatch = Stopwatch.createStarted(); + pbkdf2PasswordEncoder.encode(TEST_PASSWORD); + stopwatch.stop(); + + long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS); + if (duration > GOAL_MILLISECONDS_PER_PASSWORD) { + return iterationNumber; + } + iterationNumber += ITERATION_STEP; + } + } +} diff --git a/spring-boot/password-encoding/src/main/resources/application.yaml b/spring-boot/password-encoding/src/main/resources/application.yaml new file mode 100644 index 0000000..3d20cdb --- /dev/null +++ b/spring-boot/password-encoding/src/main/resources/application.yaml @@ -0,0 +1,11 @@ +spring: + h2: + console: + enabled: true + datasource: + url: jdbc:h2:mem:testdb + driverClassName: org.h2.Driver + username: sa + password: + jpa: + database-platform: org.hibernate.dialect.H2Dialect diff --git a/spring-boot/password-encoding/src/main/resources/db/migration/V1_1__create_user_tables.sql b/spring-boot/password-encoding/src/main/resources/db/migration/V1_1__create_user_tables.sql new file mode 100644 index 0000000..e7ace3a --- /dev/null +++ b/spring-boot/password-encoding/src/main/resources/db/migration/V1_1__create_user_tables.sql @@ -0,0 +1,14 @@ +create table users +( + username varchar_ignorecase(50) not null primary key, + password varchar(2048) not null, + enabled boolean not null +); + +create table authorities +( + username varchar_ignorecase(50) not null, + authority varchar_ignorecase(50) not null, + constraint fk_authorities_users foreign key (username) references users (username) +); +create unique index ix_auth_username on authorities (username, authority); \ No newline at end of file diff --git a/spring-boot/password-encoding/src/main/resources/db/migration/V1_2__add user.sql b/spring-boot/password-encoding/src/main/resources/db/migration/V1_2__add user.sql new file mode 100644 index 0000000..6eeef98 --- /dev/null +++ b/spring-boot/password-encoding/src/main/resources/db/migration/V1_2__add user.sql @@ -0,0 +1,5 @@ +insert into users (username, password, enabled) VALUES ('admin', '{bcrypt}$2a$10$4V9kA793Pi2xf94dYFgKWuw8ukyETxWb7tZ4/mfco9sWkwvBQndxW', true); +insert into users (username, password, enabled) VALUES ('user', '{SHA-256}{4Cc0+yDMHnTUy+zOHeMH7yaPhxvlJT//tQTwEhyegiQ=}446d06130bfc254527a7bbd95b50595a977c0058110f8dccb54bd273d99325b8', true); +insert into users (username, password, enabled) VALUES ('user with working factor 5', '{bcrypt}$2a$05$Zz4rToG8YXKMbuAPgm3qj.HpTFsGEdZHhCf9ikIHAoI5elX7ajNm.', true); +insert into users (username, password, enabled) VALUES ('user with sha1 encoding', '{SHA-1}{6tND0AZfFH3aE1VDg7QkWT6DzFg/NUHtukntgwu8JV4=}804c6e8efebf4e91f88e3baf9fd383e28a21378c', true); +insert into users (username, password, enabled) VALUES ('scrypt user', '{scrypt}$e0801$fUx3MxN07zdH3UyARJqOwv3WiWCvE7f6qRm9A5KQfNo5ovSwxMHknQ4vERO4csj/I3imG2HJQg1HHp7Rqzbp7g==$Fm5F9PSoE/jBYLOmnCJcvX1Euf952r5b3BjAl+SwQMs=', true); \ No newline at end of file diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/authentication/UserDetailsMapperTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/authentication/UserDetailsMapperTest.java new file mode 100644 index 0000000..666ab15 --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/authentication/UserDetailsMapperTest.java @@ -0,0 +1,32 @@ +package io.reflectoring.passwordencoding.authentication; + +import org.junit.jupiter.api.Test; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class UserDetailsMapperTest { + + private UserDetailsMapper userDetailsMapper = new UserDetailsMapper(); + + @Test + void toUserDetails() { + // given + UserCredentials userCredentials = UserCredentials.builder() + .enabled(true) + .password("password") + .username("user") + .roles(Set.of("USER", "ADMIN")) + .build(); + + // when + UserDetails userDetails = userDetailsMapper.toUserDetails(userCredentials); + + // then + assertThat(userDetails.getUsername()).isEqualTo("user"); + assertThat(userDetails.getPassword()).isEqualTo("password"); + assertThat(userDetails.isEnabled()).isTrue(); + } +} \ No newline at end of file diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/authentication/UserRepositoryTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/authentication/UserRepositoryTest.java new file mode 100644 index 0000000..4d2bc2a --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/authentication/UserRepositoryTest.java @@ -0,0 +1,26 @@ +package io.reflectoring.passwordencoding.authentication; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@DataJpaTest +class UserRepositoryTest { + + @Autowired + private UserRepository userRepository; + + @Test + void findUserByUsername() { + // given + String username = "user"; + + // when + UserCredentials userCredentials = userRepository.findByUsername(username); + + // then + assertThat(userCredentials).isNotNull(); + } +} \ No newline at end of file diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/resources/CarResourcesTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/resources/CarResourcesTest.java new file mode 100644 index 0000000..99a734f --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/resources/CarResourcesTest.java @@ -0,0 +1,94 @@ +package io.reflectoring.passwordencoding.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.reflectoring.passwordencoding.authentication.UserCredentials; +import io.reflectoring.passwordencoding.authentication.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class CarResourcesTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserRepository userRepository; + + @Test + void getCarsShouldReturnUnauthorizedIfTheRequestHasNoBasicAuthentication() throws Exception { + mockMvc.perform(get("/cars")) + .andExpect(status().isUnauthorized()); + } + + @Test + void getCarsShouldReturnCarsForTheAuthenticatedUser() throws Exception { + mockMvc.perform(get("/cars") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + } + + @Test + void registrationShouldReturnCreated() throws Exception { + + // register + UserCredentialsDto userCredentialsDto = UserCredentialsDto.builder().username("toyota").password("my secret").build(); + mockMvc.perform(post("/registration") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(userCredentialsDto))) + .andExpect(status().isCreated()); + } + + @Test + void registrationShouldReturnUnauthorizedWithWrongCredentials() throws Exception { + + mockMvc.perform(get("/cars") + .with(httpBasic("user", "wrong password"))) + .andExpect(status().isUnauthorized()); + } + + @Test + void getCarsShouldUpdatePasswordFromWorkingFactor5toHigherValue() throws Exception { + mockMvc.perform(get("/cars") + .with(httpBasic("user with working factor 5", "password"))) + .andExpect(status().isOk()); + + UserCredentials userCredentials = userRepository.findByUsername("user with working factor 5"); + // we don't know what strength the BcCryptWorkFactorService returns, + // but it should be more than 5 + assertThat(userCredentials.getPassword()).doesNotStartWith("{bcrypt}$2a$05"); + } + + @Test + void getCarsShouldUpdateSha1PasswordToBcrypt() throws Exception { + mockMvc.perform(get("/cars") + .with(httpBasic("user with sha1 encoding", "password"))) + .andExpect(status().isOk()); + + UserCredentials userCredentials = userRepository.findByUsername("user with sha1 encoding"); + assertThat(userCredentials.getPassword()).startsWith("{bcrypt}"); + } + + @Test + void getCarsShouldReturnOkForScryptUser() throws Exception { + mockMvc.perform(get("/cars") + .with(httpBasic("scrypt user", "password"))) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/workfactor/BcCryptWorkFactorServiceTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/workfactor/BcCryptWorkFactorServiceTest.java new file mode 100644 index 0000000..6cc2c1d --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/workfactor/BcCryptWorkFactorServiceTest.java @@ -0,0 +1,121 @@ +package io.reflectoring.passwordencoding.workfactor; + +import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class BcCryptWorkFactorServiceTest { + + private BcCryptWorkFactorService bcCryptWorkFactorService = new BcCryptWorkFactorService(); + + @Test + void calculateStrength() { + // given + + // when + int strength = bcCryptWorkFactorService.calculateStrength(); + + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength); + + // then + assertThat(strength).isBetween(4, 31); + } + + @Test + void calculateRounds() { + // given + + // when + int strength = bcCryptWorkFactorService.calculateStrengthClosestToTimeGoal(); + + // then + assertThat(strength).isBetween(4, 31); + } + + @Test + void findCloserToShouldReturnNumber1IfItCloserToGoalThanNumber2() { + // given + int number1 = 950; + int number2 = 1051; + + // when + boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2); + + // then + assertThat(actual).isTrue(); + } + + @Test + void findCloserToShouldReturnNUmber2IfItCloserToGoalThanNumber1() { + // given + int number1 = 1002; + int number2 = 999; + + // when + boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2); + + // then + assertThat(actual).isFalse(); + } + + @Test + void findCloserToShouldReturnGoalIfNumber2IsEqualGoal() { + // given + int number1 = 999; + int number2 = 1000; + + // when + boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2); + + // then + assertThat(actual).isFalse(); + } + + @Test + void findCloserToShouldReturnGoalIfNumber1IsEqualGoal() { + // given + int number1 = 1000; + int number2 = 1001; + + // when + boolean actual = bcCryptWorkFactorService.isPreviousDurationCloserToGoal(number1, number2); + + // then + assertThat(actual).isTrue(); + } + + @Test + void getStrengthShouldReturn4IfStrengthIs4() { + // given + int currentStrength = 4; + + // when + int actual = bcCryptWorkFactorService.getStrength(0, 0, currentStrength); + + // then + assertThat(actual).isEqualTo(4); + } + + @Test + void getStrengthShouldReturnPreviousStrengthIfPreviousDurationCloserToGoal() { + // given + + // when + int actual = bcCryptWorkFactorService.getStrength(980, 1021, 5); + + // then + assertThat(actual).isEqualTo(4); + } + + @Test + void getStrengthShouldReturnCurrentStrengthIfCurrentDurationCloserToGoal() { + // given + + // when + int actual = bcCryptWorkFactorService.getStrength(960, 1021, 5); + + // then + assertThat(actual).isEqualTo(5); + } +} \ No newline at end of file diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/workfactor/Pbkdf2WorkFactorServiceTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/workfactor/Pbkdf2WorkFactorServiceTest.java new file mode 100644 index 0000000..49f5447 --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/workfactor/Pbkdf2WorkFactorServiceTest.java @@ -0,0 +1,22 @@ +package io.reflectoring.passwordencoding.workfactor; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class Pbkdf2WorkFactorServiceTest { + + + private Pbkdf2WorkFactorService pbkdf2WorkFactorService = new Pbkdf2WorkFactorService(); + + @Test + void calculateIteration() { + // given + + // when + int iterationNumber = pbkdf2WorkFactorService.calculateIteration(); + + // then + assertThat(iterationNumber).isGreaterThanOrEqualTo(150000); + } +} \ No newline at end of file