• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Avec112 / commons-security / 18795851267

25 Oct 2025 01:01AM UTC coverage: 90.146% (+0.1%) from 90.018%
18795851267

push

github

Avec112
Cache `DelegatingPasswordEncoder` instances and refactor `matches` to auto-detect encoder type

- Added pre-built `DelegatingPasswordEncoder` instances for all `PasswordEncoderType` values.
- Updated `matches` method to automatically detect encoder type from `{id}` prefix in encoded passwords, simplifying usage.
- Enhanced unit tests to ensure compatibility with multiple encoder types.

39 of 52 branches covered (75.0%)

Branch coverage included in aggregate %.

455 of 496 relevant lines covered (91.73%)

3.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.91
src/main/java/com/github/avec112/security/crypto/password/PasswordEncoderUtils.java
1
package com.github.avec112.security.crypto.password;
2

3
import org.apache.commons.lang3.Validate;
4
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
5
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
6
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
7
import org.springframework.security.crypto.password.PasswordEncoder;
8
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
9
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
10

11
import java.util.Collections;
12
import java.util.HashMap;
13
import java.util.Map;
14
import java.util.Objects;
15
import java.util.regex.Matcher;
16
import java.util.regex.Pattern;
17

18
/**
19
 * Utility class for handling password encoding and matching using various encoding schemes.
20
 * Supports common encoding types such as Argon2, BCrypt, SCrypt, and PBKDF2.
21
 * This class provides methods for encoding plaintext passwords and verifying encoded passwords.
22
 *
23
 * The encoded password can optionally include a prefix indicating the encoding type.
24
 * For example, "{argon2}$argon2id$v=..." indicates the use of Argon2 encoding.
25
 *
26
 * This class relies on the DelegatingPasswordEncoder to delegate encoding and matching
27
 * operations to the appropriate password encoder based on the specified or implied encoding type.
28
 */
