diff --git a/gsrs-controlled-vocabulary-api/pom.xml b/gsrs-controlled-vocabulary-api/pom.xml index e0047940..d4596e06 100644 --- a/gsrs-controlled-vocabulary-api/pom.xml +++ b/gsrs-controlled-vocabulary-api/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-controlled-vocabulary/pom.xml b/gsrs-controlled-vocabulary/pom.xml index b36f092d..cbc3fe36 100644 --- a/gsrs-controlled-vocabulary/pom.xml +++ b/gsrs-controlled-vocabulary/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-core-entities/pom.xml b/gsrs-core-entities/pom.xml index f6cb65e1..e0027a55 100644 --- a/gsrs-core-entities/pom.xml +++ b/gsrs-core-entities/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-core-entities/src/main/java/gsrs/model/UserProfileAuthenticationResult.java b/gsrs-core-entities/src/main/java/gsrs/model/UserProfileAuthenticationResult.java new file mode 100644 index 00000000..3169edbb --- /dev/null +++ b/gsrs-core-entities/src/main/java/gsrs/model/UserProfileAuthenticationResult.java @@ -0,0 +1,28 @@ +package gsrs.model; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +public class UserProfileAuthenticationResult { + private boolean matchesRepository; + + public boolean matchesRepository() { + return matchesRepository; + } + + public void setMatchesRepository(boolean matchesRepository) { + this.matchesRepository = matchesRepository; + } + + public boolean needsSave() { + return needsSave; + } + + public void setNeedsSave(boolean needsSave) { + this.needsSave = needsSave; + } + + private boolean needsSave; +} diff --git a/gsrs-core-entities/src/main/java/ix/core/models/UserProfile.java b/gsrs-core-entities/src/main/java/ix/core/models/UserProfile.java index dd744772..8a8f1237 100644 --- a/gsrs-core-entities/src/main/java/ix/core/models/UserProfile.java +++ b/gsrs-core-entities/src/main/java/ix/core/models/UserProfile.java @@ -1,17 +1,20 @@ package ix.core.models; -import ch.qos.logback.core.rolling.helper.TokenConverter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.ObjectMapper; import gov.nih.ncats.common.util.CachedSupplier; import gov.nih.ncats.common.util.TimeUtil; +import gsrs.model.UserProfileAuthenticationResult; import gsrs.security.TokenConfiguration; import gsrs.springUtils.StaticContextAccessor; +import gsrs.util.GsrsPasswordHasher; +import gsrs.util.Hasher; +import gsrs.util.LegacyTypeSalter; +import gsrs.util.Salter; import ix.utils.Util; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import javax.persistence.*; import java.util.*; @@ -22,7 +25,14 @@ @SequenceGenerator(name = "LONG_SEQ_ID", sequenceName = "ix_core_userprof_seq", allocationSize = 1) @EntityListeners(UserProfileEntityProcessor.class) public class UserProfile extends IxModel{ - private static ObjectMapper om = new ObjectMapper(); + private final static String SALT_PREFIX = "G"; + + private static ObjectMapper om = new ObjectMapper(); + + //todo: look into autowiring the salter and hasher + private static Salter salter = new LegacyTypeSalter(new GsrsPasswordHasher(), SALT_PREFIX); + + private static Hasher hasher = new GsrsPasswordHasher(); private static CachedSupplier GUEST_PROF= CachedSupplier.of(()->{ UserProfile up = new UserProfile(new Principal("GUEST")); @@ -182,21 +192,38 @@ public boolean acceptToken(String token) { return false; } - public boolean acceptPassword(String password) { - if (this.hashp == null || this.salt == null) - return false; - return this.hashp.equals(Util.encrypt(password, this.salt)); + public UserProfileAuthenticationResult acceptPassword(String password) { + UserProfileAuthenticationResult result = new UserProfileAuthenticationResult(false, false); + if (this.hashp == null || this.salt == null) { + return result; + } + boolean pwOk = this.hashp.equals(hasher.hash(password, this.salt)); + log.trace("pwOk: {}", pwOk); + boolean legacyPwOk = false; + //when authentication using latest algorithms fails, see if the password works with the legacy methods + if( !pwOk) { + legacyPwOk= this.hashp.equals(Util.encrypt(password, this.salt)); + } + if( legacyPwOk && !salter.mayBeOneOfMine(this.salt)) { + //we have a pw assigned using the older algorithm so we need to resalt and rehash + log.trace("going to request rehash of password"); + setPassword(password); + result.setNeedsSave(true); + } + result.setMatchesRepository(pwOk || legacyPwOk); + return result; } public void setPassword(String password) { if (password == null || password.length() <= 0) { password = UUID.randomUUID().toString(); } - this.salt = Util.generateSalt(); - this.hashp = Util.encrypt(password, this.salt); + this.salt = salter.generateSalt(); + this.hashp = hasher.hash(password, salt); //Util.encrypt(password, this.salt); setIsDirty("salt"); setIsDirty("hashp"); } + @Indexable(indexed = false) @JsonIgnore public String getEncodePassword(){ diff --git a/gsrs-core-entities/src/test/java/gsrs/LegacySalterTests.java b/gsrs-core-entities/src/test/java/gsrs/LegacySalterTests.java new file mode 100644 index 00000000..7c608969 --- /dev/null +++ b/gsrs-core-entities/src/test/java/gsrs/LegacySalterTests.java @@ -0,0 +1,38 @@ +package gsrs; + +import gsrs.util.GsrsPasswordHasher; +import gsrs.util.Hasher; +import gsrs.util.LegacyTypeSalter; +import gsrs.util.Salter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class LegacySalterTests { + + private final String SALT_PREFIX = "G"; + + private final Hasher hasher = new GsrsPasswordHasher(); + + private final Salter salter = new LegacyTypeSalter(hasher, SALT_PREFIX); + + @Test + void testGenerateSalt() { + String salt1 = salter.generateSalt(); + Assertions.assertNotNull(salt1); + System.out.printf("salt: %s\n", salt1); + } + + @Test + void testGenerateOwnSalt() { + String salt1 = salter.generateSalt(); + Assertions.assertTrue(salter.mayBeOneOfMine(salt1)); + } + + @Test + void testGenerateNotOwnSalt() { + String salt1 = salter.generateSalt(); + String changedSalt = salt1.replace('G', 'N'); + Assertions.assertFalse(salter.mayBeOneOfMine(changedSalt)); + } + +} diff --git a/gsrs-core-test/pom.xml b/gsrs-core-test/pom.xml index fd0b93c7..39e367fc 100644 --- a/gsrs-core-test/pom.xml +++ b/gsrs-core-test/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-core/pom.xml b/gsrs-core/pom.xml index 1c8ef9ff..f9eea0ee 100644 --- a/gsrs-core/pom.xml +++ b/gsrs-core/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-core/src/main/java/gsrs/util/GsrsPasswordHasher.java b/gsrs-core/src/main/java/gsrs/util/GsrsPasswordHasher.java new file mode 100644 index 00000000..3955e2d1 --- /dev/null +++ b/gsrs-core/src/main/java/gsrs/util/GsrsPasswordHasher.java @@ -0,0 +1,62 @@ +package gsrs.util; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +@Slf4j +public class GsrsPasswordHasher implements Hasher { + + String preferredHashAlgorithm = "PBKDF2"; + static int iterations = 1000; + static String characterSet ="utf8"; + + private final static String HASHING_ALGORITHM = "PBKDF2WithHmacSHA512"; + + @Override + public String getHashType() { + return this.preferredHashAlgorithm; + } + + @Override + public String hash(String... values) { + if (values == null) { + return null; + } + try { + if(preferredHashAlgorithm.equals("PBKDF2")) { + return hash(values[0], values.length > 1 ? values[1] : null, iterations); + } + MessageDigest md = MessageDigest.getInstance(preferredHashAlgorithm); + for (String v : values) { + md.update(v.getBytes(characterSet)); + } + return toHex(md.digest()); + } catch (Exception ex) { + log.error("Can't generate hash!", ex); + throw new RuntimeException(ex); + } + } + + public static String toHex(byte[] d) { + StringBuilder sb = new StringBuilder(); + for (byte b : d) { + sb.append(String.format("%1$02x", b & 0xff)); + } + return sb.toString(); + } + + public static String hash(String input, String salt, int iterations) throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException { + PBEKeySpec spec = new PBEKeySpec(input.toCharArray(), salt != null ? salt.getBytes(characterSet) : + input.getBytes(characterSet), iterations, 64 * 8); + SecretKeyFactory skf = SecretKeyFactory.getInstance(HASHING_ALGORITHM); + + byte[] hash = skf.generateSecret(spec).getEncoded(); + return toHex(hash); + } +} diff --git a/gsrs-core/src/main/java/gsrs/util/Hasher.java b/gsrs-core/src/main/java/gsrs/util/Hasher.java new file mode 100644 index 00000000..1b7370ca --- /dev/null +++ b/gsrs-core/src/main/java/gsrs/util/Hasher.java @@ -0,0 +1,8 @@ +package gsrs.util; + +public interface Hasher { + + String getHashType(); + + String hash(String... values); +} diff --git a/gsrs-core/src/main/java/gsrs/util/LegacyTypeSalter.java b/gsrs-core/src/main/java/gsrs/util/LegacyTypeSalter.java new file mode 100644 index 00000000..89de3fa0 --- /dev/null +++ b/gsrs-core/src/main/java/gsrs/util/LegacyTypeSalter.java @@ -0,0 +1,34 @@ +package gsrs.util; + +import gov.nih.ncats.common.util.TimeUtil; +import lombok.Data; + +import static java.lang.String.valueOf; + +@Data +public class LegacyTypeSalter implements Salter { + + Hasher hasher; + + String prefix = ""; + + public LegacyTypeSalter(Hasher newHasher, String newPrefix) { + hasher = newHasher; + prefix = newPrefix; + } + @Override + public void setHasher(Hasher hasher) { + this.hasher = hasher; + } + + @Override + public String generateSalt() { + String text = "---" + TimeUtil.getCurrentDate().toString() + "---" + String.valueOf(Math.random()) + "---"; + return prefix + hasher.hash(text); + } + + @Override + public boolean mayBeOneOfMine(String testHash) { + return testHash != null && testHash.startsWith(prefix); + } +} diff --git a/gsrs-core/src/main/java/gsrs/util/Salter.java b/gsrs-core/src/main/java/gsrs/util/Salter.java new file mode 100644 index 00000000..eb43a72e --- /dev/null +++ b/gsrs-core/src/main/java/gsrs/util/Salter.java @@ -0,0 +1,10 @@ +package gsrs.util; + +public interface Salter { + + void setHasher(Hasher hasher); + + String generateSalt(); + + boolean mayBeOneOfMine(String testHash); +} diff --git a/gsrs-data-exchange/pom.xml b/gsrs-data-exchange/pom.xml index 91aeee39..533484ed 100644 --- a/gsrs-data-exchange/pom.xml +++ b/gsrs-data-exchange/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-discovery/pom.xml b/gsrs-discovery/pom.xml index fb57e785..cc41afc7 100644 --- a/gsrs-discovery/pom.xml +++ b/gsrs-discovery/pom.xml @@ -10,7 +10,7 @@ gov.nih.ncats gsrs-discovery - 3.1 + 3.1.1-SNAPSHOT gsrs-discovery Demo project for Spring Boot diff --git a/gsrs-rest-api/pom.xml b/gsrs-rest-api/pom.xml index 7016d341..55f79121 100644 --- a/gsrs-rest-api/pom.xml +++ b/gsrs-rest-api/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-scheduled-tasks/pom.xml b/gsrs-scheduled-tasks/pom.xml index 2d061a54..387a5b15 100644 --- a/gsrs-scheduled-tasks/pom.xml +++ b/gsrs-scheduled-tasks/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-spring-akka/pom.xml b/gsrs-spring-akka/pom.xml index 6c2ec8ba..c7a91e11 100644 --- a/gsrs-spring-akka/pom.xml +++ b/gsrs-spring-akka/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-spring-boot-autoconfigure/pom.xml b/gsrs-spring-boot-autoconfigure/pom.xml index 525ead72..c82f3dff 100644 --- a/gsrs-spring-boot-autoconfigure/pom.xml +++ b/gsrs-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/AbstractGsrsEntityController.java b/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/AbstractGsrsEntityController.java index 4ee2d7e5..105670c1 100644 --- a/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/AbstractGsrsEntityController.java +++ b/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/AbstractGsrsEntityController.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; import java.net.URLDecoder; import java.security.Principal; import java.util.ArrayList; @@ -16,8 +18,12 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.persistence.EntityManager; +import javax.persistence.Id; +import javax.persistence.metamodel.Metamodel; import javax.servlet.http.HttpServletRequest; +import org.hibernate.metadata.ClassMetadata; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; @@ -76,8 +82,6 @@ public abstract class AbstractGsrsEntityController page(@RequestParam(value = "top", defaultValue = " @RequestParam(value = "skip", defaultValue = "0") long skip, @RequestParam(value = "order", required = false) String order, @RequestParam Map queryParameters){ - - + Page page = getEntityService().page(new OffsetBasedPageRequest(skip, top,parseSortFromOrderParam(order))); String view=queryParameters.get("view"); @@ -430,10 +433,27 @@ public ResponseEntity page(@RequestParam(value = "top", defaultValue = " return new ResponseEntity<>(new PagedResult(page, queryParameters), HttpStatus.OK); } - private Sort parseSortFromOrderParam(String order){ - //match Gsrs Play API - if(order ==null || order.trim().isEmpty()){ - return Sort.sort(getEntityService().getEntityClass()); + private Sort parseSortFromOrderParam(String order){ + if(order == null || order.trim().isEmpty()){ + Field[] fields = getEntityService().getEntityClass().getFields(); + + boolean found = false; + String name = ""; + for(Field field: fields) { + if(found) + break; + name = field.getName(); + Annotation[] annotations = field.getAnnotations(); + if(annotations.length > 0) { + for(Annotation annotation : annotations) { + if(annotation.annotationType().equals(Id.class)) { + found = true; + break; + } + } + } + } + return Sort.by(Sort.Direction.ASC, name); } char firstChar = order.charAt(0); if('$'==firstChar){ diff --git a/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/AbstractLegacyTextSearchGsrsEntityController.java b/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/AbstractLegacyTextSearchGsrsEntityController.java index 42988367..773c991b 100644 --- a/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/AbstractLegacyTextSearchGsrsEntityController.java +++ b/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/AbstractLegacyTextSearchGsrsEntityController.java @@ -182,9 +182,11 @@ public ResponseEntity forceFullReindex(@RequestParam(value= "wipeIndex", default @GetGsrsRestApiMapping(value="/@reindexBulk({id})", apiVersions = 1) public ResponseEntity bulkReindexStatus(@PathVariable("id") String id, @RequestParam Map queryParameters, - HttpServletRequest request){ + HttpServletRequest request){ + + String self_url = StaticContextAccessor.getBean(IxContext.class).getEffectiveAdaptedURI(request).toString(); return Optional.ofNullable(reindexing.get(id)).map(o->{ - o.set_self(request.getRequestURL().toString()); + o.set_self(self_url); return new ResponseEntity<>(o, HttpStatus.OK); }) .map(oo->(ResponseEntity)oo) diff --git a/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/LoginController.java b/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/LoginController.java index aad73967..95f0be1f 100644 --- a/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/LoginController.java +++ b/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/controller/LoginController.java @@ -2,6 +2,7 @@ import gsrs.cache.GsrsCache; import gsrs.controller.hateoas.GsrsUnwrappedEntityModel; +import gsrs.model.UserProfileAuthenticationResult; import gsrs.repository.GroupRepository; import gsrs.repository.SessionRepository; import gsrs.repository.UserProfileRepository; @@ -10,7 +11,6 @@ import ix.core.models.Session; import ix.core.models.UserProfile; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -135,7 +135,8 @@ public ResponseEntity changePassword(Principal principal, return gsrsControllerConfiguration.handleBadRequest(400,"password can not be blank or all whitespace", queryParameters); } //check old password - if(!up.acceptPassword(passwordChangeRequest.getOldPassword())){ + UserProfileAuthenticationResult authenticationResult =up.acceptPassword(passwordChangeRequest.getOldPassword()); + if(!authenticationResult.matchesRepository()){ return gsrsControllerConfiguration.unauthorized("incorrect password", queryParameters); } up.setPassword(passwordChangeRequest.getNewPassword()); diff --git a/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/security/LegacyAuthenticationFilter.java b/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/security/LegacyAuthenticationFilter.java index 989b725a..a9ae172b 100644 --- a/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/security/LegacyAuthenticationFilter.java +++ b/gsrs-spring-boot-autoconfigure/src/main/java/gsrs/security/LegacyAuthenticationFilter.java @@ -12,7 +12,9 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.transaction.Transactional; +import gsrs.model.UserProfileAuthenticationResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; @@ -172,9 +174,22 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } if(up!=null && up.active){ - if(up.acceptPassword(pass)){ + UserProfileAuthenticationResult authenticationResult =up.acceptPassword(pass); + if(authenticationResult.matchesRepository()){ //valid password! - auth = new UserProfilePasswordAuthentication(up); + if(authenticationResult.needsSave()){ + UserProfile finalUp = up; + TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager); + UserProfile savedUp=transactionTemplate.execute(status -> { + Optional opt = Optional.ofNullable(repository.findByUser_UsernameIgnoreCase(finalUp.user.username)); + opt.get().setPassword(pass); + saveUserProfile(opt.get()); + return opt.get(); + }); + auth = new UserProfilePasswordAuthentication(savedUp); + } else { + auth = new UserProfilePasswordAuthentication(up); + } }else{ throw new BadCredentialsException("invalid credentials for username: " + username); @@ -263,4 +278,10 @@ private UserProfile autoregisterNewUser(String username, String email, Listrepository.saveAndFlush(finalUp)); + } return new UserProfilePasswordAuthentication(up); - }else{ throw new BadCredentialsException("invalid credentials for username" + auth.getUsername()); } diff --git a/gsrs-spring-boot-starter/pom.xml b/gsrs-spring-boot-starter/pom.xml index d910606b..e97432df 100644 --- a/gsrs-spring-boot-starter/pom.xml +++ b/gsrs-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-spring-legacy-cache/pom.xml b/gsrs-spring-legacy-cache/pom.xml index 29d2226e..33663dc5 100644 --- a/gsrs-spring-legacy-cache/pom.xml +++ b/gsrs-spring-legacy-cache/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 diff --git a/gsrs-spring-legacy-indexer/pom.xml b/gsrs-spring-legacy-indexer/pom.xml index 128c01d9..5356b173 100644 --- a/gsrs-spring-legacy-indexer/pom.xml +++ b/gsrs-spring-legacy-indexer/pom.xml @@ -5,7 +5,7 @@ gsrs-spring-boot gov.nih.ncats - 3.1 + 3.1.1-SNAPSHOT 4.0.0 @@ -117,10 +117,17 @@ - +