Created
January 17, 2018 17:26
-
-
Save sonOfRa/2d3463d109fc217ecb4965ba188a65ac to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2014 Red Hat, Inc. | |
* | |
* All rights reserved. This program and the accompanying materials | |
* are made available under the terms of the Eclipse Public License v1.0 | |
* and Apache License v2.0 which accompanies this distribution. | |
* | |
* The Eclipse Public License is available at | |
* http://www.eclipse.org/legal/epl-v10.html | |
* | |
* The Apache License v2.0 is available at | |
* http://www.opensource.org/licenses/apache2.0.php | |
* | |
* You may elect to redistribute this code under either of these licenses. | |
*/ | |
package io.vertx.ext.auth.jdbc.impl; | |
import io.vertx.core.*; | |
import io.vertx.core.json.JsonArray; | |
import io.vertx.core.json.JsonObject; | |
import io.vertx.ext.auth.AuthProvider; | |
import io.vertx.ext.auth.PRNG; | |
import io.vertx.ext.auth.User; | |
import io.vertx.ext.auth.jdbc.JDBCAuth; | |
import io.vertx.ext.auth.jdbc.JDBCHashStrategy; | |
import io.vertx.ext.jdbc.JDBCClient; | |
import io.vertx.ext.sql.ResultSet; | |
import io.vertx.ext.sql.SQLConnection; | |
import java.nio.charset.StandardCharsets; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.util.Collections; | |
import java.util.List; | |
import java.util.function.Consumer; | |
/** | |
* @author <a href="http://tfox.org">Tim Fox</a> | |
*/ | |
public class JDBCAuthImpl implements AuthProvider, JDBCAuth { | |
private JDBCClient client; | |
private String authenticateQuery = DEFAULT_AUTHENTICATE_QUERY; | |
private String rolesQuery = DEFAULT_ROLES_QUERY; | |
private String permissionsQuery = DEFAULT_PERMISSIONS_QUERY; | |
private String rolePrefix = DEFAULT_ROLE_PREFIX; | |
private JDBCHashStrategy strategy; | |
public JDBCAuthImpl(Vertx vertx, JDBCClient client) { | |
this.client = client; | |
strategy = new DefaultHashStrategy(vertx); | |
} | |
@Override | |
public void authenticate(JsonObject authInfo, Handler<AsyncResult<User>> resultHandler) { | |
String username = authInfo.getString("username"); | |
if (username == null) { | |
resultHandler.handle(Future.failedFuture("authInfo must contain username in 'username' field")); | |
return; | |
} | |
String password = authInfo.getString("password"); | |
if (password == null) { | |
resultHandler.handle(Future.failedFuture("authInfo must contain password in 'password' field")); | |
return; | |
} | |
executeQuery(authenticateQuery, new JsonArray().add(username), resultHandler, rs -> { | |
switch (rs.getNumRows()) { | |
case 0: { | |
// Unknown user/password | |
resultHandler.handle(Future.failedFuture("Invalid username/password")); | |
break; | |
} | |
case 1: { | |
JsonArray row = rs.getResults().get(0); | |
Optional<Boolean> verifyResult = strategy.verify(password, row); | |
if (verifyResult.isPresent()) { | |
if (verifyResult.get()) { | |
resultHandler.handle(Future.succeededFuture(new JDBCUser(username, this, rolePrefix))) | |
} else { | |
resultHandler.handle(Future.failedFuture("Invalid username/password")); | |
} | |
} else { | |
String hashedStoredPwd = strategy.getHashedStoredPwd(row); | |
String salt = strategy.getSalt(row); | |
// extract the version (-1 means no version) | |
int version = -1; | |
int sep = hashedStoredPwd.lastIndexOf('$'); | |
if (sep != -1) { | |
try { | |
version = Integer.parseInt(hashedStoredPwd.substring(sep + 1)); | |
} catch (NumberFormatException e) { | |
// the nonce version is not a number | |
resultHandler.handle(Future.failedFuture("Invalid nonce version: " + version)); | |
return; | |
} | |
} | |
String hashedPassword = strategy.computeHash(password, salt, version); | |
if (hashedStoredPwd.equals(hashedPassword)) { | |
resultHandler.handle(Future.succeededFuture(new JDBCUser(username, this, rolePrefix))); | |
} else { | |
resultHandler.handle(Future.failedFuture("Invalid username/password")); | |
} | |
break; | |
} | |
} | |
default: { | |
// More than one row returned! | |
resultHandler.handle(Future.failedFuture("Failure in authentication")); | |
break; | |
} | |
} | |
}); | |
} | |
@Override | |
public JDBCAuth setAuthenticationQuery(String authenticationQuery) { | |
this.authenticateQuery = authenticationQuery; | |
return this; | |
} | |
@Override | |
public JDBCAuth setRolesQuery(String rolesQuery) { | |
this.rolesQuery = rolesQuery; | |
return this; | |
} | |
@Override | |
public JDBCAuth setPermissionsQuery(String permissionsQuery) { | |
this.permissionsQuery = permissionsQuery; | |
return this; | |
} | |
@Override | |
public JDBCAuth setRolePrefix(String rolePrefix) { | |
this.rolePrefix = rolePrefix; | |
return this; | |
} | |
@Override | |
public JDBCAuth setHashStrategy(JDBCHashStrategy strategy) { | |
this.strategy = strategy; | |
return this; | |
} | |
<T> void executeQuery(String query, JsonArray params, Handler<AsyncResult<T>> resultHandler, | |
Consumer<ResultSet> resultSetConsumer) { | |
client.getConnection(res -> { | |
if (res.succeeded()) { | |
SQLConnection conn = res.result(); | |
conn.queryWithParams(query, params, queryRes -> { | |
if (queryRes.succeeded()) { | |
ResultSet rs = queryRes.result(); | |
resultSetConsumer.accept(rs); | |
} else { | |
resultHandler.handle(Future.failedFuture(queryRes.cause())); | |
} | |
conn.close(closeRes -> { | |
}); | |
}); | |
} else { | |
resultHandler.handle(Future.failedFuture(res.cause())); | |
} | |
}); | |
} | |
@Override | |
public String computeHash(String password, String salt, int version) { | |
return strategy.computeHash(password, salt, version); | |
} | |
@Override | |
public String generateSalt() { | |
return strategy.generateSalt(); | |
} | |
@Override | |
public JDBCAuth setNonces(JsonArray nonces) { | |
strategy.setNonces(nonces.getList()); | |
return this; | |
} | |
String getRolesQuery() { | |
return rolesQuery; | |
} | |
String getPermissionsQuery() { | |
return permissionsQuery; | |
} | |
private class DefaultHashStrategy implements JDBCHashStrategy { | |
private final PRNG random; | |
private List<String> nonces; | |
DefaultHashStrategy(Vertx vertx) { | |
random = new PRNG(vertx); | |
} | |
@Override | |
public String generateSalt() { | |
byte[] salt = new byte[32]; | |
random.nextBytes(salt); | |
return bytesToHex(salt); | |
} | |
@Override | |
public String computeHash(String password, String salt, int version) { | |
try { | |
String concat = | |
(salt == null ? "" : salt) + | |
password; | |
if (version >= 0) { | |
if (nonces == null) { | |
// the nonce version is not a number | |
throw new VertxException("nonces are not available"); | |
} | |
if (version < nonces.size()) { | |
concat += nonces.get(version); | |
} | |
} | |
MessageDigest md = MessageDigest.getInstance("SHA-512"); | |
byte[] bHash = md.digest(concat.getBytes(StandardCharsets.UTF_8)); | |
if (version >= 0) { | |
return bytesToHex(bHash) + '$' + version; | |
} else { | |
return bytesToHex(bHash); | |
} | |
} catch (NoSuchAlgorithmException e) { | |
throw new VertxException(e); | |
} | |
} | |
@Override | |
public String getHashedStoredPwd(JsonArray row) { | |
return row.getString(0); | |
} | |
@Override | |
public String getSalt(JsonArray row) { | |
return row.getString(1); | |
} | |
@Override | |
public void setNonces(List<String> nonces) { | |
this.nonces = Collections.unmodifiableList(nonces); | |
} | |
private final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray(); | |
private String bytesToHex(byte[] bytes) { | |
char[] chars = new char[bytes.length * 2]; | |
for (int i = 0; i < bytes.length; i++) { | |
int x = 0xFF & bytes[i]; | |
chars[i * 2] = HEX_CHARS[x >>> 4]; | |
chars[1 + i * 2] = HEX_CHARS[0x0F & x]; | |
} | |
return new String(chars); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2014 Red Hat, Inc. | |
* | |
* All rights reserved. This program and the accompanying materials | |
* are made available under the terms of the Eclipse Public License v1.0 | |
* and Apache License v2.0 which accompanies this distribution. | |
* | |
* The Eclipse Public License is available at | |
* http://www.eclipse.org/legal/epl-v10.html | |
* | |
* The Apache License v2.0 is available at | |
* http://www.opensource.org/licenses/apache2.0.php | |
* | |
* You may elect to redistribute this code under either of these licenses. | |
*/ | |
package io.vertx.ext.auth.jdbc; | |
import io.vertx.core.json.JsonArray; | |
import java.util.List; | |
import java.util.Optional; | |
/** | |
* Determines how the hashing is computed in the implementation | |
* | |
* You can implement this to provide a different hashing strategy to the default. | |
* | |
* @author <a href="http://tfox.org">Tim Fox</a> | |
*/ | |
public interface JDBCHashStrategy { | |
/** | |
* Compute a random salt. | |
* | |
* @return a non null salt value | |
*/ | |
String generateSalt(); | |
/** | |
* Compute the hashed password given the unhashed password and the salt | |
* @param password the unhashed password | |
* @param salt the salt | |
* @param version the nonce version to use | |
* @return the hashed password | |
*/ | |
String computeHash(String password, String salt, int version); | |
/** | |
* Retrieve the hashed password from the result of the authentication query | |
* @param row the row | |
* @return the hashed password | |
*/ | |
String getHashedStoredPwd(JsonArray row); | |
/** | |
* Retrieve the salt from the result of the authentication query | |
* @param row the row | |
* @return the salt | |
*/ | |
String getSalt(JsonArray row); | |
/** | |
* Sets a ordered list of nonces where each position corresponds to a version. | |
* | |
* The nonces are supposed not to be stored in the underlying jdbc storage but to | |
* be provided as a application configuration. The idea is to add one extra variable | |
* to the hash function in order to make breaking the passwords using rainbow tables | |
* or precomputed hashes harder. Leaving the attacker only with the brute force | |
* approach. | |
* | |
* @param nonces a List of non null Strings. | |
*/ | |
void setNonces(List<String> nonces); | |
/** | |
* Attempt to verify the given password against a database row. | |
* | |
* Modern password hashing schemes often come with their own verify() implementation, | |
* taking into account settings inside the hash result string. If this is the case, | |
* implementations of this interface should return Optional.of(true) if authentication succeeds, | |
* and Optional.of(false) if authentication fails. In order to delegate authentication to JDBCAuthImpl, | |
* this method should return Optional.empty() like its default implementation. | |
* | |
* @param password the password to verify | |
* @param row the row | |
*/ | |
default Optional<Boolean> verify(String password, JsonArray row) { | |
return Optional.empty(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment