Compare commits
2 Commits
20bb2a2a63
...
c3dfab19ea
| Author | SHA1 | Date | |
|---|---|---|---|
| c3dfab19ea | |||
| abb89dbc6e |
@ -4,7 +4,6 @@
|
|||||||
<option name="SPRING_BOOT_MAIN_CLASS" value="at.nanopenguin.fhtw.itse.pw_demo.PwDemoApplication" />
|
<option name="SPRING_BOOT_MAIN_CLASS" value="at.nanopenguin.fhtw.itse.pw_demo.PwDemoApplication" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
||||||
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Docker Image" run_configuration_type="docker-deploy" />
|
|
||||||
</method>
|
</method>
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
19
.run/Setup tables.run.xml
Normal file
19
.run/Setup tables.run.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Setup tables" type="DatabaseScript">
|
||||||
|
<data-source id="1c5ced7c-10f3-4662-9fcf-f6601b0cfc94" />
|
||||||
|
<script-text>CREATE TABLE users (
|
||||||
|
username VARCHAR(50) NOT NULL PRIMARY KEY,
|
||||||
|
password VARCHAR(500) NOT NULL,
|
||||||
|
enabled BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE authorities (
|
||||||
|
username VARCHAR(50) NOT NULL,
|
||||||
|
authority VARCHAR(50) NOT NULL,
|
||||||
|
CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users(username)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);</script-text>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
FROM maven:4.0.0-rc-4-eclipse-temurin-21-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# separate dependency stage to reduce build time when dependencies are unchanged
|
||||||
|
COPY pom.xml .
|
||||||
|
RUN mvn dependency:go-offline -B
|
||||||
|
|
||||||
|
COPY src ./src
|
||||||
|
# issues when not skipping, idk why
|
||||||
|
RUN mvn clean package -DskipTests
|
||||||
|
|
||||||
|
# openjdk is now deprecated over temurin
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/target/*.jar app.jar
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
45
docker-compose.yml
Normal file
45
docker-compose.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
container_name: itse-b08-database
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: pw_demo
|
||||||
|
POSTGRES_USER: pw_demo
|
||||||
|
POSTGRES_PASSWORD: pw_demo
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init-scripts:/docker-entrypoint-initdb.d
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U myuser -d myappdb"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: itse-b08-spring
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/pw_demo
|
||||||
|
SPRING_DATASOURCE_USERNAME: pw_demo
|
||||||
|
SPRING_DATASOURCE_PASSWORD: pw_demo
|
||||||
|
SPRING_JPA_HIBERNATE_DDL_AUTO: update
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
13
init-scripts/init.sql
Normal file
13
init-scripts/init.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
username VARCHAR(50) NOT NULL PRIMARY KEY,
|
||||||
|
password VARCHAR(500) NOT NULL,
|
||||||
|
enabled BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS authorities (
|
||||||
|
username VARCHAR(50) NOT NULL,
|
||||||
|
authority VARCHAR(50) NOT NULL,
|
||||||
|
CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users(username)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS ix_auth_username ON authorities (username, authority);
|
||||||
@ -40,7 +40,7 @@ Note: Die angeführte Struktur ist die Langfassung von Punkten, die ich in irgen
|
|||||||
- Character-Set (Buchstaben (klein/groß), Zahlen, (welche) Sonderzeichen?)
|
- Character-Set (Buchstaben (klein/groß), Zahlen, (welche) Sonderzeichen?)
|
||||||
- Wörter & Namen? → Wenn ja, Tauschregeln von Buchstaben (0 ↔ O, 1 ↔ l ↔ I ↔ !)
|
- Wörter & Namen? → Wenn ja, Tauschregeln von Buchstaben (0 ↔ O, 1 ↔ l ↔ I ↔ !)
|
||||||
|
|
||||||
### Salt & Pepper
|
### Salt
|
||||||
|
|
||||||
- "Gratis" Entropy, am Server angehängt
|
- "Gratis" Entropy, am Server angehängt
|
||||||
- Salt: Unique pro Nutzer
|
- Salt: Unique pro Nutzer
|
||||||
@ -50,6 +50,12 @@ Note: Die angeführte Struktur ist die Langfassung von Punkten, die ich in irgen
|
|||||||
|
|
||||||
- Tunable CPU & Memory usage um Brute-Force zu erschweren
|
- Tunable CPU & Memory usage um Brute-Force zu erschweren
|
||||||
- Beispiele (Pbkdf, bcrypt, scrypt, ...)
|
- Beispiele (Pbkdf, bcrypt, scrypt, ...)
|
||||||
|
|
||||||
|
#### PBKDF2
|
||||||
|
|
||||||
|
- key = pbkdf2(password, salt, iterations-count, hash-function, derived-key-len)
|
||||||
|
-
|
||||||
|
|
||||||
- (Überleitung zu Demo)
|
- (Überleitung zu Demo)
|
||||||
|
|
||||||
### Demo
|
### Demo
|
||||||
|
|||||||
@ -0,0 +1,115 @@
|
|||||||
|
package at.nanopenguin.fhtw.itse.pw_demo;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.springframework.boot.web.servlet.server.Encoding;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.util.EncodingUtils;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class CustomPbkdf2PasswordEncoder implements PasswordEncoder {
|
||||||
|
|
||||||
|
private static final int SALT_LENGTH = 16; // 128 bits
|
||||||
|
private static final int ITERATIONS = 1024;
|
||||||
|
|
||||||
|
private final SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encode(CharSequence rawPassword) {
|
||||||
|
|
||||||
|
byte[] salt = generateSalt();
|
||||||
|
byte[] hash = pbkdf2(
|
||||||
|
rawPassword.toString().getBytes(StandardCharsets.UTF_8),
|
||||||
|
salt,
|
||||||
|
ITERATIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
return Base64.getEncoder().encodeToString(salt) + ":" + Base64.getEncoder().encodeToString(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generateSalt() {
|
||||||
|
byte[] salt = new byte[SALT_LENGTH];
|
||||||
|
random.nextBytes(salt);
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] pbkdf2(byte[] password, byte[] salt, int iterations) {
|
||||||
|
|
||||||
|
byte[] saltWithBlockNum = new byte[salt.length + 4];
|
||||||
|
System.arraycopy(salt, 0, saltWithBlockNum, 0, salt.length);
|
||||||
|
saltWithBlockNum[salt.length + 3] = 1;
|
||||||
|
|
||||||
|
byte[] u = hmac(password, saltWithBlockNum);
|
||||||
|
byte[] result = u.clone();
|
||||||
|
|
||||||
|
for (int i = 2; i <= iterations; i++) {
|
||||||
|
u = hmac(password, u);
|
||||||
|
xorInPlace(result, u);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void xorInPlace(byte[] a, byte[] b) {
|
||||||
|
for (int i = 0; i < a.length; i++) {
|
||||||
|
a[i] ^= b[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] hmac(byte[] key, byte[] message) {
|
||||||
|
try {
|
||||||
|
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||||
|
|
||||||
|
// Key preparation
|
||||||
|
byte[] k = key;
|
||||||
|
if (key.length > 64) {
|
||||||
|
k = sha256.digest(key);
|
||||||
|
sha256.reset();
|
||||||
|
}
|
||||||
|
byte[] keyPadded = new byte[64];
|
||||||
|
System.arraycopy(k, 0, keyPadded, 0, k.length);
|
||||||
|
|
||||||
|
// Inner and outer padding
|
||||||
|
byte[] innerPad = new byte[64];
|
||||||
|
byte[] outerPad = new byte[64];
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
innerPad[i] = (byte) (keyPadded[i] ^ 0x36);
|
||||||
|
outerPad[i] = (byte) (keyPadded[i] ^ 0x5C);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMAC = SHA256(outerPad || SHA256(innerPad || message))
|
||||||
|
sha256.update(innerPad);
|
||||||
|
sha256.update(message);
|
||||||
|
byte[] innerHash = sha256.digest();
|
||||||
|
|
||||||
|
sha256.reset();
|
||||||
|
sha256.update(outerPad);
|
||||||
|
sha256.update(innerHash);
|
||||||
|
return sha256.digest();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||||
|
String[] parts = encodedPassword.split(":");
|
||||||
|
byte[] salt = Base64.getDecoder().decode(parts[0]);
|
||||||
|
byte[] storedHash = Base64.getDecoder().decode(parts[1]);
|
||||||
|
byte[] testHash = pbkdf2(rawPassword.toString().getBytes(StandardCharsets.UTF_8), salt, ITERATIONS);
|
||||||
|
|
||||||
|
int diff = 0;
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
diff |= storedHash[i] ^ testHash[i];
|
||||||
|
}
|
||||||
|
return diff == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package at.nanopenguin.fhtw.itse.pw_demo;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.provisioning.JdbcUserDetailsManager;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RegistrationController {
|
||||||
|
|
||||||
|
private final JdbcUserDetailsManager userDetailsService;
|
||||||
|
private final SelectablePasswordEncoder encoder;
|
||||||
|
|
||||||
|
@GetMapping("/register")
|
||||||
|
public String showRegistrationForm(Model model) {
|
||||||
|
model.addAttribute("user", new UserDTO());
|
||||||
|
return "register";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public String registerUser(@ModelAttribute UserDTO user, Model model) {
|
||||||
|
userDetailsService.createUser(
|
||||||
|
new User(user.getName(), encoder.encodeSelect(user.getPassword(), user.getEncoder()), Collections.singleton(new SimpleGrantedAuthority("USER")))
|
||||||
|
);
|
||||||
|
model.addAttribute("user", new UserDTO());
|
||||||
|
model.addAttribute("message", "User registered successfully!");
|
||||||
|
return "register";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package at.nanopenguin.fhtw.itse.pw_demo;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class SHA256PasswordEncoder implements PasswordEncoder {
|
||||||
|
|
||||||
|
MessageDigest encoder;
|
||||||
|
|
||||||
|
@SneakyThrows // just a demo, so we can throw best practices out of the window :)
|
||||||
|
public SHA256PasswordEncoder() {
|
||||||
|
encoder = MessageDigest.getInstance("SHA-256");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encode(CharSequence rawPassword) {
|
||||||
|
return Base64.getEncoder().encodeToString(encoder.digest(rawPassword.toString().getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||||
|
return String.valueOf(this.encode(rawPassword)).equals(encodedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|||||||
import org.springframework.security.crypto.password.*;
|
import org.springframework.security.crypto.password.*;
|
||||||
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
|
import org.springframework.security.provisioning.JdbcUserDetailsManager;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
@ -35,6 +36,7 @@ public class SecurityConfig {
|
|||||||
http
|
http
|
||||||
.formLogin(Customizer.withDefaults())
|
.formLogin(Customizer.withDefaults())
|
||||||
.authorizeHttpRequests(authorize -> authorize
|
.authorizeHttpRequests(authorize -> authorize
|
||||||
|
.requestMatchers("/register").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -42,25 +44,24 @@ public class SecurityConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
|
public JdbcUserDetailsManager jdbcUserDetailsManager(DataSource dataSource) {
|
||||||
UserDetailsService userDetailsService = new InMemoryUserDetailsManager( // TODO: JDBC
|
return new JdbcUserDetailsManager(dataSource);
|
||||||
new User("Beni", passwordEncoder.encode("password"), Set.of(new SimpleGrantedAuthority("USER")))
|
|
||||||
);
|
|
||||||
|
|
||||||
return userDetailsService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public SelectablePasswordEncoder passwordEncoder() {
|
||||||
String defaultEncoder = "pbkdf2";
|
String defaultEncoder = "pbkdf2";
|
||||||
|
|
||||||
Map<String, PasswordEncoder> encoders = new HashMap<>();
|
Map<String, PasswordEncoder> encoders = new HashMap<>();
|
||||||
encoders.put("noop", NoOpPasswordEncoder.getInstance());
|
encoders.put("noop", NoOpPasswordEncoder.getInstance());
|
||||||
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
encoders.put("sha256", new SHA256PasswordEncoder());
|
||||||
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
|
encoders.put("md5", new MessageDigestPasswordEncoder("md5"));
|
||||||
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
encoders.put("pbkdf2", new CustomPbkdf2PasswordEncoder());
|
||||||
encoders.put("bcrypt", new BCryptPasswordEncoder());
|
//encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||||
encoders.put("sha256", new StandardPasswordEncoder());
|
//encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||||
return new DelegatingPasswordEncoder(defaultEncoder, encoders);
|
//encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
|
||||||
|
//encoders.put("bcrypt", new BCryptPasswordEncoder());
|
||||||
|
//encoders.put("sha256", new StandardPasswordEncoder());
|
||||||
|
return new SelectablePasswordEncoder(defaultEncoder, encoders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,144 @@
|
|||||||
|
package at.nanopenguin.fhtw.itse.pw_demo;
|
||||||
|
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SelectablePasswordEncoder implements PasswordEncoder {
|
||||||
|
private static final String DEFAULT_ID_PREFIX = "{";
|
||||||
|
private static final String DEFAULT_ID_SUFFIX = "}";
|
||||||
|
private static final String NO_PASSWORD_ENCODER_MAPPED = "There is no password encoder mapped for the id '%s'. Check your configuration to ensure it matches one of the registered encoders.";
|
||||||
|
private static final String NO_PASSWORD_ENCODER_PREFIX = "Given that there is no default password encoder configured, each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `SelectablePasswordEncoder`.";
|
||||||
|
private static final String MALFORMED_PASSWORD_ENCODER_PREFIX = "The name of the password encoder is improperly formatted or incomplete. The format should be '%sENCODER%spassword'.";
|
||||||
|
private final String idPrefix;
|
||||||
|
private final String idSuffix;
|
||||||
|
private final String idForEncode;
|
||||||
|
private final PasswordEncoder passwordEncoderForEncode;
|
||||||
|
private final Map<String, PasswordEncoder> idToPasswordEncoder;
|
||||||
|
private PasswordEncoder defaultPasswordEncoderForMatches;
|
||||||
|
|
||||||
|
public SelectablePasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
|
||||||
|
this(idForEncode, idToPasswordEncoder, "{", "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SelectablePasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder, String idPrefix, String idSuffix) {
|
||||||
|
this.defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
|
||||||
|
if (idForEncode == null) {
|
||||||
|
throw new IllegalArgumentException("idForEncode cannot be null");
|
||||||
|
} else if (idPrefix == null) {
|
||||||
|
throw new IllegalArgumentException("prefix cannot be null");
|
||||||
|
} else if (idSuffix != null && !idSuffix.isEmpty()) {
|
||||||
|
if (idPrefix.contains(idSuffix)) {
|
||||||
|
throw new IllegalArgumentException("idPrefix " + idPrefix + " cannot contain idSuffix " + idSuffix);
|
||||||
|
} else if (!idToPasswordEncoder.containsKey(idForEncode)) {
|
||||||
|
throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + String.valueOf(idToPasswordEncoder));
|
||||||
|
} else {
|
||||||
|
for(String id : idToPasswordEncoder.keySet()) {
|
||||||
|
if (id != null) {
|
||||||
|
if (!idPrefix.isEmpty() && id.contains(idPrefix)) {
|
||||||
|
throw new IllegalArgumentException("id " + id + " cannot contain " + idPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id.contains(idSuffix)) {
|
||||||
|
throw new IllegalArgumentException("id " + id + " cannot contain " + idSuffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.idForEncode = idForEncode;
|
||||||
|
this.passwordEncoderForEncode = (PasswordEncoder)idToPasswordEncoder.get(idForEncode);
|
||||||
|
this.idToPasswordEncoder = new HashMap(idToPasswordEncoder);
|
||||||
|
this.idPrefix = idPrefix;
|
||||||
|
this.idSuffix = idSuffix;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("suffix cannot be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) {
|
||||||
|
if (defaultPasswordEncoderForMatches == null) {
|
||||||
|
throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
|
||||||
|
} else {
|
||||||
|
this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String encode(CharSequence rawPassword) {
|
||||||
|
String var10000 = this.idPrefix;
|
||||||
|
return var10000 + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String encodeSelect(CharSequence rawPassword, String encoder) {
|
||||||
|
String var10000 = this.idPrefix;;
|
||||||
|
return var10000 + encoder + this.idSuffix + this.idToPasswordEncoder.get(encoder).encode(rawPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
|
||||||
|
if (rawPassword == null && prefixEncodedPassword == null) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
String id = this.extractId(prefixEncodedPassword);
|
||||||
|
PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
|
||||||
|
if (delegate == null) {
|
||||||
|
return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
|
||||||
|
} else {
|
||||||
|
String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
|
||||||
|
return delegate.matches(rawPassword, encodedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractId(String prefixEncodedPassword) {
|
||||||
|
if (prefixEncodedPassword == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
int start = prefixEncodedPassword.indexOf(this.idPrefix);
|
||||||
|
if (start != 0) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
int end = prefixEncodedPassword.indexOf(this.idSuffix, start);
|
||||||
|
return end < 0 ? null : prefixEncodedPassword.substring(start + this.idPrefix.length(), end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean upgradeEncoding(String prefixEncodedPassword) {
|
||||||
|
String id = this.extractId(prefixEncodedPassword);
|
||||||
|
if (!this.idForEncode.equalsIgnoreCase(id)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
|
||||||
|
return ((PasswordEncoder)this.idToPasswordEncoder.get(id)).upgradeEncoding(encodedPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractEncodedPassword(String prefixEncodedPassword) {
|
||||||
|
int start = prefixEncodedPassword.indexOf(this.idSuffix);
|
||||||
|
return prefixEncodedPassword.substring(start + this.idSuffix.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
|
||||||
|
public String encode(CharSequence rawPassword) {
|
||||||
|
throw new UnsupportedOperationException("encode is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
|
||||||
|
String id = SelectablePasswordEncoder.this.extractId(prefixEncodedPassword);
|
||||||
|
if (id != null && !id.isBlank()) {
|
||||||
|
throw new IllegalArgumentException(String.format("There is no password encoder mapped for the id '%s'. Check your configuration to ensure it matches one of the registered encoders.", id));
|
||||||
|
} else {
|
||||||
|
if (prefixEncodedPassword != null && !prefixEncodedPassword.isBlank()) {
|
||||||
|
int start = prefixEncodedPassword.indexOf(SelectablePasswordEncoder.this.idPrefix);
|
||||||
|
int end = prefixEncodedPassword.indexOf(SelectablePasswordEncoder.this.idSuffix, start);
|
||||||
|
if (start < 0 && end < 0) {
|
||||||
|
throw new IllegalArgumentException("Given that there is no default password encoder configured, each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `SelectablePasswordEncoder`.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException(String.format("The name of the password encoder is improperly formatted or incomplete. The format should be '%sENCODER%spassword'.", SelectablePasswordEncoder.this.idPrefix, SelectablePasswordEncoder.this.idSuffix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/java/at/nanopenguin/fhtw/itse/pw_demo/UserDTO.java
Normal file
12
src/main/java/at/nanopenguin/fhtw/itse/pw_demo/UserDTO.java
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package at.nanopenguin.fhtw.itse.pw_demo;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class UserDTO {
|
||||||
|
private String name;
|
||||||
|
private String password;
|
||||||
|
private String encoder;
|
||||||
|
}
|
||||||
@ -17,5 +17,4 @@ public class WelcomeController {
|
|||||||
|
|
||||||
return "welcome";
|
return "welcome";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/main/resources/templates/register.html
Normal file
43
src/main/resources/templates/register.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Welcome!</title>
|
||||||
|
<link href="/default-ui.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
<h2>Register User</h2>
|
||||||
|
|
||||||
|
<form th:action="@{/register}" th:object="${user}" method="post">
|
||||||
|
<div>
|
||||||
|
<label for="name">Name:</label>
|
||||||
|
<input type="text" id="name" th:field="*{name}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" th:field="*{password}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="encoder">Password Encoder:</label>
|
||||||
|
<select id="encoder" th:field="*{encoder}" required>
|
||||||
|
<option value="">-- Select Encoder --</option>
|
||||||
|
<option value="pbkdf2">PBKDF2</option>
|
||||||
|
<option value="sha256">Unsalted SHA256</option>
|
||||||
|
<option value="md5">MD5</option>
|
||||||
|
<option value="noop">NoOp (Plain Text)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div th:if="${message}">
|
||||||
|
<p th:text="${message}"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user