1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.spring.security.ldaptive.authentication;
18
19 import static java.util.Objects.isNull;
20 import static java.util.Objects.nonNull;
21 import static java.util.Objects.requireNonNullElseGet;
22 import static org.springframework.util.ObjectUtils.isEmpty;
23
24 import java.util.Collection;
25 import java.util.Optional;
26 import java.util.stream.Stream;
27 import lombok.AccessLevel;
28 import lombok.Getter;
29 import lombok.Setter;
30 import org.apache.commons.logging.Log;
31 import org.apache.commons.logging.LogFactory;
32 import org.bremersee.ldaptive.DefaultLdaptiveErrorHandler;
33 import org.bremersee.ldaptive.LdaptiveTemplate;
34 import org.bremersee.spring.security.core.EmailToUsernameResolver;
35 import org.bremersee.spring.security.ldaptive.authentication.provider.NoAccountControlEvaluator;
36 import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveRememberMeTokenProvider;
37 import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUserDetails;
38 import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUserDetailsService;
39 import org.jspecify.annotations.NonNull;
40 import org.ldaptive.BindOperation;
41 import org.ldaptive.BindResponse;
42 import org.ldaptive.CompareRequest;
43 import org.ldaptive.ConnectionConfig;
44 import org.ldaptive.ConnectionFactory;
45 import org.ldaptive.DefaultConnectionFactory;
46 import org.ldaptive.LdapException;
47 import org.ldaptive.SimpleBindRequest;
48 import org.ldaptive.SingleConnectionFactory;
49 import org.springframework.context.MessageSource;
50 import org.springframework.context.MessageSourceAware;
51 import org.springframework.context.support.MessageSourceAccessor;
52 import org.springframework.core.convert.converter.Converter;
53 import org.springframework.security.authentication.AccountExpiredException;
54 import org.springframework.security.authentication.AuthenticationManager;
55 import org.springframework.security.authentication.AuthenticationProvider;
56 import org.springframework.security.authentication.BadCredentialsException;
57 import org.springframework.security.authentication.CredentialsExpiredException;
58 import org.springframework.security.authentication.DisabledException;
59 import org.springframework.security.authentication.LockedException;
60 import org.springframework.security.authentication.RememberMeAuthenticationToken;
61 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
62 import org.springframework.security.core.Authentication;
63 import org.springframework.security.core.AuthenticationException;
64 import org.springframework.security.core.SpringSecurityMessageSource;
65 import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
66 import org.springframework.security.crypto.password.PasswordEncoder;
67 import org.springframework.util.Assert;
68
69
70
71
72
73
74 public class LdaptiveAuthenticationManager
75 implements AuthenticationManager, AuthenticationProvider, MessageSourceAware {
76
77
78
79
80 protected final Log logger = LogFactory.getLog(this.getClass());
81
82
83
84
85 @Getter(AccessLevel.PROTECTED)
86 private final LdaptiveAuthenticationProperties authenticationProperties;
87
88
89
90
91 @Getter(AccessLevel.PROTECTED)
92 private final String rememberMeKey;
93
94
95
96
97 @Getter(AccessLevel.PROTECTED)
98 private final LdaptiveTemplate applicationLdaptiveTemplate;
99
100
101
102
103 @Getter(AccessLevel.PROTECTED)
104 private EmailToUsernameResolver emailToUsernameResolver;
105
106
107
108
109 @Getter(AccessLevel.PROTECTED)
110 @Setter
111 private PasswordEncoder passwordEncoder;
112
113
114
115
116 @Getter(AccessLevel.PROTECTED)
117 private AccountControlEvaluator accountControlEvaluator;
118
119
120
121
122 @Getter(AccessLevel.PROTECTED)
123 @Setter
124 private GrantedAuthoritiesMapper grantedAuthoritiesMapper;
125
126
127
128
129 @Getter(AccessLevel.PROTECTED)
130 @Setter
131 private LdaptiveRememberMeTokenProvider passwordProvider;
132
133
134
135
136 @Getter(AccessLevel.PROTECTED)
137 @Setter
138 private Converter<LdaptiveUserDetails, LdaptiveAuthentication> tokenConverter;
139
140
141
142
143 @Getter(AccessLevel.PROTECTED)
144 private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
145
146
147
148
149
150
151
152
153 public LdaptiveAuthenticationManager(
154 ConnectionConfig connectionConfig,
155 LdaptiveAuthenticationProperties authenticationProperties,
156 String rememberMeKey) {
157 this(new DefaultConnectionFactory(connectionConfig), authenticationProperties, rememberMeKey);
158 }
159
160
161
162
163
164
165
166
167 public LdaptiveAuthenticationManager(
168 ConnectionFactory connectionFactory,
169 LdaptiveAuthenticationProperties authenticationProperties,
170 String rememberMeKey) {
171 this(new LdaptiveTemplate(connectionFactory), authenticationProperties, rememberMeKey);
172 }
173
174
175
176
177
178
179
180
181 public LdaptiveAuthenticationManager(
182 LdaptiveTemplate applicationLdaptiveTemplate,
183 LdaptiveAuthenticationProperties authenticationProperties,
184 String rememberMeKey) {
185
186 this.applicationLdaptiveTemplate = applicationLdaptiveTemplate;
187 Assert.notNull(getApplicationLdaptiveTemplate(), "Application ldaptive template is required.");
188 this.authenticationProperties = authenticationProperties;
189 Assert.notNull(getAuthenticationProperties(), "Authentication properties are required.");
190 this.rememberMeKey = rememberMeKey;
191
192
193 setEmailToUsernameResolver(new EmailToUsernameResolverByLdapAttribute(
194 getAuthenticationProperties(), getApplicationLdaptiveTemplate()));
195
196
197 if (isNull(getAuthenticationProperties().getAccountControlEvaluator())) {
198 setAccountControlEvaluator(new NoAccountControlEvaluator());
199 } else {
200 setAccountControlEvaluator(getAuthenticationProperties().getAccountControlEvaluator().get());
201 }
202 }
203
204
205
206
207
208
209 public void setEmailToUsernameResolver(
210 EmailToUsernameResolver emailToUsernameResolver) {
211 if (nonNull(emailToUsernameResolver)) {
212 this.emailToUsernameResolver = emailToUsernameResolver;
213 }
214 }
215
216
217
218
219
220
221 public void setAccountControlEvaluator(
222 AccountControlEvaluator accountControlEvaluator) {
223 if (nonNull(accountControlEvaluator)) {
224 this.accountControlEvaluator = accountControlEvaluator;
225 }
226 }
227
228 @Override
229 public void setMessageSource(@NonNull MessageSource messageSource) {
230 this.messages = new MessageSourceAccessor(messageSource);
231 }
232
233
234
235
236 public void init() {
237 if (!isSimpleBindAuthentication() && isNull(getPasswordEncoder())) {
238 throw new IllegalStateException(String.format("A password attribute is set (%s) but no "
239 + "password encoder is present. Either delete the password attribute to perform a "
240 + "bind to authenticate or set a password encoder.",
241 getAuthenticationProperties().getPasswordAttribute()));
242 }
243 }
244
245 @Override
246 public boolean supports(@NonNull Class<?> authentication) {
247 return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)
248 || isRememberMeAuthentication(authentication);
249 }
250
251 private boolean isRememberMeAuthentication(Class<?> authentication) {
252 return !isEmpty(getRememberMeKey())
253 && RememberMeAuthenticationToken.class.isAssignableFrom(authentication);
254 }
255
256
257
258
259
260
261
262 protected boolean rememberMeKeyMatches(RememberMeAuthenticationToken authentication) {
263 return Optional.ofNullable(getRememberMeKey())
264 .filter(key -> key.hashCode() == authentication.getKeyHash())
265 .isPresent();
266 }
267
268 @NonNull
269 @Override
270 public Authentication authenticate(Authentication authentication)
271 throws AuthenticationException {
272
273 if (!supports(authentication.getClass())) {
274 logger.debug(String.format("Authentication [%s] is not supported.",
275 authentication.getClass().getName()));
276
277 return null;
278 }
279 if (authentication instanceof RememberMeAuthenticationToken rma) {
280 if (!rememberMeKeyMatches(rma)) {
281 throw new BadCredentialsException(getMessages().getMessage(
282 "RememberMeAuthenticationProvider.incorrectKey",
283 "The presented RememberMeAuthenticationToken does not contain the expected key"));
284 }
285 return rma;
286 }
287
288 String name = getName(authentication);
289 logger.debug("Authenticating user '" + name + "' ...");
290 String password = Optional.ofNullable(authentication.getCredentials())
291 .map(String::valueOf)
292 .orElseThrow(() -> new BadCredentialsException("Password is required."));
293 String username = getEmailToUsernameResolver()
294 .getUsernameByEmail(name)
295 .orElse(name);
296 if (isRefusedUsername(username)) {
297 throw new DisabledException(String
298 .format("Username '%s' is refused by configuration.", username));
299 }
300
301 LdaptiveUserDetails userDetails = getUserDetailsService().loadUserByUsername(username);
302 checkPassword(userDetails, password);
303 checkAccountControl(userDetails);
304
305 if (nonNull(getTokenConverter())) {
306 return getTokenConverter().convert(userDetails);
307 }
308 return new LdaptiveAuthenticationToken(userDetails);
309 }
310
311
312
313
314
315
316
317 protected boolean isRefusedUsername(String username) {
318 if (isEmpty(username)) {
319 return true;
320 }
321 return Stream.ofNullable(getAuthenticationProperties().getRefusedUsernames())
322 .flatMap(Collection::stream)
323 .filter(refusedUsername -> !isEmpty(refusedUsername))
324 .anyMatch(refusedUsername -> refusedUsername.equalsIgnoreCase(username));
325 }
326
327
328
329
330
331
332
333 protected String getName(Authentication authentication) {
334 Object principal = authentication.getPrincipal();
335 if (principal instanceof LdaptiveUserDetails ldaptiveUserDetails) {
336 return requireNonNullElseGet(ldaptiveUserDetails.getDn(), authentication::getName);
337 }
338 return authentication.getName();
339 }
340
341
342
343
344
345
346 public LdaptiveUserDetailsService getUserDetailsService() {
347 LdaptiveUserDetailsService userDetailsService = new LdaptiveUserDetailsService(
348 getAuthenticationProperties(), getApplicationLdaptiveTemplate());
349 userDetailsService.setAccountControlEvaluator(getAccountControlEvaluator());
350 userDetailsService.setGrantedAuthoritiesMapper(getGrantedAuthoritiesMapper());
351 userDetailsService.setRememberMeTokenProvider(getPasswordProvider());
352 return userDetailsService;
353 }
354
355
356
357
358
359
360 protected boolean isSimpleBindAuthentication() {
361 return isNull(getAuthenticationProperties().getPasswordAttribute())
362 || getAuthenticationProperties().getPasswordAttribute().isBlank();
363 }
364
365
366
367
368
369
370
371 protected void checkPassword(LdaptiveUserDetails user, String password) {
372 if (isSimpleBindAuthentication()) {
373 checkPasswordWithSimpleBind(user, password);
374 } else {
375 checkPasswordWithCompareRequest(user, password);
376 }
377 }
378
379
380
381
382
383
384
385 protected void checkPasswordWithCompareRequest(LdaptiveUserDetails user, String password) {
386 Assert.notNull(getPasswordEncoder(), "No password encoder is present.");
387 boolean matches = getApplicationLdaptiveTemplate().compare(CompareRequest.builder()
388 .dn(user.getDn())
389 .name(getAuthenticationProperties().getPasswordAttribute())
390 .value(getPasswordEncoder().encode(password))
391 .build());
392 if (!matches) {
393 throw new BadCredentialsException("Password doesn't match.");
394 }
395 }
396
397
398
399
400
401
402
403 protected void checkPasswordWithSimpleBind(LdaptiveUserDetails user, String password) {
404 SingleConnectionFactory connectionFactory = getSingleConnectionFactory();
405 try {
406 connectionFactory.initialize();
407 BindOperation bind = getBindOperation(connectionFactory);
408 BindResponse response = bind.execute(SimpleBindRequest.builder()
409 .dn(user.getDn())
410 .password(password)
411 .build());
412 if (!response.isSuccess()) {
413 throw new BadCredentialsException("Password doesn't match.");
414 }
415
416 } catch (LdapException ldapException) {
417 new DefaultLdaptiveErrorHandler().handleError(ldapException);
418 } finally {
419 connectionFactory.close();
420 }
421 }
422
423
424
425
426
427
428 SingleConnectionFactory getSingleConnectionFactory() {
429 ConnectionConfig connectionConfig = ConnectionConfig
430 .copy(getApplicationLdaptiveTemplate().getConnectionFactory().getConnectionConfig());
431 connectionConfig.setConnectionInitializers();
432 return new SingleConnectionFactory(connectionConfig);
433 }
434
435
436
437
438
439
440
441 BindOperation getBindOperation(SingleConnectionFactory cf) {
442 return new BindOperation(cf);
443 }
444
445
446
447
448
449
450 protected void checkAccountControl(LdaptiveUserDetails user) {
451 if (!user.isEnabled()) {
452 throw new DisabledException("Account is disabled.");
453 }
454 if (!user.isAccountNonLocked()) {
455 throw new LockedException("Account is locked.");
456 }
457 if (!user.isAccountNonExpired()) {
458 throw new AccountExpiredException("Account is expired.");
459 }
460 if (!user.isCredentialsNonExpired()) {
461 throw new CredentialsExpiredException("Credentials are expired.");
462 }
463 }
464
465 }