001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.control.user.authentication.server.command; 018 019import com.echothree.model.control.contact.common.ContactMechanismPurposes; 020import com.echothree.model.control.contact.common.ContactMechanismTypes; 021import com.echothree.model.control.contact.server.control.ContactControl; 022import static com.echothree.model.control.party.common.PartyTypes.CUSTOMER; 023import static com.echothree.model.control.party.common.PartyTypes.EMPLOYEE; 024import static com.echothree.model.control.party.common.PartyTypes.VENDOR; 025import com.echothree.model.control.party.server.control.PartyControl; 026import com.echothree.model.control.security.common.SecurityRoleGroups; 027import static com.echothree.model.control.security.common.SecurityRoleGroups.Employee; 028import com.echothree.model.control.sequence.common.SequenceTypes; 029import com.echothree.model.control.sequence.server.logic.SequenceGeneratorLogic; 030import com.echothree.model.control.uom.common.UomConstants; 031import com.echothree.model.control.uom.server.control.UomControl; 032import com.echothree.model.control.user.common.UserConstants; 033import com.echothree.model.data.party.server.entity.Party; 034import com.echothree.model.data.party.server.entity.PartyRelationship; 035import com.echothree.model.data.party.server.entity.PartyType; 036import com.echothree.model.data.user.server.entity.UserLoginPasswordString; 037import com.echothree.model.data.user.server.entity.UserLoginStatus; 038import com.echothree.util.common.form.BaseForm; 039import com.echothree.util.common.message.ExecutionErrors; 040import com.echothree.util.common.message.ExecutionWarnings; 041import com.echothree.util.common.validation.FieldDefinition; 042import com.echothree.util.server.control.BaseSimpleCommand; 043import com.echothree.util.server.control.CommandSecurityDefinition; 044import com.echothree.util.server.persistence.Sha1Utils; 045import com.echothree.util.server.string.UnitOfMeasureUtils; 046import java.util.List; 047import javax.inject.Inject; 048 049public abstract class BaseLoginCommand<F extends BaseForm> 050 extends BaseSimpleCommand<F> { 051 052 @Inject 053 ContactControl contactControl; 054 055 @Inject 056 PartyControl partyControl; 057 058 @Inject 059 UomControl uomControl; 060 061 @Inject 062 SequenceGeneratorLogic sequenceGeneratorLogic; 063 064 @Inject 065 Sha1Utils sha1Utils; 066 067 /** Creates a new instance of BaseLoginCommand */ 068 protected BaseLoginCommand(final CommandSecurityDefinition commandSecurityDefinition, 069 final List<FieldDefinition> formFieldDefinition) { 070 super(commandSecurityDefinition, formFieldDefinition, false); 071 } 072 073 protected UserLoginPasswordString checkPassword(final String password, final Party party, final String userLoginPasswordTypeName, 074 final boolean deleteOnSuccess) { 075 var userLoginPasswordType = userControl.getUserLoginPasswordTypeByName(userLoginPasswordTypeName); 076 var userLoginPassword = deleteOnSuccess? userControl.getUserLoginPasswordForUpdate(party, userLoginPasswordType): 077 userControl.getUserLoginPassword(party, userLoginPasswordType); 078 UserLoginPasswordString result = null; 079 080 if(userLoginPassword != null) { 081 var userLoginPasswordEncoderType = userLoginPassword.getUserLoginPasswordType().getUserLoginPasswordEncoderType(); 082 var userLoginPasswordEncoderTypeName = userLoginPasswordEncoderType.getUserLoginPasswordEncoderTypeName(); 083 var userLoginPasswordString = userControl.getUserLoginPasswordString(userLoginPassword); 084 085 if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_SHA1)) { 086 result = sha1Utils.encode(userLoginPasswordString.getSalt(), password).equals(userLoginPasswordString.getPassword()) ? userLoginPasswordString: null; 087 } else if(userLoginPasswordEncoderTypeName.equals(UserConstants.UserLoginPasswordEncoderType_TEXT)) { 088 result = password.equals(userLoginPasswordString.getPassword())? userLoginPasswordString: null; 089 } 090 091 if(deleteOnSuccess && result != null) { 092 userControl.deleteUserLoginPassword(userLoginPassword, getPartyPK()); 093 } 094 } 095 096 return result; 097 } 098 099 // TODO: Recovered password should become regular password if that ends up being the password that matches, also, 100 // the recovered password should be deleted if the user logs in using their regular one. Changing a password should also 101 // make sure a recovered password does not exist. 102 protected boolean checkPasswords(final UserLoginStatus userLoginStatus, final String password, final Party party, final boolean doStatusChecks) { 103 var result = checkPassword(password, party, UserConstants.UserLoginPasswordType_STRING, false); 104 105 if(result == null) { 106 result = checkPassword(password, party, UserConstants.UserLoginPasswordType_RECOVERED_STRING, true); 107 } 108 109 if(result == null) { 110 addExecutionError(ExecutionErrors.IncorrectPassword.name()); 111 } else if(doStatusChecks) { 112 var partyTypePasswordStringPolicy = partyControl.getPartyTypePasswordStringPolicy(party.getLastDetail().getPartyType()); 113 114 if(partyTypePasswordStringPolicy != null) { 115 var partyTypePasswordStringPolicyDetail = partyTypePasswordStringPolicy.getLastDetail(); 116 var maximumPasswordLifetime = partyTypePasswordStringPolicyDetail.getMaximumPasswordLifetime(); 117 var expiredLoginsPermitted = partyTypePasswordStringPolicyDetail.getExpiredLoginsPermitted(); 118 119 if(maximumPasswordLifetime != null) { 120 var expirationWarningTime = partyTypePasswordStringPolicyDetail.getExpirationWarningTime(); 121 var changedTime = result.getChangedTime(); 122 123 if((session.getStartTime() - changedTime) > maximumPasswordLifetime) { 124 userLoginStatus.setExpiredCount(userLoginStatus.getExpiredCount() + 1); 125 126 addExecutionWarning(ExecutionWarnings.PasswordExpired.name()); 127 } else if(expirationWarningTime != null) { 128 var expirationTime = changedTime + maximumPasswordLifetime; 129 var warningTime = expirationTime - expirationWarningTime; 130 131 if(session.getStartTime() > warningTime) { 132 var timeUnitOfMeasureKind = uomControl.getUnitOfMeasureKindByUnitOfMeasureKindUseTypeUsingNames(UomConstants.UnitOfMeasureKindUseType_TIME); 133 var remainingTime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(getUserVisit(), timeUnitOfMeasureKind, Long.valueOf(expirationTime - session.getStartTime())); 134 135 addExecutionWarning(ExecutionWarnings.PasswordExpiration.name(), remainingTime); 136 } 137 } 138 } 139 140 if(expiredLoginsPermitted != null && userLoginStatus.getExpiredCount() > expiredLoginsPermitted) { 141 result = null; 142 addExecutionError(ExecutionErrors.MaximumExpiredLoginsPermittedExceeded.name(), expiredLoginsPermitted); 143 } 144 145 if(userLoginStatus.getForceChange()) { 146 addExecutionWarning(ExecutionWarnings.ForcePasswordChange.name()); 147 } 148 } 149 } 150 151 return result != null; 152 } 153 154 protected String getSecurityRoleGroupName(final PartyType partyType) { 155 String securityRoleGroupName = null; 156 var partyTypeName = partyType.getPartyTypeName(); 157 158 if(partyTypeName.equals(CUSTOMER.name())) { 159 securityRoleGroupName = SecurityRoleGroups.Customer.name(); 160 } else if(partyTypeName.equals(EMPLOYEE.name())) { 161 securityRoleGroupName = Employee.name(); 162 } else if(partyTypeName.equals(VENDOR.name())) { 163 securityRoleGroupName = SecurityRoleGroups.Vendor.name(); 164 } 165 166 return securityRoleGroupName; 167 } 168 169 protected void clearLoginFailures(final UserLoginStatus userLoginStatus) { 170 // Audit trail for callers of this function should be created by the callers. 171 userLoginStatus.setFailureCount(0); 172 userLoginStatus.setFirstFailureTime(null); 173 userLoginStatus.setLastFailureTime(null); 174 } 175 176 protected void addRemoteInet4AddressToParty(final Party party, final Integer remoteInet4Address) { 177 if(remoteInet4Address != null) { 178 var partyPK = party.getPrimaryKey(); 179 var partyContactMechanism = contactControl.getPartyContactMechanismByInet4Address(party, remoteInet4Address); 180 181 if(partyContactMechanism == null) { 182 var contactMechanismName = sequenceGeneratorLogic.getNextSequenceValue(null, SequenceTypes.CONTACT_MECHANISM.name()); 183 var contactMechanismType = contactControl.getContactMechanismTypeByName(ContactMechanismTypes.INET_4.name()); 184 var contactMechanism = contactControl.createContactMechanism(contactMechanismName, contactMechanismType, false, partyPK); 185 186 contactControl.createContactInet4Address(contactMechanism, remoteInet4Address, partyPK); 187 partyContactMechanism = contactControl.createPartyContactMechanism(party, contactMechanism, null, false, 1, partyPK); 188 } 189 190 var contactMechanismPurpose = contactControl.getContactMechanismPurposeByName(ContactMechanismPurposes.INET_4_LOGIN.name()); 191 var partyContactMechanismPurpose = contactControl.getPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose); 192 if(partyContactMechanismPurpose == null) { 193 contactControl.createPartyContactMechanismPurpose(partyContactMechanism, contactMechanismPurpose, false, 1, partyPK); 194 } 195 } 196 } 197 198 protected void successfulLogin(final UserLoginStatus userLoginStatus, final Party party, final PartyRelationship partyRelationship, 199 final Integer remoteInet4Address) { 200 var userVisit = getUserVisitForUpdate(); 201 var userKey = userVisit.getUserKey(); 202 var userKeyDetailValue = userControl.getUserKeyDetailValueByPKForUpdate(userKey.getLastDetail().getPrimaryKey()); 203 204 userControl.associatePartyToUserVisit(userVisit, party, partyRelationship, session.getStartTime()); 205 206 // Only update the UserKeyDetail if the party has changed 207 var partyPK = party.getPrimaryKey(); 208 var partyRelationshipPK = partyRelationship == null? null: partyRelationship.getPrimaryKey(); 209 var userKeyPartyPK = userKeyDetailValue.getPartyPK(); 210 var userKeyPartyRelationshipPK = userKeyDetailValue.getPartyRelationshipPK(); 211 212 if(userKeyPartyPK == null || !userKeyPartyPK.equals(partyPK) 213 || userKeyPartyRelationshipPK == null || !userKeyPartyRelationshipPK.equals(partyRelationshipPK)) { 214 userKeyDetailValue.setPartyPK(partyPK); 215 userKeyDetailValue.setPartyRelationshipPK(partyRelationshipPK); 216 userControl.updateUserKeyFromValue(userKeyDetailValue); 217 } 218 219 clearLoginFailures(userLoginStatus); 220 221 userLoginStatus.setLastLoginTime(session.getStartTime()); 222 223 addRemoteInet4AddressToParty(party, remoteInet4Address); 224 225 // TODO: Create audit trail 226 } 227 228 protected void unsuccessfulLogin(final UserLoginStatus userLoginStatus) { 229 var failureCount = userLoginStatus.getFailureCount(); 230 231 userLoginStatus.setFailureCount(failureCount + 1); 232 if(userLoginStatus.getFirstFailureTime() == null) { 233 userLoginStatus.setFirstFailureTime(session.getStartTime()); 234 } 235 userLoginStatus.setLastFailureTime(session.getStartTime()); 236 237 // TODO: Create audit trail 238 } 239 240}