/** * Copyright (c) 2016-2017 Evolveum * * 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 * * http://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 com.evolveum.midpoint.model.impl.security; import java.util.Collection; import javax.xml.datatype.Duration; import javax.xml.datatype.XMLGregorianCalendar; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.stereotype.Component; import com.evolveum.midpoint.common.Clock; import com.evolveum.midpoint.model.api.AuthenticationEvaluator; import com.evolveum.midpoint.model.api.context.AbstractAuthenticationContext; import com.evolveum.midpoint.prism.crypto.EncryptionException; import com.evolveum.midpoint.prism.crypto.Protector; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.schema.util.MiscSchemaUtil; import com.evolveum.midpoint.security.api.Authorization; import com.evolveum.midpoint.security.api.ConnectionEnvironment; import com.evolveum.midpoint.security.api.MidPointPrincipal; import com.evolveum.midpoint.security.api.SecurityUtil; import com.evolveum.midpoint.security.api.UserProfileService; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.*; import com.evolveum.prism.xml.ns._public.types_3.ProtectedStringType; /** * @author semancik * */ public abstract class AuthenticationEvaluatorImpl<C extends AbstractCredentialType, T extends AbstractAuthenticationContext> implements AuthenticationEvaluator<T> { private static final Trace LOGGER = TraceManager.getTrace(AuthenticationEvaluatorImpl.class); @Autowired private Protector protector; @Autowired private Clock clock; // Has to be package-private so the tests can manipulate it @Autowired UserProfileService userProfileService; @Autowired private SecurityHelper securityHelper; protected abstract void checkEnteredCredentials(ConnectionEnvironment connEnv, T authCtx); protected abstract boolean suportsAuthzCheck(); protected abstract C getCredential(CredentialsType credentials); protected abstract void validateCredentialNotNull(ConnectionEnvironment connEnv, @NotNull MidPointPrincipal principal, C credential); protected abstract boolean passwordMatches(ConnectionEnvironment connEnv, @NotNull MidPointPrincipal principal, C passwordType, T authCtx); protected abstract CredentialPolicyType getEffectiveCredentialPolicy(SecurityPolicyType securityPolicy, T authnCtx) throws SchemaException; protected abstract boolean supportsActivation(); @Override public UsernamePasswordAuthenticationToken authenticate(ConnectionEnvironment connEnv, T authnCtx) throws BadCredentialsException, AuthenticationCredentialsNotFoundException, DisabledException, LockedException, CredentialsExpiredException, AuthenticationServiceException, AccessDeniedException, UsernameNotFoundException { checkEnteredCredentials(connEnv, authnCtx); MidPointPrincipal principal = getAndCheckPrincipal(connEnv, authnCtx.getUsername(), true); UserType userType = principal.getUser(); CredentialsType credentials = userType.getCredentials(); CredentialPolicyType credentialsPolicy = getCredentialsPolicy(principal, authnCtx); if (checkCredentials(principal, authnCtx, connEnv)) { recordPasswordAuthenticationSuccess(principal, connEnv, getCredential(credentials), credentialsPolicy); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, authnCtx.getEnteredCredential(), principal.getAuthorities()); return token; } else { recordPasswordAuthenticationFailure(principal, connEnv, getCredential(credentials), credentialsPolicy, "password mismatch"); throw new BadCredentialsException("web.security.provider.invalid"); } } @Override public UserType checkCredentials(ConnectionEnvironment connEnv, T authnCtx) throws BadCredentialsException, AuthenticationCredentialsNotFoundException, DisabledException, LockedException, CredentialsExpiredException, AuthenticationServiceException, AccessDeniedException, UsernameNotFoundException { checkEnteredCredentials(connEnv, authnCtx); MidPointPrincipal principal = getAndCheckPrincipal(connEnv, authnCtx.getUsername(), false); UserType userType = principal.getUser(); CredentialsType credentials = userType.getCredentials(); CredentialPolicyType credentialsPolicy = getCredentialsPolicy(principal, authnCtx); if (checkCredentials(principal, authnCtx, connEnv)) { return userType; } else { recordPasswordAuthenticationFailure(principal, connEnv, getCredential(credentials), credentialsPolicy, "password mismatch"); throw new BadCredentialsException("web.security.provider.invalid"); } } private boolean checkCredentials(MidPointPrincipal principal, T authnCtx, ConnectionEnvironment connEnv) { UserType userType = principal.getUser(); CredentialsType credentials = userType.getCredentials(); if (credentials == null || getCredential(credentials) == null) { recordAuthenticationFailure(principal, connEnv, "no credentials in user"); throw new AuthenticationCredentialsNotFoundException("web.security.provider.invalid"); } CredentialPolicyType credentialsPolicy = getCredentialsPolicy(principal, authnCtx); // Lockout if (isLockedOut(getCredential(credentials), credentialsPolicy)) { recordAuthenticationFailure(principal, connEnv, "password locked-out"); throw new LockedException("web.security.provider.locked"); } if (suportsAuthzCheck()) { // Authorizations if (!hasAnyAuthorization(principal)) { recordAuthenticationFailure(principal, connEnv, "no authorizations"); throw new DisabledException("web.security.provider.access.denied"); } } // Password age checkPasswordValidityAndAge(connEnv, principal, getCredential(credentials), credentialsPolicy); return passwordMatches(connEnv, principal, getCredential(credentials), authnCtx); } private CredentialPolicyType getCredentialsPolicy(MidPointPrincipal principal, T authnCtx){ SecurityPolicyType securityPolicy = principal.getApplicableSecurityPolicy(); CredentialPolicyType credentialsPolicy = null; try { credentialsPolicy = getEffectiveCredentialPolicy(securityPolicy, authnCtx); } catch (SchemaException e) { // TODO how to properly hanlde the error???? throw new AuthenticationServiceException("Bad config"); } return credentialsPolicy; } /** * Special-purpose method used for Web Service authentication based on javax.security callbacks. * * In that case there is no reasonable way how to reuse existing methods. Therefore this method is NOT part of the * AuthenticationEvaluator interface. It is mostly a glue to make the old Java security code work. */ public String getAndCheckUserPassword(ConnectionEnvironment connEnv, String enteredUsername) throws AuthenticationCredentialsNotFoundException, DisabledException, LockedException, CredentialsExpiredException, AuthenticationServiceException, AccessDeniedException, UsernameNotFoundException { MidPointPrincipal principal = getAndCheckPrincipal(connEnv, enteredUsername, true); UserType userType = principal.getUser(); CredentialsType credentials = userType.getCredentials(); if (credentials == null) { recordAuthenticationFailure(principal, connEnv, "no credentials in user"); throw new AuthenticationCredentialsNotFoundException("web.security.provider.invalid"); } PasswordType passwordType = credentials.getPassword(); SecurityPolicyType securityPolicy = principal.getApplicableSecurityPolicy(); PasswordCredentialsPolicyType passwordCredentialsPolicy = SecurityUtil.getEffectivePasswordCredentialsPolicy(securityPolicy); // Lockout if (isLockedOut(passwordType, passwordCredentialsPolicy)) { recordAuthenticationFailure(principal, connEnv, "password locked-out"); throw new LockedException("web.security.provider.locked"); } // Authorizations if (!hasAnyAuthorization(principal)) { recordAuthenticationFailure(principal, connEnv, "no authorizations"); throw new AccessDeniedException("web.security.provider.access.denied"); } // Password age checkPasswordValidityAndAge(connEnv, principal, passwordType.getValue(), passwordType.getMetadata(), passwordCredentialsPolicy); return getPassword(connEnv, principal, passwordType.getValue()); } @Override public PreAuthenticatedAuthenticationToken authenticateUserPreAuthenticated(ConnectionEnvironment connEnv, String enteredUsername) { MidPointPrincipal principal = getAndCheckPrincipal(connEnv, enteredUsername, true); // Authorizations if (!hasAnyAuthorization(principal)) { recordAuthenticationFailure(principal, connEnv, "no authorizations"); throw new AccessDeniedException("web.security.provider.access.denied"); } PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(principal, null, principal.getAuthorities()); recordAuthenticationSuccess(principal, connEnv); return token; } @NotNull private MidPointPrincipal getAndCheckPrincipal(ConnectionEnvironment connEnv, String enteredUsername, boolean supportsActivationCheck) { if (StringUtils.isBlank(enteredUsername)) { recordAuthenticationFailure(enteredUsername, connEnv, "no username"); throw new UsernameNotFoundException("web.security.provider.invalid"); } MidPointPrincipal principal; try { principal = userProfileService.getPrincipal(enteredUsername); } catch (ObjectNotFoundException e) { recordAuthenticationFailure(enteredUsername, connEnv, "no user"); throw new UsernameNotFoundException("web.security.provider.invalid"); } catch (SchemaException e) { recordAuthenticationFailure(enteredUsername, connEnv, "schema error"); throw new AccessDeniedException("web.security.provider.invalid"); } if (principal == null) { recordAuthenticationFailure(enteredUsername, connEnv, "no user"); throw new UsernameNotFoundException("web.security.provider.invalid"); } if (supportsActivationCheck && !principal.isEnabled()) { recordAuthenticationFailure(principal, connEnv, "user disabled"); throw new DisabledException("web.security.provider.disabled"); } return principal; } private boolean hasAnyAuthorization(MidPointPrincipal principal) { Collection<Authorization> authorizations = principal.getAuthorities(); if (authorizations == null || authorizations.isEmpty()){ return false; } for (Authorization auth : authorizations){ if (auth.getAction() != null && !auth.getAction().isEmpty()){ return true; } } return false; } private <P extends CredentialPolicyType> void checkPasswordValidityAndAge(ConnectionEnvironment connEnv, @NotNull MidPointPrincipal principal, C credentials, P passwordCredentialsPolicy) { if (credentials == null) { recordAuthenticationFailure(principal, connEnv, "no stored credential value"); throw new AuthenticationCredentialsNotFoundException("web.security.provider.credential.bad"); } validateCredentialNotNull(connEnv, principal, credentials); if (passwordCredentialsPolicy == null) { return; } Duration maxAge = passwordCredentialsPolicy.getMaxAge(); if (maxAge != null) { MetadataType credentialMetedata = credentials.getMetadata(); XMLGregorianCalendar changeTimestamp = MiscSchemaUtil.getChangeTimestamp(credentialMetedata); if (changeTimestamp != null) { XMLGregorianCalendar passwordValidUntil = XmlTypeConverter.addDuration(changeTimestamp, maxAge); if (clock.isPast(passwordValidUntil)) { recordAuthenticationFailure(principal, connEnv, "password expired"); throw new CredentialsExpiredException("web.security.provider.password.bad"); } } } } private void checkPasswordValidityAndAge(ConnectionEnvironment connEnv, @NotNull MidPointPrincipal principal, ProtectedStringType protectedString, MetadataType passwordMetadata, CredentialPolicyType passwordCredentialsPolicy) { if (protectedString == null) { recordAuthenticationFailure(principal, connEnv, "no stored password value"); throw new AuthenticationCredentialsNotFoundException("web.security.provider.password.bad"); } if (passwordCredentialsPolicy == null) { return; } Duration maxAge = passwordCredentialsPolicy.getMaxAge(); if (maxAge != null) { XMLGregorianCalendar changeTimestamp = MiscSchemaUtil.getChangeTimestamp(passwordMetadata); if (changeTimestamp != null) { XMLGregorianCalendar passwordValidUntil = XmlTypeConverter.addDuration(changeTimestamp, maxAge); if (clock.isPast(passwordValidUntil)) { recordAuthenticationFailure(principal, connEnv, "password expired"); throw new CredentialsExpiredException("web.security.provider.password.bad"); } } } } // protected boolean matchDecryptedValue(ConnectionEnvironment connEnv, @NotNull MidPointPrincipal principal, String decryptedValue, // String enteredPassword){ // return enteredPassword.equals(decryptedValue); // } // protected boolean decryptAndMatch(ConnectionEnvironment connEnv, @NotNull MidPointPrincipal principal, ProtectedStringType protectedString, String enteredPassword) { ProtectedStringType entered = new ProtectedStringType(); entered.setClearValue(enteredPassword); try { return protector.compare(entered, protectedString); } catch (SchemaException | EncryptionException e) { recordAuthenticationFailure(principal, connEnv, "error decrypting password: "+e.getMessage()); throw new AuthenticationServiceException("web.security.provider.unavailable", e); } } protected String getDecryptedValue(ConnectionEnvironment connEnv, @NotNull MidPointPrincipal principal, ProtectedStringType protectedString) { String decryptedPassword; if (protectedString.getEncryptedDataType() != null) { try { decryptedPassword = protector.decryptString(protectedString); } catch (EncryptionException e) { recordAuthenticationFailure(principal, connEnv, "error decrypting password: "+e.getMessage()); throw new AuthenticationServiceException("web.security.provider.unavailable", e); } } else { LOGGER.warn("Authenticating user based on clear value. Please check objects, " + "this should not happen. Protected string should be encrypted."); decryptedPassword = protectedString.getClearValue(); } return decryptedPassword; } private String getPassword(ConnectionEnvironment connEnv, @NotNull MidPointPrincipal principal, ProtectedStringType protectedString) { String decryptedPassword; if (protectedString.getEncryptedDataType() != null) { try { decryptedPassword = protector.decryptString(protectedString); } catch (EncryptionException e) { recordAuthenticationFailure(principal, connEnv, "error decrypting password: "+e.getMessage()); throw new AuthenticationServiceException("web.security.provider.unavailable", e); } } else { LOGGER.warn("Authenticating user based on clear value. Please check objects, " + "this should not happen. Protected string should be encrypted."); decryptedPassword = protectedString.getClearValue(); } return decryptedPassword; } private boolean isLockedOut(AbstractCredentialType credentialsType, CredentialPolicyType credentialsPolicy) { return isOverFailedLockoutAttempts(credentialsType, credentialsPolicy) && !isLockoutExpired(credentialsType, credentialsPolicy); } private boolean isOverFailedLockoutAttempts(AbstractCredentialType credentialsType, CredentialPolicyType credentialsPolicy) { int failedLogins = credentialsType.getFailedLogins() != null ? credentialsType.getFailedLogins() : 0; return isOverFailedLockoutAttempts(failedLogins, credentialsPolicy); } private boolean isOverFailedLockoutAttempts(int failedLogins, CredentialPolicyType credentialsPolicy) { return credentialsPolicy != null && credentialsPolicy.getLockoutMaxFailedAttempts() != null && credentialsPolicy.getLockoutMaxFailedAttempts() > 0 && failedLogins >= credentialsPolicy.getLockoutMaxFailedAttempts(); } private boolean isLockoutExpired(AbstractCredentialType credentialsType, CredentialPolicyType credentialsPolicy) { Duration lockoutDuration = credentialsPolicy.getLockoutDuration(); if (lockoutDuration == null) { return false; } LoginEventType lastFailedLogin = credentialsType.getLastFailedLogin(); if (lastFailedLogin == null) { return true; } XMLGregorianCalendar lastFailedLoginTimestamp = lastFailedLogin.getTimestamp(); if (lastFailedLoginTimestamp == null) { return true; } XMLGregorianCalendar lockedUntilTimestamp = XmlTypeConverter.addDuration(lastFailedLoginTimestamp, lockoutDuration); return clock.isPast(lockedUntilTimestamp); } private void recordPasswordAuthenticationSuccess(MidPointPrincipal principal, ConnectionEnvironment connEnv, C passwordType, CredentialPolicyType passwordCredentialsPolicy) { Integer failedLogins = passwordType.getFailedLogins(); if (failedLogins != null && failedLogins > 0) { passwordType.setFailedLogins(0); } LoginEventType event = new LoginEventType(); event.setTimestamp(clock.currentTimeXMLGregorianCalendar()); event.setFrom(connEnv.getRemoteHost()); passwordType.setPreviousSuccessfulLogin(passwordType.getLastSuccessfulLogin()); passwordType.setLastSuccessfulLogin(event); ActivationType activation = principal.getUser().getActivation(); if (activation != null) { activation.setLockoutStatus(LockoutStatusType.NORMAL); activation.setLockoutExpirationTimestamp(null); } userProfileService.updateUser(principal); recordAuthenticationSuccess(principal, connEnv); } private void recordAuthenticationSuccess(@NotNull MidPointPrincipal principal, @NotNull ConnectionEnvironment connEnv) { securityHelper.auditLoginSuccess(principal.getUser(), connEnv); } private void recordPasswordAuthenticationFailure(@NotNull MidPointPrincipal principal, @NotNull ConnectionEnvironment connEnv, @NotNull C passwordType, CredentialPolicyType credentialsPolicy, String reason) { Integer failedLogins = passwordType.getFailedLogins(); LoginEventType lastFailedLogin = passwordType.getLastFailedLogin(); XMLGregorianCalendar lastFailedLoginTs = null; if (lastFailedLogin != null) { lastFailedLoginTs = lastFailedLogin.getTimestamp(); } if (credentialsPolicy != null) { Duration lockoutFailedAttemptsDuration = credentialsPolicy.getLockoutFailedAttemptsDuration(); if (lockoutFailedAttemptsDuration != null) { if (lastFailedLoginTs != null) { XMLGregorianCalendar failedLoginsExpirationTs = XmlTypeConverter.addDuration(lastFailedLoginTs, lockoutFailedAttemptsDuration); if (clock.isPast(failedLoginsExpirationTs)) { failedLogins = 0; } } } } if (failedLogins == null) { failedLogins = 1; } else { failedLogins++; } passwordType.setFailedLogins(failedLogins); LoginEventType event = new LoginEventType(); event.setTimestamp(clock.currentTimeXMLGregorianCalendar()); event.setFrom(connEnv.getRemoteHost()); passwordType.setLastFailedLogin(event); ActivationType activationType = principal.getUser().getActivation(); if (failedLogins != null && isOverFailedLockoutAttempts(failedLogins, credentialsPolicy)) { if (activationType == null) { activationType = new ActivationType(); principal.getUser().setActivation(activationType); } activationType.setLockoutStatus(LockoutStatusType.LOCKED); XMLGregorianCalendar lockoutExpirationTs = null; if (credentialsPolicy != null) { Duration lockoutDuration = credentialsPolicy.getLockoutDuration(); if (lockoutDuration != null) { lockoutExpirationTs = XmlTypeConverter.addDuration(event.getTimestamp(), lockoutDuration); } } activationType.setLockoutExpirationTimestamp(lockoutExpirationTs); } userProfileService.updateUser(principal); recordAuthenticationFailure(principal, connEnv, reason); } protected void recordAuthenticationFailure(@NotNull MidPointPrincipal principal, ConnectionEnvironment connEnv, String reason) { securityHelper.auditLoginFailure(principal.getUsername(), principal.getUser(), connEnv, reason); } protected void recordAuthenticationFailure(String username, ConnectionEnvironment connEnv, String reason) { securityHelper.auditLoginFailure(username, null, connEnv, reason); } }