From a6cb22e28c9bad2d5e733cd4aa0d19dc0358e121 Mon Sep 17 00:00:00 2001 From: antoniodvr Date: Tue, 2 Feb 2021 09:13:14 +0100 Subject: [PATCH] Add custom password type to jpa-security --- docs/src/main/asciidoc/security-jpa.adoc | 35 +++++++++++- .../QuarkusSecurityJpaProcessor.java | 55 +++++++++++++++---- .../jpa/CustomPasswordMapperTest.java | 19 +++++++ .../security/jpa/CustomPasswordProvider.java | 14 +++++ .../jpa/CustomPasswordUserEntity.java | 27 +++++++++ .../application.properties | 8 +++ .../custom-password-mapper/import.sql | 3 + .../io/quarkus/security/jpa/Password.java | 6 ++ .../security/jpa/PasswordProvider.java | 10 ++++ .../io/quarkus/security/jpa/PasswordType.java | 4 ++ .../runtime/AbstractJpaIdentityProvider.java | 2 +- 11 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordMapperTest.java create mode 100644 extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordProvider.java create mode 100644 extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordUserEntity.java create mode 100644 extensions/security-jpa/deployment/src/test/resources/custom-password-mapper/application.properties create mode 100644 extensions/security-jpa/deployment/src/test/resources/custom-password-mapper/import.sql create mode 100644 extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/PasswordProvider.java diff --git a/docs/src/main/asciidoc/security-jpa.adoc b/docs/src/main/asciidoc/security-jpa.adoc index 33e413808244a..3c56b9017b985 100644 --- a/docs/src/main/asciidoc/security-jpa.adoc +++ b/docs/src/main/asciidoc/security-jpa.adoc @@ -208,7 +208,7 @@ The `security-jpa` extension is only initialized if there is a single entity ann <1> This annotation must be present on a single entity. It can be a regular Hibernate ORM entity or a Hibernate ORM with Panache entity as in this example. <2> This indicates the field used for the user name. -<3> This indicates the field used for the password. This defaults to using bcrypt hashed passwords, but you can also configure it for clear text passwords. +<3> This indicates the field used for the password. This defaults to using bcrypt hashed passwords, but you can also configure it for clear text passwords or custom passwords. <4> This indicates the comma-separated list of roles added to the target Principal representation attributes. <5> This method allows us to add users while hashing the password with the proper bcrypt hash. @@ -379,6 +379,39 @@ too). NOTE: with MCF you don't need dedicated columns to store the hashing algorithm, the iterations count or the salt because they're all stored in the hashed value. +You also have the possibility to store password using different hashing algorithm `@Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class)`: + +[source,java] +---- +@UserDefinition +@Table(name = "test_user") +@Entity +public class CustomPasswordUserEntity { + @Id + @GeneratedValue + public Long id; + + @Column(name = "username") + @Username + public String name; + + @Column(name = "password") + @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class) + public String pass; + + @Roles + public String role; +} + +public class CustomPasswordProvider implements PasswordProvider { + @Override + public Password getPassword(String pass) { + byte[] digest = DatatypeConverter.parseHexBinary(pass); + return SimpleDigestPassword.createRaw(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_SHA_256, digest); + } +} +---- + WARN: you can also store passwords in clear text with `@Password(PasswordType.CLEAR)` but we strongly recommend against it in production. diff --git a/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java b/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java index 1dec2bbfd3087..4fb7b6d070660 100644 --- a/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java +++ b/extensions/security-jpa/deployment/src/main/java/io/quarkus/security/jpa/deployment/QuarkusSecurityJpaProcessor.java @@ -37,6 +37,7 @@ import io.quarkus.gizmo.BranchResult; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -46,6 +47,7 @@ import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; import io.quarkus.security.jpa.Password; +import io.quarkus.security.jpa.PasswordProvider; import io.quarkus.security.jpa.PasswordType; import io.quarkus.security.jpa.Roles; import io.quarkus.security.jpa.RolesValue; @@ -110,9 +112,10 @@ void configureJpaAuthConfig(ApplicationIndexBuildItem index, annotatedRoles); AnnotationInstance passAnnotation = jpaSecurityDefinition.password.annotation(DOTNAME_PASSWORD); AnnotationValue passwordType = passAnnotation.value(); + AnnotationValue customPasswordProvider = passAnnotation.value("provider"); + generateIdentityProvider(index.getIndex(), jpaSecurityDefinition, - passwordType != null ? passwordType.asEnum() : PasswordType.MCF.name(), - beanProducer, panacheEntities); + passwordType, customPasswordProvider, beanProducer, panacheEntities); generateTrustedIdentityProvider(index.getIndex(), jpaSecurityDefinition, beanProducer, panacheEntities); @@ -141,7 +144,8 @@ private AnnotationTarget getSingleAnnotatedElement(Index index, DotName annotati return annotations.get(0).target(); } - private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecurityDefinition, String passwordType, + private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecurityDefinition, + AnnotationValue passwordTypeValue, AnnotationValue passwordProviderValue, BuildProducer beanProducer, Set panacheClasses) { GeneratedBeanGizmoAdaptor gizmoAdaptor = new GeneratedBeanGizmoAdaptor(beanProducer); @@ -152,6 +156,10 @@ private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecu .classOutput(gizmoAdaptor) .build()) { classCreator.addAnnotation(Singleton.class); + FieldDescriptor passwordProviderField = classCreator.getFieldCreator("passwordProvider", PasswordProvider.class) + .setModifiers(Modifier.PRIVATE) + .getFieldDescriptor(); + try (MethodCreator methodCreator = classCreator.getMethodCreator("authenticate", SecurityIdentity.class, EntityManager.class, UsernamePasswordAuthenticationRequest.class)) { methodCreator.setModifiers(Modifier.PUBLIC); @@ -178,25 +186,50 @@ private void generateIdentityProvider(Index index, JpaSecurityDefinition jpaSecu // :pass = user.pass | user.getPass() ResultHandle pass = jpaSecurityDefinition.password.readValue(methodCreator, userVar); - String getPasswordMethod; - if (passwordType == null) { - passwordType = PasswordType.MCF.name(); + + PasswordType passwordType = passwordTypeValue != null ? PasswordType.valueOf(passwordTypeValue.asEnum()) + : PasswordType.MCF; + + if (passwordType == PasswordType.CUSTOM && passwordProviderValue == null) { + throw new RuntimeException("Missing password provider for password type: " + passwordType); } - switch (PasswordType.valueOf(passwordType)) { + + ResultHandle objectToInvokeOn; + String passwordProviderClassStr; + String passwordProviderMethod; + switch (passwordType) { + case CUSTOM: + passwordProviderClassStr = passwordProviderValue.asString(); + passwordProviderMethod = "getPassword"; + ResultHandle passwordProviderInstanceField = methodCreator.readInstanceField(passwordProviderField, + methodCreator.getThis()); + BytecodeCreator trueBranch = methodCreator.ifNull(passwordProviderInstanceField).trueBranch(); + ResultHandle passwordProviderInstance = trueBranch + .newInstance(MethodDescriptor.ofConstructor(passwordProviderClassStr)); + trueBranch.writeInstanceField(passwordProviderField, trueBranch.getThis(), passwordProviderInstance); + trueBranch.close(); + objectToInvokeOn = methodCreator.readInstanceField(passwordProviderField, methodCreator.getThis()); + break; case CLEAR: - getPasswordMethod = "getClearPassword"; + passwordProviderClassStr = name; + passwordProviderMethod = "getClearPassword"; + objectToInvokeOn = methodCreator.getThis(); break; case MCF: - getPasswordMethod = "getMcfPassword"; + passwordProviderClassStr = name; + passwordProviderMethod = "getMcfPassword"; + objectToInvokeOn = methodCreator.getThis(); break; default: throw new RuntimeException("Unknown password type: " + passwordType); } + // :getPasswordMethod(:pass); ResultHandle storedPassword = methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(name, getPasswordMethod, org.wildfly.security.password.Password.class, + MethodDescriptor.ofMethod(passwordProviderClassStr, passwordProviderMethod, + org.wildfly.security.password.Password.class, String.class), - methodCreator.getThis(), pass); + objectToInvokeOn, pass); // Builder builder = checkPassword(storedPassword, request); ResultHandle builder = methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(name, "checkPassword", diff --git a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordMapperTest.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordMapperTest.java new file mode 100644 index 0000000000000..f98dc3c28b7be --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordMapperTest.java @@ -0,0 +1,19 @@ +package io.quarkus.security.jpa; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class CustomPasswordMapperTest extends JpaSecurityRealmTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(testClasses) + .addClasses(CustomPasswordUserEntity.class, CustomPasswordProvider.class) + .addAsResource("custom-password-mapper/import.sql", "import.sql") + .addAsResource("custom-password-mapper/application.properties", "application.properties")); + +} diff --git a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordProvider.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordProvider.java new file mode 100644 index 0000000000000..7c01ea35bb754 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordProvider.java @@ -0,0 +1,14 @@ +package io.quarkus.security.jpa; + +import javax.xml.bind.DatatypeConverter; + +import org.wildfly.security.password.Password; +import org.wildfly.security.password.interfaces.SimpleDigestPassword; + +public class CustomPasswordProvider implements PasswordProvider { + @Override + public Password getPassword(String pass) { + byte[] digest = DatatypeConverter.parseHexBinary(pass); + return SimpleDigestPassword.createRaw(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_SHA_256, digest); + } +} diff --git a/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordUserEntity.java b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordUserEntity.java new file mode 100644 index 0000000000000..f5d24c3188af1 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/java/io/quarkus/security/jpa/CustomPasswordUserEntity.java @@ -0,0 +1,27 @@ +package io.quarkus.security.jpa; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; + +@UserDefinition +@Table(name = "test_user") +@Entity +public class CustomPasswordUserEntity { + @Id + @GeneratedValue + public Long id; + + @Column(name = "username") + @Username + public String name; + + @Column(name = "password") + @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class) + public String pass; + + @Roles + public String role; +} diff --git a/extensions/security-jpa/deployment/src/test/resources/custom-password-mapper/application.properties b/extensions/security-jpa/deployment/src/test/resources/custom-password-mapper/application.properties new file mode 100644 index 0000000000000..2552afd82beeb --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/resources/custom-password-mapper/application.properties @@ -0,0 +1,8 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password=sa +quarkus.datasource.jdbc.url=jdbc:h2:mem:custom-password-mapper' + +quarkus.hibernate-orm.sql-load-script=import.sql +quarkus.hibernate-orm.database.generation=drop-and-create +#quarkus.hibernate-orm.log.sql=true diff --git a/extensions/security-jpa/deployment/src/test/resources/custom-password-mapper/import.sql b/extensions/security-jpa/deployment/src/test/resources/custom-password-mapper/import.sql new file mode 100644 index 0000000000000..a03c2d2438a45 --- /dev/null +++ b/extensions/security-jpa/deployment/src/test/resources/custom-password-mapper/import.sql @@ -0,0 +1,3 @@ +INSERT INTO test_user (id, username, password, role) VALUES (1, 'admin', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918', 'admin'); +INSERT INTO test_user (id, username, password, role) VALUES (2, 'user','04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb', 'user'); +INSERT INTO test_user (id, username, password, role) VALUES (3, 'noRoleUser','bc84dfdb831a33641357ef365ddafd8d2c2d190242893355dab9b33067e99083', ''); \ No newline at end of file diff --git a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/Password.java b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/Password.java index ad78d79fe4f82..2d1c302a75cb6 100644 --- a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/Password.java +++ b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/Password.java @@ -22,4 +22,10 @@ * Sets the password storage type. defaults to {@link PasswordType#MCF}. */ PasswordType value() default PasswordType.MCF; + + /** + * Sets a custom password provider when the type is {@link PasswordType#CUSTOM} + */ + Class provider() default PasswordProvider.class; + } diff --git a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/PasswordProvider.java b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/PasswordProvider.java new file mode 100644 index 0000000000000..10ac7d508896e --- /dev/null +++ b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/PasswordProvider.java @@ -0,0 +1,10 @@ +package io.quarkus.security.jpa; + +import org.wildfly.security.password.Password; + +/** + * Provides the {@link Password} according to how the password is hashed in the database. + */ +public interface PasswordProvider { + Password getPassword(String pass); +} diff --git a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/PasswordType.java b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/PasswordType.java index 62b5dc84ea3db..25975d3edbb21 100644 --- a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/PasswordType.java +++ b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/PasswordType.java @@ -4,6 +4,10 @@ * Describes how the password is hashed in the database. */ public enum PasswordType { + /** + * The password is stored hashed using a custom format. + */ + CUSTOM, /** * The password is stored hashed using bcrypt in the Modular Crypt Format. */ diff --git a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/AbstractJpaIdentityProvider.java b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/AbstractJpaIdentityProvider.java index 4f466a3cd0157..45fa5d261fbc5 100644 --- a/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/AbstractJpaIdentityProvider.java +++ b/extensions/security-jpa/runtime/src/main/java/io/quarkus/security/jpa/runtime/AbstractJpaIdentityProvider.java @@ -59,7 +59,7 @@ protected T getSingleUser(Query query) { } protected Password getClearPassword(String pass) { - return ClearPassword.createRaw("clear", pass.toCharArray()); + return ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, pass.toCharArray()); } protected Password getMcfPassword(String pass) {