29
public class PasswordEncoderUtils {
30

31
    private static final Pattern PREFIX_PATTERN = Pattern.compile("^\\{([a-zA-Z0-9_-]+)}");
3✔
32
    private static final Map<String, PasswordEncoder> ENCODERS;
33
    private static final Map<PasswordEncoderType, PasswordEncoder> DELEGATING_ENCODERS;
34
    private static final PasswordEncoderType DEFAULT_ENCODER = PasswordEncoderType.ARGON2;
2✔
35

36
    static {
37
        Map<String, PasswordEncoder> map = new HashMap<>();
4✔
38
        map.put(PasswordEncoderType.ARGON2.id(), Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
6✔
39
        map.put(PasswordEncoderType.SCRYPT.id(), SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
6✔
40
        map.put(PasswordEncoderType.BCRYPT.id(), new BCryptPasswordEncoder());
8✔
41
        map.put(PasswordEncoderType.PBKDF2.id(), Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
6✔
42
        ENCODERS = Collections.unmodifiableMap(map);
3✔
43

44
        // Pre-build cached delegating encoders for each type
45
        Map<PasswordEncoderType, PasswordEncoder> delegatingMap = new HashMap<>();
4✔
46
        for (PasswordEncoderType type : PasswordEncoderType.values()) {
16✔
47
            delegatingMap.put(type, new DelegatingPasswordEncoder(type.id(), ENCODERS));
10✔
48
        }
49
        DELEGATING_ENCODERS = Collections.unmodifiableMap(delegatingMap);
3✔
50
    }
1✔
51

52
    /**
53
     * Private constructor to prevent instantiation of the {@code PasswordEncoderUtils} utility class.
54
     *
55
     * This class is designed to provide static utility methods related to password encoding
56
     * and matching, and should not be instantiated.
57
     */
58
    private PasswordEncoderUtils(){}
59

60
    /**
61
     * Returns a cached delegating password encoder for the specified type.
62
     *
63
     * @param type the password encoder type to delegate to; must not be null
64
     * @return a {@link PasswordEncoder} instance configured to delegate to the specified type
65
     */
66
    private static PasswordEncoder delegating(PasswordEncoderType type) {
67
        return DELEGATING_ENCODERS.get(type);
5✔
68
    }
69

70
    /**
71
     * Will encode with ARGON2 encoder
72
     * @param password plaintext password to encode
73
     * @return encoded password
74
     */
75
    public static String encode(String password) {
76
        Validate.notBlank(password);
3✔
77
        return encode(password, PasswordEncoderType.ARGON2);
4✔
78
    }
79

80
    /**
81
     * Encodes the provided plaintext password using the specified password encoder type.
82
     *
83
     * @param password the plaintext password to encode; must not be blank
84
     * @param encoderType the type of password encoder to use for encoding; must not be null
85
     * @return the encoded password as a string
86
     */
87
    public static String encode(String password, PasswordEncoderType encoderType) {
88
        Validate.notBlank(password);
3✔
89
        Objects.requireNonNull(encoderType);
3✔
90

91
        PasswordEncoder passwordEncoder = delegating(encoderType);
3✔
92
        return passwordEncoder.encode(password);
4✔
93
    }
94

95
    /**
96
     * Automatically detects the encoder type from the encoded password's prefix
97
     * and verifies if the plaintext password matches.
98
     * This method parses the {id} prefix from the encoded password to determine
99
     * which encoder to use for matching.
100
     *
101
     * @param password plaintext password to verify; must not be blank
102
     * @param encodedPassword encoded password with {id} prefix; must not be blank
103
     * @return true if password match
104
     * @throws IllegalArgumentException if the encoded password doesn't have a valid prefix
105
     */
106
    public static boolean matches(String password, String encodedPassword) {
107
        Validate.notBlank(password);
3✔
108
        Validate.notBlank(encodedPassword);
3✔
109

110
        PasswordEncoderType detectedType = getPasswordEncoderType(encodedPassword);
3✔
111
        return matches(password, encodedPassword, detectedType);
5✔
112
    }
113

114

115
    /**
116
     * Verifies whether a plaintext password matches an encoded password using a specified password encoder type.
117
     * This method is useful for explicit encoder type control or for legacy systems.
118
     *
119
     * @param password the plaintext password to verify; must not be blank
120
     * @param encodedPassword the encoded password to compare against; must not be blank
121
     * @param encoderType the type of password encoder to use for matching; must not be null
122
     * @return true if the plaintext password matches the encoded password, false otherwise
123
     */
124
    public static boolean matches(String password, String encodedPassword, PasswordEncoderType encoderType) {
125
        Validate.notBlank(password);
3✔
126
        Validate.notBlank(encodedPassword);
3✔
127
        Objects.requireNonNull(encoderType);
3✔
128

129
        PasswordEncoder passwordEncoder = delegating(encoderType);
3✔
130
        return passwordEncoder.matches(password, encodedPassword);
5✔
131
    }
132

133
    /**
134
     * Extracts the password encoder type (e.g. "argon2", "bcrypt", "scrypt", "pbkdf2")
135
     * from an encoded password string.
136
     *
137
     * @param encodedPassword the encoded password string, must start with {id}
138
     * @return the PasswordEncoderType if recognized
139
     * @throws IllegalArgumentException if no valid prefix is found or unsupported type
140
     */
141
    public static PasswordEncoderType getPasswordEncoderType(String encodedPassword) {
142
        Validate.notBlank(encodedPassword, "Encoded password cannot be null or blank");
6✔
143

144
        Matcher matcher = PREFIX_PATTERN.matcher(encodedPassword);
4✔
145
        if (matcher.find()) {
3!
146
            String id = matcher.group(1).toLowerCase();
5✔
147
            for (PasswordEncoderType type : PasswordEncoderType.values()) {
16!
148
                if (type.id().equalsIgnoreCase(id)) {
5✔
149
                    return type;
2✔
150
                }
151
            }
152
            throw new IllegalArgumentException("Unsupported password encoder type: " + id);
×
153
        }
154

155
        throw new IllegalArgumentException("Encoded password does not contain a valid prefix: " + encodedPassword);
×
156
    }
157

158
    /**
159
     * Convenience method returning the encoder type as string.
160
     * @return the password encoder type as string
161
     */
162
    public static String getPasswordEncoderTypeAsString(String encodedPassword) {
163
        return getPasswordEncoderType(encodedPassword).id();
4✔
164
    }
165

166
    /**
167
     * Checks if an encoded password needs to be upgraded to a stronger algorithm.
168
     * Returns true if the current encoding type is different from the target type.
169
     *
170
     * @param encodedPassword the currently encoded password
171
     * @param targetType the desired password encoder type (typically ARGON2)
172
     * @return true if the password should be re-encoded with the target type
173
     */
174
    public static boolean needsUpgrade(String encodedPassword, PasswordEncoderType targetType) {
175
        Validate.notBlank(encodedPassword);
3✔
176
        Objects.requireNonNull(targetType);
3✔
177

178
        try {
179
            PasswordEncoderType currentType = getPasswordEncoderType(encodedPassword);
3✔
180
            return currentType != targetType;
7✔
181
        } catch (IllegalArgumentException e) {
×
182
            // If we can't determine the type, assume it needs upgrade
183
            return true;
×
184
        }
185
    }
186

187
    /**
188
     * Checks if an encoded password needs to be upgraded to the default algorithm (ARGON2).
189
     *
190
     * @param encodedPassword the currently encoded password
191
     * @return true if the password should be re-encoded with ARGON2
192
     */
193
    public static boolean needsUpgrade(String encodedPassword) {
194
        return needsUpgrade(encodedPassword, DEFAULT_ENCODER);
4✔
195
    }
196

197
    /**
198
     * Upgrades an encoded password from one encoder type to another.
199
     * This method verifies the raw password against the old encoded password,
200
     * and if valid, re-encodes it with the target encoder type.
201
     *
202
     * @param rawPassword the plaintext password to verify and re-encode
203
     * @param oldEncodedPassword the currently encoded password
204
     * @param targetType the desired password encoder type for the upgrade
205
     * @return the newly encoded password with the target encoder type
206
     * @throws IllegalArgumentException if the raw password does not match the old encoded password
207
     */
208
    public static String upgradePassword(String rawPassword, String oldEncodedPassword, PasswordEncoderType targetType) {
209
        Validate.notBlank(rawPassword);
3✔
210
        Validate.notBlank(oldEncodedPassword);
3✔
211
        Objects.requireNonNull(targetType);
3✔
212

213
        // Get the current encoder type
214
        PasswordEncoderType currentType = getPasswordEncoderType(oldEncodedPassword);
3✔
215

216
        // Verify the raw password matches the old encoded password
217
        if (!matches(rawPassword, oldEncodedPassword, currentType)) {
5✔
218
            throw new IllegalArgumentException("Raw password does not match the encoded password");
5✔
219
        }
220

221
        // Re-encode with the target encoder type
222
        return encode(rawPassword, targetType);
4✔
223
    }
224

225
    /**
226
     * Upgrades an encoded password to the default encoder type (ARGON2).
227
     * This method verifies the raw password against the old encoded password,
228
     * and if valid, re-encodes it with ARGON2.
229
     *
230
     * @param rawPassword the plaintext password to verify and re-encode
231
     * @param oldEncodedPassword the currently encoded password
232
     * @return the newly encoded password with ARGON2
233
     * @throws IllegalArgumentException if the raw password does not match the old encoded password
234
     */
235
    public static String upgradePassword(String rawPassword, String oldEncodedPassword) {
236
        return upgradePassword(rawPassword, oldEncodedPassword, DEFAULT_ENCODER);
5✔
237
    }
238

239
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc