From 489f8801bbb2c74f76c5e0a89e4d7a5e5ab5916b Mon Sep 17 00:00:00 2001 From: jnbdz Date: Sun, 9 Aug 2026 09:45:10 -0400 Subject: [PATCH] Allow mapping authentication query columns to user attributes The SQL authentication provider discards every column of the authentication query except the password, forcing a second query to load user data such as its id right after authentication. Add a SqlAuthentication.create overload taking a mapping function that receives the authenticated row and returns a JSON object to merge into the user attributes. The password remains expected in the first column and options stay data only, following the guidance in the issue. Fixes eclipse-vertx/vertx-auth#694 --- .../src/main/asciidoc/index.adoc | 12 ++++++++++ .../main/java/examples/AuthSqlExamples.java | 15 ++++++++++++ .../ext/auth/sqlclient/SqlAuthentication.java | 21 ++++++++++++++++ .../sqlclient/impl/SqlAuthenticationImpl.java | 15 ++++++++++++ .../test/java/io/vertx/tests/MySQLTest.java | 24 +++++++++++++++++++ .../test/resources/mysql-auth-ddl-test.sql | 6 +++-- 6 files changed, 91 insertions(+), 2 deletions(-) diff --git a/vertx-auth-sql-client/src/main/asciidoc/index.adoc b/vertx-auth-sql-client/src/main/asciidoc/index.adoc index 352b2293f..b2af96727 100644 --- a/vertx-auth-sql-client/src/main/asciidoc/index.adoc +++ b/vertx-auth-sql-client/src/main/asciidoc/index.adoc @@ -39,6 +39,18 @@ The out of the box config assumes certain queries for authentication and authori {@link io.vertx.ext.auth.sqlclient.SqlAuthorizationOptions#setPermissionsQuery(String)} and {@link io.vertx.ext.auth.sqlclient.SqlAuthorizationOptions#setRolesQuery(String)}, if you want to use them with a different database schema. +When the authentication query returns more columns than the password, the extra columns can be mapped to attributes of the +authenticated {@link io.vertx.ext.auth.User} by providing a mapping function: + +[source,$lang] +---- +{@link examples.AuthSqlExamples#example10} +---- + +The password is always expected in the first column, any other column is available to the mapping function and the returned +JSON object is merged into the user attributes. This avoids a second query to load user data, such as its id, right after +authentication. + The basic data definition for the storage should look like this: [source,sql] diff --git a/vertx-auth-sql-client/src/main/java/examples/AuthSqlExamples.java b/vertx-auth-sql-client/src/main/java/examples/AuthSqlExamples.java index 6f7cceb9a..ccae8c54b 100644 --- a/vertx-auth-sql-client/src/main/java/examples/AuthSqlExamples.java +++ b/vertx-auth-sql-client/src/main/java/examples/AuthSqlExamples.java @@ -17,6 +17,7 @@ package examples; import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; import io.vertx.ext.auth.User; import io.vertx.ext.auth.prng.VertxContextPRNG; import io.vertx.ext.auth.authentication.AuthenticationProvider; @@ -46,6 +47,20 @@ public void example5(Vertx vertx, SqlClient sqlClient) { SqlAuthentication.create(sqlClient, options); } + public void example10(Vertx vertx, SqlClient sqlClient) { + + SqlAuthenticationOptions options = new SqlAuthenticationOptions() + // the password is expected in the first column, any other + // column is available to the attribute mapper + .setAuthenticationQuery( + "SELECT password, email FROM users WHERE username = ?"); + + AuthenticationProvider authenticationProvider = + SqlAuthentication.create(sqlClient, options, row -> + new JsonObject() + .put("email", row.getString("email"))); + } + public void example6(AuthenticationProvider authProvider) { Credentials authInfo = new UsernamePasswordCredentials( diff --git a/vertx-auth-sql-client/src/main/java/io/vertx/ext/auth/sqlclient/SqlAuthentication.java b/vertx-auth-sql-client/src/main/java/io/vertx/ext/auth/sqlclient/SqlAuthentication.java index 00198fbf6..4bf8c108c 100644 --- a/vertx-auth-sql-client/src/main/java/io/vertx/ext/auth/sqlclient/SqlAuthentication.java +++ b/vertx-auth-sql-client/src/main/java/io/vertx/ext/auth/sqlclient/SqlAuthentication.java @@ -17,11 +17,14 @@ package io.vertx.ext.auth.sqlclient; import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.json.JsonObject; import io.vertx.ext.auth.authentication.AuthenticationProvider; import io.vertx.ext.auth.sqlclient.impl.SqlAuthenticationImpl; +import io.vertx.sqlclient.Row; import io.vertx.sqlclient.SqlClient; import java.util.Map; +import java.util.function.Function; /** * Factory interface for creating {@link io.vertx.ext.auth.authentication.AuthenticationProvider} instances that use the Vert.x SQL client. @@ -52,6 +55,24 @@ static SqlAuthentication create(SqlClient client, SqlAuthenticationOptions optio return new SqlAuthenticationImpl(client, options); } + /** + * Create a JDBC auth provider implementation that enriches the authenticated user with + * attributes extracted from the authentication query row. + *

