import java.util.HashMap;
import java.util.Map;

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;

/**
 * Shows the id-prefix format of DelegatingPasswordEncoder and how a stored hash
 * is silently upgraded at login (the "upgrade on login" pattern).
 */
public class DelegatingUpgrade {

    // pretend this is the users table: username -> stored hash
    static final Map<String, String> db = new HashMap<>();

    static DelegatingPasswordEncoder encoder() {
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder(10));
        encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
        encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
        // new hashes use argon2; old ones still verify via their own prefix
        return new DelegatingPasswordEncoder("argon2@SpringSecurity_v5_8", encoders);
    }

    static boolean login(DelegatingPasswordEncoder enc, String user, String password) {
        String stored = db.get(user);
        if (!enc.matches(password, stored)) {
            return false;
        }
        if (enc.upgradeEncoding(stored)) {              // stored hash uses an old algorithm / cost
            db.put(user, enc.encode(password));          // re-hash now: we hold the plaintext only here
            System.out.println("  -> upgraded stored hash for " + user);
        }
        return true;
    }

    public static void main(String[] args) {
        DelegatingPasswordEncoder enc = encoder();
        // a legacy user whose hash was created with bcrypt cost 10 years ago
        db.put("alice", "{bcrypt}" + new BCryptPasswordEncoder(10).encode("s3cret!"));
        System.out.println("before login : " + db.get("alice").substring(0, 40) + "...");
        System.out.println("wrong password -> " + login(enc, "alice", "nope"));
        System.out.println("good password  -> " + login(enc, "alice", "s3cret!"));
        System.out.println("after login  : " + db.get("alice").substring(0, 40) + "...");
        System.out.println("second login   -> " + login(enc, "alice", "s3cret!") + " (no upgrade message expected)");
        System.out.println();
        System.out.println("id prefix of a fresh hash: " + enc.encode("x").substring(0, 27));
        try {
            enc.matches("x", "plainhashwithoutprefix");
        } catch (IllegalArgumentException e) {
            System.out.println("no {id} prefix -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }
}
