diff --git a/build-all.sh b/build-all.sh index d630237..eb2e72d 100644 --- a/build-all.sh +++ b/build-all.sh @@ -56,6 +56,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..950eb31 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/PasswordEncodingSpringBootApplication.java @@ -0,0 +1,12 @@ +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/DatabaseUserDetailPasswordService.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/DatabaseUserDetailPasswordService.java new file mode 100644 index 0000000..8b6389f --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/DatabaseUserDetailPasswordService.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 DatabaseUserDetailPasswordService implements UserDetailsPasswordService { + + private final UserRepository userRepository; + + private final UserDetailsMapper userDetailsMapper; + + public DatabaseUserDetailPasswordService( + 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/DatabaseUserDetailsService.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/DatabaseUserDetailsService.java new file mode 100644 index 0000000..91dbe8e --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/DatabaseUserDetailsService.java @@ -0,0 +1,29 @@ +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 DatabaseUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + private final UserDetailsMapper userDetailsMapper; + + public DatabaseUserDetailsService( + 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..54a2ee0 --- /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..f3828ab --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/authentication/UserDetailsMapper.java @@ -0,0 +1,17 @@ +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 +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..b916c2c --- /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..da5291c --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/configuration/SecurityConfiguration.java @@ -0,0 +1,82 @@ +package io.reflectoring.passwordencoding.configuration; + +import io.reflectoring.passwordencoding.authentication.DatabaseUserDetailPasswordService; +import io.reflectoring.passwordencoding.authentication.DatabaseUserDetailsService; +import io.reflectoring.passwordencoding.workfactor.BcCryptWorkFactorService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +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.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.*; +import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableWebSecurity +class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + private final BcCryptWorkFactorService bcCryptWorkFactorService; + private final DatabaseUserDetailsService databaseUserDetailsService; + private final DatabaseUserDetailPasswordService databaseUserDetailPasswordService; + + public SecurityConfiguration( + BcCryptWorkFactorService bcCryptWorkFactorService, + DatabaseUserDetailsService databaseUserDetailsService, + DatabaseUserDetailPasswordService databaseUserDetailPasswordService) { + this.bcCryptWorkFactorService = bcCryptWorkFactorService; + this.databaseUserDetailsService = databaseUserDetailsService; + this.databaseUserDetailPasswordService = databaseUserDetailPasswordService; + } + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity + .csrf() + .disable() + .authorizeRequests() + .antMatchers("/registration") + .permitAll() + .anyRequest() + .authenticated() + .and() + .httpBasic(); + + httpSecurity.headers().frameOptions().disable(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + // we must use deprecated encoder to support their encoding + String encodingId = "bcrypt"; + Map encoders = new HashMap<>(); + encoders.put(encodingId, new BCryptPasswordEncoder(bcCryptWorkFactorService.calculateStrength())); + encoders.put("ldap", new LdapShaPasswordEncoder()); + encoders.put("MD4", new Md4PasswordEncoder()); + encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); + encoders.put("noop", NoOpPasswordEncoder.getInstance()); + encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); + encoders.put("scrypt", new SCryptPasswordEncoder()); + encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1")); + encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256")); + encoders.put("sha256", new StandardPasswordEncoder()); + encoders.put("argon2", new Argon2PasswordEncoder()); + + return new DelegatingPasswordEncoder(encodingId, encoders); + } + + @Bean + public AuthenticationProvider daoAuthenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(passwordEncoder()); + provider.setUserDetailsPasswordService(this.databaseUserDetailPasswordService); + provider.setUserDetailsService(this.databaseUserDetailsService); + return provider; + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/Argon2Example.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/Argon2Example.java new file mode 100644 index 0000000..7441b42 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/Argon2Example.java @@ -0,0 +1,18 @@ +package io.reflectoring.passwordencoding.encoder; + +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; + +class Argon2Example { + + public String encode(String plainPassword) { + int saltLength = 16; // salt length in bytes + int hashLength = 32; // hash length in bytes + int parallelism = 1; // currently is not supported + int memory = 4096; // memory costs + int iterations = 3; + + Argon2PasswordEncoder argon2PasswordEncoder = + new Argon2PasswordEncoder(saltLength, hashLength, parallelism, memory, iterations); + return argon2PasswordEncoder.encode(plainPassword); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/BCryptExample.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/BCryptExample.java new file mode 100644 index 0000000..c16da50 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/BCryptExample.java @@ -0,0 +1,15 @@ +package io.reflectoring.passwordencoding.encoder; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.security.SecureRandom; + +class BCryptExample { + + public String encode(String plainPassword) { + int strength = 10; + BCryptPasswordEncoder bCryptPasswordEncoder = + new BCryptPasswordEncoder(strength, new SecureRandom()); + return bCryptPasswordEncoder.encode(plainPassword); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/Pbkdf2Example.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/Pbkdf2Example.java new file mode 100644 index 0000000..ced80d3 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/Pbkdf2Example.java @@ -0,0 +1,17 @@ +package io.reflectoring.passwordencoding.encoder; + +import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; + +class Pbkdf2Example { + + public String encode(String plainPassword) { + + String pepper = "pepper"; // secret key used by password encoding + int iterations = 200000; // number of hash iteration + int hashWidth = 256; // hash with in bits + + Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = + new Pbkdf2PasswordEncoder(pepper, iterations, hashWidth); + return pbkdf2PasswordEncoder.encode(plainPassword); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/SCryptExample.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/SCryptExample.java new file mode 100644 index 0000000..83f5203 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/encoder/SCryptExample.java @@ -0,0 +1,18 @@ +package io.reflectoring.passwordencoding.encoder; + +import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; + +class SCryptExample { + + public String encode(String plainPassword) { + int cpuCost = (int) Math.pow(2, 14); // factor to increase CPU costs + int memoryCost = 8; // factor to increases memory usage + int parallelization = 1; // currently nor supported by Spring Security + int keyLength = 32; // key length in bytes + int saltLength = 64; // salt length in bytes + + SCryptPasswordEncoder sCryptPasswordEncoder = + new SCryptPasswordEncoder(cpuCost, memoryCost, parallelization, keyLength, saltLength); + return sCryptPasswordEncoder.encode(plainPassword); + } +} 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..11c3186 --- /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 +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..f8a64a4 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/CarResources.java @@ -0,0 +1,15 @@ +package io.reflectoring.passwordencoding.resources; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Set; + +@RestController +class CarResources { + + @GetMapping("/cars") + public Set cars() { + return Set.of(new Car("vw", "black"), new Car("bmw", "white")); + } +} diff --git a/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/RegistrationResource.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/RegistrationResource.java new file mode 100644 index 0000000..131c638 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/RegistrationResource.java @@ -0,0 +1,37 @@ +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.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 +class RegistrationResource { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public RegistrationResource(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/resources/UserCredentialsDto.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/UserCredentialsDto.java new file mode 100644 index 0000000..ce59f42 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/resources/UserCredentialsDto.java @@ -0,0 +1,15 @@ +package io.reflectoring.passwordencoding.resources; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +class UserCredentialsDto { + private String username; + private String password; +} 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..403e0c7 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/BcCryptWorkFactorService.java @@ -0,0 +1,101 @@ +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 uses the divide-and-conquer algorithm. + */ + public BcryptWorkFactor calculateStrengthDivideAndConquer() { + return calculateStrengthDivideAndConquer( + new BcryptWorkFactor(MIN_STRENGTH, Integer.MIN_VALUE), + new BcryptWorkFactor(MAX_STRENGTH, Integer.MAX_VALUE)); + } + + private BcryptWorkFactor calculateStrengthDivideAndConquer( + BcryptWorkFactor smallFactor, BcryptWorkFactor bigFactor) { + if (bigFactor.getStrength() - smallFactor.getStrength() == 1) { + return getClosestStrength(smallFactor, bigFactor); + } + int midStrength = + (bigFactor.getStrength() - smallFactor.getStrength()) / 2 + smallFactor.getStrength(); + long duration = calculateDuration(midStrength); + BcryptWorkFactor midFactor = new BcryptWorkFactor(midStrength, duration); + if (duration < GOAL_MILLISECONDS_PER_PASSWORD) { + return calculateStrengthDivideAndConquer(midFactor, bigFactor); + } + return calculateStrengthDivideAndConquer(smallFactor, midFactor); + } + + private BcryptWorkFactor getClosestStrength( + BcryptWorkFactor smallFactor, BcryptWorkFactor bigFactor) { + if (isPreviousDurationCloserToGoal(smallFactor.getDuration(), bigFactor.getDuration())) { + return smallFactor; + } + return bigFactor; + } + + private long calculateDuration(int strength) { + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(strength); + Stopwatch stopwatch = Stopwatch.createStarted(); + bCryptPasswordEncoder.encode(TEST_PASSWORD); + stopwatch.stop(); + return stopwatch.elapsed(TimeUnit.MILLISECONDS); + } + + /** + * 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 1s + */ + public int calculateStrength() { + for (int strength = MIN_STRENGTH; strength <= MAX_STRENGTH; strength++) { + + long duration = calculateDuration(strength); + 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)); + } + + /** + * @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; + } + } + + /** + * 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/BcryptWorkFactor.java b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/BcryptWorkFactor.java new file mode 100644 index 0000000..f966548 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/BcryptWorkFactor.java @@ -0,0 +1,12 @@ +package io.reflectoring.passwordencoding.workfactor; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +class BcryptWorkFactor { + + private int strength; + private long duration; +} 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..c094b00 --- /dev/null +++ b/spring-boot/password-encoding/src/main/java/io/reflectoring/passwordencoding/workfactor/Pbkdf2WorkFactorService.java @@ -0,0 +1,41 @@ +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 +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..254e7c0 --- /dev/null +++ b/spring-boot/password-encoding/src/main/resources/db/migration/V1_2__add user.sql @@ -0,0 +1,15 @@ +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..dce400e --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/authentication/UserDetailsMapperTest.java @@ -0,0 +1,33 @@ +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(); + } +} 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..38970de --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/authentication/UserRepositoryTest.java @@ -0,0 +1,25 @@ +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(); + } +} diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/Argon2ExampleTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/Argon2ExampleTest.java new file mode 100644 index 0000000..cc12d93 --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/Argon2ExampleTest.java @@ -0,0 +1,22 @@ +package io.reflectoring.passwordencoding.encoder; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class Argon2ExampleTest { + + private Argon2Example argon2Example = new Argon2Example(); + + @Test + void encode() { + // given + String plainPassword = "password"; + + // when + String actual = argon2Example.encode(plainPassword); + + // then + assertThat(actual).startsWith("$argon2id$v=19$m=4096,t=3,p=1"); + } +} diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/BCryptExampleTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/BCryptExampleTest.java new file mode 100644 index 0000000..7fa6bc0 --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/BCryptExampleTest.java @@ -0,0 +1,22 @@ +package io.reflectoring.passwordencoding.encoder; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class BCryptExampleTest { + + private BCryptExample bcryptExample = new BCryptExample(); + + @Test + void encode() { + // given + String plainPassword = "password"; + + // when + String encoded = bcryptExample.encode(plainPassword); + + // then + assertThat(encoded).startsWith("$2a$10"); + } +} diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/Pbkdf2ExampleTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/Pbkdf2ExampleTest.java new file mode 100644 index 0000000..3ccd734 --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/Pbkdf2ExampleTest.java @@ -0,0 +1,22 @@ +package io.reflectoring.passwordencoding.encoder; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class Pbkdf2ExampleTest { + + private Pbkdf2Example pbkdf2Example = new Pbkdf2Example(); + + @Test + void encode() { + // given + String plainPassword = "plainPassword"; + + // when + String actual = pbkdf2Example.encode(plainPassword); + + // then + assertThat(actual).hasSize(80); + } +} diff --git a/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/SCryptExampleTest.java b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/SCryptExampleTest.java new file mode 100644 index 0000000..0ddb647 --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/encoder/SCryptExampleTest.java @@ -0,0 +1,23 @@ +package io.reflectoring.passwordencoding.encoder; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class SCryptExampleTest { + + private SCryptExample sCryptExample = new SCryptExample(); + + @Test + void encode() { + // given + String plainPassword = "password"; + + // when + String actual = sCryptExample.encode(plainPassword); + + // then + assertThat(actual).hasSize(140); + assertThat(actual).startsWith("$e0801"); + } +} 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..36b4c55 --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/resources/CarResourcesTest.java @@ -0,0 +1,91 @@ +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 getCarsShouldMigrateSha1PasswordToBcrypt() 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()); + } +} 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..54e64bd --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/workfactor/BcCryptWorkFactorServiceTest.java @@ -0,0 +1,119 @@ +package io.reflectoring.passwordencoding.workfactor; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class BcCryptWorkFactorServiceTest { + + private BcCryptWorkFactorService bcCryptWorkFactorService = new BcCryptWorkFactorService(); + + @Test + void calculateStrength() { + // given + + // when + int strength = bcCryptWorkFactorService.calculateStrength(); + + // then + assertThat(strength).isBetween(4, 31); + } + + @Test + void calculateStrengthBi() { + // given + + // when + BcryptWorkFactor bcryptWorkFactor = + bcCryptWorkFactorService.calculateStrengthDivideAndConquer(); + + // then + assertThat(bcryptWorkFactor.getStrength()).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); + } +} 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..c0b2a59 --- /dev/null +++ b/spring-boot/password-encoding/src/test/java/io/reflectoring/passwordencoding/workfactor/Pbkdf2WorkFactorServiceTest.java @@ -0,0 +1,21 @@ +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); + } +}