+ * The authentication query is expected to return the password in the first column, any other + * column is available to the given {@code attributeMapper}. The JSON object returned by the + * mapper is merged into the {@link io.vertx.ext.auth.User#attributes()} of the authenticated + * user. + * + * @param client the JDBC client instance + * @param options authentication options + * @param attributeMapper maps the authenticated row to extra user attributes, may return {@code null} + * @return the auth provider + */ + static SqlAuthentication create(SqlClient client, SqlAuthenticationOptions options, Function attributeMapper) { + return new SqlAuthenticationImpl(client, options, attributeMapper); + } + /** * Hashes a password to be stored. *

diff --git a/vertx-auth-sql-client/src/main/java/io/vertx/ext/auth/sqlclient/impl/SqlAuthenticationImpl.java b/vertx-auth-sql-client/src/main/java/io/vertx/ext/auth/sqlclient/impl/SqlAuthenticationImpl.java index 0716fa454..e71cba308 100644 --- a/vertx-auth-sql-client/src/main/java/io/vertx/ext/auth/sqlclient/impl/SqlAuthenticationImpl.java +++ b/vertx-auth-sql-client/src/main/java/io/vertx/ext/auth/sqlclient/impl/SqlAuthenticationImpl.java @@ -28,9 +28,12 @@ import io.vertx.sqlclient.SqlClient; import io.vertx.sqlclient.Tuple; +import io.vertx.core.json.JsonObject; + import java.util.Collections; import java.util.Map; import java.util.Objects; +import java.util.function.Function; /** * @author Tim Fox @@ -39,11 +42,17 @@ public class SqlAuthenticationImpl implements SqlAuthentication { private final SqlClient client; private final SqlAuthenticationOptions options; + private final Function attributeMapper; private final HashingStrategy strategy = HashingStrategy.load(); public SqlAuthenticationImpl(SqlClient client, SqlAuthenticationOptions options) { + this(client, options, null); + } + + public SqlAuthenticationImpl(SqlClient client, SqlAuthenticationOptions options, Function attributeMapper) { this.client = Objects.requireNonNull(client); this.options = Objects.requireNonNull(options); + this.attributeMapper = attributeMapper; } @Override @@ -77,6 +86,12 @@ public Future authenticate(Credentials credentials) { User user = User.fromName(authInfo.getUsername()); // metadata "amr" user.principal().put("amr", Collections.singletonList("pwd")); + if (attributeMapper != null) { + JsonObject attributes = attributeMapper.apply(row); + if (attributes != null) { + user.attributes().mergeIn(attributes); + } + } return Future.succeededFuture(user); } else { return Future.failedFuture("Invalid username/password"); diff --git a/vertx-auth-sql-client/src/test/java/io/vertx/tests/MySQLTest.java b/vertx-auth-sql-client/src/test/java/io/vertx/tests/MySQLTest.java index e121efaab..17c0db23f 100644 --- a/vertx-auth-sql-client/src/test/java/io/vertx/tests/MySQLTest.java +++ b/vertx-auth-sql-client/src/test/java/io/vertx/tests/MySQLTest.java @@ -1,5 +1,6 @@ package io.vertx.tests; +import io.vertx.core.json.JsonObject; import io.vertx.ext.auth.User; import io.vertx.ext.auth.authentication.AuthenticationProvider; import io.vertx.ext.auth.authentication.Credentials; @@ -8,6 +9,7 @@ import io.vertx.ext.auth.authorization.PermissionBasedAuthorization; import io.vertx.ext.auth.authorization.RoleBasedAuthorization; import io.vertx.ext.auth.sqlclient.SqlAuthentication; +import io.vertx.ext.auth.sqlclient.SqlAuthenticationOptions; import io.vertx.ext.auth.sqlclient.SqlAuthorization; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; @@ -111,6 +113,28 @@ public void testAuthenticateBadUser(TestContext should) { }); } + @Test + public void testAuthenticateWithAttributeMapper(TestContext should) { + final Async test = should.async(); + + AuthenticationProvider authn = SqlAuthentication.create(mysql, + new SqlAuthenticationOptions() + .setAuthenticationQuery("SELECT password, email FROM users WHERE username = ?"), + row -> new JsonObject().put("email", row.getString("email"))); + + Credentials authInfo = new UsernamePasswordCredentials("lopus", "secret"); + + authn.authenticate(authInfo) + .onComplete(authenticate -> { + should.assertTrue(authenticate.succeeded()); + final User user = authenticate.result(); + should.assertNotNull(user); + should.assertEquals("lopus", user.principal().getString("username")); + should.assertEquals("lopus@vertx.io", user.attributes().getString("email")); + test.complete(); + }); + } + @Test public void testAuthoriseHasRole(TestContext should) { final Async test = should.async(); diff --git a/vertx-auth-sql-client/src/test/resources/mysql-auth-ddl-test.sql b/vertx-auth-sql-client/src/test/resources/mysql-auth-ddl-test.sql index 5d76201d9..8f80892c2 100644 --- a/vertx-auth-sql-client/src/test/resources/mysql-auth-ddl-test.sql +++ b/vertx-auth-sql-client/src/test/resources/mysql-auth-ddl-test.sql @@ -1,7 +1,8 @@ CREATE TABLE `users` ( username VARCHAR(255) NOT NULL, - password VARCHAR(255) NOT NULL + password VARCHAR(255) NOT NULL, + email VARCHAR(255) ); CREATE TABLE `users_roles` @@ -30,7 +31,8 @@ ALTER TABLE users_roles insert into users values ('lopus', - '$pbkdf2$1drH02tXcgS5ipJIf8v/AlL/qm3CjAgAp7Qt3hyJx/c$/lONU4cTa3ayMRJbHIup47nX/1HhysyzDA0dpoFpsf727LoGH2OZ+SyFCGtv/pIEZK3mQtJv+yjzD+W0quF6xg'); + '$pbkdf2$1drH02tXcgS5ipJIf8v/AlL/qm3CjAgAp7Qt3hyJx/c$/lONU4cTa3ayMRJbHIup47nX/1HhysyzDA0dpoFpsf727LoGH2OZ+SyFCGtv/pIEZK3mQtJv+yjzD+W0quF6xg', + 'lopus@vertx.io'); insert into roles_perms values ('dev', 'commit_code');