/
DelegatingPasswordEncoder.java
301 lines (274 loc) · 11.8 KB
/
DelegatingPasswordEncoder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/*
* Copyright 2002-2024 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.
*/
package org.springframework.security.crypto.password;
import java.util.HashMap;
import java.util.Map;
/**
* A password encoder that delegates to another PasswordEncoder based upon a prefixed
* identifier.
*
* <h2>Constructing an instance</h2>
*
* You can easily construct an instance using
* {@link org.springframework.security.crypto.factory.PasswordEncoderFactories}.
* Alternatively, you may create your own custom instance. For example:
*
* <pre>
* String idForEncode = "bcrypt";
* Map<String,PasswordEncoder> encoders = new HashMap<>();
* encoders.put(idForEncode, new BCryptPasswordEncoder());
* encoders.put("noop", NoOpPasswordEncoder.getInstance());
* encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
* encoders.put("scrypt", new SCryptPasswordEncoder());
* encoders.put("sha256", new StandardPasswordEncoder());
*
* PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
* </pre>
*
*
* <h2>Password Storage Format</h2>
*
* The general format for a password is:
*
* <pre>
* {id}encodedPassword
* </pre>
*
* Such that "id" is an identifier used to look up which {@link PasswordEncoder} should be
* used and "encodedPassword" is the original encoded password for the selected
* {@link PasswordEncoder}. The "id" must be at the beginning of the password, start with
* "{" (id prefix) and end with "}" (id suffix). Both id prefix and id suffix can be
* customized via {@link #DelegatingPasswordEncoder(String, Map, String, String)}. If the
* "id" cannot be found, the "id" will be null.
*
* For example, the following might be a list of passwords encoded using different "id".
* All of the original passwords are "password".
*
* <pre>
* {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
* {noop}password
* {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
* {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
* {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
* </pre>
*
* For the DelegatingPasswordEncoder that we constructed above:
*
* <ol>
* <li>The first password would have a {@code PasswordEncoder} id of "bcrypt" and
* encodedPassword of "$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG". When
* matching it would delegate to
* {@link org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder}</li>
* <li>The second password would have a {@code PasswordEncoder} id of "noop" and
* encodedPassword of "password". When matching it would delegate to
* {@link NoOpPasswordEncoder}</li>
* <li>The third password would have a {@code PasswordEncoder} id of "pbkdf2" and
* encodedPassword of
* "5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc".
* When matching it would delegate to {@link Pbkdf2PasswordEncoder}</li>
* <li>The fourth password would have a {@code PasswordEncoder} id of "scrypt" and
* encodedPassword of
* "$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc="
* When matching it would delegate to
* {@link org.springframework.security.crypto.scrypt.SCryptPasswordEncoder}</li>
* <li>The final password would have a {@code PasswordEncoder} id of "sha256" and
* encodedPassword of
* "97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0".
* When matching it would delegate to {@link StandardPasswordEncoder}</li>
* </ol>
*
* <h2>Password Encoding</h2>
*
* The {@code idForEncode} passed into the constructor determines which
* {@link PasswordEncoder} will be used for encoding passwords. In the
* {@code DelegatingPasswordEncoder} we constructed above, that means that the result of
* encoding "password" would be delegated to {@code BCryptPasswordEncoder} and be prefixed
* with "{bcrypt}". The end result would look like:
*
* <pre>
* {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
* </pre>
*
* <h2>Password Matching</h2>
*
* Matching is done based upon the "id" and the mapping of the "id" to the
* {@link PasswordEncoder} provided in the constructor. Our example in "Password Storage
* Format" provides a working example of how this is done.
*
* By default the result of invoking {@link #matches(CharSequence, String)} with a
* password with an "id" that is not mapped (including a null id) will result in an
* {@link IllegalArgumentException}. This behavior can be customized using
* {@link #setDefaultPasswordEncoderForMatches(PasswordEncoder)}.
*
* @author Rob Winch
* @author Michael Simons
* @author heowc
* @author Jihoon Cha
* @since 5.0
* @see org.springframework.security.crypto.factory.PasswordEncoderFactories
*/
public class DelegatingPasswordEncoder implements PasswordEncoder {
private static final String DEFAULT_ID_PREFIX = "{";
private static final String DEFAULT_ID_SUFFIX = "}";
public static final String NO_PASSWORD_ENCODER_MAPPED = "There is no PasswordEncoder mapped for the id \"%s\"";
public static final String NO_PASSWORD_ENCODER_PREFIX = "You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.";
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 = new UnmappedIdPasswordEncoder();
/**
* Creates a new instance
* @param idForEncode the id used to lookup which {@link PasswordEncoder} should be
* used for {@link #encode(CharSequence)}
* @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine
* which {@link PasswordEncoder} should be used for
* {@link #matches(CharSequence, String)}
*/
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
this(idForEncode, idToPasswordEncoder, DEFAULT_ID_PREFIX, DEFAULT_ID_SUFFIX);
}
/**
* Creates a new instance
* @param idForEncode the id used to lookup which {@link PasswordEncoder} should be
* used for {@link #encode(CharSequence)}
* @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine
* which {@link PasswordEncoder} should be used for
* @param idPrefix the prefix that denotes the start of the id in the encoded results
* @param idSuffix the suffix that denotes the end of an id in the encoded results
* {@link #matches(CharSequence, String)}
*/
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder,
String idPrefix, String idSuffix) {
if (idForEncode == null) {
throw new IllegalArgumentException("idForEncode cannot be null");
}
if (idPrefix == null) {
throw new IllegalArgumentException("prefix cannot be null");
}
if (idSuffix == null || idSuffix.isEmpty()) {
throw new IllegalArgumentException("suffix cannot be empty");
}
if (idPrefix.contains(idSuffix)) {
throw new IllegalArgumentException("idPrefix " + idPrefix + " cannot contain idSuffix " + idSuffix);
}
if (!idToPasswordEncoder.containsKey(idForEncode)) {
throw new IllegalArgumentException(
"idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
}
for (String id : idToPasswordEncoder.keySet()) {
if (id == null) {
continue;
}
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 = idToPasswordEncoder.get(idForEncode);
this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
this.idPrefix = idPrefix;
this.idSuffix = idSuffix;
}
/**
* Sets the {@link PasswordEncoder} to delegate to for
* {@link #matches(CharSequence, String)} if the id is not mapped to a
* {@link PasswordEncoder}.
*
* <p>
* The encodedPassword provided will be the full password passed in including the
* {"id"} portion.* For example, if the password of "{notmapped}foobar" was used, the
* "id" would be "notmapped" and the encodedPassword passed into the
* {@link PasswordEncoder} would be "{notmapped}foobar".
* </p>
* @param defaultPasswordEncoderForMatches the encoder to use. The default is to throw
* an {@link IllegalArgumentException}
*/
public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) {
if (defaultPasswordEncoderForMatches == null) {
throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
}
this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
}
@Override
public String encode(CharSequence rawPassword) {
return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
String id = extractId(prefixEncodedPassword);
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
}
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
private String extractId(String prefixEncodedPassword) {
if (prefixEncodedPassword == null) {
return null;
}
int start = prefixEncodedPassword.indexOf(this.idPrefix);
if (start != 0) {
return null;
}
int end = prefixEncodedPassword.indexOf(this.idSuffix, start);
if (end < 0) {
return null;
}
return prefixEncodedPassword.substring(start + this.idPrefix.length(), end);
}
@Override
public boolean upgradeEncoding(String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
if (!this.idForEncode.equalsIgnoreCase(id)) {
return true;
}
else {
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
}
}
private String extractEncodedPassword(String prefixEncodedPassword) {
int start = prefixEncodedPassword.indexOf(this.idSuffix);
return prefixEncodedPassword.substring(start + this.idSuffix.length());
}
/**
* Default {@link PasswordEncoder} that throws an exception telling that a suitable
* {@link PasswordEncoder} for the id could not be found.
*/
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
if (id != null && !id.isEmpty()) {
throw new IllegalArgumentException(String.format(NO_PASSWORD_ENCODER_MAPPED, id));
}
throw new IllegalArgumentException(NO_PASSWORD_ENCODER_PREFIX);
}
}
}