KEYCLOAK-1900 Add password hash SPI and provider
- Default encoder set to Pbkdf2PasswordEncoder
This commit is contained in:
1 parent
4a57bcc0b4
commit
162dd4051d
32 files changed
+257
-78
No files matched your search
@@ -0,0 +1,41 @@
|
||||
package org.keycloak.hash;
|
||||
|
||||
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
|
||||
|
||||
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||
*/
|
||||
public class DefaultPasswordHashProvider implements PasswordHashProvider {
|
||||
|
||||
private final String algorithm;
|
||||
private final int iterations;
|
||||
|
||||
public DefaultPasswordHashProvider() {
|
||||
this.algorithm = "pbkdf2";
|
||||
this.iterations = 1;
|
||||
}
|
||||
|
||||
public String encode(String rawPassword, byte[] salt) {
|
||||
return this.encode(rawPassword, salt, this.iterations);
|
||||
}
|
||||
|
||||
public String encode(String rawPassword, byte[] salt, int iterations) {
|
||||
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(salt, iterations);
|
||||
return encoder.encode(rawPassword);
|
||||
}
|
||||
|
||||
public boolean verify(String rawPassword, String encodedPassword, byte[] salt) {
|
||||
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(salt);
|
||||
return encoder.verify(rawPassword, encodedPassword);
|
||||
}
|
||||
|
||||
public String getAlgorithm() {
|
||||
return this.algorithm;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.keycloak.hash;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||
*/
|
||||
public class DefaultPasswordHashProviderFactory implements PasswordHashProviderFactory {
|
||||
|
||||
@Override
|
||||
public PasswordHashProvider create(KeycloakSession session) {
|
||||
return new DefaultPasswordHashProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "pbkdf2";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.keycloak.hash;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||
*/
|
||||
public interface PasswordHashProvider extends Provider {
|
||||
|
||||
String encode(String rawPassword, byte[] salt);
|
||||
|
||||
String encode(String rawPassword, byte[] salt, int iterations);
|
||||
|
||||
boolean verify(String rawPassword, String encodedPassword, byte[] salt);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.keycloak.hash;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||
*/
|
||||
public interface PasswordHashProviderFactory extends ProviderFactory<PasswordHashProvider> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.keycloak.hash;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||
*/
|
||||
public class PasswordHashSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "password-hash";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return PasswordHashProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return PasswordHashProviderFactory.class;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ public interface Constants {
|
||||
String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
|
||||
String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS;
|
||||
|
||||
String DEFAULT_HASH_ALGORITHM = "pbkdf2";
|
||||
|
||||
// 15 minutes
|
||||
int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900;
|
||||
// 30 days
|
||||
|
||||
@@ -21,6 +21,7 @@ public class UserCredentialModel {
|
||||
protected String type;
|
||||
protected String value;
|
||||
protected String device;
|
||||
protected String algorithm;
|
||||
|
||||
public UserCredentialModel() {
|
||||
}
|
||||
@@ -107,4 +108,12 @@ public class UserCredentialModel {
|
||||
public void setDevice(String device) {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public void setAlgorithm(String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
}
|
||||
@@ -403,7 +403,7 @@ public class UserFederationManager implements UserProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> input) {
|
||||
public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input) {
|
||||
UserFederationProvider link = getFederationLink(realm, user);
|
||||
if (link != null) {
|
||||
validateUser(realm, user);
|
||||
@@ -421,10 +421,10 @@ public class UserFederationManager implements UserProvider {
|
||||
if (!link.validCredentials(realm, user, fedCreds)) {
|
||||
return false;
|
||||
}
|
||||
return session.userStorage().validCredentials(realm, user, localCreds);
|
||||
return session.userStorage().validCredentials(session, realm, user, localCreds);
|
||||
}
|
||||
}
|
||||
return session.userStorage().validCredentials(realm, user, input);
|
||||
return session.userStorage().validCredentials(session, realm, user, input);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,12 +466,12 @@ public class UserFederationManager implements UserProvider {
|
||||
|
||||
|
||||
@Override
|
||||
public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) {
|
||||
return validCredentials(realm, user, Arrays.asList(input));
|
||||
public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input) {
|
||||
return validCredentials(session, realm, user, Arrays.asList(input));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input) {
|
||||
public CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input) {
|
||||
List<UserFederationProviderModel> fedProviderModels = realm.getUserFederationProviders();
|
||||
List<UserFederationProvider> fedProviders = new ArrayList<UserFederationProvider>();
|
||||
for (UserFederationProviderModel fedProviderModel : fedProviderModels) {
|
||||
|
||||
@@ -59,9 +59,9 @@ public interface UserProvider extends Provider {
|
||||
void preRemove(RealmModel realm, ClientModel client);
|
||||
void preRemove(ClientModel realm, ProtocolMapperModel protocolMapper);
|
||||
|
||||
boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> input);
|
||||
boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input);
|
||||
CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input);
|
||||
boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input);
|
||||
boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input);
|
||||
CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input);
|
||||
|
||||
void close();
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.keycloak.models.utils;
|
||||
|
||||
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
|
||||
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OTPPolicy;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
@@ -11,6 +15,7 @@ import org.keycloak.models.UserCredentialValueModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.PasswordToken;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.hash.PasswordHashProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -38,7 +43,7 @@ public class CredentialValidation {
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
public static boolean validPassword(RealmModel realm, UserModel user, String password) {
|
||||
public static boolean validPassword(KeycloakSession session, RealmModel realm, UserModel user, String password) {
|
||||
UserCredentialValueModel passwordCred = null;
|
||||
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
@@ -47,16 +52,30 @@ public class CredentialValidation {
|
||||
}
|
||||
if (passwordCred == null) return false;
|
||||
|
||||
return validateHashedCredential(realm, user, password, passwordCred);
|
||||
return validateHashedCredential(session, realm, user, password, passwordCred);
|
||||
|
||||
}
|
||||
|
||||
public static boolean validateHashedCredential(RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) {
|
||||
|
||||
public static boolean validateHashedCredential(KeycloakSession session, RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) {
|
||||
if(unhashedCredValue == null){
|
||||
return false;
|
||||
}
|
||||
String algorithm;
|
||||
if (credential.getAlgorithm() == null || credential.getAlgorithm().trim().equals("")) {
|
||||
algorithm = Constants.DEFAULT_HASH_ALGORITHM;
|
||||
} else {
|
||||
algorithm = credential.getAlgorithm();
|
||||
}
|
||||
|
||||
byte[] salt = getSalt();
|
||||
|
||||
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, algorithm);
|
||||
|
||||
byte[] credSalt = credential.getSalt();
|
||||
|
||||
boolean validated = provider.verify(unhashedCredValue, credential.getValue(), credSalt);
|
||||
|
||||
boolean validated = new Pbkdf2PasswordEncoder(credential.getSalt()).verify(unhashedCredValue, credential.getValue(), credential.getHashIterations());
|
||||
if (validated) {
|
||||
int iterations = hashIterations(realm);
|
||||
if (iterations > -1 && iterations != credential.getHashIterations()) {
|
||||
@@ -65,7 +84,7 @@ public class CredentialValidation {
|
||||
newCred.setDevice(credential.getDevice());
|
||||
newCred.setSalt(credential.getSalt());
|
||||
newCred.setHashIterations(iterations);
|
||||
newCred.setValue(new Pbkdf2PasswordEncoder(newCred.getSalt()).encode(unhashedCredValue, iterations));
|
||||
newCred.setValue(provider.encode(unhashedCredValue, salt, iterations));
|
||||
user.updateCredentialDirectly(newCred);
|
||||
}
|
||||
|
||||
@@ -157,9 +176,9 @@ public class CredentialValidation {
|
||||
* @param credentials
|
||||
* @return
|
||||
*/
|
||||
public static boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> credentials) {
|
||||
public static boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> credentials) {
|
||||
for (UserCredentialModel credential : credentials) {
|
||||
if (!validCredential(realm, user, credential)) return false;
|
||||
if (!validCredential(session, realm, user, credential)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -172,16 +191,16 @@ public class CredentialValidation {
|
||||
* @param credentials
|
||||
* @return
|
||||
*/
|
||||
public static boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... credentials) {
|
||||
public static boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... credentials) {
|
||||
for (UserCredentialModel credential : credentials) {
|
||||
if (!validCredential(realm, user, credential)) return false;
|
||||
if (!validCredential(session, realm, user, credential)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean validCredential(RealmModel realm, UserModel user, UserCredentialModel credential) {
|
||||
private static boolean validCredential(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel credential) {
|
||||
if (credential.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
if (!validPassword(realm, user, credential.getValue())) {
|
||||
if (!validPassword(session, realm, user, credential.getValue())) {
|
||||
return false;
|
||||
}
|
||||
} else if (credential.getType().equals(UserCredentialModel.PASSWORD_TOKEN)) {
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
org.keycloak.hash.DefaultPasswordHashProviderFactory
|
||||
@@ -4,4 +4,5 @@ org.keycloak.models.RealmSpi
|
||||
org.keycloak.models.UserSessionSpi
|
||||
org.keycloak.models.UserSpi
|
||||
org.keycloak.models.session.UserSessionPersisterSpi
|
||||
org.keycloak.migration.MigrationSpi
|
||||
org.keycloak.migration.MigrationSpi
|
||||
org.keycloak.hash.PasswordHashSpi
|
||||
Reference in new issue
Block a user