KEYCLOAK-11745 Multi-factor authentication (#6459)

Co-authored-by: Christophe Frattino <christophe.frattino@elca.ch>
Co-authored-by: Francis PEROT <francis.perot@elca.ch>
Co-authored-by: rpo <harture414@gmail.com>
Co-authored-by: mposolda <mposolda@gmail.com>
Co-authored-by: Jan Lieskovsky <jlieskov@redhat.com>
Co-authored-by: Denis <drichtar@redhat.com>
Co-authored-by: Tomas Kyjovsky <tkyjovsk@redhat.com>
This commit is contained in:
AlistairDoswald 2019-11-14 14:45:05 +01:00 committed by Marek Posolda
parent e7e49c13d5
commit 4553234f64
292 changed files with 9505 additions and 4154 deletions

View file

@ -25,14 +25,38 @@ package org.keycloak.common.util;
public class Base64Url { public class Base64Url {
public static String encode(byte[] bytes) { public static String encode(byte[] bytes) {
String s = Base64.encodeBytes(bytes); String s = Base64.encodeBytes(bytes);
s = s.split("=")[0]; // Remove any trailing '='s return encodeBase64ToBase64Url(s);
}
public static byte[] decode(String s) {
s = encodeBase64UrlToBase64(s);
try {
// KEYCLOAK-2479 : Avoid to try gzip decoding as for some objects, it may display exception to STDERR. And we know that object wasn't encoded as GZIP
return Base64.decode(s, Base64.DONT_GUNZIP);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param base64 String in base64 encoding
* @return String in base64Url encoding
*/
public static String encodeBase64ToBase64Url(String base64) {
String s = base64.split("=")[0]; // Remove any trailing '='s
s = s.replace('+', '-'); // 62nd char of encoding s = s.replace('+', '-'); // 62nd char of encoding
s = s.replace('/', '_'); // 63rd char of encoding s = s.replace('/', '_'); // 63rd char of encoding
return s; return s;
} }
public static byte[] decode(String s) {
s = s.replace('-', '+'); // 62nd char of encoding /**
* @param base64Url String in base64Url encoding
* @return String in base64 encoding
*/
public static String encodeBase64UrlToBase64(String base64Url) {
String s = base64Url.replace('-', '+'); // 62nd char of encoding
s = s.replace('_', '/'); // 63rd char of encoding s = s.replace('_', '/'); // 63rd char of encoding
switch (s.length() % 4) // Pad with trailing '='s switch (s.length() % 4) // Pad with trailing '='s
{ {
@ -48,12 +72,8 @@ public class Base64Url {
throw new RuntimeException( throw new RuntimeException(
"Illegal base64url string!"); "Illegal base64url string!");
} }
try {
// KEYCLOAK-2479 : Avoid to try gzip decoding as for some objects, it may display exception to STDERR. And we know that object wasn't encoded as GZIP return s;
return Base64.decode(s, Base64.DONT_GUNZIP);
} catch (Exception e) {
throw new RuntimeException(e);
}
} }

View file

@ -28,150 +28,164 @@ public class CredentialRepresentation {
public static final String PASSWORD = "password"; public static final String PASSWORD = "password";
public static final String TOTP = "totp"; public static final String TOTP = "totp";
public static final String HOTP = "hotp"; public static final String HOTP = "hotp";
public static final String CLIENT_CERT = "cert";
public static final String KERBEROS = "kerberos"; public static final String KERBEROS = "kerberos";
protected String type; private String id;
protected String device; private String type;
private String userLabel;
// Plain-text value of credential (used for example during import from manually created JSON file)
protected String value;
// Value stored in DB (used for example during export/import)
protected String hashedSaltedValue;
protected String salt;
protected Integer hashIterations;
protected Integer counter;
private String algorithm;
private Integer digits;
private Integer period;
private Long createdDate; private Long createdDate;
private MultivaluedHashMap<String, String> config; private String secretData;
private String credentialData;
private Integer priority;
private String value;
// only used when updating a credential. Might set required action // only used when updating a credential. Might set required action
protected Boolean temporary; protected Boolean temporary;
// All those fields are just for backwards compatibility
@Deprecated
protected String device;
@Deprecated
protected String hashedSaltedValue;
@Deprecated
protected String salt;
@Deprecated
protected Integer hashIterations;
@Deprecated
protected Integer counter;
@Deprecated
private String algorithm;
@Deprecated
private Integer digits;
@Deprecated
private Integer period;
@Deprecated
private MultivaluedHashMap<String, String> config;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() { public String getType() {
return type; return type;
} }
public void setType(String type) { public void setType(String type) {
this.type = type; this.type = type;
} }
public String getValue() { public String getUserLabel() {
return value; return userLabel;
}
public void setUserLabel(String userLabel) {
this.userLabel = userLabel;
} }
public void setValue(String value) { public String getSecretData() {
this.value = value; return secretData;
}
public void setSecretData(String secretData) {
this.secretData = secretData;
} }
public String getDevice() { public String getCredentialData() {
return device; return credentialData;
}
public void setCredentialData(String credentialData) {
this.credentialData = credentialData;
} }
public void setDevice(String device) { public Integer getPriority() {
this.device = device; return priority;
} }
public String getHashedSaltedValue() { public void setPriority(Integer priority) {
return hashedSaltedValue; this.priority = priority;
}
public void setHashedSaltedValue(String hashedSaltedValue) {
this.hashedSaltedValue = hashedSaltedValue;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public Integer getHashIterations() {
return hashIterations;
}
public void setHashIterations(Integer hashIterations) {
this.hashIterations = hashIterations;
}
public Boolean isTemporary() {
return temporary;
}
public void setTemporary(Boolean temporary) {
this.temporary = temporary;
}
public Integer getCounter() {
return counter;
}
public void setCounter(Integer counter) {
this.counter = counter;
}
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public Integer getDigits() {
return digits;
}
public void setDigits(Integer digits) {
this.digits = digits;
}
public Integer getPeriod() {
return period;
}
public void setPeriod(Integer period) {
this.period = period;
} }
public Long getCreatedDate() { public Long getCreatedDate() {
return createdDate; return createdDate;
} }
public void setCreatedDate(Long createdDate) { public void setCreatedDate(Long createdDate) {
this.createdDate = createdDate; this.createdDate = createdDate;
} }
public MultivaluedHashMap<String, String> getConfig() {
return config; public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
} }
public void setConfig(MultivaluedHashMap<String, String> config) { public Boolean isTemporary() {
this.config = config; return temporary;
}
public void setTemporary(Boolean temporary) {
this.temporary = temporary;
}
@Deprecated
public String getDevice() {
return device;
}
@Deprecated
public String getHashedSaltedValue() {
return hashedSaltedValue;
}
@Deprecated
public String getSalt() {
return salt;
}
@Deprecated
public Integer getHashIterations() {
return hashIterations;
}
@Deprecated
public Integer getCounter() {
return counter;
}
@Deprecated
public String getAlgorithm() {
return algorithm;
}
@Deprecated
public Integer getDigits() {
return digits;
}
@Deprecated
public Integer getPeriod() {
return period;
}
@Deprecated
public MultivaluedHashMap<String, String> getConfig() {
return config;
} }
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = 1; int result = 1;
result = prime * result + ((algorithm == null) ? 0 : algorithm.hashCode());
result = prime * result + ((config == null) ? 0 : config.hashCode());
result = prime * result + ((counter == null) ? 0 : counter.hashCode());
result = prime * result + ((createdDate == null) ? 0 : createdDate.hashCode()); result = prime * result + ((createdDate == null) ? 0 : createdDate.hashCode());
result = prime * result + ((device == null) ? 0 : device.hashCode()); result = prime * result + ((userLabel == null) ? 0 : userLabel.hashCode());
result = prime * result + ((digits == null) ? 0 : digits.hashCode()); result = prime * result + ((secretData == null) ? 0 : secretData.hashCode());
result = prime * result + ((hashIterations == null) ? 0 : hashIterations.hashCode()); result = prime * result + ((credentialData == null) ? 0 : credentialData.hashCode());
result = prime * result + ((hashedSaltedValue == null) ? 0 : hashedSaltedValue.hashCode());
result = prime * result + ((period == null) ? 0 : period.hashCode());
result = prime * result + ((salt == null) ? 0 : salt.hashCode());
result = prime * result + ((temporary == null) ? 0 : temporary.hashCode()); result = prime * result + ((temporary == null) ? 0 : temporary.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((value == null) ? 0 : value.hashCode()); result = prime * result + ((value == null) ? 0 : value.hashCode());
result = prime * result + ((priority == null) ? 0 : priority);
return result; return result;
} }
@ -184,55 +198,25 @@ public class CredentialRepresentation {
if (getClass() != obj.getClass()) if (getClass() != obj.getClass())
return false; return false;
CredentialRepresentation other = (CredentialRepresentation) obj; CredentialRepresentation other = (CredentialRepresentation) obj;
if (algorithm == null) { if (secretData == null) {
if (other.algorithm != null) if (other.secretData != null)
return false; return false;
} else if (!algorithm.equals(other.algorithm)) } else if (!secretData.equals(other.secretData))
return false; return false;
if (config == null) { if (credentialData == null) {
if (other.config != null) if (other.credentialData != null)
return false; return false;
} else if (!config.equals(other.config)) } else if (!credentialData.equals(other.credentialData))
return false;
if (counter == null) {
if (other.counter != null)
return false;
} else if (!counter.equals(other.counter))
return false; return false;
if (createdDate == null) { if (createdDate == null) {
if (other.createdDate != null) if (other.createdDate != null)
return false; return false;
} else if (!createdDate.equals(other.createdDate)) } else if (!createdDate.equals(other.createdDate))
return false; return false;
if (device == null) { if (userLabel == null) {
if (other.device != null) if (other.userLabel != null)
return false; return false;
} else if (!device.equals(other.device)) } else if (!userLabel.equals(other.userLabel))
return false;
if (digits == null) {
if (other.digits != null)
return false;
} else if (!digits.equals(other.digits))
return false;
if (hashIterations == null) {
if (other.hashIterations != null)
return false;
} else if (!hashIterations.equals(other.hashIterations))
return false;
if (hashedSaltedValue == null) {
if (other.hashedSaltedValue != null)
return false;
} else if (!hashedSaltedValue.equals(other.hashedSaltedValue))
return false;
if (period == null) {
if (other.period != null)
return false;
} else if (!period.equals(other.period))
return false;
if (salt == null) {
if (other.salt != null)
return false;
} else if (!salt.equals(other.salt))
return false; return false;
if (temporary == null) { if (temporary == null) {
if (other.temporary != null) if (other.temporary != null)
@ -244,11 +228,23 @@ public class CredentialRepresentation {
return false; return false;
} else if (!type.equals(other.type)) } else if (!type.equals(other.type))
return false; return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (value == null) { if (value == null) {
if (other.value != null) if (other.value != null)
return false; return false;
} else if (!value.equals(other.value)) } else if (!value.equals(other.value))
return false; return false;
if (priority == null) {
if (other.priority != null)
return false;
} else if (!priority.equals(other.priority))
return false;
return true; return true;
} }
} }

View file

@ -30,5 +30,9 @@
<module name="org.apache.httpcomponents"/> <module name="org.apache.httpcomponents"/>
<module name="org.jboss.resteasy.resteasy-jaxrs"/> <module name="org.jboss.resteasy.resteasy-jaxrs"/>
<module name="javax.transaction.api"/> <module name="javax.transaction.api"/>
<module name="com.fasterxml.jackson.core.jackson-core"/>
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
<module name="com.fasterxml.jackson.core.jackson-databind"/>
<module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
</dependencies> </dependencies>
</module> </module>

View file

@ -17,7 +17,7 @@ Example Custom Authenticator
6. In your copy, click the "Actions" menu item and "Add Execution". Pick Secret Question 6. In your copy, click the "Actions" menu item and "Add Execution". Pick Secret Question
7. Next you have to register the required action that you created. Click on the Required Actions tab in the Authenticaiton menu. 7. Next you have to register the required action that you created. Click on the Required Actions tab in the Authentication menu.
Click on the Register button and choose your new Required Action. Click on the Register button and choose your new Required Action.
Your new required action should now be displayed and enabled in the required actions list. Your new required action should now be displayed and enabled in the required actions list.

View file

@ -1,4 +1,4 @@
<#import "template.ftl" as layout> <#import "select.ftl" as layout>
<@layout.registrationLayout; section> <@layout.registrationLayout; section>
<#if section = "title"> <#if section = "title">
${msg("loginTitle",realm.name)} ${msg("loginTitle",realm.name)}
@ -24,8 +24,9 @@
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}"> <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<div class="${properties.kcFormButtonsWrapperClass!}"> <div class="${properties.kcFormButtonsWrapperClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/> <input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doCancel")}"/> <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -21,7 +21,9 @@ import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.CredentialValidator;
import org.keycloak.common.util.ServerCookie; import org.keycloak.common.util.ServerCookie;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -38,9 +40,7 @@ import java.net.URI;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SecretQuestionAuthenticator implements Authenticator { public class SecretQuestionAuthenticator implements Authenticator, CredentialValidator<SecretQuestionCredentialProvider> {
public static final String CREDENTIAL_TYPE = "secret_question";
protected boolean hasCookie(AuthenticationFlowContext context) { protected boolean hasCookie(AuthenticationFlowContext context) {
Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED"); Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
@ -57,17 +57,13 @@ public class SecretQuestionAuthenticator implements Authenticator {
context.success(); context.success();
return; return;
} }
Response challenge = context.form().createForm("secret-question.ftl"); Response challenge = context.form()
.createForm("secret-question.ftl");
context.challenge(challenge); context.challenge(challenge);
} }
@Override @Override
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("cancel")) {
context.cancelLogin();
return;
}
boolean validated = validateAnswer(context); boolean validated = validateAnswer(context);
if (!validated) { if (!validated) {
Response challenge = context.form() Response challenge = context.form()
@ -107,10 +103,15 @@ public class SecretQuestionAuthenticator implements Authenticator {
protected boolean validateAnswer(AuthenticationFlowContext context) { protected boolean validateAnswer(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String secret = formData.getFirst("secret_answer"); String secret = formData.getFirst("secret_answer");
UserCredentialModel input = new UserCredentialModel(); String credentialId = context.getSelectedCredentialId();
input.setType(SecretQuestionCredentialProvider.SECRET_QUESTION); if (credentialId == null || credentialId.isEmpty()) {
input.setValue(secret); credentialId = getCredentialProvider(context.getSession())
return context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), input); .getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId();
context.setSelectedCredentialId(credentialId);
}
UserCredentialModel input = new UserCredentialModel(credentialId, getType(context.getSession()), secret);
return getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), input);
} }
@Override @Override
@ -120,7 +121,7 @@ public class SecretQuestionAuthenticator implements Authenticator {
@Override @Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return session.userCredentialManager().isConfiguredFor(realm, user, SecretQuestionCredentialProvider.SECRET_QUESTION); return getCredentialProvider(session).isConfiguredFor(realm, user, getType(session));
} }
@Override @Override
@ -128,8 +129,17 @@ public class SecretQuestionAuthenticator implements Authenticator {
user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID); user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID);
} }
public List<RequiredActionFactory> getRequiredActions(KeycloakSession session) {
return Collections.singletonList((SecretQuestionRequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, SecretQuestionRequiredAction.PROVIDER_ID));
}
@Override @Override
public void close() { public void close() {
} }
@Override
public SecretQuestionCredentialProvider getCredentialProvider(KeycloakSession session) {
return (SecretQuestionCredentialProvider)session.getProvider(CredentialProvider.class, SecretQuestionCredentialProviderFactory.PROVIDER_ID);
}
} }

View file

@ -50,6 +50,7 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.DISABLED AuthenticationExecutionModel.Requirement.DISABLED
}; };
@Override @Override

View file

@ -16,31 +16,25 @@
*/ */
package org.keycloak.examples.authenticator; package org.keycloak.examples.authenticator;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator; import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.UserCredentialStore;
import org.keycloak.examples.authenticator.credential.SecretQuestionCredentialModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.cache.OnUserCache;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SecretQuestionCredentialProvider implements CredentialProvider, CredentialInputValidator, CredentialInputUpdater, OnUserCache { public class SecretQuestionCredentialProvider implements CredentialProvider<SecretQuestionCredentialModel>, CredentialInputValidator {
public static final String SECRET_QUESTION = "SECRET_QUESTION"; private static final Logger logger = Logger.getLogger(SecretQuestionCredentialProvider.class);
public static final String CACHE_KEY = SecretQuestionCredentialProvider.class.getName() + "." + SECRET_QUESTION;
protected KeycloakSession session; protected KeycloakSession session;
@ -48,87 +42,60 @@ public class SecretQuestionCredentialProvider implements CredentialProvider, Cre
this.session = session; this.session = session;
} }
public CredentialModel getSecret(RealmModel realm, UserModel user) { private UserCredentialStore getCredentialStore() {
CredentialModel secret = null; return session.userCredentialManager();
if (user instanceof CachedUserModel) {
CachedUserModel cached = (CachedUserModel)user;
secret = (CredentialModel)cached.getCachedWith().get(CACHE_KEY);
} else {
List<CredentialModel> creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION);
if (!creds.isEmpty()) secret = creds.get(0);
}
return secret;
} }
@Override @Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!SECRET_QUESTION.equals(input.getType())) return false; if (!(input instanceof UserCredentialModel)) {
if (!(input instanceof UserCredentialModel)) return false; logger.debug("Expected instance of UserCredentialModel for CredentialInput");
UserCredentialModel credInput = (UserCredentialModel) input; return false;
List<CredentialModel> creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION);
if (creds.isEmpty()) {
CredentialModel secret = new CredentialModel();
secret.setType(SECRET_QUESTION);
secret.setValue(credInput.getValue());
secret.setCreatedDate(Time.currentTimeMillis());
session.userCredentialManager().createCredential(realm ,user, secret);
} else {
creds.get(0).setValue(credInput.getValue());
session.userCredentialManager().updateCredential(realm, user, creds.get(0));
} }
session.userCache().evict(realm, user); if (!input.getType().equals(getType())) {
return true; return false;
} }
String challengeResponse = input.getChallengeResponse();
@Override if (challengeResponse == null) {
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { return false;
if (!SECRET_QUESTION.equals(credentialType)) return;
List<CredentialModel> credentials = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION);
for (CredentialModel cred : credentials) {
session.userCredentialManager().removeStoredCredential(realm, user, cred.getId());
} }
session.userCache().evict(realm, user); CredentialModel credentialModel = getCredentialStore().getStoredCredentialById(realm, user, input.getCredentialId());
} SecretQuestionCredentialModel sqcm = getCredentialFromModel(credentialModel);
return sqcm.getSecretQuestionSecretData().getAnswer().equals(challengeResponse);
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
if (!session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION).isEmpty()) {
Set<String> set = new HashSet<>();
set.add(SECRET_QUESTION);
return set;
} else {
return Collections.EMPTY_SET;
}
} }
@Override @Override
public boolean supportsCredentialType(String credentialType) { public boolean supportsCredentialType(String credentialType) {
return SECRET_QUESTION.equals(credentialType); return getType().equals(credentialType);
} }
@Override @Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
if (!SECRET_QUESTION.equals(credentialType)) return false; if (!supportsCredentialType(credentialType)) return false;
return getSecret(realm, user) != null; return !getCredentialStore().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
} }
@Override @Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { public CredentialModel createCredential(RealmModel realm, UserModel user, SecretQuestionCredentialModel credentialModel) {
if (!SECRET_QUESTION.equals(input.getType())) return false; if (credentialModel.getCreatedDate() == null) {
if (!(input instanceof UserCredentialModel)) return false; credentialModel.setCreatedDate(Time.currentTimeMillis());
}
String secret = getSecret(realm, user).getValue(); return getCredentialStore().createCredential(realm, user, credentialModel);
return secret != null && ((UserCredentialModel)input).getValue().equals(secret);
} }
@Override @Override
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) { public void deleteCredential(RealmModel realm, UserModel user, String credentialId) {
List<CredentialModel> creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION); getCredentialStore().removeStoredCredential(realm, user, credentialId);
if (!creds.isEmpty()) user.getCachedWith().put(CACHE_KEY, creds.get(0)); }
@Override
public SecretQuestionCredentialModel getCredentialFromModel(CredentialModel model) {
return SecretQuestionCredentialModel.createFromCredentialModel(model);
}
@Override
public String getType() {
return SecretQuestionCredentialModel.TYPE;
} }
} }

View file

@ -25,9 +25,12 @@ import org.keycloak.models.KeycloakSession;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SecretQuestionCredentialProviderFactory implements CredentialProviderFactory<SecretQuestionCredentialProvider> { public class SecretQuestionCredentialProviderFactory implements CredentialProviderFactory<SecretQuestionCredentialProvider> {
public static final String PROVIDER_ID = "secret-question";
@Override @Override
public String getId() { public String getId() {
return "secret-question"; return PROVIDER_ID;
} }
@Override @Override

View file

@ -17,9 +17,11 @@
package org.keycloak.examples.authenticator; package org.keycloak.examples.authenticator;
import org.keycloak.authentication.CredentialRegistrator;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.UserCredentialModel; import org.keycloak.credential.CredentialProvider;
import org.keycloak.examples.authenticator.credential.SecretQuestionCredentialModel;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -27,7 +29,7 @@ import javax.ws.rs.core.Response;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SecretQuestionRequiredAction implements RequiredActionProvider { public class SecretQuestionRequiredAction implements RequiredActionProvider, CredentialRegistrator {
public static final String PROVIDER_ID = "secret_question_config"; public static final String PROVIDER_ID = "secret_question_config";
@Override @Override
@ -45,10 +47,8 @@ public class SecretQuestionRequiredAction implements RequiredActionProvider {
@Override @Override
public void processAction(RequiredActionContext context) { public void processAction(RequiredActionContext context) {
String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer")); String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer"));
UserCredentialModel input = new UserCredentialModel(); SecretQuestionCredentialProvider sqcp = (SecretQuestionCredentialProvider) context.getSession().getProvider(CredentialProvider.class, "secret-question");
input.setType(SecretQuestionCredentialProvider.SECRET_QUESTION); sqcp.createCredential(context.getRealm(), context.getUser(), SecretQuestionCredentialModel.createSecretQuestion("What is your mom's first name?", answer));
input.setValue(answer);
context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), input);
context.success(); context.success();
} }

View file

@ -0,0 +1,91 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.authenticator.credential;
import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.examples.authenticator.credential.dto.SecretQuestionCredentialData;
import org.keycloak.examples.authenticator.credential.dto.SecretQuestionSecretData;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
/**
* @author <a href="mailto:alistair.doswald@elca.ch">Alistair Doswald</a>
* @version $Revision: 1 $
*/
public class SecretQuestionCredentialModel extends CredentialModel {
public static final String TYPE = "SECRET_QUESTION";
private final SecretQuestionCredentialData credentialData;
private final SecretQuestionSecretData secretData;
private SecretQuestionCredentialModel(SecretQuestionCredentialData credentialData, SecretQuestionSecretData secretData) {
this.credentialData = credentialData;
this.secretData = secretData;
}
private SecretQuestionCredentialModel(String question, String answer) {
credentialData = new SecretQuestionCredentialData(question);
secretData = new SecretQuestionSecretData(answer);
}
public static SecretQuestionCredentialModel createSecretQuestion(String question, String answer) {
SecretQuestionCredentialModel credentialModel = new SecretQuestionCredentialModel(question, answer);
credentialModel.fillCredentialModelFields();
return credentialModel;
}
public static SecretQuestionCredentialModel createFromCredentialModel(CredentialModel credentialModel){
try {
SecretQuestionCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), SecretQuestionCredentialData.class);
SecretQuestionSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), SecretQuestionSecretData.class);
SecretQuestionCredentialModel secretQuestionCredentialModel = new SecretQuestionCredentialModel(credentialData, secretData);
secretQuestionCredentialModel.setUserLabel(credentialModel.getUserLabel());
secretQuestionCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
secretQuestionCredentialModel.setType(TYPE);
secretQuestionCredentialModel.setId(credentialModel.getId());
secretQuestionCredentialModel.setSecretData(credentialModel.getSecretData());
secretQuestionCredentialModel.setCredentialData(credentialModel.getCredentialData());
return secretQuestionCredentialModel;
} catch (IOException e){
throw new RuntimeException(e);
}
}
public SecretQuestionCredentialData getSecretQuestionCredentialData() {
return credentialData;
}
public SecretQuestionSecretData getSecretQuestionSecretData() {
return secretData;
}
private void fillCredentialModelFields(){
try {
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setType(TYPE);
setCreatedDate(Time.currentTimeMillis());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.authenticator.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:alistair.doswald@elca.ch">Alistair Doswald</a>
* @version $Revision: 1 $
*/
public class SecretQuestionCredentialData {
private final String question;
@JsonCreator
public SecretQuestionCredentialData(@JsonProperty("question") String question) {
this.question = question;
}
public String getQuestion() {
return question;
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.examples.authenticator.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:alistair.doswald@elca.ch">Alistair Doswald</a>
* @version $Revision: 1 $
*/
public class SecretQuestionSecretData {
private final String answer;
@JsonCreator
public SecretQuestionSecretData(@JsonProperty("answer") String answer) {
this.answer = answer;
}
public String getAnswer() {
return answer;
}
}

View file

@ -34,6 +34,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserManager; import org.keycloak.models.UserManager;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.UserStorageProviderModel;
@ -132,7 +133,7 @@ public class KerberosFederationProvider implements UserStorageProvider,
@Override @Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel) || !CredentialModel.PASSWORD.equals(input.getType())) return false; if (!(input instanceof UserCredentialModel) || !PasswordCredentialModel.TYPE.equals(input.getType())) return false;
if (kerberosConfig.getEditMode() == EditMode.READ_ONLY) { if (kerberosConfig.getEditMode() == EditMode.READ_ONLY) {
throw new ReadOnlyException("Can't change password in Keycloak database. Change password with your Kerberos server"); throw new ReadOnlyException("Can't change password in Keycloak database. Change password with your Kerberos server");
} }
@ -151,12 +152,12 @@ public class KerberosFederationProvider implements UserStorageProvider,
@Override @Override
public boolean supportsCredentialType(String credentialType) { public boolean supportsCredentialType(String credentialType) {
return credentialType.equals(CredentialModel.KERBEROS) || (kerberosConfig.isAllowPasswordAuthentication() && credentialType.equals(CredentialModel.PASSWORD)); return credentialType.equals(UserCredentialModel.KERBEROS) || (kerberosConfig.isAllowPasswordAuthentication() && credentialType.equals(PasswordCredentialModel.TYPE));
} }
@Override @Override
public boolean supportsCredentialAuthenticationFor(String type) { public boolean supportsCredentialAuthenticationFor(String type) {
return CredentialModel.KERBEROS.equals(type); return UserCredentialModel.KERBEROS.equals(type);
} }
@Override @Override
@ -167,8 +168,8 @@ public class KerberosFederationProvider implements UserStorageProvider,
@Override @Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false; if (!(input instanceof UserCredentialModel)) return false;
if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) { if (input.getType().equals(PasswordCredentialModel.TYPE) && !session.userCredentialManager().isConfiguredLocally(realm, user, PasswordCredentialModel.TYPE)) {
return validPassword(user.getUsername(), ((UserCredentialModel)input).getValue()); return validPassword(user.getUsername(), input.getChallengeResponse());
} else { } else {
return false; // invalid cred type return false; // invalid cred type
} }
@ -188,7 +189,7 @@ public class KerberosFederationProvider implements UserStorageProvider,
if (!(input instanceof UserCredentialModel)) return null; if (!(input instanceof UserCredentialModel)) return null;
UserCredentialModel credential = (UserCredentialModel)input; UserCredentialModel credential = (UserCredentialModel)input;
if (credential.getType().equals(UserCredentialModel.KERBEROS)) { if (credential.getType().equals(UserCredentialModel.KERBEROS)) {
String spnegoToken = credential.getValue(); String spnegoToken = credential.getChallengeResponse();
SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig);
spnegoAuthenticator.authenticate(); spnegoAuthenticator.authenticate();

View file

@ -40,14 +40,12 @@ import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticat
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.models.*; import org.keycloak.models.*;
import org.keycloak.models.cache.CachedUserModel; import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.utils.DefaultRoles; import org.keycloak.models.utils.DefaultRoles;
import org.keycloak.models.utils.ReadOnlyUserModelDelegate; import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
import org.keycloak.policy.PasswordPolicyManagerProvider; import org.keycloak.policy.PasswordPolicyManagerProvider;
import org.keycloak.policy.PolicyError; import org.keycloak.policy.PolicyError;
import org.keycloak.models.cache.UserCache; import org.keycloak.models.cache.UserCache;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
import org.keycloak.storage.StorageId; import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
@ -110,7 +108,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
this.mapperManager = new LDAPStorageMapperManager(this); this.mapperManager = new LDAPStorageMapperManager(this);
this.userManager = new LDAPStorageUserManager(this); this.userManager = new LDAPStorageUserManager(this);
supportedCredentialTypes.add(UserCredentialModel.PASSWORD); supportedCredentialTypes.add(PasswordCredentialModel.TYPE);
if (kerberosConfig.isAllowKerberosAuthentication()) { if (kerberosConfig.isAllowKerberosAuthentication()) {
supportedCredentialTypes.add(UserCredentialModel.KERBEROS); supportedCredentialTypes.add(UserCredentialModel.KERBEROS);
} }
@ -218,7 +216,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
@Override @Override
public boolean supportsCredentialAuthenticationFor(String type) { public boolean supportsCredentialAuthenticationFor(String type) {
return type.equals(CredentialModel.KERBEROS) && kerberosConfig.isAllowKerberosAuthentication(); return type.equals(UserCredentialModel.KERBEROS) && kerberosConfig.isAllowKerberosAuthentication();
} }
@Override @Override
@ -613,14 +611,13 @@ public class LDAPStorageProvider implements UserStorageProvider,
@Override @Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!CredentialModel.PASSWORD.equals(input.getType()) || ! (input instanceof PasswordUserCredentialModel)) return false; if (!PasswordCredentialModel.TYPE.equals(input.getType()) || ! (input instanceof UserCredentialModel)) return false;
if (editMode == UserStorageProvider.EditMode.READ_ONLY) { if (editMode == UserStorageProvider.EditMode.READ_ONLY) {
throw new ReadOnlyException("Federated storage is not writable"); throw new ReadOnlyException("Federated storage is not writable");
} else if (editMode == UserStorageProvider.EditMode.WRITABLE) { } else if (editMode == UserStorageProvider.EditMode.WRITABLE) {
LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore(); LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore();
PasswordUserCredentialModel cred = (PasswordUserCredentialModel)input; String password = input.getChallengeResponse();
String password = cred.getValue();
LDAPObject ldapUser = loadAndValidateUser(realm, user); LDAPObject ldapUser = loadAndValidateUser(realm, user);
if (ldapIdentityStore.getConfig().isValidatePasswordPolicy()) { if (ldapIdentityStore.getConfig().isValidatePasswordPolicy()) {
PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm, user, password); PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm, user, password);
@ -629,16 +626,16 @@ public class LDAPStorageProvider implements UserStorageProvider,
try { try {
LDAPOperationDecorator operationDecorator = null; LDAPOperationDecorator operationDecorator = null;
if (updater != null) { if (updater != null) {
operationDecorator = updater.beforePasswordUpdate(user, ldapUser, cred); operationDecorator = updater.beforePasswordUpdate(user, ldapUser, (UserCredentialModel)input);
} }
ldapIdentityStore.updatePassword(ldapUser, password, operationDecorator); ldapIdentityStore.updatePassword(ldapUser, password, operationDecorator);
if (updater != null) updater.passwordUpdated(user, ldapUser, cred); if (updater != null) updater.passwordUpdated(user, ldapUser, (UserCredentialModel)input);
return true; return true;
} catch (ModelException me) { } catch (ModelException me) {
if (updater != null) { if (updater != null) {
updater.passwordUpdateFailed(user, ldapUser, cred, me); updater.passwordUpdateFailed(user, ldapUser, (UserCredentialModel)input, me);
return false; return false;
} else { } else {
throw me; throw me;
@ -678,8 +675,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
@Override @Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) return false; if (!(input instanceof UserCredentialModel)) return false;
if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) { if (input.getType().equals(PasswordCredentialModel.TYPE) && !session.userCredentialManager().isConfiguredLocally(realm, user, PasswordCredentialModel.TYPE)) {
return validPassword(realm, user, ((UserCredentialModel)input).getValue()); return validPassword(realm, user, input.getChallengeResponse());
} else { } else {
return false; // invalid cred type return false; // invalid cred type
} }
@ -691,7 +688,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
UserCredentialModel credential = (UserCredentialModel)cred; UserCredentialModel credential = (UserCredentialModel)cred;
if (credential.getType().equals(UserCredentialModel.KERBEROS)) { if (credential.getType().equals(UserCredentialModel.KERBEROS)) {
if (kerberosConfig.isAllowKerberosAuthentication()) { if (kerberosConfig.isAllowKerberosAuthentication()) {
String spnegoToken = credential.getValue(); String spnegoToken = credential.getChallengeResponse();
SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig);
spnegoAuthenticator.authenticate(); spnegoAuthenticator.authenticate();

View file

@ -17,8 +17,8 @@
package org.keycloak.storage.ldap.mappers; package org.keycloak.storage.ldap.mappers;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.model.LDAPObject;
/** /**
@ -27,9 +27,9 @@ import org.keycloak.storage.ldap.idm.model.LDAPObject;
*/ */
public interface PasswordUpdateCallback { public interface PasswordUpdateCallback {
LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password); LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, UserCredentialModel password);
void passwordUpdated(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password); void passwordUpdated(UserModel user, LDAPObject ldapUser, UserCredentialModel password);
void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password, ModelException exception) throws ModelException; void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, UserCredentialModel password, ModelException exception) throws ModelException;
} }

View file

@ -19,13 +19,11 @@ package org.keycloak.storage.ldap.mappers.msad;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.model.LDAPObject;
@ -75,7 +73,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
} }
@Override @Override
public LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password) { public LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, UserCredentialModel password) {
// Not apply policies if password is reset by admin (not by user himself) // Not apply policies if password is reset by admin (not by user himself)
if (password.isAdminRequest()) { if (password.isAdminRequest()) {
return null; return null;
@ -86,7 +84,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
} }
@Override @Override
public void passwordUpdated(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password) { public void passwordUpdated(UserModel user, LDAPObject ldapUser, UserCredentialModel password) {
logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString()); logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString());
// Normally it's read-only // Normally it's read-only
@ -106,7 +104,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
} }
@Override @Override
public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password, ModelException exception) { public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, UserCredentialModel password, ModelException exception) {
throw processFailedPasswordUpdateException(exception); throw processFailedPasswordUpdateException(exception);
} }

View file

@ -19,12 +19,11 @@ package org.keycloak.storage.ldap.mappers.msadlds;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordUserCredentialModel;
import org.keycloak.models.utils.UserModelDelegate; import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProvider;
@ -73,12 +72,12 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
} }
@Override @Override
public LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password) { public LDAPOperationDecorator beforePasswordUpdate(UserModel user, LDAPObject ldapUser, UserCredentialModel password) {
return null; // Not supported for now. Not sure if LDAP_SERVER_POLICY_HINTS_OID works in MSAD LDS return null; // Not supported for now. Not sure if LDAP_SERVER_POLICY_HINTS_OID works in MSAD LDS
} }
@Override @Override
public void passwordUpdated(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password) { public void passwordUpdated(UserModel user, LDAPObject ldapUser, UserCredentialModel password) {
logger.debugf("Going to update pwdLastSet for ldap user '%s' after successful password update", ldapUser.getDn().toString()); logger.debugf("Going to update pwdLastSet for ldap user '%s' after successful password update", ldapUser.getDn().toString());
// Normally it's read-only // Normally it's read-only
@ -96,7 +95,7 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
} }
@Override @Override
public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, PasswordUserCredentialModel password, ModelException exception) { public void passwordUpdateFailed(UserModel user, LDAPObject ldapUser, UserCredentialModel password, ModelException exception) {
throw processFailedPasswordUpdateException(exception); throw processFailedPasswordUpdateException(exception);
} }

View file

@ -26,11 +26,13 @@ import org.keycloak.federation.sssd.api.Sssd;
import org.keycloak.federation.sssd.api.Sssd.User; import org.keycloak.federation.sssd.api.Sssd.User;
import org.keycloak.federation.sssd.impl.PAMAuthenticator; import org.keycloak.federation.sssd.impl.PAMAuthenticator;
import org.keycloak.models.*; import org.keycloak.models.*;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.user.ImportedUserValidation; import org.keycloak.storage.user.ImportedUserValidation;
import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserLookupProvider;
import sun.security.util.Password;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -63,7 +65,7 @@ public class SSSDFederationProvider implements UserStorageProvider,
} }
static { static {
supportedCredentialTypes.add(UserCredentialModel.PASSWORD); supportedCredentialTypes.add(PasswordCredentialModel.TYPE);
} }
@ -163,12 +165,12 @@ public class SSSDFederationProvider implements UserStorageProvider,
@Override @Override
public boolean supportsCredentialType(String credentialType) { public boolean supportsCredentialType(String credentialType) {
return CredentialModel.PASSWORD.equals(credentialType); return PasswordCredentialModel.TYPE.equals(credentialType);
} }
@Override @Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
return CredentialModel.PASSWORD.equals(credentialType); return PasswordCredentialModel.TYPE.equals(credentialType);
} }
@Override @Override
@ -176,7 +178,7 @@ public class SSSDFederationProvider implements UserStorageProvider,
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
UserCredentialModel cred = (UserCredentialModel)input; UserCredentialModel cred = (UserCredentialModel)input;
PAMAuthenticator pam = factory.createPAMAuthenticator(user.getUsername(), cred.getValue()); PAMAuthenticator pam = factory.createPAMAuthenticator(user.getUsername(), cred.getChallengeResponse());
return (pam.authenticate() != null); return (pam.authenticate() != null);
} }

View file

@ -83,20 +83,59 @@ public interface UserResource {
@Path("logout") @Path("logout")
public void logout(); public void logout();
@GET
@Path("credentials")
@Produces(MediaType.APPLICATION_JSON)
List<CredentialRepresentation> credentials();
/**
* Remove a credential for a user
*
*/
@DELETE
@Path("credentials/{credentialId}")
void removeCredential(@PathParam("credentialId")String credentialId);
/**
* Update a credential label for a user
*/
@PUT @PUT
@Path("remove-totp") @Consumes(javax.ws.rs.core.MediaType.TEXT_PLAIN)
public void removeTotp(); @Path("credentials/{credentialId}/userLabel")
void setCredentialUserLabel(final @PathParam("credentialId") String credentialId, String userLabel);
/**
* Move a credential to a first position in the credentials list of the user
* @param credentialId The credential to move
*/
@Path("credentials/{credentialId}/moveToFirst")
@POST
void moveCredentialToFirst(final @PathParam("credentialId") String credentialId);
/**
* Move a credential to a position behind another credential
* @param credentialId The credential to move
* @param newPreviousCredentialId The credential that will be the previous element in the list. If set to null, the moved credential will be the first element in the list.
*/
@Path("credentials/{credentialId}/moveAfter/{newPreviousCredentialId}")
@POST
void moveCredentialAfter(final @PathParam("credentialId") String credentialId, final @PathParam("newPreviousCredentialId") String newPreviousCredentialId);
/** /**
* Disables or deletes all credentials for specific types. * Disables or deletes all credentials for specific types.
* Type examples "otp", "password" * Type examples "otp", "password"
* *
* This endpoint is deprecated as it is not supported to disable credentials, just delete them
* *
* @param credentialTypes * @param credentialTypes
*/ */
@Path("disable-credential-types") @Path("disable-credential-types")
@PUT @PUT
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Deprecated
public void disableCredentialType(List<String> credentialTypes); public void disableCredentialType(List<String> credentialTypes);
@PUT @PUT

View file

@ -1213,6 +1213,11 @@ public class RealmAdapter implements CachedRealmModel {
return cached.getExecutionsById().get(id); return cached.getExecutionsById().get(id);
} }
public AuthenticationExecutionModel getAuthenticationExecutionByFlowId(String flowId) {
getDelegateForUpdate();
return updated.getAuthenticationExecutionByFlowId(flowId);
}
@Override @Override
public AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model) { public AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model) {
getDelegateForUpdate(); getDelegateForUpdate();

View file

@ -137,7 +137,8 @@ public class ResourceAdapter extends AbstractAuthorizationModel implements Resou
@Override @Override
public ResourceServer getResourceServer() { public ResourceServer getResourceServer() {
return storeFactory.getResourceServerStore().findById(entity.getResourceServer().getId()); ResourceServer temp = storeFactory.getResourceServerStore().findById(entity.getResourceServer().getId());
return temp;
} }
@Override @Override

View file

@ -16,23 +16,24 @@
*/ */
package org.keycloak.models.jpa; package org.keycloak.models.jpa;
import org.keycloak.common.util.MultivaluedHashMap; import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.UserCredentialStore; import org.keycloak.credential.UserCredentialStore;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.CredentialAttributeEntity;
import org.keycloak.models.jpa.entities.CredentialEntity; import org.keycloak.models.jpa.entities.CredentialEntity;
import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import java.util.Iterator;
import java.util.LinkedList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.persistence.LockModeType; import javax.persistence.LockModeType;
import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -40,6 +41,11 @@ import javax.persistence.LockModeType;
*/ */
public class JpaUserCredentialStore implements UserCredentialStore { public class JpaUserCredentialStore implements UserCredentialStore {
// Typical priority difference between 2 credentials
public static final int PRIORITY_DIFFERENCE = 10;
protected static final Logger logger = Logger.getLogger(JpaUserCredentialStore.class);
private final KeycloakSession session; private final KeycloakSession session;
protected final EntityManager em; protected final EntityManager em;
@ -52,99 +58,23 @@ public class JpaUserCredentialStore implements UserCredentialStore {
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
CredentialEntity entity = em.find(CredentialEntity.class, cred.getId()); CredentialEntity entity = em.find(CredentialEntity.class, cred.getId());
if (entity == null) return; if (entity == null) return;
entity.setAlgorithm(cred.getAlgorithm());
entity.setCounter(cred.getCounter());
entity.setCreatedDate(cred.getCreatedDate()); entity.setCreatedDate(cred.getCreatedDate());
entity.setDevice(cred.getDevice()); entity.setUserLabel(cred.getUserLabel());
entity.setDigits(cred.getDigits());
entity.setHashIterations(cred.getHashIterations());
entity.setPeriod(cred.getPeriod());
entity.setSalt(cred.getSalt());
entity.setType(cred.getType()); entity.setType(cred.getType());
entity.setValue(cred.getValue()); entity.setSecretData(cred.getSecretData());
if (entity.getCredentialAttributes().isEmpty() && (cred.getConfig() == null || cred.getConfig().isEmpty())) { entity.setCredentialData(cred.getCredentialData());
} else {
MultivaluedHashMap<String, String> attrs = cred.getConfig();
MultivaluedHashMap<String, String> config = cred.getConfig();
if (config == null) config = new MultivaluedHashMap<>();
Iterator<CredentialAttributeEntity> it = entity.getCredentialAttributes().iterator();
while (it.hasNext()) {
CredentialAttributeEntity attr = it.next();
List<String> values = config.getList(attr.getName());
if (values == null || !values.contains(attr.getValue())) {
em.remove(attr);
it.remove();
} else {
attrs.add(attr.getName(), attr.getValue());
}
}
for (String key : config.keySet()) {
List<String> values = config.getList(key);
List<String> attrValues = attrs.getList(key);
for (String val : values) {
if (attrValues == null || !attrValues.contains(val)) {
CredentialAttributeEntity attr = new CredentialAttributeEntity();
attr.setId(KeycloakModelUtils.generateId());
attr.setValue(val);
attr.setName(key);
attr.setCredential(entity);
em.persist(attr);
entity.getCredentialAttributes().add(attr);
}
}
}
}
} }
@Override @Override
public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) {
CredentialEntity entity = new CredentialEntity(); CredentialEntity entity = createCredentialEntity(realm, user, cred);
String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId();
entity.setId(id);
entity.setAlgorithm(cred.getAlgorithm());
entity.setCounter(cred.getCounter());
entity.setCreatedDate(cred.getCreatedDate());
entity.setDevice(cred.getDevice());
entity.setDigits(cred.getDigits());
entity.setHashIterations(cred.getHashIterations());
entity.setPeriod(cred.getPeriod());
entity.setSalt(cred.getSalt());
entity.setType(cred.getType());
entity.setValue(cred.getValue());
UserEntity userRef = em.getReference(UserEntity.class, user.getId());
entity.setUser(userRef);
em.persist(entity);
MultivaluedHashMap<String, String> config = cred.getConfig();
if (config != null && !config.isEmpty()) {
for (String key : config.keySet()) {
List<String> values = config.getList(key);
for (String val : values) {
CredentialAttributeEntity attr = new CredentialAttributeEntity();
attr.setId(KeycloakModelUtils.generateId());
attr.setValue(val);
attr.setName(key);
attr.setCredential(entity);
em.persist(attr);
entity.getCredentialAttributes().add(attr);
}
}
}
return toModel(entity); return toModel(entity);
} }
@Override @Override
public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) {
CredentialEntity entity = em.find(CredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE); CredentialEntity entity = removeCredentialEntity(realm, user, id);
if (entity == null) return false; return entity != null;
em.remove(entity);
return true;
} }
@Override @Override
@ -155,67 +85,152 @@ public class JpaUserCredentialStore implements UserCredentialStore {
return model; return model;
} }
protected CredentialModel toModel(CredentialEntity entity) { CredentialModel toModel(CredentialEntity entity) {
CredentialModel model = new CredentialModel(); CredentialModel model = new CredentialModel();
model.setId(entity.getId()); model.setId(entity.getId());
model.setType(entity.getType()); model.setType(entity.getType());
model.setValue(entity.getValue());
model.setAlgorithm(entity.getAlgorithm());
model.setSalt(entity.getSalt());
model.setPeriod(entity.getPeriod());
model.setCounter(entity.getCounter());
model.setCreatedDate(entity.getCreatedDate()); model.setCreatedDate(entity.getCreatedDate());
model.setDevice(entity.getDevice()); model.setUserLabel(entity.getUserLabel());
model.setDigits(entity.getDigits());
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>(); // Backwards compatibility - users from previous version still have "salt" in the DB filled.
model.setConfig(config); // We migrate it to new secretData format on-the-fly
for (CredentialAttributeEntity attr : entity.getCredentialAttributes()) { if (entity.getSalt() != null) {
config.add(attr.getName(), attr.getValue()); String newSecretData = entity.getSecretData().replace("__SALT__", Base64.encodeBytes(entity.getSalt()));
entity.setSecretData(newSecretData);
entity.setSalt(null);
} }
model.setSecretData(entity.getSecretData());
model.setCredentialData(entity.getCredentialData());
return model; return model;
} }
@Override @Override
public List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user) { public List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user) {
List<CredentialEntity> results = getStoredCredentialEntities(realm, user);
// list is ordered correctly by priority (lowest priority value first)
return results.stream().map(this::toModel).collect(Collectors.toList());
}
private List<CredentialEntity> getStoredCredentialEntities(RealmModel realm, UserModel user) {
UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByUser", CredentialEntity.class) TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByUser", CredentialEntity.class)
.setParameter("user", userEntity); .setParameter("user", userEntity);
List<CredentialEntity> results = query.getResultList(); return query.getResultList();
List<CredentialModel> rtn = new LinkedList<>();
for (CredentialEntity entity : results) {
rtn.add(toModel(entity));
}
return rtn;
} }
@Override @Override
public List<CredentialModel> getStoredCredentialsByType(RealmModel realm, UserModel user, String type) { public List<CredentialModel> getStoredCredentialsByType(RealmModel realm, UserModel user, String type) {
UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); return getStoredCredentials(realm, user).stream().filter(credential -> type.equals(credential.getType())).collect(Collectors.toList());
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByUserAndType", CredentialEntity.class)
.setParameter("type", type)
.setParameter("user", userEntity);
List<CredentialEntity> results = query.getResultList();
List<CredentialModel> rtn = new LinkedList<>();
for (CredentialEntity entity : results) {
rtn.add(toModel(entity));
}
return rtn;
} }
@Override @Override
public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) {
UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); List<CredentialModel> results = getStoredCredentials(realm, user).stream().filter(credential ->
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByNameAndType", CredentialEntity.class) type.equals(credential.getType()) && name.equals(credential.getUserLabel())).collect(Collectors.toList());
.setParameter("type", type)
.setParameter("device", name)
.setParameter("user", userEntity);
List<CredentialEntity> results = query.getResultList();
if (results.isEmpty()) return null; if (results.isEmpty()) return null;
return toModel(results.get(0)); return results.get(0);
} }
@Override @Override
public void close() { public void close() {
} }
CredentialEntity createCredentialEntity(RealmModel realm, UserModel user, CredentialModel cred) {
CredentialEntity entity = new CredentialEntity();
String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId();
entity.setId(id);
entity.setCreatedDate(cred.getCreatedDate());
entity.setUserLabel(cred.getUserLabel());
entity.setType(cred.getType());
entity.setSecretData(cred.getSecretData());
entity.setCredentialData(cred.getCredentialData());
UserEntity userRef = em.getReference(UserEntity.class, user.getId());
entity.setUser(userRef);
//add in linkedlist to last position
List<CredentialEntity> credentials = getStoredCredentialEntities(realm, user);
int priority = credentials.isEmpty() ? PRIORITY_DIFFERENCE : credentials.get(credentials.size() - 1).getPriority() + PRIORITY_DIFFERENCE;
entity.setPriority(priority);
em.persist(entity);
return entity;
}
CredentialEntity removeCredentialEntity(RealmModel realm, UserModel user, String id) {
CredentialEntity entity = em.find(CredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
if (entity == null) return null;
int currentPriority = entity.getPriority();
List<CredentialEntity> credentials = getStoredCredentialEntities(realm, user);
// Decrease priority of all credentials after our
for (CredentialEntity cred : credentials) {
if (cred.getPriority() > currentPriority) {
cred.setPriority(cred.getPriority() - PRIORITY_DIFFERENCE);
}
}
em.remove(entity);
return entity;
}
////Operations to handle the linked list of credentials
@Override
public boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId) {
List<CredentialEntity> sortedCreds = getStoredCredentialEntities(realm, user);
// 1 - Create new list and move everything to it.
List<CredentialEntity> newList = new ArrayList<>();
newList.addAll(sortedCreds);
// 2 - Find indexes of our and newPrevious credential
int ourCredentialIndex = -1;
int newPreviousCredentialIndex = -1;
CredentialEntity ourCredential = null;
int i = 0;
for (CredentialEntity credential : newList) {
if (id.equals(credential.getId())) {
ourCredentialIndex = i;
ourCredential = credential;
} else if(newPreviousCredentialId != null && newPreviousCredentialId.equals(credential.getId())) {
newPreviousCredentialIndex = i;
}
i++;
}
if (ourCredentialIndex == -1) {
logger.warnf("Not found credential with id [%s] of user [%s]", id, user.getUsername());
return false;
}
if (newPreviousCredentialId != null && newPreviousCredentialIndex == -1) {
logger.warnf("Can't move up credential with id [%s] of user [%s]", id, user.getUsername());
return false;
}
// 3 - Compute index where we move our credential
int toMoveIndex = newPreviousCredentialId==null ? 0 : newPreviousCredentialIndex + 1;
// 4 - Insert our credential to new position, remove it from the old position
newList.add(toMoveIndex, ourCredential);
int indexToRemove = toMoveIndex < ourCredentialIndex ? ourCredentialIndex + 1 : ourCredentialIndex;
newList.remove(indexToRemove);
// 5 - newList contains credentials in requested order now. Iterate through whole list and change priorities accordingly.
int expectedPriority = 0;
for (CredentialEntity credential : newList) {
expectedPriority += PRIORITY_DIFFERENCE;
if (credential.getPriority() != expectedPriority) {
credential.setPriority(expectedPriority);
logger.tracef("Priority of credential [%s] of user [%s] changed to [%d]", credential.getId(), user.getUsername(), expectedPriority);
}
}
return true;
}
} }

View file

@ -18,7 +18,6 @@
package org.keycloak.models.jpa; package org.keycloak.models.jpa;
import org.keycloak.authorization.jpa.entities.ResourceEntity; import org.keycloak.authorization.jpa.entities.ResourceEntity;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
@ -37,7 +36,6 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider; import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.CredentialAttributeEntity;
import org.keycloak.models.jpa.entities.CredentialEntity; import org.keycloak.models.jpa.entities.CredentialEntity;
import org.keycloak.models.jpa.entities.FederatedIdentityEntity; import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
import org.keycloak.models.jpa.entities.UserConsentClientScopeEntity; import org.keycloak.models.jpa.entities.UserConsentClientScopeEntity;
@ -60,7 +58,6 @@ import javax.persistence.criteria.Subquery;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -68,12 +65,12 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.persistence.LockModeType; import javax.persistence.LockModeType;
import javax.persistence.criteria.Expression; import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
@SuppressWarnings("JpaQueryApiInspection")
public class JpaUserProvider implements UserProvider, UserCredentialStore { public class JpaUserProvider implements UserProvider, UserCredentialStore {
private static final String EMAIL = "email"; private static final String EMAIL = "email";
@ -83,10 +80,12 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
private final KeycloakSession session; private final KeycloakSession session;
protected EntityManager em; protected EntityManager em;
private final JpaUserCredentialStore credentialStore;
public JpaUserProvider(KeycloakSession session, EntityManager em) { public JpaUserProvider(KeycloakSession session, EntityManager em) {
this.session = session; this.session = session;
this.em = em; this.em = em;
credentialStore = new JpaUserCredentialStore(session, em);
} }
@Override @Override
@ -382,8 +381,6 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteFederatedIdentityByRealm") num = em.createNamedQuery("deleteFederatedIdentityByRealm")
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteCredentialAttributeByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteCredentialsByRealm") num = em.createNamedQuery("deleteCredentialsByRealm")
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserAttributesByRealm") num = em.createNamedQuery("deleteUserAttributesByRealm")
@ -408,10 +405,6 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
.setParameter("realmId", realm.getId()) .setParameter("realmId", realm.getId())
.setParameter("link", storageProviderId) .setParameter("link", storageProviderId)
.executeUpdate(); .executeUpdate();
num = em.createNamedQuery("deleteCredentialAttributeByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", storageProviderId)
.executeUpdate();
num = em.createNamedQuery("deleteCredentialsByRealmAndLink") num = em.createNamedQuery("deleteCredentialsByRealmAndLink")
.setParameter("realmId", realm.getId()) .setParameter("realmId", realm.getId())
.setParameter("link", storageProviderId) .setParameter("link", storageProviderId)
@ -858,93 +851,12 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
@Override @Override
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
CredentialEntity entity = em.find(CredentialEntity.class, cred.getId()); credentialStore.updateCredential(realm, user, cred);
if (entity == null) return;
entity.setAlgorithm(cred.getAlgorithm());
entity.setCounter(cred.getCounter());
entity.setCreatedDate(cred.getCreatedDate());
entity.setDevice(cred.getDevice());
entity.setDigits(cred.getDigits());
entity.setHashIterations(cred.getHashIterations());
entity.setPeriod(cred.getPeriod());
entity.setSalt(cred.getSalt());
entity.setType(cred.getType());
entity.setValue(cred.getValue());
if (entity.getCredentialAttributes().isEmpty() && (cred.getConfig() == null || cred.getConfig().isEmpty())) {
} else {
MultivaluedHashMap<String, String> attrs = cred.getConfig();
MultivaluedHashMap<String, String> config = cred.getConfig();
if (config == null) config = new MultivaluedHashMap<>();
Iterator<CredentialAttributeEntity> it = entity.getCredentialAttributes().iterator();
while (it.hasNext()) {
CredentialAttributeEntity attr = it.next();
List<String> values = config.getList(attr.getName());
if (values == null || !values.contains(attr.getValue())) {
em.remove(attr);
it.remove();
} else {
attrs.add(attr.getName(), attr.getValue());
}
}
for (String key : config.keySet()) {
List<String> values = config.getList(key);
List<String> attrValues = attrs.getList(key);
for (String val : values) {
if (attrValues == null || !attrValues.contains(val)) {
CredentialAttributeEntity attr = new CredentialAttributeEntity();
attr.setId(KeycloakModelUtils.generateId());
attr.setValue(val);
attr.setName(key);
attr.setCredential(entity);
em.persist(attr);
entity.getCredentialAttributes().add(attr);
}
}
}
}
} }
@Override @Override
public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) {
CredentialEntity entity = new CredentialEntity(); CredentialEntity entity = credentialStore.createCredentialEntity(realm, user, cred);
String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId();
entity.setId(id);
entity.setAlgorithm(cred.getAlgorithm());
entity.setCounter(cred.getCounter());
entity.setCreatedDate(cred.getCreatedDate());
entity.setDevice(cred.getDevice());
entity.setDigits(cred.getDigits());
entity.setHashIterations(cred.getHashIterations());
entity.setPeriod(cred.getPeriod());
entity.setSalt(cred.getSalt());
entity.setType(cred.getType());
entity.setValue(cred.getValue());
UserEntity userRef = em.getReference(UserEntity.class, user.getId());
entity.setUser(userRef);
em.persist(entity);
MultivaluedHashMap<String, String> config = cred.getConfig();
if (config != null && !config.isEmpty()) {
for (String key : config.keySet()) {
List<String> values = config.getList(key);
for (String val : values) {
CredentialAttributeEntity attr = new CredentialAttributeEntity();
attr.setId(KeycloakModelUtils.generateId());
attr.setValue(val);
attr.setName(key);
attr.setCredential(entity);
em.persist(attr);
entity.getCredentialAttributes().add(attr);
}
}
}
UserEntity userEntity = userInEntityManagerContext(user.getId()); UserEntity userEntity = userInEntityManagerContext(user.getId());
if (userEntity != null) { if (userEntity != null) {
@ -955,56 +867,26 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
@Override @Override
public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) {
CredentialEntity entity = em.find(CredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE); CredentialEntity entity = credentialStore.removeCredentialEntity(realm, user, id);
if (entity == null) return false;
em.remove(entity);
UserEntity userEntity = userInEntityManagerContext(user.getId()); UserEntity userEntity = userInEntityManagerContext(user.getId());
if (userEntity != null) { if (entity != null && userEntity != null) {
userEntity.getCredentials().remove(entity); userEntity.getCredentials().remove(entity);
} }
return true; return entity != null;
} }
@Override @Override
public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) { public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) {
CredentialEntity entity = em.find(CredentialEntity.class, id); return credentialStore.getStoredCredentialById(realm, user, id);
if (entity == null) return null;
CredentialModel model = toModel(entity);
return model;
} }
protected CredentialModel toModel(CredentialEntity entity) { protected CredentialModel toModel(CredentialEntity entity) {
CredentialModel model = new CredentialModel(); return credentialStore.toModel(entity);
model.setId(entity.getId());
model.setType(entity.getType());
model.setValue(entity.getValue());
model.setAlgorithm(entity.getAlgorithm());
model.setSalt(entity.getSalt());
model.setPeriod(entity.getPeriod());
model.setCounter(entity.getCounter());
model.setCreatedDate(entity.getCreatedDate());
model.setDevice(entity.getDevice());
model.setDigits(entity.getDigits());
model.setHashIterations(entity.getHashIterations());
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
model.setConfig(config);
for (CredentialAttributeEntity attr : entity.getCredentialAttributes()) {
config.add(attr.getName(), attr.getValue());
}
return model;
} }
@Override @Override
public List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user) { public List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user) {
UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); return credentialStore.getStoredCredentials(realm, user);
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByUser", CredentialEntity.class)
.setParameter("user", userEntity);
List<CredentialEntity> results = query.getResultList();
List<CredentialModel> rtn = new LinkedList<>();
for (CredentialEntity entity : results) {
rtn.add(toModel(entity));
}
return rtn;
} }
@Override @Override
@ -1014,31 +896,25 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
if (userEntity != null) { if (userEntity != null) {
// user already in persistence context, no need to execute a query // user already in persistence context, no need to execute a query
results = userEntity.getCredentials().stream().filter(it -> it.getType().equals(type)).collect(Collectors.toList()); results = userEntity.getCredentials().stream().filter(it -> type.equals(it.getType())).collect(Collectors.toList());
} else {
userEntity = em.getReference(UserEntity.class, user.getId());
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByUserAndType", CredentialEntity.class)
.setParameter("type", type)
.setParameter("user", userEntity);
results = query.getResultList();
}
List<CredentialModel> rtn = new LinkedList<>(); List<CredentialModel> rtn = new LinkedList<>();
for (CredentialEntity entity : results) { for (CredentialEntity entity : results) {
rtn.add(toModel(entity)); rtn.add(toModel(entity));
} }
return rtn; return rtn;
} else {
return credentialStore.getStoredCredentialsByType(realm, user, type);
}
} }
@Override @Override
public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) {
UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); return credentialStore.getStoredCredentialByNameAndType(realm, user, name, type);
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByNameAndType", CredentialEntity.class) }
.setParameter("type", type)
.setParameter("device", name) @Override
.setParameter("user", userEntity); public boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId) {
List<CredentialEntity> results = query.getResultList(); return credentialStore.moveCredentialTo(realm, user, id, newPreviousCredentialId);
if (results.isEmpty()) return null;
return toModel(results.get(0));
} }
// Could override this to provide a custom behavior. // Could override this to provide a custom behavior.

View file

@ -587,6 +587,9 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override @Override
public int getActionTokenGeneratedByUserLifespan(String actionTokenId) { public int getActionTokenGeneratedByUserLifespan(String actionTokenId) {
if (actionTokenId == null || getAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN + "." + actionTokenId) == null) {
return getActionTokenGeneratedByUserLifespan();
}
return getAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN + "." + actionTokenId, getAccessCodeLifespanUserAction()); return getAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN + "." + actionTokenId, getAccessCodeLifespanUserAction());
} }
@ -1669,6 +1672,16 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
return entityToModel(entity); return entityToModel(entity);
} }
public AuthenticationExecutionModel getAuthenticationExecutionByFlowId(String flowId) {
TypedQuery<AuthenticationExecutionEntity> query = em.createNamedQuery("authenticationFlowExecution", AuthenticationExecutionEntity.class)
.setParameter("flowId", flowId);
if (query.getResultList().isEmpty()) {
return null;
}
AuthenticationExecutionEntity authenticationFlowExecution = query.getResultList().get(0);
return entityToModel(authenticationFlowExecution);
}
@Override @Override
public AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model) { public AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model) {
AuthenticationExecutionEntity entity = new AuthenticationExecutionEntity(); AuthenticationExecutionEntity entity = new AuthenticationExecutionEntity();
@ -1700,6 +1713,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
entity.setRequirement(model.getRequirement()); entity.setRequirement(model.getRequirement());
entity.setAuthenticatorConfig(model.getAuthenticatorConfig()); entity.setAuthenticatorConfig(model.getAuthenticatorConfig());
entity.setFlowId(model.getFlowId()); entity.setFlowId(model.getFlowId());
if (model.getParentFlow() != null) {
AuthenticationFlowEntity flow = em.find(AuthenticationFlowEntity.class, model.getParentFlow());
entity.setParentFlow(flow);
}
em.flush(); em.flush();
} }

View file

@ -27,12 +27,17 @@ import javax.persistence.FetchType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table; import javax.persistence.Table;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
@NamedQueries({
@NamedQuery(name = "authenticationFlowExecution", query = "select authExec from AuthenticationExecutionEntity authExec where authExec.flowId = :flowId")
})
@Table(name="AUTHENTICATION_EXECUTION") @Table(name="AUTHENTICATION_EXECUTION")
@Entity @Entity
public class AuthenticationExecutionEntity { public class AuthenticationExecutionEntity {

View file

@ -28,6 +28,8 @@ import javax.persistence.FetchType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.ArrayList; import java.util.ArrayList;

View file

@ -1,111 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.jpa.entities;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="getCredentialAttribute", query="select attr from CredentialAttributeEntity attr where attr.credential = :credential"),
@NamedQuery(name="deleteCredentialAttributeByCredential", query="delete from CredentialAttributeEntity attr where attr.credential = :credential"),
@NamedQuery(name="deleteCredentialAttributeByRealm", query="delete from CredentialAttributeEntity attr where attr.credential IN (select cred from CredentialEntity cred where cred.user IN (select u from UserEntity u where u.realmId=:realmId))"),
@NamedQuery(name="deleteCredentialAttributeByRealmAndLink", query="delete from CredentialAttributeEntity attr where attr.credential IN (select cred from CredentialEntity cred where cred.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link))"),
@NamedQuery(name="deleteCredentialAttributeByUser", query="delete from CredentialAttributeEntity attr where attr.credential IN (select cred from CredentialEntity cred where cred.user = :user)"),
})
@Table(name="CREDENTIAL_ATTRIBUTE")
@Entity
public class CredentialAttributeEntity {
@Id
@Column(name="ID", length = 36)
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
protected String id;
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "CREDENTIAL_ID")
protected CredentialEntity credential;
@Column(name = "NAME")
protected String name;
@Column(name = "VALUE")
protected String value;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public CredentialEntity getCredential() {
return credential;
}
public void setCredential(CredentialEntity credential) {
this.credential = credential;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof CredentialAttributeEntity)) return false;
CredentialAttributeEntity that = (CredentialAttributeEntity) o;
if (!id.equals(that.getId())) return false;
return true;
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View file

@ -25,6 +25,7 @@ import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries; import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
@ -38,9 +39,7 @@ import java.util.Collection;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
@NamedQueries({ @NamedQueries({
@NamedQuery(name="credentialByUser", query="select cred from CredentialEntity cred where cred.user = :user"), @NamedQuery(name="credentialByUser", query="select cred from CredentialEntity cred where cred.user = :user order by cred.priority"),
@NamedQuery(name="credentialByUserAndType", query="select cred from CredentialEntity cred where cred.user = :user and cred.type = :type"),
@NamedQuery(name="credentialByNameAndType", query="select cred from CredentialEntity cred where cred.user = :user and cred.type = :type and cred.device = :device"),
@NamedQuery(name="deleteCredentialsByRealm", query="delete from CredentialEntity cred where cred.user IN (select u from UserEntity u where u.realmId=:realmId)"), @NamedQuery(name="deleteCredentialsByRealm", query="delete from CredentialEntity cred where cred.user IN (select u from UserEntity u where u.realmId=:realmId)"),
@NamedQuery(name="deleteCredentialsByRealmAndLink", query="delete from CredentialEntity cred where cred.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)") @NamedQuery(name="deleteCredentialsByRealmAndLink", query="delete from CredentialEntity cred where cred.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)")
@ -55,14 +54,10 @@ public class CredentialEntity {
@Column(name="TYPE") @Column(name="TYPE")
protected String type; protected String type;
@Column(name="VALUE")
protected String value; @Column(name="USER_LABEL")
@Column(name="DEVICE") protected String userLabel;
protected String device;
@Column(name="SALT")
protected byte[] salt;
@Column(name="HASH_ITERATIONS")
protected int hashIterations;
@Column(name="CREATED_DATE") @Column(name="CREATED_DATE")
protected Long createdDate; protected Long createdDate;
@ -70,121 +65,84 @@ public class CredentialEntity {
@JoinColumn(name="USER_ID") @JoinColumn(name="USER_ID")
protected UserEntity user; protected UserEntity user;
@Column(name="COUNTER") @Column(name="SECRET_DATA")
protected int counter; protected String secretData;
@Column(name="ALGORITHM") @Column(name="CREDENTIAL_DATA")
protected String algorithm; protected String credentialData;
@Column(name="DIGITS")
protected int digits;
@Column(name="PERIOD")
protected int period;
@OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.EAGER, orphanRemoval = true, mappedBy="credential") @Column(name="PRIORITY")
protected Collection<CredentialAttributeEntity> credentialAttributes = new ArrayList<>(); protected int priority;
@Deprecated // Needed just for backwards compatibility when migrating old credentials
@Column(name="SALT")
protected byte[] salt;
public String getId() { public String getId() {
return id; return id;
} }
public void setId(String id) { public void setId(String id) {
this.id = id; this.id = id;
} }
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getType() { public String getType() {
return type; return type;
} }
public void setType(String type) { public void setType(String type) {
this.type = type; this.type = type;
} }
public String getDevice() { public String getUserLabel() {
return device; return userLabel;
} }
public void setUserLabel(String userLabel) {
public void setDevice(String device) { this.userLabel = userLabel;
this.device = device;
} }
public UserEntity getUser() { public UserEntity getUser() {
return user; return user;
} }
public void setUser(UserEntity user) { public void setUser(UserEntity user) {
this.user = user; this.user = user;
} }
@Deprecated
public byte[] getSalt() { public byte[] getSalt() {
return salt; return salt;
} }
@Deprecated
public void setSalt(byte[] salt) { public void setSalt(byte[] salt) {
this.salt = salt; this.salt = salt;
} }
public int getHashIterations() {
return hashIterations;
}
public void setHashIterations(int hashIterations) {
this.hashIterations = hashIterations;
}
public Long getCreatedDate() { public Long getCreatedDate() {
return createdDate; return createdDate;
} }
public void setCreatedDate(Long createdDate) { public void setCreatedDate(Long createdDate) {
this.createdDate = createdDate; this.createdDate = createdDate;
} }
public int getCounter() { public String getSecretData() {
return counter; return secretData;
}
public void setSecretData(String secretData) {
this.secretData = secretData;
} }
public void setCounter(int counter) { public String getCredentialData() {
this.counter = counter; return credentialData;
}
public void setCredentialData(String credentialData) {
this.credentialData = credentialData;
} }
public String getAlgorithm() { public int getPriority() {
return algorithm; return priority;
} }
public void setAlgorithm(String algorithm) { public void setPriority(int priority) {
this.algorithm = algorithm; this.priority = priority;
}
public int getDigits() {
return digits;
}
public void setDigits(int digits) {
this.digits = digits;
}
public int getPeriod() {
return period;
}
public void setPeriod(int period) {
this.period = period;
}
public Collection<CredentialAttributeEntity> getCredentialAttributes() {
return credentialAttributes;
}
public void setCredentialAttributes(Collection<CredentialAttributeEntity> credentialAttributes) {
this.credentialAttributes = credentialAttributes;
} }
@Override @Override

View file

@ -16,6 +16,8 @@
*/ */
package org.keycloak.storage.jpa; package org.keycloak.storage.jpa;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
@ -33,6 +35,8 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.JpaUserCredentialStore;
import org.keycloak.models.jpa.entities.CredentialEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.StorageId; import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
@ -43,7 +47,6 @@ import org.keycloak.storage.jpa.entity.FederatedUser;
import org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity; import org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity;
import org.keycloak.storage.jpa.entity.FederatedUserConsentClientScopeEntity; import org.keycloak.storage.jpa.entity.FederatedUserConsentClientScopeEntity;
import org.keycloak.storage.jpa.entity.FederatedUserConsentEntity; import org.keycloak.storage.jpa.entity.FederatedUserConsentEntity;
import org.keycloak.storage.jpa.entity.FederatedUserCredentialAttributeEntity;
import org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity; import org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity;
import org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity; import org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity;
import org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity; import org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity;
@ -55,9 +58,9 @@ import javax.persistence.TypedQuery;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.ListIterator;
import java.util.Set; import java.util.Set;
import javax.persistence.LockModeType; import javax.persistence.LockModeType;
@ -69,6 +72,8 @@ public class JpaUserFederatedStorageProvider implements
UserFederatedStorageProvider, UserFederatedStorageProvider,
UserCredentialStore { UserCredentialStore {
protected static final Logger logger = Logger.getLogger(JpaUserFederatedStorageProvider.class);
private final KeycloakSession session; private final KeycloakSession session;
protected EntityManager em; protected EntityManager em;
@ -565,53 +570,11 @@ public class JpaUserFederatedStorageProvider implements
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, cred.getId()); FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, cred.getId());
if (entity == null) return; if (entity == null) return;
createIndex(realm, userId); createIndex(realm, userId);
entity.setAlgorithm(cred.getAlgorithm());
entity.setCounter(cred.getCounter());
entity.setCreatedDate(cred.getCreatedDate()); entity.setCreatedDate(cred.getCreatedDate());
entity.setDevice(cred.getDevice());
entity.setDigits(cred.getDigits());
entity.setHashIterations(cred.getHashIterations());
entity.setPeriod(cred.getPeriod());
entity.setSalt(cred.getSalt());
entity.setType(cred.getType()); entity.setType(cred.getType());
entity.setValue(cred.getValue()); entity.setCredentialData(cred.getCredentialData());
if (entity.getCredentialAttributes().isEmpty() && (cred.getConfig() == null || cred.getConfig().isEmpty())) { entity.setSecretData(cred.getSecretData());
cred.setUserLabel(entity.getUserLabel());
} else {
MultivaluedHashMap<String, String> attrs = new MultivaluedHashMap<>();
MultivaluedHashMap<String, String> config = cred.getConfig();
if (config == null) config = new MultivaluedHashMap<>();
Iterator<FederatedUserCredentialAttributeEntity> it = entity.getCredentialAttributes().iterator();
while (it.hasNext()) {
FederatedUserCredentialAttributeEntity attr = it.next();
List<String> values = config.getList(attr.getName());
if (values == null || !values.contains(attr.getValue())) {
em.remove(attr);
it.remove();
} else {
attrs.add(attr.getName(), attr.getValue());
}
}
for (String key : config.keySet()) {
List<String> values = config.getList(key);
List<String> attrValues = attrs.getList(key);
for (String val : values) {
if (attrValues == null || !attrValues.contains(val)) {
FederatedUserCredentialAttributeEntity attr = new FederatedUserCredentialAttributeEntity();
attr.setId(KeycloakModelUtils.generateId());
attr.setValue(val);
attr.setName(key);
attr.setCredential(entity);
em.persist(attr);
entity.getCredentialAttributes().add(attr);
}
}
}
}
} }
@Override @Override
@ -620,37 +583,22 @@ public class JpaUserFederatedStorageProvider implements
FederatedUserCredentialEntity entity = new FederatedUserCredentialEntity(); FederatedUserCredentialEntity entity = new FederatedUserCredentialEntity();
String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId(); String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId();
entity.setId(id); entity.setId(id);
entity.setAlgorithm(cred.getAlgorithm());
entity.setCounter(cred.getCounter());
entity.setCreatedDate(cred.getCreatedDate()); entity.setCreatedDate(cred.getCreatedDate());
entity.setDevice(cred.getDevice());
entity.setDigits(cred.getDigits());
entity.setHashIterations(cred.getHashIterations());
entity.setPeriod(cred.getPeriod());
entity.setSalt(cred.getSalt());
entity.setType(cred.getType()); entity.setType(cred.getType());
entity.setValue(cred.getValue()); entity.setCredentialData(cred.getCredentialData());
entity.setSecretData(cred.getSecretData());
entity.setUserLabel(cred.getUserLabel());
entity.setUserId(userId); entity.setUserId(userId);
entity.setRealmId(realm.getId()); entity.setRealmId(realm.getId());
entity.setStorageProviderId(new StorageId(userId).getProviderId()); entity.setStorageProviderId(new StorageId(userId).getProviderId());
//add in linkedlist to last position
List<FederatedUserCredentialEntity> credentials = getStoredCredentialEntities(userId);
int priority = credentials.isEmpty() ? JpaUserCredentialStore.PRIORITY_DIFFERENCE : credentials.get(credentials.size() - 1).getPriority() + JpaUserCredentialStore.PRIORITY_DIFFERENCE;
entity.setPriority(priority);
em.persist(entity); em.persist(entity);
MultivaluedHashMap<String, String> config = cred.getConfig();
if (config != null && !config.isEmpty()) {
for (String key : config.keySet()) {
List<String> values = config.getList(key);
for (String val : values) {
FederatedUserCredentialAttributeEntity attr = new FederatedUserCredentialAttributeEntity();
attr.setId(KeycloakModelUtils.generateId());
attr.setValue(val);
attr.setName(key);
attr.setCredential(entity);
em.persist(attr);
entity.getCredentialAttributes().add(attr);
}
}
}
return toModel(entity); return toModel(entity);
} }
@ -658,6 +606,18 @@ public class JpaUserFederatedStorageProvider implements
public boolean removeStoredCredential(RealmModel realm, String userId, String id) { public boolean removeStoredCredential(RealmModel realm, String userId, String id) {
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE); FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
if (entity == null) return false; if (entity == null) return false;
int currentPriority = entity.getPriority();
List<FederatedUserCredentialEntity> credentials = getStoredCredentialEntities(userId);
// Decrease priority of all credentials after our
for (FederatedUserCredentialEntity cred : credentials) {
if (cred.getPriority() > currentPriority) {
cred.setPriority(cred.getPriority() - JpaUserCredentialStore.PRIORITY_DIFFERENCE);
}
}
em.remove(entity); em.remove(entity);
return true; return true;
} }
@ -674,28 +634,25 @@ public class JpaUserFederatedStorageProvider implements
CredentialModel model = new CredentialModel(); CredentialModel model = new CredentialModel();
model.setId(entity.getId()); model.setId(entity.getId());
model.setType(entity.getType()); model.setType(entity.getType());
model.setValue(entity.getValue());
model.setAlgorithm(entity.getAlgorithm());
model.setSalt(entity.getSalt());
model.setPeriod(entity.getPeriod());
model.setCounter(entity.getCounter());
model.setCreatedDate(entity.getCreatedDate()); model.setCreatedDate(entity.getCreatedDate());
model.setDevice(entity.getDevice()); model.setUserLabel(entity.getUserLabel());
model.setDigits(entity.getDigits());
model.setHashIterations(entity.getHashIterations()); // Backwards compatibility - users from previous version still have "salt" in the DB filled.
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>(); // We migrate it to new secretData format on-the-fly
model.setConfig(config); if (entity.getSalt() != null) {
for (FederatedUserCredentialAttributeEntity attr : entity.getCredentialAttributes()) { String newSecretData = entity.getSecretData().replace("__SALT__", Base64.encodeBytes(entity.getSalt()));
config.add(attr.getName(), attr.getValue()); entity.setSecretData(newSecretData);
entity.setSalt(null);
} }
model.setSecretData(entity.getSecretData());
model.setCredentialData(entity.getCredentialData());
return model; return model;
} }
@Override @Override
public List<CredentialModel> getStoredCredentials(RealmModel realm, String userId) { public List<CredentialModel> getStoredCredentials(RealmModel realm, String userId) {
TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByUser", FederatedUserCredentialEntity.class) List<FederatedUserCredentialEntity> results = getStoredCredentialEntities(userId);
.setParameter("userId", userId);
List<FederatedUserCredentialEntity> results = query.getResultList();
List<CredentialModel> rtn = new LinkedList<>(); List<CredentialModel> rtn = new LinkedList<>();
for (FederatedUserCredentialEntity entity : results) { for (FederatedUserCredentialEntity entity : results) {
rtn.add(toModel(entity)); rtn.add(toModel(entity));
@ -703,6 +660,12 @@ public class JpaUserFederatedStorageProvider implements
return rtn; return rtn;
} }
private List<FederatedUserCredentialEntity> getStoredCredentialEntities(String userId) {
TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByUser", FederatedUserCredentialEntity.class)
.setParameter("userId", userId);
return query.getResultList();
}
@Override @Override
public List<CredentialModel> getStoredCredentialsByType(RealmModel realm, String userId, String type) { public List<CredentialModel> getStoredCredentialsByType(RealmModel realm, String userId, String type) {
TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByUserAndType", FederatedUserCredentialEntity.class) TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByUserAndType", FederatedUserCredentialEntity.class)
@ -720,7 +683,7 @@ public class JpaUserFederatedStorageProvider implements
public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, String userId, String name, String type) { public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, String userId, String name, String type) {
TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByNameAndType", FederatedUserCredentialEntity.class) TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByNameAndType", FederatedUserCredentialEntity.class)
.setParameter("type", type) .setParameter("type", type)
.setParameter("device", name) .setParameter("userLabel", name)
.setParameter("userId", userId); .setParameter("userId", userId);
List<FederatedUserCredentialEntity> results = query.getResultList(); List<FederatedUserCredentialEntity> results = query.getResultList();
if (results.isEmpty()) return null; if (results.isEmpty()) return null;
@ -771,6 +734,60 @@ public class JpaUserFederatedStorageProvider implements
return getStoredCredentialByNameAndType(realm, user.getId(), name, type); return getStoredCredentialByNameAndType(realm, user.getId(), name, type);
} }
@Override
public boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId) {
List<FederatedUserCredentialEntity> sortedCreds = getStoredCredentialEntities(user.getId());
// 1 - Create new list and move everything to it.
List<FederatedUserCredentialEntity> newList = new ArrayList<>();
newList.addAll(sortedCreds);
// 2 - Find indexes of our and newPrevious credential
int ourCredentialIndex = -1;
int newPreviousCredentialIndex = -1;
FederatedUserCredentialEntity ourCredential = null;
int i = 0;
for (FederatedUserCredentialEntity credential : newList) {
if (id.equals(credential.getId())) {
ourCredentialIndex = i;
ourCredential = credential;
} else if(newPreviousCredentialId != null && newPreviousCredentialId.equals(credential.getId())) {
newPreviousCredentialIndex = i;
}
i++;
}
if (ourCredentialIndex == -1) {
logger.warnf("Not found credential with id [%s] of user [%s]", id, user.getUsername());
return false;
}
if (newPreviousCredentialId != null && newPreviousCredentialIndex == -1) {
logger.warnf("Can't move up credential with id [%s] of user [%s]", id, user.getUsername());
return false;
}
// 3 - Compute index where we move our credential
int toMoveIndex = newPreviousCredentialId==null ? 0 : newPreviousCredentialIndex + 1;
// 4 - Insert our credential to new position, remove it from the old position
newList.add(toMoveIndex, ourCredential);
int indexToRemove = toMoveIndex < ourCredentialIndex ? ourCredentialIndex + 1 : ourCredentialIndex;
newList.remove(indexToRemove);
// 5 - newList contains credentials in requested order now. Iterate through whole list and change priorities accordingly.
int expectedPriority = 0;
for (FederatedUserCredentialEntity credential : newList) {
expectedPriority += JpaUserCredentialStore.PRIORITY_DIFFERENCE;
if (credential.getPriority() != expectedPriority) {
credential.setPriority(expectedPriority);
logger.tracef("Priority of credential [%s] of user [%s] changed to [%d]", credential.getId(), user.getUsername(), expectedPriority);
}
}
return true;
}
@Override @Override
public int getStoredUsersCount(RealmModel realm) { public int getStoredUsersCount(RealmModel realm) {
Object count = em.createNamedQuery("getFederatedUserCount") Object count = em.createNamedQuery("getFederatedUserCount")
@ -791,8 +808,6 @@ public class JpaUserFederatedStorageProvider implements
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteBrokerLinkByRealm") num = em.createNamedQuery("deleteBrokerLinkByRealm")
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteFederatedCredentialAttributeByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteFederatedUserCredentialsByRealm") num = em.createNamedQuery("deleteFederatedUserCredentialsByRealm")
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserFederatedAttributesByRealm") num = em.createNamedQuery("deleteUserFederatedAttributesByRealm")
@ -862,10 +877,6 @@ public class JpaUserFederatedStorageProvider implements
.setParameter("userId", user.getId()) .setParameter("userId", user.getId())
.setParameter("realmId", realm.getId()) .setParameter("realmId", realm.getId())
.executeUpdate(); .executeUpdate();
em.createNamedQuery("deleteFederatedCredentialAttributeByUser")
.setParameter("userId", user.getId())
.setParameter("realmId", realm.getId())
.executeUpdate();
em.createNamedQuery("deleteFederatedUserCredentialByUser") em.createNamedQuery("deleteFederatedUserCredentialByUser")
.setParameter("userId", user.getId()) .setParameter("userId", user.getId())
.setParameter("realmId", realm.getId()) .setParameter("realmId", realm.getId())
@ -905,9 +916,6 @@ public class JpaUserFederatedStorageProvider implements
em.createNamedQuery("deleteFederatedUserConsentsByStorageProvider") em.createNamedQuery("deleteFederatedUserConsentsByStorageProvider")
.setParameter("storageProviderId", model.getId()) .setParameter("storageProviderId", model.getId())
.executeUpdate(); .executeUpdate();
em.createNamedQuery("deleteFederatedCredentialAttributeByStorageProvider")
.setParameter("storageProviderId", model.getId())
.executeUpdate();
em.createNamedQuery("deleteFederatedUserCredentialsByStorageProvider") em.createNamedQuery("deleteFederatedUserCredentialsByStorageProvider")
.setParameter("storageProviderId", model.getId()) .setParameter("storageProviderId", model.getId())
.executeUpdate(); .executeUpdate();

View file

@ -1,111 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.jpa.entity;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="deleteFederatedCredentialAttributeByCredential", query="delete from FederatedUserCredentialAttributeEntity attr where attr.credential = :credential"),
@NamedQuery(name="deleteFederatedCredentialAttributeByStorageProvider", query="delete from FederatedUserCredentialAttributeEntity attr where attr.credential IN (select cred from FederatedUserCredentialEntity cred where cred.storageProviderId=:storageProviderId)"),
@NamedQuery(name="deleteFederatedCredentialAttributeByRealm", query="delete from FederatedUserCredentialAttributeEntity attr where attr.credential IN (select cred from FederatedUserCredentialEntity cred where cred.realmId=:realmId)"),
@NamedQuery(name="deleteFederatedCredentialAttributeByRealmAndLink", query="delete from FederatedUserCredentialAttributeEntity attr where attr.credential IN (select cred from FederatedUserCredentialEntity cred where cred.userId IN (select u.id from UserEntity u where u.realmId=:realmId and u.federationLink=:link))"),
@NamedQuery(name="deleteFederatedCredentialAttributeByUser", query="delete from FederatedUserCredentialAttributeEntity attr where attr.credential IN (select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.realmId = :realmId)"),
})
@Table(name="FED_CREDENTIAL_ATTRIBUTE")
@Entity
public class FederatedUserCredentialAttributeEntity {
@Id
@Column(name="ID", length = 36)
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
protected String id;
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "CREDENTIAL_ID")
protected FederatedUserCredentialEntity credential;
@Column(name = "NAME")
protected String name;
@Column(name = "VALUE")
protected String value;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public FederatedUserCredentialEntity getCredential() {
return credential;
}
public void setCredential(FederatedUserCredentialEntity credential) {
this.credential = credential;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof FederatedUserCredentialAttributeEntity)) return false;
FederatedUserCredentialAttributeEntity that = (FederatedUserCredentialAttributeEntity) o;
if (!id.equals(that.getId())) return false;
return true;
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View file

@ -17,6 +17,8 @@
package org.keycloak.storage.jpa.entity; package org.keycloak.storage.jpa.entity;
import org.keycloak.models.jpa.entities.UserEntity;
import javax.persistence.Access; import javax.persistence.Access;
import javax.persistence.AccessType; import javax.persistence.AccessType;
import javax.persistence.CascadeType; import javax.persistence.CascadeType;
@ -24,6 +26,7 @@ import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.NamedQueries; import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
@ -36,12 +39,12 @@ import java.util.Collection;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
@NamedQueries({ @NamedQueries({
@NamedQuery(name="federatedUserCredentialByUser", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId"), @NamedQuery(name="federatedUserCredentialByUser", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId order by cred.priority"),
@NamedQuery(name="federatedUserCredentialByUserAndType", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type"), @NamedQuery(name="federatedUserCredentialByUserAndType", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type order by cred.priority"),
@NamedQuery(name="federatedUserCredentialByNameAndType", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type and cred.device = :device"), @NamedQuery(name="federatedUserCredentialByNameAndType", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type and cred.userLabel = :userLabel order by cred.priority"),
@NamedQuery(name="deleteFederatedUserCredentialByUser", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.realmId = :realmId"), @NamedQuery(name="deleteFederatedUserCredentialByUser", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.realmId = :realmId"),
@NamedQuery(name="deleteFederatedUserCredentialByUserAndType", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type"), @NamedQuery(name="deleteFederatedUserCredentialByUserAndType", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type"),
@NamedQuery(name="deleteFederatedUserCredentialByUserAndTypeAndDevice", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type and cred.device = :device"), @NamedQuery(name="deleteFederatedUserCredentialByUserAndTypeAndUserLabel", query="delete from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type and cred.userLabel = :userLabel"),
@NamedQuery(name="deleteFederatedUserCredentialsByRealm", query="delete from FederatedUserCredentialEntity cred where cred.realmId=:realmId"), @NamedQuery(name="deleteFederatedUserCredentialsByRealm", query="delete from FederatedUserCredentialEntity cred where cred.realmId=:realmId"),
@NamedQuery(name="deleteFederatedUserCredentialsByStorageProvider", query="delete from FederatedUserCredentialEntity cred where cred.storageProviderId=:storageProviderId"), @NamedQuery(name="deleteFederatedUserCredentialsByStorageProvider", query="delete from FederatedUserCredentialEntity cred where cred.storageProviderId=:storageProviderId"),
@NamedQuery(name="deleteFederatedUserCredentialsByRealmAndLink", query="delete from FederatedUserCredentialEntity cred where cred.userId IN (select u.id from UserEntity u where u.realmId=:realmId and u.federationLink=:link)") @NamedQuery(name="deleteFederatedUserCredentialsByRealmAndLink", query="delete from FederatedUserCredentialEntity cred where cred.userId IN (select u.id from UserEntity u where u.realmId=:realmId and u.federationLink=:link)")
@ -55,16 +58,18 @@ public class FederatedUserCredentialEntity {
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
protected String id; protected String id;
@Column(name="SECRET_DATA")
protected String secretData;
@Column(name="CREDENTIAL_DATA")
protected String credentialData;
@Column(name="TYPE") @Column(name="TYPE")
protected String type; protected String type;
@Column(name="VALUE")
protected String value; @Column(name="USER_LABEL")
@Column(name="DEVICE") protected String userLabel;
protected String device;
@Column(name="SALT")
protected byte[] salt;
@Column(name="HASH_ITERATIONS")
protected int hashIterations;
@Column(name="CREATED_DATE") @Column(name="CREATED_DATE")
protected Long createdDate; protected Long createdDate;
@ -77,57 +82,62 @@ public class FederatedUserCredentialEntity {
@Column(name = "STORAGE_PROVIDER_ID") @Column(name = "STORAGE_PROVIDER_ID")
protected String storageProviderId; protected String storageProviderId;
@Column(name="PRIORITY")
protected int priority;
@Deprecated // Needed just for backwards compatibility when migrating old credentials
@Column(name="COUNTER") @Column(name="SALT")
protected int counter; protected byte[] salt;
@Column(name="ALGORITHM")
protected String algorithm;
@Column(name="DIGITS")
protected int digits;
@Column(name="PERIOD")
protected int period;
@OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.EAGER, orphanRemoval = true, mappedBy="credential")
protected Collection<FederatedUserCredentialAttributeEntity> credentialAttributes = new ArrayList<>();
public String getId() { public String getId() {
return id; return id;
} }
public void setId(String id) { public void setId(String id) {
this.id = id; this.id = id;
} }
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getType() { public String getType() {
return type; return type;
} }
public void setType(String type) { public void setType(String type) {
this.type = type; this.type = type;
} }
public String getDevice() { public String getUserLabel() {
return device; return userLabel;
}
public void setUserLabel(String userLabel) {
this.userLabel = userLabel;
} }
public void setDevice(String device) { public Long getCreatedDate() {
this.device = device; return createdDate;
} }
public void setCreatedDate(Long createdDate) {
this.createdDate = createdDate;
}
public String getSecretData() {
return secretData;
}
public void setSecretData(String secretData) {
this.secretData = secretData;
}
public String getCredentialData() {
return credentialData;
}
public void setCredentialData(String credentialData) {
this.credentialData = credentialData;
}
public String getUserId() { public String getUserId() {
return userId; return userId;
} }
public void setUserId(String userId) { public void setUserId(String userId) {
this.userId = userId; this.userId = userId;
} }
@ -135,7 +145,6 @@ public class FederatedUserCredentialEntity {
public String getRealmId() { public String getRealmId() {
return realmId; return realmId;
} }
public void setRealmId(String realmId) { public void setRealmId(String realmId) {
this.realmId = realmId; this.realmId = realmId;
} }
@ -143,75 +152,28 @@ public class FederatedUserCredentialEntity {
public String getStorageProviderId() { public String getStorageProviderId() {
return storageProviderId; return storageProviderId;
} }
public void setStorageProviderId(String storageProviderId) { public void setStorageProviderId(String storageProviderId) {
this.storageProviderId = storageProviderId; this.storageProviderId = storageProviderId;
} }
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
@Deprecated
public byte[] getSalt() { public byte[] getSalt() {
return salt; return salt;
} }
@Deprecated
public void setSalt(byte[] salt) { public void setSalt(byte[] salt) {
this.salt = salt; this.salt = salt;
} }
public int getHashIterations() {
return hashIterations;
}
public void setHashIterations(int hashIterations) {
this.hashIterations = hashIterations;
}
public Long getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Long createdDate) {
this.createdDate = createdDate;
}
public int getCounter() {
return counter;
}
public void setCounter(int counter) {
this.counter = counter;
}
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public int getDigits() {
return digits;
}
public void setDigits(int digits) {
this.digits = digits;
}
public int getPeriod() {
return period;
}
public void setPeriod(int period) {
this.period = period;
}
public Collection<FederatedUserCredentialAttributeEntity> getCredentialAttributes() {
return credentialAttributes;
}
public void setCredentialAttributes(Collection<FederatedUserCredentialAttributeEntity> credentialAttributes) {
this.credentialAttributes = credentialAttributes;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -0,0 +1,205 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2018 Red Hat, Inc. and/or its affiliates
~ * and other contributors as indicated by the @author tags.
~ *
~ * Licensed under the Apache License, Version 2.0 (the "License");
~ * you may not use this file except in compliance with the License.
~ * You may obtain a copy of the License at
~ *
~ * http://www.apache.org/licenses/LICENSE-2.0
~ *
~ * Unless required by applicable law or agreed to in writing, software
~ * distributed under the License is distributed on an "AS IS" BASIS,
~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ * See the License for the specific language governing permissions and
~ * limitations under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<!--modifies the credentials to the new format, while copying the data as json in the new fields-->
<changeSet author="keycloak" id="8.0.0-adding-credential-columns">
<addColumn tableName="CREDENTIAL">
<column name="USER_LABEL" type="VARCHAR(255)">
<constraints nullable="true"/>
</column>
<column name="SECRET_DATA" type="CLOB">
<constraints nullable="true"/>
</column>
<column name="CREDENTIAL_DATA" type="CLOB">
<constraints nullable="true"/>
</column>
<column name="PRIORITY" type="INT">
<constraints nullable="true"/>
</column>
</addColumn>
<addColumn tableName="FED_USER_CREDENTIAL">
<column name="USER_LABEL" type="VARCHAR(255)">
<constraints nullable="true"/>
</column>
<column name="SECRET_DATA" type="CLOB">
<constraints nullable="true"/>
</column>
<column name="CREDENTIAL_DATA" type="CLOB">
<constraints nullable="true"/>
</column>
<column name="PRIORITY" type="INT">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<!--Update format of credential to fill secret_data and credential_data - used on all databases beside Oracle DB -->
<changeSet author="keycloak" id="8.0.0-updating-credential-data-not-oracle">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<not>
<dbms type="oracle" />
</not>
</preConditions>
<!-- SALT was saved in tinyblob in previous version. -->
<!-- Can't be automatically updated for all users to new format in single UPDATE statement, so existing users will be updated on-the-fly -->
<update tableName="CREDENTIAL">
<column name="PRIORITY" value="10" />
<column name="SECRET_DATA" valueComputed="CONCAT('{&quot;value&quot;:&quot;', VALUE, '&quot;,&quot;salt&quot;:&quot;__SALT__&quot;}')"/>
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{&quot;hashIterations&quot;:', HASH_ITERATIONS, ',&quot;algorithm&quot;:&quot;', ALGORITHM, '&quot;}')"/>
<where>TYPE = 'password' OR TYPE = 'password-history'</where>
</update>
<update tableName="CREDENTIAL">
<column name="PRIORITY" value="20" />
<column name="TYPE" value="otp" />
<column name="SECRET_DATA" valueComputed="CONCAT('{&quot;value&quot;:&quot;', VALUE, '&quot;}')"/>
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{&quot;subType&quot;:&quot;totp&quot;,&quot;digits&quot;:', DIGITS, ',&quot;period&quot;:', PERIOD, ',&quot;algorithm&quot;:&quot;', ALGORITHM, '&quot;}')"/>
<where>TYPE = 'totp'</where>
</update>
<update tableName="CREDENTIAL">
<column name="PRIORITY" value="20" />
<column name="TYPE" value="otp" />
<column name="SECRET_DATA" valueComputed="CONCAT('{&quot;value&quot;:&quot;', VALUE, '&quot;}')"/>
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{&quot;subType&quot;:&quot;hotp&quot;,&quot;digits&quot;:', DIGITS, ',&quot;counter&quot;:', COUNTER, ',&quot;algorithm&quot;:&quot;', ALGORITHM, '&quot;}')"/>
<where>TYPE = 'hotp'</where>
</update>
<!--Update format of fed_user_credential to fill secret_data and credential_data-->
<update tableName="FED_USER_CREDENTIAL">
<column name="PRIORITY" value="10" />
<column name="SECRET_DATA" valueComputed="CONCAT('{&quot;value&quot;:&quot;', VALUE, '&quot;,&quot;salt&quot;:&quot;__SALT__&quot;}')"/>
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{&quot;hashIterations&quot;:', HASH_ITERATIONS, ',&quot;algorithm&quot;:&quot;', ALGORITHM, '&quot;}')"/>
<where>TYPE = 'password' OR TYPE = 'password-history'</where>
</update>
<update tableName="FED_USER_CREDENTIAL">
<column name="PRIORITY" value="20" />
<column name="TYPE" value="otp" />
<column name="SECRET_DATA" valueComputed="CONCAT('{&quot;value&quot;:&quot;', VALUE, '&quot;}')"/>
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{&quot;subType&quot;:&quot;totp&quot;,&quot;digits&quot;:', DIGITS, ',&quot;period&quot;:', PERIOD, ',&quot;algorithm&quot;:&quot;', ALGORITHM, '&quot;}')"/>
<where>TYPE = 'totp'</where>
</update>
<update tableName="FED_USER_CREDENTIAL">
<column name="PRIORITY" value="20" />
<column name="TYPE" value="otp" />
<column name="SECRET_DATA" valueComputed="CONCAT('{&quot;value&quot;:&quot;', VALUE, '&quot;}')"/>
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{&quot;subType&quot;:&quot;hotp&quot;,&quot;digits&quot;:', DIGITS, ',&quot;counter&quot;:', COUNTER, ',&quot;algorithm&quot;:&quot;', ALGORITHM, '&quot;}')"/>
<where>TYPE = 'hotp'</where>
</update>
</changeSet>
<!--Update format of credential to fill secret_data and credential_data - used on Oracle DB. Oracle doesn't support CONCAT with more than 2 arguments -->
<changeSet author="keycloak" id="8.0.0-updating-credential-data-oracle">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<dbms type="oracle" />
</preConditions>
<!-- SALT was saved in tinyblob in previous version. -->
<!-- Can't be automatically updated for all users to new format in single UPDATE statement, so existing users will be updated on-the-fly -->
<update tableName="CREDENTIAL">
<column name="PRIORITY" value="10" />
<column name="SECRET_DATA" valueComputed="'{&quot;value&quot;:&quot;' || VALUE || '&quot;,&quot;salt&quot;:&quot;__SALT__&quot;}'"/>
<column name="CREDENTIAL_DATA" valueComputed="'{&quot;hashIterations&quot;:' || HASH_ITERATIONS || ',&quot;algorithm&quot;:&quot;' || ALGORITHM || '&quot;}'"/>
<where>TYPE = 'password' OR TYPE = 'password-history'</where>
</update>
<update tableName="CREDENTIAL">
<column name="PRIORITY" value="20" />
<column name="TYPE" value="otp" />
<column name="SECRET_DATA" valueComputed="'{&quot;value&quot;:&quot;' || VALUE || '&quot;}'"/>
<column name="CREDENTIAL_DATA" valueComputed="'{&quot;subType&quot;:&quot;totp&quot;,&quot;digits&quot;:' || DIGITS || ',&quot;period&quot;:' || PERIOD || ',&quot;algorithm&quot;:&quot;' || ALGORITHM || '&quot;}'"/>
<where>TYPE = 'totp'</where>
</update>
<update tableName="CREDENTIAL">
<column name="PRIORITY" value="20" />
<column name="TYPE" value="otp" />
<column name="SECRET_DATA" valueComputed="'{&quot;value&quot;:&quot;' || VALUE || '&quot;}'"/>
<column name="CREDENTIAL_DATA" valueComputed="'{&quot;subType&quot;:&quot;hotp&quot;,&quot;digits&quot;:' || DIGITS || ',&quot;counter&quot;:' || COUNTER || ',&quot;algorithm&quot;:&quot;' || ALGORITHM || '&quot;}'"/>
<where>TYPE = 'hotp'</where>
</update>
<!--Update format of fed_user_credential to fill secret_data and credential_data-->
<update tableName="FED_USER_CREDENTIAL">
<column name="PRIORITY" value="10" />
<column name="SECRET_DATA" valueComputed="'{&quot;value&quot;:&quot;' || VALUE || '&quot;,&quot;salt&quot;:&quot;__SALT__&quot;}'"/>
<column name="CREDENTIAL_DATA" valueComputed="'{&quot;hashIterations&quot;:' || HASH_ITERATIONS || ',&quot;algorithm&quot;:&quot;' || ALGORITHM || '&quot;}'"/>
<where>TYPE = 'password' OR TYPE = 'password-history'</where>
</update>
<update tableName="FED_USER_CREDENTIAL">
<column name="PRIORITY" value="20" />
<column name="TYPE" value="otp" />
<column name="SECRET_DATA" valueComputed="'{&quot;value&quot;:&quot;' || VALUE || '&quot;}'"/>
<column name="CREDENTIAL_DATA" valueComputed="'{&quot;subType&quot;:&quot;totp&quot;,&quot;digits&quot;:' || DIGITS || ',&quot;period&quot;:' || PERIOD || ',&quot;algorithm&quot;:&quot;' || ALGORITHM || '&quot;}'"/>
<where>TYPE = 'totp'</where>
</update>
<update tableName="FED_USER_CREDENTIAL">
<column name="PRIORITY" value="20" />
<column name="TYPE" value="otp" />
<column name="SECRET_DATA" valueComputed="'{&quot;value&quot;:&quot;' || VALUE || '&quot;}'"/>
<column name="CREDENTIAL_DATA" valueComputed="'{&quot;subType&quot;:&quot;hotp&quot;,&quot;digits&quot;:' || DIGITS || ',&quot;counter&quot;:' || COUNTER || ',&quot;algorithm&quot;:&quot;' || ALGORITHM || '&quot;}'"/>
<where>TYPE = 'hotp'</where>
</update>
</changeSet>
<changeSet author="keycloak" id="8.0.0-credential-cleanup">
<dropDefaultValue tableName="CREDENTIAL" columnName="COUNTER" />
<dropDefaultValue tableName="CREDENTIAL" columnName="DIGITS" />
<dropDefaultValue tableName="CREDENTIAL" columnName="PERIOD" />
<dropDefaultValue tableName="CREDENTIAL" columnName="ALGORITHM" />
<dropColumn tableName="CREDENTIAL" columnName="DEVICE"/>
<dropColumn tableName="CREDENTIAL" columnName="HASH_ITERATIONS"/>
<dropColumn tableName="CREDENTIAL" columnName="VALUE"/>
<dropColumn tableName="CREDENTIAL" columnName="COUNTER"/>
<dropColumn tableName="CREDENTIAL" columnName="DIGITS"/>
<dropColumn tableName="CREDENTIAL" columnName="PERIOD"/>
<dropColumn tableName="CREDENTIAL" columnName="ALGORITHM"/>
<!--credential attributes are now held within the json of secret_data and credential_data (not this it was used in any case)-->
<dropTable tableName="CREDENTIAL_ATTRIBUTE"/>
<dropDefaultValue tableName="FED_USER_CREDENTIAL" columnName="COUNTER" />
<dropDefaultValue tableName="FED_USER_CREDENTIAL" columnName="DIGITS" />
<dropDefaultValue tableName="FED_USER_CREDENTIAL" columnName="PERIOD" />
<dropDefaultValue tableName="FED_USER_CREDENTIAL" columnName="ALGORITHM" />
<dropColumn tableName="FED_USER_CREDENTIAL" columnName="DEVICE"/>
<dropColumn tableName="FED_USER_CREDENTIAL" columnName="HASH_ITERATIONS"/>
<dropColumn tableName="FED_USER_CREDENTIAL" columnName="VALUE"/>
<dropColumn tableName="FED_USER_CREDENTIAL" columnName="COUNTER"/>
<dropColumn tableName="FED_USER_CREDENTIAL" columnName="DIGITS"/>
<dropColumn tableName="FED_USER_CREDENTIAL" columnName="PERIOD"/>
<dropColumn tableName="FED_USER_CREDENTIAL" columnName="ALGORITHM"/>
<!--credential attributes are now held within the json of secret_data and credential_data (not this it was used in any case)-->
<dropTable tableName="FED_CREDENTIAL_ATTRIBUTE "/>
</changeSet>
</databaseChangeLog>

View file

@ -63,4 +63,5 @@
<include file="META-INF/jpa-changelog-4.7.0.xml"/> <include file="META-INF/jpa-changelog-4.7.0.xml"/>
<include file="META-INF/jpa-changelog-4.8.0.xml"/> <include file="META-INF/jpa-changelog-4.8.0.xml"/>
<include file="META-INF/jpa-changelog-authz-7.0.0.xml"/> <include file="META-INF/jpa-changelog-authz-7.0.0.xml"/>
<include file="META-INF/jpa-changelog-8.0.0.xml"/>
</databaseChangeLog> </databaseChangeLog>

View file

@ -23,7 +23,6 @@
<class>org.keycloak.models.jpa.entities.ClientEntity</class> <class>org.keycloak.models.jpa.entities.ClientEntity</class>
<class>org.keycloak.models.jpa.entities.ClientAttributeEntity</class> <class>org.keycloak.models.jpa.entities.ClientAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.CredentialEntity</class> <class>org.keycloak.models.jpa.entities.CredentialEntity</class>
<class>org.keycloak.models.jpa.entities.CredentialAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.RealmEntity</class> <class>org.keycloak.models.jpa.entities.RealmEntity</class>
<class>org.keycloak.models.jpa.entities.RealmAttributeEntity</class> <class>org.keycloak.models.jpa.entities.RealmAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.RequiredCredentialEntity</class> <class>org.keycloak.models.jpa.entities.RequiredCredentialEntity</class>
@ -80,7 +79,6 @@
<class>org.keycloak.storage.jpa.entity.FederatedUserConsentEntity</class> <class>org.keycloak.storage.jpa.entity.FederatedUserConsentEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserConsentClientScopeEntity</class> <class>org.keycloak.storage.jpa.entity.FederatedUserConsentClientScopeEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity</class> <class>org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserCredentialAttributeEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity</class> <class>org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity</class> <class>org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity</class> <class>org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity</class>

View file

@ -154,7 +154,7 @@
<surefire.memory.Xms>512m</surefire.memory.Xms> <surefire.memory.Xms>512m</surefire.memory.Xms>
<surefire.memory.Xmx>2048m</surefire.memory.Xmx> <surefire.memory.Xmx>2048m</surefire.memory.Xmx>
<surefire.memory.metaspace>96m</surefire.memory.metaspace> <surefire.memory.metaspace>96m</surefire.memory.metaspace>
<surefire.memory.metaspace.max>256m</surefire.memory.metaspace.max> <surefire.memory.metaspace.max>512m</surefire.memory.metaspace.max>
<surefire.memory.settings>-Xms${surefire.memory.Xms} -Xmx${surefire.memory.Xmx} -XX:MetaspaceSize=${surefire.memory.metaspace} -XX:MaxMetaspaceSize=${surefire.memory.metaspace.max}</surefire.memory.settings> <surefire.memory.settings>-Xms${surefire.memory.Xms} -Xmx${surefire.memory.Xmx} -XX:MetaspaceSize=${surefire.memory.metaspace} -XX:MaxMetaspaceSize=${surefire.memory.metaspace.max}</surefire.memory.settings>
<!-- Tomcat versions --> <!-- Tomcat versions -->

View file

@ -18,6 +18,8 @@
package org.keycloak.authentication; package org.keycloak.authentication;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -30,4 +32,8 @@ public interface AuthenticationFlow {
Response processAction(String actionExecution); Response processAction(String actionExecution);
Response processFlow(); Response processFlow();
boolean isSuccessful();
default List<AuthenticationFlowException> getFlowExceptions(){
return Collections.emptyList();
}
} }

View file

@ -17,13 +17,17 @@
package org.keycloak.authentication; package org.keycloak.authentication;
import org.keycloak.credential.CredentialModel;
import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import java.net.URI; import java.net.URI;
import java.util.List;
import java.util.Map;
/** /**
* This interface encapsulates information about an execution in an AuthenticationFlow. It is also used to set * This interface encapsulates information about an execution in an AuthenticationFlow. It is also used to set
@ -49,6 +53,23 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/ */
void setUser(UserModel user); void setUser(UserModel user);
/**
* Gets the credential currently selected in this flow
*
* @return
*/
String getSelectedCredentialId();
/**
* Sets a selected credential for this flow
* @param credentialModel
*/
void setSelectedCredentialId(String credentialModel);
List<AuthenticationSelectionOption> getAuthenticationSelections();
void setAuthenticationSelections(List<AuthenticationSelectionOption> credentialAuthExecMap);
/** /**
* Clear the user from the flow. * Clear the user from the flow.
*/ */
@ -64,6 +85,11 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/ */
AuthenticationSessionModel getAuthenticationSession(); AuthenticationSessionModel getAuthenticationSession();
/**
* @return current flow path (EG. authenticate, reset-credentials)
*/
String getFlowPath();
/** /**
* Create a Freemarker form builder that presets the user, action URI, and a generated access code * Create a Freemarker form builder that presets the user, action URI, and a generated access code
* *

View file

@ -18,6 +18,7 @@
package org.keycloak.authentication; package org.keycloak.authentication;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.List;
/** /**
* Throw this exception from an Authenticator, FormAuthenticator, or FormAction if you want to completely abort the flow. * Throw this exception from an Authenticator, FormAuthenticator, or FormAction if you want to completely abort the flow.
@ -28,6 +29,7 @@ import javax.ws.rs.core.Response;
public class AuthenticationFlowException extends RuntimeException { public class AuthenticationFlowException extends RuntimeException {
private AuthenticationFlowError error; private AuthenticationFlowError error;
private Response response; private Response response;
private List<AuthenticationFlowException> afeList;
public AuthenticationFlowException(AuthenticationFlowError error) { public AuthenticationFlowException(AuthenticationFlowError error) {
this.error = error; this.error = error;
@ -53,6 +55,11 @@ public class AuthenticationFlowException extends RuntimeException {
this.error = error; this.error = error;
} }
public AuthenticationFlowException(List<AuthenticationFlowException> afeList){
this.error = AuthenticationFlowError.INTERNAL_ERROR;
this.afeList = afeList;
}
public AuthenticationFlowException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, AuthenticationFlowError error) { public AuthenticationFlowException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, AuthenticationFlowError error) {
super(message, cause, enableSuppression, writableStackTrace); super(message, cause, enableSuppression, writableStackTrace);
this.error = error; this.error = error;
@ -65,4 +72,8 @@ public class AuthenticationFlowException extends RuntimeException {
public Response getResponse() { public Response getResponse() {
return response; return response;
} }
public List<AuthenticationFlowException> getAfeList() {
return afeList;
}
} }

View file

@ -0,0 +1,100 @@
package org.keycloak.authentication;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
public class AuthenticationSelectionOption {
private final AuthenticationExecutionModel authExec;
private final CredentialModel credential;
private final AuthenticationFlowModel authFlow;
private boolean showCredentialName = true;
private boolean showCredentialType = true;
public AuthenticationSelectionOption(AuthenticationExecutionModel authExec) {
this.authExec = authExec;
this.credential = new CredentialModel();
this.authFlow = null;
}
public AuthenticationSelectionOption(AuthenticationExecutionModel authExec, CredentialModel credential) {
this.authExec = authExec;
//Allow themes to get all credential information, but not secret data
this.credential = credential.shallowClone();
this.credential.setSecretData("");
this.authFlow = null;
}
public AuthenticationSelectionOption(AuthenticationExecutionModel authExec, AuthenticationFlowModel authFlow) {
this.authExec = authExec;
this.credential = new CredentialModel();
this.authFlow = authFlow;
}
public void setShowCredentialName(boolean showCredentialName) {
this.showCredentialName = showCredentialName;
}
public void setShowCredentialType(boolean showCredentialType) {
this.showCredentialType = showCredentialType;
}
public boolean showCredentialName(){
if (credential.getId() == null) {
return false;
}
return showCredentialName;
}
public boolean showCredentialType(){
return showCredentialType;
}
public AuthenticationExecutionModel getAuthenticationExecution() {
return authExec;
}
public String getCredentialId(){
return credential.getId();
}
public String getAuthExecId(){
return authExec.getId();
}
public String getCredentialName() {
StringBuilder sb = new StringBuilder();
if (showCredentialName()) {
if (showCredentialType()) {
sb.append(" - ");
}
if (credential.getUserLabel() == null || credential.getUserLabel().isEmpty()) {
sb.append(credential.getId());
} else {
sb.append(credential.getUserLabel());
}
}
return sb.toString();
}
public String getAuthExecName() {
if (authFlow != null) {
String authFlowLabel = authFlow.getAlias();
if (authFlowLabel == null || authFlowLabel.isEmpty()) {
authFlowLabel = authFlow.getId();
}
return authFlowLabel;
}
return authExec.getAuthenticator();
}
public String getId() {
if (getCredentialId() == null) {
return getAuthExecId() + "|";
}
return getAuthExecId() + "|" + getCredentialId();
}
public CredentialModel getCredential(){
return credential;
}
}

View file

@ -19,9 +19,13 @@ package org.keycloak.authentication;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import java.util.Collections;
import java.util.List;
/** /**
* This interface is for users that want to add custom authenticators to an authentication flow. * This interface is for users that want to add custom authenticators to an authentication flow.
* You must implement this interface as well as an AuthenticatorFactory. * You must implement this interface as well as an AuthenticatorFactory.
@ -83,6 +87,28 @@ public interface Authenticator extends Provider {
*/ */
void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user); void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user);
/**
* Overwrite this if the authenticator is associated with
* @return
*/
default List<RequiredActionFactory> getRequiredActions(KeycloakSession session) {
return Collections.emptyList();
}
/**
* Checks if all required actions are configured in the realm and are enabled
* @return
*/
default boolean areRequiredActionsEnabled(KeycloakSession session, RealmModel realm) {
for (RequiredActionFactory raf : getRequiredActions(session)) {
RequiredActionProviderModel rafpm = realm.getRequiredActionProviderByAlias(raf.getId());
if (rafpm == null) {
return false;
}
if (!rafpm.isEnabled()) {
return false;
}
}
return true;
}
} }

View file

@ -30,4 +30,5 @@ import org.keycloak.provider.ProviderFactory;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface AuthenticatorFactory extends ProviderFactory<Authenticator>, ConfigurableAuthenticatorFactory { public interface AuthenticatorFactory extends ProviderFactory<Authenticator>, ConfigurableAuthenticatorFactory {
} }

View file

@ -25,6 +25,12 @@ import org.keycloak.provider.ConfiguredProvider;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface ConfigurableAuthenticatorFactory extends ConfiguredProvider { public interface ConfigurableAuthenticatorFactory extends ConfiguredProvider {
AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.DISABLED};
/** /**
* Friendly name for the authenticator * Friendly name for the authenticator
* *

View file

@ -0,0 +1,4 @@
package org.keycloak.authentication;
public interface CredentialRegistrator {
}

View file

@ -0,0 +1,19 @@
package org.keycloak.authentication;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import java.util.List;
public interface CredentialValidator<T extends CredentialProvider> {
T getCredentialProvider(KeycloakSession session);
default List<CredentialModel> getCredentials(KeycloakSession session, RealmModel realm, UserModel user) {
return session.userCredentialManager().getStoredCredentialsByType(realm, user, getCredentialProvider(session).getType());
}
default String getType(KeycloakSession session) {
return getCredentialProvider(session).getType();
}
}

View file

@ -18,9 +18,8 @@
package org.keycloak.credential.hash; package org.keycloak.credential.hash;
import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel;
import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEKeySpec;
@ -53,31 +52,27 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
} }
@Override @Override
public boolean policyCheck(PasswordPolicy policy, CredentialModel credential) { public boolean policyCheck(PasswordPolicy policy, PasswordCredentialModel credential) {
int policyHashIterations = policy.getHashIterations(); int policyHashIterations = policy.getHashIterations();
if (policyHashIterations == -1) { if (policyHashIterations == -1) {
policyHashIterations = defaultIterations; policyHashIterations = defaultIterations;
} }
return credential.getHashIterations() == policyHashIterations return credential.getPasswordCredentialData().getHashIterations() == policyHashIterations
&& providerId.equals(credential.getAlgorithm()) && providerId.equals(credential.getPasswordCredentialData().getAlgorithm())
&& derivedKeySize == keySize(credential); && derivedKeySize == keySize(credential);
} }
@Override @Override
public void encode(String rawPassword, int iterations, CredentialModel credential) { public PasswordCredentialModel encodedCredential(String rawPassword, int iterations) {
if (iterations == -1) { if (iterations == -1) {
iterations = defaultIterations; iterations = defaultIterations;
} }
byte[] salt = getSalt(); byte[] salt = getSalt();
String encodedPassword = encode(rawPassword, iterations, salt, derivedKeySize); String encodedPassword = encodedCredential(rawPassword, iterations, salt, derivedKeySize);
credential.setAlgorithm(providerId); return PasswordCredentialModel.createFromValues(providerId, salt, iterations, encodedPassword);
credential.setType(UserCredentialModel.PASSWORD);
credential.setSalt(salt);
credential.setHashIterations(iterations);
credential.setValue(encodedPassword);
} }
@Override @Override
@ -87,17 +82,17 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
} }
byte[] salt = getSalt(); byte[] salt = getSalt();
return encode(rawPassword, iterations, salt, derivedKeySize); return encodedCredential(rawPassword, iterations, salt, derivedKeySize);
} }
@Override @Override
public boolean verify(String rawPassword, CredentialModel credential) { public boolean verify(String rawPassword, PasswordCredentialModel credential) {
return encode(rawPassword, credential.getHashIterations(), credential.getSalt(), keySize(credential)).equals(credential.getValue()); return encodedCredential(rawPassword, credential.getPasswordCredentialData().getHashIterations(), credential.getPasswordSecretData().getSalt(), keySize(credential)).equals(credential.getPasswordSecretData().getValue());
} }
private int keySize(CredentialModel credential) { private int keySize(PasswordCredentialModel credential) {
try { try {
byte[] bytes = Base64.decode(credential.getValue()); byte[] bytes = Base64.decode(credential.getPasswordSecretData().getValue());
return bytes.length * 8; return bytes.length * 8;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Credential could not be decoded", e); throw new RuntimeException("Credential could not be decoded", e);
@ -107,7 +102,7 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
public void close() { public void close() {
} }
private String encode(String rawPassword, int iterations, byte[] salt, int derivedKeySize) { private String encodedCredential(String rawPassword, int iterations, byte[] salt, int derivedKeySize) {
KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, derivedKeySize); KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, derivedKeySize);
try { try {

View file

@ -22,7 +22,7 @@ package org.keycloak.forms.login;
*/ */
public enum LoginFormsPages { public enum LoginFormsPages {
LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, LOGIN, LOGIN_USERNAME, LOGIN_PASSWORD, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_WEBAUTHN, LOGIN_VERIFY_EMAIL,
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL, LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE,
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM; LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM;

View file

@ -17,6 +17,7 @@
package org.keycloak.forms.login; package org.keycloak.forms.login;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
@ -55,12 +56,18 @@ public interface LoginFormsProvider extends Provider {
String getMessage(String message, String... parameters); String getMessage(String message, String... parameters);
Response createLogin(); Response createLoginUsernamePassword();
Response createLoginUsername();
Response createLoginPassword();
Response createPasswordReset(); Response createPasswordReset();
Response createLoginTotp(); Response createLoginTotp();
Response createLoginWebAuthn();
Response createRegistration(); Response createRegistration();
Response createInfoPage(); Response createInfoPage();
@ -133,4 +140,6 @@ public interface LoginFormsProvider extends Provider {
LoginFormsProvider setActionUri(URI requestUri); LoginFormsProvider setActionUri(URI requestUri);
LoginFormsProvider setExecution(String execution); LoginFormsProvider setExecution(String execution);
LoginFormsProvider setAuthContext(AuthenticationFlowContext context);
} }

View file

@ -17,7 +17,10 @@
package org.keycloak.migration.migrators; package org.keycloak.migration.migrators;
import org.jboss.logging.Logger;
import org.keycloak.migration.ModelVersion; import org.keycloak.migration.ModelVersion;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -26,10 +29,15 @@ import org.keycloak.representations.idm.RealmRepresentation;
import java.util.Collections; import java.util.Collections;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class MigrateTo8_0_0 implements Migration { public class MigrateTo8_0_0 implements Migration {
public static final ModelVersion VERSION = new ModelVersion("8.0.0"); public static final ModelVersion VERSION = new ModelVersion("8.0.0");
private static final Logger LOG = Logger.getLogger(MigrateTo8_0_0.class);
@Override @Override
public ModelVersion getVersion() { public ModelVersion getVersion() {
return VERSION; return VERSION;
@ -37,15 +45,22 @@ public class MigrateTo8_0_0 implements Migration {
@Override @Override
public void migrate(KeycloakSession session) { public void migrate(KeycloakSession session) {
session.realms().getRealms().stream().forEach(realm -> migrateRealm(realm)); // Perform basic realm migration first (non multi-factor authentication)
session.realms().getRealms().stream().forEach(realm -> migrateRealmCommon(realm));
// Moreover, for multi-factor authentication migrate optional execution of realm flows to subflows
session.realms().getRealms().stream().forEach(r -> {
migrateRealmMFA(session, r, false);
});
} }
@Override @Override
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) { public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
migrateRealm(realm); migrateRealmCommon(realm);
// No-additional-op for multi-factor authentication besides the basic migrateRealmCommon() in previous statement
// Migration of optional authentication executions was already handled in RepresentationToModel.importRealm
} }
protected void migrateRealm(RealmModel realm) { protected void migrateRealmCommon(RealmModel realm) {
ClientModel adminConsoleClient = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID); ClientModel adminConsoleClient = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
adminConsoleClient.setRootUrl(Constants.AUTH_ADMIN_URL_PROP); adminConsoleClient.setRootUrl(Constants.AUTH_ADMIN_URL_PROP);
String adminConsoleBaseUrl = "/admin/" + realm.getName() + "/console/"; String adminConsoleBaseUrl = "/admin/" + realm.getName() + "/console/";
@ -59,4 +74,54 @@ public class MigrateTo8_0_0 implements Migration {
accountClient.setBaseUrl(accountClientBaseUrl); accountClient.setBaseUrl(accountClientBaseUrl);
accountClient.setRedirectUris(Collections.singleton(accountClientBaseUrl + "*")); accountClient.setRedirectUris(Collections.singleton(accountClientBaseUrl + "*"));
} }
protected void migrateRealmMFA(KeycloakSession session, RealmModel realm, boolean jsn) {
for (AuthenticationFlowModel authFlow : realm.getAuthenticationFlows()) {
for (AuthenticationExecutionModel authExecution : realm.getAuthenticationExecutions(authFlow.getId())) {
// Those were OPTIONAL executions in previous version
if (authExecution.getRequirement() == AuthenticationExecutionModel.Requirement.CONDITIONAL) {
migrateOptionalAuthenticationExecution(realm, authFlow, authExecution, true);
}
}
}
}
public static void migrateOptionalAuthenticationExecution(RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionModel optionalExecution, boolean updateOptionalExecution) {
LOG.debugf("Migrating optional execution '%s' of flow '%s' of realm '%s' to subflow", optionalExecution.getAuthenticator(), parentFlow.getAlias(), realm.getName());
AuthenticationFlowModel conditionalOTP = new AuthenticationFlowModel();
conditionalOTP.setTopLevel(false);
conditionalOTP.setBuiltIn(parentFlow.isBuiltIn());
conditionalOTP.setAlias(parentFlow.getAlias() + " - " + optionalExecution.getAuthenticator() + " - Conditional");
conditionalOTP.setDescription("Flow to determine if the " + optionalExecution.getAuthenticator() + " authenticator should be used or not.");
conditionalOTP.setProviderId("basic-flow");
conditionalOTP = realm.addAuthenticationFlow(conditionalOTP);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(parentFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
execution.setFlowId(conditionalOTP.getId());
execution.setPriority(optionalExecution.getPriority());
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("conditional-user-configured");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
// Move optionalExecution as child of newly created parent flow
optionalExecution.setParentFlow(conditionalOTP.getId());
optionalExecution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
optionalExecution.setPriority(20);
// In case of DB migration, we're updating existing execution, which is already in DB.
// In case of JSON migration, the execution is not yet in DB and will be added later
if (updateOptionalExecution) {
realm.updateAuthenticatorExecution(optionalExecution);
}
}
} }

View file

@ -78,6 +78,8 @@ public final class Constants {
public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST"; public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST";
public static final String AIA_SILENT_CANCEL = "silent_cancel"; public static final String AIA_SILENT_CANCEL = "silent_cancel";
public static final String AUTHENTICATION_EXECUTION = "authenticationExecution";
public static final String CREDENTIAL_ID = "credentialId";
public static final String SKIP_LINK = "skipLink"; public static final String SKIP_LINK = "skipLink";
public static final String TEMPLATE_ATTR_ACTION_URI = "actionUri"; public static final String TEMPLATE_ATTR_ACTION_URI = "actionUri";

View file

@ -17,9 +17,7 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -27,14 +25,17 @@ import org.keycloak.models.UserCredentialModel;
*/ */
public class CredentialValidation { public class CredentialValidation {
public static boolean validOTP(RealmModel realm, String token, String secret) { public static boolean validOTP(String token, OTPCredentialModel credentialModel, int lookAheadWindow) {
OTPPolicy policy = realm.getOTPPolicy(); if (credentialModel.getOTPCredentialData().getSubType().equals(OTPCredentialModel.TOTP)) {
if (policy.getType().equals(UserCredentialModel.TOTP)) { TimeBasedOTP validator = new TimeBasedOTP(credentialModel.getOTPCredentialData().getAlgorithm(),
TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), policy.getPeriod(), policy.getLookAheadWindow()); credentialModel.getOTPCredentialData().getDigits(), credentialModel.getOTPCredentialData().getPeriod(),
return validator.validateTOTP(token, secret.getBytes()); lookAheadWindow);
return validator.validateTOTP(token, credentialModel.getOTPSecretData().getValue().getBytes());
} else { } else {
HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow()); HmacOTP validator = new HmacOTP(credentialModel.getOTPCredentialData().getDigits(),
int c = validator.validateHOTP(token, secret, policy.getInitialCounter()); credentialModel.getOTPCredentialData().getAlgorithm(), lookAheadWindow);
int c = validator.validateHOTP(token, credentialModel.getOTPSecretData().getValue(),
credentialModel.getOTPCredentialData().getCounter());
return c > -1; return c > -1;
} }

View file

@ -143,9 +143,6 @@ public class DefaultAuthenticationFlows {
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
//execution.setAuthenticatorConfig(captchaConfig.getId()); //execution.setAuthenticatorConfig(captchaConfig.getId());
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
} }
public static void browserFlow(RealmModel realm) { public static void browserFlow(RealmModel realm) {
@ -163,18 +160,18 @@ public class DefaultAuthenticationFlows {
} }
public static void resetCredentialsFlow(RealmModel realm) { public static void resetCredentialsFlow(RealmModel realm) {
AuthenticationFlowModel grant = new AuthenticationFlowModel(); AuthenticationFlowModel reset = new AuthenticationFlowModel();
grant.setAlias(RESET_CREDENTIALS_FLOW); reset.setAlias(RESET_CREDENTIALS_FLOW);
grant.setDescription("Reset credentials for a user if they forgot their password or something"); reset.setDescription("Reset credentials for a user if they forgot their password or something");
grant.setProviderId("basic-flow"); reset.setProviderId("basic-flow");
grant.setTopLevel(true); reset.setTopLevel(true);
grant.setBuiltIn(true); reset.setBuiltIn(true);
grant = realm.addAuthenticationFlow(grant); reset = realm.addAuthenticationFlow(reset);
realm.setResetCredentialsFlow(grant); realm.setResetCredentialsFlow(reset);
// username // username
AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(grant.getId()); execution.setParentFlow(reset.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("reset-credentials-choose-user"); execution.setAuthenticator("reset-credentials-choose-user");
execution.setPriority(10); execution.setPriority(10);
@ -183,7 +180,7 @@ public class DefaultAuthenticationFlows {
// send email // send email
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(grant.getId()); execution.setParentFlow(reset.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("reset-credential-email"); execution.setAuthenticator("reset-credential-email");
execution.setPriority(20); execution.setPriority(20);
@ -192,19 +189,41 @@ public class DefaultAuthenticationFlows {
// password // password
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(grant.getId()); execution.setParentFlow(reset.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("reset-password"); execution.setAuthenticator("reset-password");
execution.setPriority(30); execution.setPriority(30);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
// otp AuthenticationFlowModel conditionalOTP = new AuthenticationFlowModel();
conditionalOTP.setTopLevel(false);
conditionalOTP.setBuiltIn(true);
conditionalOTP.setAlias("Reset - Conditional OTP");
conditionalOTP.setDescription("Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.");
conditionalOTP.setProviderId("basic-flow");
conditionalOTP = realm.addAuthenticationFlow(conditionalOTP);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(grant.getId()); execution.setParentFlow(reset.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL); execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
execution.setAuthenticator("reset-otp"); execution.setFlowId(conditionalOTP.getId());
execution.setPriority(40); execution.setPriority(40);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("conditional-user-configured");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("reset-otp");
execution.setPriority(20);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
} }
@ -241,14 +260,37 @@ public class DefaultAuthenticationFlows {
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
// otp // otp
AuthenticationFlowModel conditionalOTP = new AuthenticationFlowModel();
conditionalOTP.setTopLevel(false);
conditionalOTP.setBuiltIn(true);
conditionalOTP.setAlias("Direct Grant - Conditional OTP");
conditionalOTP.setDescription("Flow to determine if the OTP is required for the authentication");
conditionalOTP.setProviderId("basic-flow");
conditionalOTP = realm.addAuthenticationFlow(conditionalOTP);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(grant.getId()); execution.setParentFlow(grant.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL); execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) { if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) {
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
} }
execution.setAuthenticator("direct-grant-validate-otp"); execution.setFlowId(conditionalOTP.getId());
execution.setPriority(30); execution.setPriority(30);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("conditional-user-configured");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("direct-grant-validate-otp");
execution.setPriority(20);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
} }
@ -309,15 +351,36 @@ public class DefaultAuthenticationFlows {
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
// otp processing AuthenticationFlowModel conditionalOTP = new AuthenticationFlowModel();
conditionalOTP.setTopLevel(false);
conditionalOTP.setBuiltIn(true);
conditionalOTP.setAlias("Browser - Conditional OTP");
conditionalOTP.setDescription("Flow to determine if the OTP is required for the authentication");
conditionalOTP.setProviderId("basic-flow");
conditionalOTP = realm.addAuthenticationFlow(conditionalOTP);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(forms.getId()); execution.setParentFlow(forms.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL); execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) { if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) {
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
} }
execution.setFlowId(conditionalOTP.getId());
execution.setPriority(20);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("conditional-user-configured");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
// otp processing
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("auth-otp-form"); execution.setAuthenticator("auth-otp-form");
execution.setPriority(20); execution.setPriority(20);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
@ -432,6 +495,20 @@ public class DefaultAuthenticationFlows {
execution.setAuthenticatorConfig(reviewProfileConfig.getId()); execution.setAuthenticatorConfig(reviewProfileConfig.getId());
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
AuthenticationFlowModel uniqueOrExistingFlow = new AuthenticationFlowModel();
uniqueOrExistingFlow.setTopLevel(false);
uniqueOrExistingFlow.setBuiltIn(true);
uniqueOrExistingFlow.setAlias("User creation or linking");
uniqueOrExistingFlow.setDescription("Flow for the existing/non-existing user alternatives");
uniqueOrExistingFlow.setProviderId("basic-flow");
uniqueOrExistingFlow = realm.addAuthenticationFlow(uniqueOrExistingFlow);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(firstBrokerLogin.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setFlowId(uniqueOrExistingFlow.getId());
execution.setPriority(20);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
AuthenticatorConfigModel createUserIfUniqueConfig = new AuthenticatorConfigModel(); AuthenticatorConfigModel createUserIfUniqueConfig = new AuthenticatorConfigModel();
createUserIfUniqueConfig.setAlias(IDP_CREATE_UNIQUE_USER_CONFIG_ALIAS); createUserIfUniqueConfig.setAlias(IDP_CREATE_UNIQUE_USER_CONFIG_ALIAS);
@ -441,10 +518,10 @@ public class DefaultAuthenticationFlows {
createUserIfUniqueConfig = realm.addAuthenticatorConfig(createUserIfUniqueConfig); createUserIfUniqueConfig = realm.addAuthenticatorConfig(createUserIfUniqueConfig);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(firstBrokerLogin.getId()); execution.setParentFlow(uniqueOrExistingFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setAuthenticator("idp-create-user-if-unique"); execution.setAuthenticator("idp-create-user-if-unique");
execution.setPriority(20); execution.setPriority(10);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
execution.setAuthenticatorConfig(createUserIfUniqueConfig.getId()); execution.setAuthenticatorConfig(createUserIfUniqueConfig.getId());
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
@ -458,10 +535,10 @@ public class DefaultAuthenticationFlows {
linkExistingAccountFlow.setProviderId("basic-flow"); linkExistingAccountFlow.setProviderId("basic-flow");
linkExistingAccountFlow = realm.addAuthenticationFlow(linkExistingAccountFlow); linkExistingAccountFlow = realm.addAuthenticationFlow(linkExistingAccountFlow);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(firstBrokerLogin.getId()); execution.setParentFlow(uniqueOrExistingFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setFlowId(linkExistingAccountFlow.getId()); execution.setFlowId(linkExistingAccountFlow.getId());
execution.setPriority(30); execution.setPriority(20);
execution.setAuthenticatorFlow(true); execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
@ -473,11 +550,26 @@ public class DefaultAuthenticationFlows {
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
AuthenticationFlowModel accountVerificationOptions = new AuthenticationFlowModel();
accountVerificationOptions.setTopLevel(false);
accountVerificationOptions.setBuiltIn(true);
accountVerificationOptions.setAlias("Account verification options");
accountVerificationOptions.setDescription("Method with which to verity the existing account");
accountVerificationOptions.setProviderId("basic-flow");
accountVerificationOptions = realm.addAuthenticationFlow(accountVerificationOptions);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(linkExistingAccountFlow.getId()); execution.setParentFlow(linkExistingAccountFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setFlowId(accountVerificationOptions.getId());
execution.setPriority(20);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(accountVerificationOptions.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setAuthenticator("idp-email-verification"); execution.setAuthenticator("idp-email-verification");
execution.setPriority(20); execution.setPriority(10);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
@ -489,10 +581,10 @@ public class DefaultAuthenticationFlows {
verifyByReauthenticationAccountFlow.setProviderId("basic-flow"); verifyByReauthenticationAccountFlow.setProviderId("basic-flow");
verifyByReauthenticationAccountFlow = realm.addAuthenticationFlow(verifyByReauthenticationAccountFlow); verifyByReauthenticationAccountFlow = realm.addAuthenticationFlow(verifyByReauthenticationAccountFlow);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(linkExistingAccountFlow.getId()); execution.setParentFlow(accountVerificationOptions.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setFlowId(verifyByReauthenticationAccountFlow.getId()); execution.setFlowId(verifyByReauthenticationAccountFlow.getId());
execution.setPriority(30); execution.setPriority(20);
execution.setAuthenticatorFlow(true); execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
@ -505,26 +597,48 @@ public class DefaultAuthenticationFlows {
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
AuthenticationFlowModel conditionalOTP = new AuthenticationFlowModel();
conditionalOTP.setTopLevel(false);
conditionalOTP.setBuiltIn(true);
conditionalOTP.setAlias("First broker login - Conditional OTP");
conditionalOTP.setDescription("Flow to determine if the OTP is required for the authentication");
conditionalOTP.setProviderId("basic-flow");
conditionalOTP = realm.addAuthenticationFlow(conditionalOTP);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(verifyByReauthenticationAccountFlow.getId()); execution.setParentFlow(verifyByReauthenticationAccountFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL); execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
if (migrate) { if (migrate) {
// Try to read OTP requirement from browser flow // Try to read OTP requirement from browser flow
AuthenticationFlowModel browserFlow = realm.getBrowserFlow(); AuthenticationFlowModel browserFlow = realm.getBrowserFlow();
if (browserFlow == null) { if (browserFlow == null) {
browserFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW); browserFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
} }
List<AuthenticationExecutionModel> browserExecutions = new LinkedList<>(); List<AuthenticationExecutionModel> browserExecutions = new LinkedList<>();
KeycloakModelUtils.deepFindAuthenticationExecutions(realm, browserFlow, browserExecutions); KeycloakModelUtils.deepFindAuthenticationExecutions(realm, browserFlow, browserExecutions);
for (AuthenticationExecutionModel browserExecution : browserExecutions) { for (AuthenticationExecutionModel browserExecution : browserExecutions) {
if (browserExecution.getAuthenticator().equals("auth-otp-form")) { if (browserExecution.isAuthenticatorFlow()){
if (realm.getAuthenticationExecutions(browserExecution.getFlowId()).stream().anyMatch(e -> e.getAuthenticator().equals("auth-otp-form"))){
execution.setRequirement(browserExecution.getRequirement()); execution.setRequirement(browserExecution.getRequirement());
} }
} }
} }
}
execution.setFlowId(conditionalOTP.getId());
execution.setPriority(20);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("conditional-user-configured");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(conditionalOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("auth-otp-form"); execution.setAuthenticator("auth-otp-form");
execution.setPriority(20); execution.setPriority(20);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
@ -591,28 +705,43 @@ public class DefaultAuthenticationFlows {
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
AuthenticationFlowModel authType = new AuthenticationFlowModel();
authType.setTopLevel(false);
authType.setBuiltIn(true);
authType.setAlias("Authentication Options");
authType.setDescription("Authentication options.");
authType.setProviderId("basic-flow");
authType = realm.addAuthenticationFlow(authType);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId()); execution.setParentFlow(challengeFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setFlowId(authType.getId());
execution.setPriority(20);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(authType.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("basic-auth"); execution.setAuthenticator("basic-auth");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(authType.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("basic-auth-otp");
execution.setPriority(20); execution.setPriority(20);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel(); execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId()); execution.setParentFlow(authType.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED); execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("basic-auth-otp"); execution.setAuthenticator("auth-spnego");
execution.setPriority(30); execution.setPriority(30);
execution.setAuthenticatorFlow(false); execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("auth-spnego");
execution.setPriority(40);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
} }
} }

View file

@ -147,7 +147,7 @@ public final class KeycloakModelUtils {
public static UserCredentialModel generateSecret(ClientModel client) { public static UserCredentialModel generateSecret(ClientModel client) {
UserCredentialModel secret = UserCredentialModel.generateSecret(); UserCredentialModel secret = UserCredentialModel.generateSecret();
client.setSecret(secret.getValue()); client.setSecret(secret.getChallengeResponse());
return secret; return secret;
} }

View file

@ -32,6 +32,7 @@ import org.keycloak.events.Event;
import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.AuthDetails; import org.keycloak.events.admin.AuthDetails;
import org.keycloak.models.*; import org.keycloak.models.*;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.*; import org.keycloak.representations.idm.*;
import org.keycloak.representations.idm.authorization.*; import org.keycloak.representations.idm.authorization.*;
@ -168,7 +169,7 @@ public class ModelToRepresentation {
rep.setEmail(user.getEmail()); rep.setEmail(user.getEmail());
rep.setEnabled(user.isEnabled()); rep.setEnabled(user.isEnabled());
rep.setEmailVerified(user.isEmailVerified()); rep.setEmailVerified(user.isEmailVerified());
rep.setTotp(session.userCredentialManager().isConfiguredFor(realm, user, CredentialModel.OTP)); rep.setTotp(session.userCredentialManager().isConfiguredFor(realm, user, OTPCredentialModel.TYPE));
rep.setDisableableCredentialTypes(session.userCredentialManager().getDisableableCredentialTypes(realm, user)); rep.setDisableableCredentialTypes(session.userCredentialManager().getDisableableCredentialTypes(realm, user));
rep.setFederationLink(user.getFederationLink()); rep.setFederationLink(user.getFederationLink());
@ -185,6 +186,7 @@ public class ModelToRepresentation {
attrs.putAll(user.getAttributes()); attrs.putAll(user.getAttributes());
rep.setAttributes(attrs); rep.setAttributes(attrs);
} }
return rep; return rep;
} }
@ -489,7 +491,18 @@ public class ModelToRepresentation {
public static CredentialRepresentation toRepresentation(UserCredentialModel cred) { public static CredentialRepresentation toRepresentation(UserCredentialModel cred) {
CredentialRepresentation rep = new CredentialRepresentation(); CredentialRepresentation rep = new CredentialRepresentation();
rep.setType(CredentialRepresentation.SECRET); rep.setType(CredentialRepresentation.SECRET);
rep.setValue(cred.getValue()); rep.setValue(cred.getChallengeResponse());
return rep;
}
public static CredentialRepresentation toRepresentation(CredentialModel cred) {
CredentialRepresentation rep = new CredentialRepresentation();
rep.setId(cred.getId());
rep.setType(cred.getType());
rep.setUserLabel(cred.getUserLabel());
rep.setCreatedDate(cred.getCreatedDate());
rep.setSecretData(cred.getSecretData());
rep.setCredentialData(cred.getCredentialData());
return rep; return rep;
} }

View file

@ -49,15 +49,14 @@ import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.ResourceStore; import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore; import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory; import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.common.Profile;
import org.keycloak.common.enums.SslRequired; import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.UriUtils; import org.keycloak.common.util.UriUtils;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.keys.KeyProvider; import org.keycloak.keys.KeyProvider;
import org.keycloak.migration.MigrationProvider; import org.keycloak.migration.MigrationProvider;
import org.keycloak.migration.migrators.MigrateTo8_0_0;
import org.keycloak.migration.migrators.MigrationUtils; import org.keycloak.migration.migrators.MigrationUtils;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
@ -86,8 +85,11 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider; import org.keycloak.models.UserProvider;
import org.keycloak.models.WebAuthnPolicy; import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.cache.UserCache; import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordUserCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.credential.dto.OTPCredentialData;
import org.keycloak.models.credential.dto.OTPSecretData;
import org.keycloak.models.credential.dto.PasswordCredentialData;
import org.keycloak.policy.PasswordPolicyNotMetException; import org.keycloak.policy.PasswordPolicyNotMetException;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.ApplicationRepresentation;
@ -660,8 +662,7 @@ public class RepresentationToModel {
for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) { for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) {
AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias()); AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias());
for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) { for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) {
AuthenticationExecutionModel execution = toModel(newRealm, exeRep); AuthenticationExecutionModel execution = toModel(newRealm, model, exeRep);
execution.setParentFlow(model.getId());
newRealm.addAuthenticatorExecution(execution); newRealm.addAuthenticatorExecution(execution);
} }
} }
@ -879,6 +880,35 @@ public class RepresentationToModel {
} }
} }
private static void convertDeprecatedCredentialsFormat(UserRepresentation user) {
if (user.getCredentials() != null) {
for (CredentialRepresentation cred : user.getCredentials()) {
try {
if ((cred.getCredentialData() == null || cred.getSecretData() == null) && cred.getValue() == null) {
logger.warnf("Using deprecated 'credentials' format in JSON representation for user '%s'. It will be removed in future versions", user.getUsername());
if (PasswordCredentialModel.TYPE.equals(cred.getType()) || PasswordCredentialModel.PASSWORD_HISTORY.equals(cred.getType())) {
PasswordCredentialData credentialData = new PasswordCredentialData(cred.getHashIterations(), cred.getAlgorithm());
cred.setCredentialData(JsonSerialization.writeValueAsString(credentialData));
// Created this manually to avoid conversion from Base64 and back
cred.setSecretData("{\"value\":\"" + cred.getHashedSaltedValue() + "\",\"salt\":\"" + cred.getSalt() + "\"}");
cred.setPriority(10);
} else if (OTPCredentialModel.TOTP.equals(cred.getType()) || OTPCredentialModel.HOTP.equals(cred.getType())) {
OTPCredentialData credentialData = new OTPCredentialData(cred.getType(), cred.getDigits(), cred.getCounter(), cred.getPeriod(), cred.getAlgorithm());
OTPSecretData secretData = new OTPSecretData(cred.getHashedSaltedValue());
cred.setCredentialData(JsonSerialization.writeValueAsString(credentialData));
cred.setSecretData(JsonSerialization.writeValueAsString(secretData));
cred.setPriority(20);
cred.setType(OTPCredentialModel.TYPE);
}
}
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}
}
public static void renameRealm(RealmModel realm, String name) { public static void renameRealm(RealmModel realm, String name) {
if (name.equals(realm.getName())) return; if (name.equals(realm.getName())) return;
@ -1677,7 +1707,11 @@ public class RepresentationToModel {
} }
if (userRep.getRequiredActions() != null) { if (userRep.getRequiredActions() != null) {
for (String requiredAction : userRep.getRequiredActions()) { for (String requiredAction : userRep.getRequiredActions()) {
try {
user.addRequiredAction(UserModel.RequiredAction.valueOf(requiredAction.toUpperCase())); user.addRequiredAction(UserModel.RequiredAction.valueOf(requiredAction.toUpperCase()));
} catch (IllegalArgumentException iae) {
user.addRequiredAction(requiredAction);
}
} }
} }
createCredentials(userRep, session, newRealm, user, false); createCredentials(userRep, session, newRealm, user, false);
@ -1722,108 +1756,38 @@ public class RepresentationToModel {
} }
public static void createCredentials(UserRepresentation userRep, KeycloakSession session, RealmModel realm, UserModel user, boolean adminRequest) { public static void createCredentials(UserRepresentation userRep, KeycloakSession session, RealmModel realm, UserModel user, boolean adminRequest) {
convertDeprecatedCredentialsFormat(userRep);
if (userRep.getCredentials() != null) { if (userRep.getCredentials() != null) {
for (CredentialRepresentation cred : userRep.getCredentials()) { for (CredentialRepresentation cred : userRep.getCredentials()) {
updateCredential(session, realm, user, cred, adminRequest); if (cred.getId() != null && session.userCredentialManager().getStoredCredentialById(realm, user, cred.getId()) != null) {
continue;
} }
} if (cred.getValue() != null && !cred.getValue().isEmpty()) {
}
// Detect if it is "plain-text" or "hashed" representation and update model according to it
private static void updateCredential(KeycloakSession session, RealmModel realm, UserModel user, CredentialRepresentation cred, boolean adminRequest) {
if (cred.getValue() != null) {
PasswordUserCredentialModel plainTextCred = convertCredential(cred);
plainTextCred.setAdminRequest(adminRequest);
//if called from import we need to change realm in context to load password policies from the newly created realm
RealmModel origRealm = session.getContext().getRealm(); RealmModel origRealm = session.getContext().getRealm();
try { try {
session.getContext().setRealm(realm); session.getContext().setRealm(realm);
session.userCredentialManager().updateCredential(realm, user, plainTextCred); session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(cred.getValue(), false));
} catch (ModelException ex) { } catch (ModelException ex) {
throw new PasswordPolicyNotMetException(ex.getMessage(), user.getUsername(), ex); throw new PasswordPolicyNotMetException(ex.getMessage(), user.getUsername(), ex);
} finally { } finally {
session.getContext().setRealm(origRealm); session.getContext().setRealm(origRealm);
} }
} else { } else {
CredentialModel hashedCred = new CredentialModel(); session.userCredentialManager().createCredentialThroughProvider(realm, user, toModel(cred));
hashedCred.setType(cred.getType());
hashedCred.setDevice(cred.getDevice());
if (cred.getHashIterations() != null) hashedCred.setHashIterations(cred.getHashIterations());
try {
if (cred.getSalt() != null) hashedCred.setSalt(Base64.decode(cred.getSalt()));
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
hashedCred.setValue(cred.getHashedSaltedValue());
if (cred.getCounter() != null) hashedCred.setCounter(cred.getCounter());
if (cred.getDigits() != null) hashedCred.setDigits(cred.getDigits());
if (cred.getAlgorithm() != null) {
// Could happen when migrating from some early version
if ((UserCredentialModel.PASSWORD.equals(cred.getType()) || UserCredentialModel.PASSWORD_HISTORY.equals(cred.getType())) &&
(cred.getAlgorithm().equals(HmacOTP.HMAC_SHA1))) {
hashedCred.setAlgorithm("pbkdf2");
} else {
hashedCred.setAlgorithm(cred.getAlgorithm());
}
} else {
if (UserCredentialModel.PASSWORD.equals(cred.getType()) || UserCredentialModel.PASSWORD_HISTORY.equals(cred.getType())) {
hashedCred.setAlgorithm("pbkdf2");
} else if (UserCredentialModel.isOtp(cred.getType())) {
hashedCred.setAlgorithm(HmacOTP.HMAC_SHA1);
}
}
if (cred.getPeriod() != null) hashedCred.setPeriod(cred.getPeriod());
if (cred.getDigits() == null && UserCredentialModel.isOtp(cred.getType())) {
hashedCred.setDigits(6);
}
if (cred.getPeriod() == null && UserCredentialModel.TOTP.equals(cred.getType())) {
hashedCred.setPeriod(30);
}
hashedCred.setCreatedDate(cred.getCreatedDate());
session.userCredentialManager().createCredential(realm, user, hashedCred);
UserCache userCache = session.userCache();
if (userCache != null) {
userCache.evict(realm, user);
} }
} }
} }
public static PasswordUserCredentialModel convertCredential(CredentialRepresentation cred) {
PasswordUserCredentialModel credential = new PasswordUserCredentialModel();
credential.setType(cred.getType());
credential.setValue(cred.getValue());
return credential;
} }
public static CredentialModel toModel(CredentialRepresentation cred) { public static CredentialModel toModel(CredentialRepresentation cred) {
CredentialModel model = new CredentialModel(); CredentialModel model = new CredentialModel();
model.setHashIterations(cred.getHashIterations());
model.setCreatedDate(cred.getCreatedDate()); model.setCreatedDate(cred.getCreatedDate());
model.setType(cred.getType()); model.setType(cred.getType());
model.setDigits(cred.getDigits()); model.setUserLabel(cred.getUserLabel());
model.setConfig(cred.getConfig()); model.setSecretData(cred.getSecretData());
model.setDevice(cred.getDevice()); model.setCredentialData(cred.getCredentialData());
model.setAlgorithm(cred.getAlgorithm()); model.setId(cred.getId());
model.setCounter(cred.getCounter());
model.setPeriod(cred.getPeriod());
if (cred.getSalt() != null) {
try {
model.setSalt(Base64.decode(cred.getSalt()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
model.setValue(cred.getValue());
if (cred.getHashedSaltedValue() != null) {
model.setValue(cred.getHashedSaltedValue());
}
return model; return model;
} }
// Role mappings // Role mappings
@ -1986,7 +1950,7 @@ public class RepresentationToModel {
} }
public static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationExecutionExportRepresentation rep) { private static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) {
AuthenticationExecutionModel model = new AuthenticationExecutionModel(); AuthenticationExecutionModel model = new AuthenticationExecutionModel();
if (rep.getAuthenticatorConfig() != null) { if (rep.getAuthenticatorConfig() != null) {
AuthenticatorConfigModel config = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig()); AuthenticatorConfigModel config = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig());
@ -1999,7 +1963,15 @@ public class RepresentationToModel {
model.setFlowId(flow.getId()); model.setFlowId(flow.getId());
} }
model.setPriority(rep.getPriority()); model.setPriority(rep.getPriority());
try {
model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement())); model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement()));
model.setParentFlow(parentFlow.getId());
} catch (IllegalArgumentException iae) {
//retro-compatible for previous OPTIONAL being changed to CONDITIONAL
if ("OPTIONAL".equals(rep.getRequirement())){
MigrateTo8_0_0.migrateOptionalAuthenticationExecution(realm, parentFlow, model, false);
}
}
return model; return model;
} }

View file

@ -24,6 +24,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -52,24 +53,29 @@ public class HistoryPasswordPolicyProvider implements PasswordPolicyProvider {
PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy(); PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
int passwordHistoryPolicyValue = policy.getPolicyConfig(PasswordPolicy.PASSWORD_HISTORY_ID); int passwordHistoryPolicyValue = policy.getPolicyConfig(PasswordPolicy.PASSWORD_HISTORY_ID);
if (passwordHistoryPolicyValue != -1) { if (passwordHistoryPolicyValue != -1) {
List<CredentialModel> storedPasswords = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD); List<CredentialModel> storedPasswords = session.userCredentialManager().getStoredCredentialsByType(realm, user, PasswordCredentialModel.TYPE);
for (CredentialModel cred : storedPasswords) { for (CredentialModel cred : storedPasswords) {
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm()); PasswordCredentialModel passwordCredential = PasswordCredentialModel.createFromCredentialModel(cred);
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, passwordCredential.getPasswordCredentialData().getAlgorithm());
if (hash == null) continue; if (hash == null) continue;
if (hash.verify(password, cred)) { if (hash.verify(password, passwordCredential)) {
return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue); return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
} }
} }
List<CredentialModel> passwordHistory = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD_HISTORY);
if (passwordHistoryPolicyValue > 0) {
List<CredentialModel> passwordHistory = session.userCredentialManager().getStoredCredentialsByType(realm, user, PasswordCredentialModel.PASSWORD_HISTORY);
List<CredentialModel> recentPasswordHistory = getRecent(passwordHistory, passwordHistoryPolicyValue - 1); List<CredentialModel> recentPasswordHistory = getRecent(passwordHistory, passwordHistoryPolicyValue - 1);
for (CredentialModel cred : recentPasswordHistory) { for (CredentialModel cred : recentPasswordHistory) {
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm()); PasswordCredentialModel passwordCredential = PasswordCredentialModel.createFromCredentialModel(cred);
if (hash.verify(password, cred)) { PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, passwordCredential.getPasswordCredentialData().getAlgorithm());
if (hash.verify(password, passwordCredential)) {
return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue); return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
} }
} }
} }
}
return null; return null;
} }

View file

@ -23,5 +23,7 @@ package org.keycloak.credential;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface CredentialInput { public interface CredentialInput {
String getCredentialId();
String getType(); String getType();
String getChallengeResponse();
} }

View file

@ -32,6 +32,13 @@ import java.util.List;
public interface CredentialInputValidator { public interface CredentialInputValidator {
boolean supportsCredentialType(String credentialType); boolean supportsCredentialType(String credentialType);
boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType); boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType);
boolean isValid(RealmModel realm, UserModel user, CredentialInput input);
/**
* Tests whether a credential is valid
* @param realm The realm in which to which the credential belongs to
* @param user The user for which to test the credential
* @param credentialInput the credential details to verify
* @return true if the passed secret is correct
*/
boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput);
} }

View file

@ -17,8 +17,6 @@
package org.keycloak.credential; package org.keycloak.credential;
import org.keycloak.common.util.MultivaluedHashMap;
import java.io.Serializable; import java.io.Serializable;
import java.util.Comparator; import java.util.Comparator;
@ -28,56 +26,50 @@ import java.util.Comparator;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class CredentialModel implements Serializable { public class CredentialModel implements Serializable {
@Deprecated /** Use PasswordCredentialModel.TYPE instead **/
public static final String PASSWORD = "password"; public static final String PASSWORD = "password";
@Deprecated /** Use PasswordCredentialModel.PASSWORD_HISTORY instead **/
public static final String PASSWORD_HISTORY = "password-history"; public static final String PASSWORD_HISTORY = "password-history";
public static final String PASSWORD_TOKEN = "password-token";
@Deprecated /** Use OTPCredentialModel.TYPE instead **/
public static final String OTP = "otp";
@Deprecated /** Use OTPCredentialModel.TOTP instead **/
public static final String TOTP = "totp";
@Deprecated /** Use OTPCredentialModel.HOTP instead **/
public static final String HOTP = "hotp";
// Secret is same as password but it is not hashed // Secret is same as password but it is not hashed
public static final String SECRET = "secret"; public static final String SECRET = "secret";
public static final String TOTP = "totp";
public static final String HOTP = "hotp";
public static final String CLIENT_CERT = "cert"; public static final String CLIENT_CERT = "cert";
public static final String KERBEROS = "kerberos"; public static final String KERBEROS = "kerberos";
public static final String OTP = "otp";
private String id; private String id;
private String type; private String type;
private String value; private String userLabel;
private String device;
private byte[] salt;
private int hashIterations;
private Long createdDate; private Long createdDate;
// otp stuff private String secretData;
private int counter; private String credentialData;
private String algorithm;
private int digits;
private int period;
private MultivaluedHashMap<String, String> config;
public CredentialModel shallowClone() { public CredentialModel shallowClone() {
CredentialModel res = new CredentialModel(); CredentialModel res = new CredentialModel();
res.id = id; res.id = id;
res.type = type; res.type = type;
res.value = value; res.userLabel = userLabel;
res.device = device;
res.salt = salt;
res.hashIterations = hashIterations;
res.createdDate = createdDate; res.createdDate = createdDate;
res.counter = counter; res.secretData = secretData;
res.algorithm = algorithm; res.credentialData = credentialData;
res.digits = digits;
res.period = period;
res.config = config;
return res; return res;
} }
public String getId() { public String getId() {
return id; return id;
} }
public void setId(String id) { public void setId(String id) {
this.id = id; this.id = id;
} }
@ -85,89 +77,36 @@ public class CredentialModel implements Serializable {
public String getType() { public String getType() {
return type; return type;
} }
public void setType(String type) { public void setType(String type) {
this.type = type; this.type = type;
} }
public String getValue() { public String getUserLabel() {
return value; return userLabel;
} }
public void setUserLabel(String userLabel) {
public void setValue(String value) { this.userLabel = userLabel;
this.value = value;
}
public String getDevice() {
return device;
}
public void setDevice(String device) {
this.device = device;
}
public byte[] getSalt() {
return salt;
}
public void setSalt(byte[] salt) {
this.salt = salt;
}
public int getHashIterations() {
return hashIterations;
}
public void setHashIterations(int iterations) {
this.hashIterations = iterations;
} }
public Long getCreatedDate() { public Long getCreatedDate() {
return createdDate; return createdDate;
} }
public void setCreatedDate(Long createdDate) { public void setCreatedDate(Long createdDate) {
this.createdDate = createdDate; this.createdDate = createdDate;
} }
public int getCounter() { public String getSecretData() {
return counter; return secretData;
}
public void setSecretData(String secretData) {
this.secretData = secretData;
} }
public void setCounter(int counter) { public String getCredentialData() {
this.counter = counter; return credentialData;
} }
public void setCredentialData(String credentialData) {
public String getAlgorithm() { this.credentialData = credentialData;
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public int getDigits() {
return digits;
}
public void setDigits(int digits) {
this.digits = digits;
}
public int getPeriod() {
return period;
}
public void setPeriod(int period) {
this.period = period;
}
public MultivaluedHashMap<String, String> getConfig() {
return config;
}
public void setConfig(MultivaluedHashMap<String, String> config) {
this.config = config;
} }
public static Comparator<CredentialModel> comparingByStartDateDesc() { public static Comparator<CredentialModel> comparingByStartDateDesc() {

View file

@ -16,16 +16,37 @@
*/ */
package org.keycloak.credential; package org.keycloak.credential;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface CredentialProvider extends Provider { public interface CredentialProvider<T extends CredentialModel> extends Provider {
@Override @Override
default default void close() {
void close() {
} }
String getType();
CredentialModel createCredential(RealmModel realm, UserModel user, T credentialModel);
void deleteCredential(RealmModel realm, UserModel user, String credentialId);
T getCredentialFromModel(CredentialModel model);
default T getDefaultCredential(KeycloakSession session, RealmModel realm, UserModel user) {
List<CredentialModel> models = session.userCredentialManager().getStoredCredentialsByType(realm, user, getType());
if (models.isEmpty()) {
return null;
}
return getCredentialFromModel(models.get(0));
}
} }

View file

@ -34,4 +34,8 @@ public interface UserCredentialStore extends Provider {
List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user); List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user);
List<CredentialModel> getStoredCredentialsByType(RealmModel realm, UserModel user, String type); List<CredentialModel> getStoredCredentialsByType(RealmModel realm, UserModel user, String type);
CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type); CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type);
//list operations
boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId);
} }

View file

@ -19,20 +19,21 @@ package org.keycloak.credential.hash;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
/** /**
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a> * @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
*/ */
public interface PasswordHashProvider extends Provider { public interface PasswordHashProvider extends Provider {
boolean policyCheck(PasswordPolicy policy, CredentialModel credential); boolean policyCheck(PasswordPolicy policy, PasswordCredentialModel credential);
void encode(String rawPassword, int iterations, CredentialModel credential); PasswordCredentialModel encodedCredential(String rawPassword, int iterations);
default default
String encode(String rawPassword, int iterations) { String encode(String rawPassword, int iterations) {
return rawPassword; return rawPassword;
} }
boolean verify(String rawPassword, CredentialModel credential); boolean verify(String rawPassword, PasswordCredentialModel credential);
} }

View file

@ -120,7 +120,7 @@ public class AuthenticationExecutionModel implements Serializable {
public enum Requirement { public enum Requirement {
REQUIRED, REQUIRED,
OPTIONAL, CONDITIONAL,
ALTERNATIVE, ALTERNATIVE,
DISABLED DISABLED
} }
@ -128,8 +128,8 @@ public class AuthenticationExecutionModel implements Serializable {
public boolean isRequired() { public boolean isRequired() {
return requirement == Requirement.REQUIRED; return requirement == Requirement.REQUIRED;
} }
public boolean isOptional() { public boolean isConditional() {
return requirement == Requirement.OPTIONAL; return requirement == Requirement.CONDITIONAL;
} }
public boolean isAlternative() { public boolean isAlternative() {
return requirement == Requirement.ALTERNATIVE; return requirement == Requirement.ALTERNATIVE;
@ -140,4 +140,21 @@ public class AuthenticationExecutionModel implements Serializable {
public boolean isEnabled() { public boolean isEnabled() {
return requirement != Requirement.DISABLED; return requirement != Requirement.DISABLED;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AuthenticationExecutionModel that = (AuthenticationExecutionModel) o;
if (id == null || that.id == null) return false;
return id.equals(that.id);
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
} }

View file

@ -18,6 +18,7 @@
package org.keycloak.models; package org.keycloak.models;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.utils.Base32; import org.keycloak.models.utils.Base32;
import org.keycloak.models.utils.HmacOTP; import org.keycloak.models.utils.HmacOTP;
@ -66,7 +67,7 @@ public class OTPPolicy implements Serializable {
this.period = period; this.period = period;
} }
public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(UserCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1, 30); public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(OTPCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1, 30);
public String getAlgorithmKey() { public String getAlgorithmKey() {
return algToKeyUriAlg.containsKey(algorithm) ? algToKeyUriAlg.get(algorithm) : algorithm; return algToKeyUriAlg.containsKey(algorithm) ? algToKeyUriAlg.get(algorithm) : algorithm;
@ -148,9 +149,9 @@ public class OTPPolicy implements Serializable {
+ "&algorithm=" + algToKeyUriAlg.get(algorithm) // + "&algorithm=" + algToKeyUriAlg.get(algorithm) //
+ "&issuer=" + issuerName; + "&issuer=" + issuerName;
if (type.equals(UserCredentialModel.HOTP)) { if (type.equals(OTPCredentialModel.HOTP)) {
parameters += "&counter=" + initialCounter; parameters += "&counter=" + initialCounter;
} else if (type.equals(UserCredentialModel.TOTP)) { } else if (type.equals(OTPCredentialModel.TOTP)) {
parameters += "&period=" + period; parameters += "&period=" + period;
} }
@ -194,11 +195,7 @@ public class OTPPolicy implements Serializable {
return false; return false;
} }
if (policy.getType().equals("totp") && policy.getPeriod() != 30) { return policy.getType().equals("totp") && policy.getPeriod() == 30;
return false;
}
return true;
} }
} }

View file

@ -298,6 +298,7 @@ public interface RealmModel extends RoleContainerModel {
List<AuthenticationExecutionModel> getAuthenticationExecutions(String flowId); List<AuthenticationExecutionModel> getAuthenticationExecutions(String flowId);
AuthenticationExecutionModel getAuthenticationExecutionById(String id); AuthenticationExecutionModel getAuthenticationExecutionById(String id);
AuthenticationExecutionModel getAuthenticationExecutionByFlowId(String flowId);
AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model); AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model);
void updateAuthenticatorExecution(AuthenticationExecutionModel model); void updateAuthenticatorExecution(AuthenticationExecutionModel model);
void removeAuthenticatorExecution(AuthenticationExecutionModel model); void removeAuthenticatorExecution(AuthenticationExecutionModel model);

View file

@ -17,6 +17,9 @@
package org.keycloak.models; package org.keycloak.models;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -78,7 +81,7 @@ public class RequiredCredentialModel implements Serializable {
static { static {
Map<String, RequiredCredentialModel> map = new HashMap<String, RequiredCredentialModel>(); Map<String, RequiredCredentialModel> map = new HashMap<String, RequiredCredentialModel>();
PASSWORD = new RequiredCredentialModel(); PASSWORD = new RequiredCredentialModel();
PASSWORD.setType(UserCredentialModel.PASSWORD); PASSWORD.setType(PasswordCredentialModel.TYPE);
PASSWORD.setInput(true); PASSWORD.setInput(true);
PASSWORD.setSecret(true); PASSWORD.setSecret(true);
PASSWORD.setFormLabel("password"); PASSWORD.setFormLabel("password");
@ -90,7 +93,7 @@ public class RequiredCredentialModel implements Serializable {
SECRET.setFormLabel("secret"); SECRET.setFormLabel("secret");
map.put(SECRET.getType(), SECRET); map.put(SECRET.getType(), SECRET);
TOTP = new RequiredCredentialModel(); TOTP = new RequiredCredentialModel();
TOTP.setType(UserCredentialModel.TOTP); TOTP.setType(OTPCredentialModel.TYPE);
TOTP.setInput(true); TOTP.setInput(true);
TOTP.setSecret(false); TOTP.setSecret(false);
TOTP.setFormLabel("authenticatorCode"); TOTP.setFormLabel("authenticatorCode");

View file

@ -17,6 +17,7 @@
package org.keycloak.models; package org.keycloak.models;
import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.UserCredentialStore; import org.keycloak.credential.UserCredentialStore;
import java.util.List; import java.util.List;
@ -60,6 +61,24 @@ public interface UserCredentialManager extends UserCredentialStore {
*/ */
void updateCredential(RealmModel realm, UserModel user, CredentialInput input); void updateCredential(RealmModel realm, UserModel user, CredentialInput input);
/**
* Creates a credential from the credentialModel, by looping through the providers to find a match for the type
* @param realm
* @param user
* @param model
* @return
*/
CredentialModel createCredentialThroughProvider(RealmModel realm, UserModel user, CredentialModel model);
/**
* Updates the credential label and invalidates the cache for the user.
* @param realm
* @param user
* @param credentialId
* @param userLabel
*/
void updateCredentialLabel(RealmModel realm, UserModel user, String credentialId, String userLabel);
/** /**
* Calls disableCredential on UserStorageProvider and UserFederationProviders first, then loop through * Calls disableCredential on UserStorageProvider and UserFederationProviders first, then loop through
* each CredentialProvider. * each CredentialProvider.

View file

@ -19,10 +19,9 @@ package org.keycloak.models;
import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialModel;
import org.keycloak.models.credential.PasswordUserCredentialModel; import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
@ -30,134 +29,80 @@ import java.util.UUID;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class UserCredentialModel implements CredentialInput { public class UserCredentialModel implements CredentialInput {
public static final String PASSWORD = CredentialModel.PASSWORD;
public static final String PASSWORD_HISTORY = CredentialModel.PASSWORD_HISTORY;
public static final String PASSWORD_TOKEN = CredentialModel.PASSWORD_TOKEN;
// Secret is same as password but it is not hashed @Deprecated /** Use PasswordCredentialModel.TYPE instead **/
public static final String PASSWORD = PasswordCredentialModel.TYPE;
@Deprecated /** Use PasswordCredentialModel.PASSWORD_HISTORY instead **/
public static final String PASSWORD_HISTORY = PasswordCredentialModel.PASSWORD_HISTORY;
@Deprecated /** Use OTPCredentialModel.TOTP instead **/
public static final String TOTP = OTPCredentialModel.TOTP;
@Deprecated /** Use OTPCredentialModel.TOTP instead **/
public static final String HOTP = OTPCredentialModel.HOTP;
public static final String SECRET = CredentialModel.SECRET; public static final String SECRET = CredentialModel.SECRET;
public static final String TOTP = CredentialModel.TOTP;
public static final String HOTP = CredentialModel.HOTP;
public static final String CLIENT_CERT = CredentialModel.CLIENT_CERT;
public static final String KERBEROS = CredentialModel.KERBEROS; public static final String KERBEROS = CredentialModel.KERBEROS;
public static final String CLIENT_CERT = CredentialModel.CLIENT_CERT;
protected String type; private final String credentialId;
protected String value; private final String type;
protected String device; private final String challengeResponse;
protected String algorithm; private final boolean adminRequest;
// Additional context informations public UserCredentialModel(String credentialId, String type, String challengeResponse) {
protected Map<String, Object> notes = new HashMap<>(); this.credentialId = credentialId;
this.type = type;
public UserCredentialModel() { this.challengeResponse = challengeResponse;
this.adminRequest = false;
} }
public static PasswordUserCredentialModel password(String password) { public UserCredentialModel(String credentialId, String type, String challengeResponse, boolean adminRequest) {
this.credentialId = credentialId;
this.type = type;
this.challengeResponse = challengeResponse;
this.adminRequest = adminRequest;
}
public static UserCredentialModel password(String password) {
return password(password, false); return password(password, false);
} }
public static PasswordUserCredentialModel password(String password, boolean adminRequest) { public static UserCredentialModel password(String password, boolean adminRequest) {
PasswordUserCredentialModel model = new PasswordUserCredentialModel(); return new UserCredentialModel("", PasswordCredentialModel.TYPE, password, adminRequest);
model.setType(PASSWORD);
model.setValue(password);
model.setAdminRequest(adminRequest);
return model;
}
public static UserCredentialModel passwordToken(String passwordToken) {
UserCredentialModel model = new UserCredentialModel();
model.setType(PASSWORD_TOKEN);
model.setValue(passwordToken);
return model;
} }
public static UserCredentialModel secret(String password) { public static UserCredentialModel secret(String password) {
UserCredentialModel model = new UserCredentialModel(); return new UserCredentialModel("", SECRET, password);
model.setType(SECRET);
model.setValue(password);
return model;
}
public static UserCredentialModel otp(String type, String key) {
if (type.equals(HOTP)) return hotp(key);
if (type.equals(TOTP)) return totp(key);
throw new RuntimeException("Unknown OTP type");
}
public static UserCredentialModel totp(String key) {
UserCredentialModel model = new UserCredentialModel();
model.setType(TOTP);
model.setValue(key);
return model;
}
public static UserCredentialModel hotp(String key) {
UserCredentialModel model = new UserCredentialModel();
model.setType(HOTP);
model.setValue(key);
return model;
} }
public static UserCredentialModel kerberos(String token) { public static UserCredentialModel kerberos(String token) {
UserCredentialModel model = new UserCredentialModel(); return new UserCredentialModel("", KERBEROS, token);
model.setType(KERBEROS);
model.setValue(token);
return model;
} }
public static UserCredentialModel generateSecret() { public static UserCredentialModel generateSecret() {
UserCredentialModel model = new UserCredentialModel(); return new UserCredentialModel("", SECRET, UUID.randomUUID().toString());
model.setType(SECRET);
model.setValue(UUID.randomUUID().toString());
return model;
} }
public static boolean isOtp(String type) { @Override
return TOTP.equals(type) || HOTP.equals(type); public String getCredentialId() {
return credentialId;
} }
@Override
public String getType() { public String getType() {
return type; return type;
} }
public void setType(String type) { @Override
this.type = type; public String getChallengeResponse() {
return challengeResponse;
} }
public String getValue() { public boolean isAdminRequest() {
return value; return adminRequest;
}
} }
public void setValue(String value) {
this.value = value;
}
public String getDevice() {
return device;
}
public void setDevice(String device) {
this.device = device;
}
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public void setNote(String key, String value) {
this.notes.put(key, value);
}
public void removeNote(String key) {
this.notes.remove(key);
}
public Object getNote(String key) {
return this.notes.get(key);
}
}

View file

@ -0,0 +1,105 @@
package org.keycloak.models.credential;
import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.credential.dto.OTPCredentialData;
import org.keycloak.models.credential.dto.OTPSecretData;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
public class OTPCredentialModel extends CredentialModel {
public final static String TYPE = "otp";
public final static String TOTP = "totp";
public final static String HOTP = "hotp";
private final OTPCredentialData credentialData;
private final OTPSecretData secretData;
private OTPCredentialModel(String secretValue, String subType, int digits, int counter, int period, String algorithm) {
credentialData = new OTPCredentialData(subType, digits, counter, period, algorithm);
secretData = new OTPSecretData(secretValue);
}
private OTPCredentialModel(OTPCredentialData credentialData, OTPSecretData secretData) {
this.credentialData = credentialData;
this.secretData = secretData;
}
public static OTPCredentialModel createTOTP(String secretValue, int digits, int period, String algorithm){
OTPCredentialModel credentialModel = new OTPCredentialModel(secretValue, TOTP, digits, 0, period, algorithm);
credentialModel.fillCredentialModelFields();
return credentialModel;
}
public static OTPCredentialModel createHOTP(String secretValue, int digits, int counter, String algorithm) {
OTPCredentialModel credentialModel = new OTPCredentialModel(secretValue, HOTP, digits, counter, 0, algorithm);
credentialModel.fillCredentialModelFields();
return credentialModel;
}
public static OTPCredentialModel createFromPolicy(RealmModel realm, String secretValue) {
return createFromPolicy(realm, secretValue, "");
}
public static OTPCredentialModel createFromPolicy(RealmModel realm, String secretValue, String userLabel) {
OTPPolicy policy = realm.getOTPPolicy();
OTPCredentialModel credentialModel = new OTPCredentialModel(secretValue, policy.getType(), policy.getDigits(),
policy.getInitialCounter(), policy.getPeriod(), policy.getAlgorithm());
credentialModel.fillCredentialModelFields();
credentialModel.setUserLabel(userLabel);
return credentialModel;
}
public static OTPCredentialModel createFromCredentialModel(CredentialModel credentialModel) {
try {
OTPCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), OTPCredentialData.class);
OTPSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), OTPSecretData.class);
OTPCredentialModel otpCredentialModel = new OTPCredentialModel(credentialData, secretData);
otpCredentialModel.setUserLabel(credentialModel.getUserLabel());
otpCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
otpCredentialModel.setType(TYPE);
otpCredentialModel.setId(credentialModel.getId());
otpCredentialModel.setSecretData(credentialModel.getSecretData());
otpCredentialModel.setCredentialData(credentialModel.getCredentialData());
return otpCredentialModel;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void updateCounter(int counter) {
credentialData.setCounter(counter);
try {
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public OTPCredentialData getOTPCredentialData() {
return credentialData;
}
public OTPSecretData getOTPSecretData() {
return secretData;
}
private void fillCredentialModelFields(){
try {
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setType(TYPE);
setCreatedDate(Time.currentTimeMillis());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,69 @@
package org.keycloak.models.credential;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.credential.dto.PasswordCredentialData;
import org.keycloak.models.credential.dto.PasswordSecretData;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
public class PasswordCredentialModel extends CredentialModel {
public final static String TYPE = "password";
public final static String PASSWORD_HISTORY = "password-history";
private final PasswordCredentialData credentialData;
private final PasswordSecretData secretData;
private PasswordCredentialModel(PasswordCredentialData credentialData, PasswordSecretData secretData) {
this.credentialData = credentialData;
this.secretData = secretData;
}
public static PasswordCredentialModel createFromValues(String algorithm, byte[] salt, int hashIterations, String encodedPassword){
PasswordCredentialData credentialData = new PasswordCredentialData(hashIterations, algorithm);
PasswordSecretData secretData = new PasswordSecretData(encodedPassword, salt);
PasswordCredentialModel passwordCredentialModel = new PasswordCredentialModel(credentialData, secretData);
try {
passwordCredentialModel.setCredentialData(JsonSerialization.writeValueAsString(credentialData));
passwordCredentialModel.setSecretData(JsonSerialization.writeValueAsString(secretData));
passwordCredentialModel.setType(TYPE);
return passwordCredentialModel;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static PasswordCredentialModel createFromCredentialModel(CredentialModel credentialModel) {
try {
PasswordCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(),
PasswordCredentialData.class);
PasswordSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), PasswordSecretData.class);
PasswordCredentialModel passwordCredentialModel = new PasswordCredentialModel(credentialData, secretData);
passwordCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
passwordCredentialModel.setCredentialData(credentialModel.getCredentialData());
passwordCredentialModel.setId(credentialModel.getId());
passwordCredentialModel.setSecretData(credentialModel.getSecretData());
passwordCredentialModel.setType(credentialModel.getType());
passwordCredentialModel.setUserLabel(credentialModel.getUserLabel());
return passwordCredentialModel;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public PasswordCredentialData getPasswordCredentialData() {
return credentialData;
}
public PasswordSecretData getPasswordSecretData() {
return secretData;
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.models.credential;
import java.io.IOException;
import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
import org.keycloak.models.credential.dto.WebAuthnSecretData;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class WebAuthnCredentialModel extends CredentialModel {
public final static String TYPE = "webauthn";
private final WebAuthnCredentialData credentialData;
private final WebAuthnSecretData secretData;
private WebAuthnCredentialModel(WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
this.credentialData = credentialData;
this.secretData = secretData;
}
public static WebAuthnCredentialModel create(String userLabel, String aaguid, String credentialId,
String attestationStatement, String credentialPublicKey, long counter) {
WebAuthnCredentialData credentialData = new WebAuthnCredentialData(aaguid, credentialId, counter, attestationStatement, credentialPublicKey);
WebAuthnSecretData secretData = new WebAuthnSecretData();
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialData, secretData);
credentialModel.fillCredentialModelFields();
credentialModel.setUserLabel(userLabel);
return credentialModel;
}
public static WebAuthnCredentialModel createFromCredentialModel(CredentialModel credentialModel) {
try {
WebAuthnCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), WebAuthnCredentialData.class);
WebAuthnSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), WebAuthnSecretData.class);
WebAuthnCredentialModel webAuthnCredentialModel = new WebAuthnCredentialModel(credentialData, secretData);
webAuthnCredentialModel.setUserLabel(credentialModel.getUserLabel());
webAuthnCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
webAuthnCredentialModel.setType(TYPE);
webAuthnCredentialModel.setId(credentialModel.getId());
webAuthnCredentialModel.setSecretData(credentialModel.getSecretData());
webAuthnCredentialModel.setCredentialData(credentialModel.getCredentialData());
return webAuthnCredentialModel;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void updateCounter(long counter) {
credentialData.setCounter(counter);
try {
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public WebAuthnCredentialData getWebAuthnCredentialData() {
return credentialData;
}
public WebAuthnSecretData getWebAuthnSecretData() {
return secretData;
}
private void fillCredentialModelFields() {
try {
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setType(TYPE);
setCreatedDate(Time.currentTimeMillis());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
return "WebAuthnCredentialModel { " +
credentialData +
", " + secretData +
" }";
}
}

View file

@ -0,0 +1,49 @@
package org.keycloak.models.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class OTPCredentialData {
private final String subType;
private final int digits;
private int counter;
private final int period;
private final String algorithm;
@JsonCreator
public OTPCredentialData(@JsonProperty("subType") String subType,
@JsonProperty("digits") int digits,
@JsonProperty("counter") int counter,
@JsonProperty("period") int period,
@JsonProperty("algorithm") String algorithm) {
this.subType = subType;
this.digits = digits;
this.counter = counter;
this.period = period;
this.algorithm = algorithm;
}
public String getSubType() {
return subType;
}
public int getDigits() {
return digits;
}
public int getCounter() {
return counter;
}
public void setCounter(int counter) {
this.counter = counter;
}
public int getPeriod() {
return period;
}
public String getAlgorithm() {
return algorithm;
}
}

View file

@ -0,0 +1,17 @@
package org.keycloak.models.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class OTPSecretData {
private final String value;
@JsonCreator
public OTPSecretData(@JsonProperty("value") String value) {
this.value = value;
}
public String getValue() {
return value;
}
}

View file

@ -0,0 +1,23 @@
package org.keycloak.models.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PasswordCredentialData {
private final int hashIterations;
private final String algorithm;
@JsonCreator
public PasswordCredentialData(@JsonProperty("hashIterations") int hashIterations, @JsonProperty("algorithm") String algorithm) {
this.hashIterations = hashIterations;
this.algorithm = algorithm;
}
public int getHashIterations() {
return hashIterations;
}
public String getAlgorithm() {
return algorithm;
}
}

View file

@ -0,0 +1,23 @@
package org.keycloak.models.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PasswordSecretData {
private final String value;
private final byte[] salt;
@JsonCreator
public PasswordSecretData(@JsonProperty("value") String value, @JsonProperty("salt") byte[] salt) {
this.value = value;
this.salt = salt;
}
public String getValue() {
return value;
}
public byte[] getSalt() {
return salt;
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.models.credential.dto;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class WebAuthnCredentialData {
private final String aaguid;
private final String credentialId;
private long counter;
private String attestationStatement;
private String credentialPublicKey;
@JsonCreator
public WebAuthnCredentialData(@JsonProperty("aaguid") String aaguid,
@JsonProperty("credentialId") String credentialId,
@JsonProperty("counter") long counter,
@JsonProperty("attestationStatement") String attestationStatement,
@JsonProperty("credentialPublicKey") String credentialPublicKey ) {
this.aaguid = aaguid;
this.credentialId = credentialId;
this.counter = counter;
this.attestationStatement = attestationStatement;
this.credentialPublicKey = credentialPublicKey;
}
public String getAaguid() {
return aaguid;
}
public String getCredentialId() {
return credentialId;
}
public String getAttestationStatement() {
return attestationStatement;
}
public String getCredentialPublicKey() {
return credentialPublicKey;
}
public long getCounter() {
return counter;
}
public void setCounter(long counter) {
this.counter = counter;
}
@Override
public String toString() {
return "WebAuthnCredentialData { " +
"aaguid='" + aaguid + '\'' +
", credentialId='" + credentialId + '\'' +
", counter=" + counter +
", credentialPublicKey=" + credentialPublicKey +
", attestationStatement='" + attestationStatement + '\'' +
", credentialPublicKey='" + credentialPublicKey + '\'' +
" }";
}
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 Red Hat, Inc. and/or its affiliates * Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags. * and other contributors as indicated by the @author tags.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -13,26 +13,25 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.keycloak.models.credential; package org.keycloak.models.credential.dto;
import org.keycloak.models.UserCredentialModel; import com.fasterxml.jackson.annotation.JsonCreator;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class PasswordUserCredentialModel extends UserCredentialModel { public class WebAuthnSecretData {
// True if we have password-update request triggered by admin, not by user himself @JsonCreator
private static final String ADMIN_REQUEST = "adminRequest"; public WebAuthnSecretData() {
public boolean isAdminRequest() {
Boolean b = (Boolean) this.notes.get(ADMIN_REQUEST);
return b!=null && b;
} }
public void setAdminRequest(boolean adminRequest) {
this.notes.put(ADMIN_REQUEST, adminRequest); @Override
public String toString() {
return "WebAuthnSecretData {}";
} }
} }

View file

@ -46,13 +46,13 @@ public interface WebAuthnConstants {
final String USER_VERIFICATION = "userVerification"; final String USER_VERIFICATION = "userVerification";
// key for storing onto UserModel's Attribute public key credential id generated by navigator.credentials.create() // Event key for credential id generated by navigator.credentials.create()
final String PUBKEY_CRED_ID_ATTR = "public_key_credential_id"; final String PUBKEY_CRED_ID_ATTR = "public_key_credential_id";
// key for storing onto UserModel's Attribute Public Key Credential's user-editable metadata // Event key for Public Key Credential's user-editable metadata
final String PUBKEY_CRED_LABEL_ATTR = "public_key_credential_label"; final String PUBKEY_CRED_LABEL_ATTR = "public_key_credential_label";
// key for storing onto UserModel's Attribute Public Key Credential's AAGUID // Event key for Public Key Credential's AAGUID
final String PUBKEY_CRED_AAGUID_ATTR = "public_key_credential_aaguid"; final String PUBKEY_CRED_AAGUID_ATTR = "public_key_credential_aaguid";
// key for storing onto AuthenticationSessionModel's Attribute challenge generated by RP(keycloak) // key for storing onto AuthenticationSessionModel's Attribute challenge generated by RP(keycloak)

View file

@ -23,6 +23,7 @@ import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAu
import org.keycloak.authentication.authenticators.client.ClientAuthUtil; import org.keycloak.authentication.authenticators.client.ClientAuthUtil;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
@ -240,6 +241,10 @@ public class AuthenticationProcessor {
return request; return request;
} }
public String getFlowPath() {
return flowPath;
}
public void setAutheticatedUser(UserModel user) { public void setAutheticatedUser(UserModel user) {
UserModel previousUser = getAuthenticationSession().getAuthenticatedUser(); UserModel previousUser = getAuthenticationSession().getAuthenticatedUser();
if (previousUser != null && !user.getId().equals(previousUser.getId())) if (previousUser != null && !user.getId().equals(previousUser.getId()))
@ -276,6 +281,8 @@ public class AuthenticationProcessor {
List<AuthenticationExecutionModel> currentExecutions; List<AuthenticationExecutionModel> currentExecutions;
FormMessage errorMessage; FormMessage errorMessage;
FormMessage successMessage; FormMessage successMessage;
String selectedCredentialId;
List<AuthenticationSelectionOption> authenticationSelections;
private Result(AuthenticationExecutionModel execution, Authenticator authenticator, List<AuthenticationExecutionModel> currentExecutions) { private Result(AuthenticationExecutionModel execution, Authenticator authenticator, List<AuthenticationExecutionModel> currentExecutions) {
this.execution = execution; this.execution = execution;
@ -393,6 +400,26 @@ public class AuthenticationProcessor {
setAutheticatedUser(user); setAutheticatedUser(user);
} }
@Override
public String getSelectedCredentialId() {
return selectedCredentialId;
}
@Override
public void setSelectedCredentialId(String selectedCredentialId) {
this.selectedCredentialId = selectedCredentialId;
}
@Override
public List<AuthenticationSelectionOption> getAuthenticationSelections() {
return authenticationSelections;
}
@Override
public void setAuthenticationSelections(List<AuthenticationSelectionOption> authenticationSelections) {
this.authenticationSelections = authenticationSelections;
}
@Override @Override
public void clearUser() { public void clearUser() {
clearAuthenticatedUser(); clearAuthenticatedUser();
@ -423,6 +450,11 @@ public class AuthenticationProcessor {
return AuthenticationProcessor.this.getAuthenticationSession(); return AuthenticationProcessor.this.getAuthenticationSession();
} }
@Override
public String getFlowPath() {
return AuthenticationProcessor.this.getFlowPath();
}
@Override @Override
public ClientConnection getConnection() { public ClientConnection getConnection() {
return AuthenticationProcessor.this.getConnection(); return AuthenticationProcessor.this.getConnection();
@ -483,6 +515,7 @@ public class AuthenticationProcessor {
String accessCode = generateAccessCode(); String accessCode = generateAccessCode();
URI action = getActionUrl(accessCode); URI action = getActionUrl(accessCode);
LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class) LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
.setAuthContext(this)
.setAuthenticationSession(getAuthenticationSession()) .setAuthenticationSession(getAuthenticationSession())
.setUser(getUser()) .setUser(getUser())
.setActionUri(action) .setActionUri(action)
@ -653,9 +686,52 @@ public class AuthenticationProcessor {
return status == AuthenticationSessionModel.ExecutionStatus.SUCCESS; return status == AuthenticationSessionModel.ExecutionStatus.SUCCESS;
} }
public Response handleBrowserExceptionList(AuthenticationFlowException e) {
LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authenticationSession);
ServicesLogger.LOGGER.failedAuthentication(e);
forms.addError(new FormMessage(Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
for (AuthenticationFlowException afe : e.getAfeList()) {
ServicesLogger.LOGGER.failedAuthentication(afe);
switch (afe.getError()){
case INVALID_USER:
event.error(Errors.USER_NOT_FOUND);
forms.addError(new FormMessage(Messages.INVALID_USER));
break;
case USER_DISABLED:
event.error(Errors.USER_DISABLED);
forms.addError(new FormMessage(Messages.ACCOUNT_DISABLED));
break;
case USER_TEMPORARILY_DISABLED:
event.error(Errors.USER_TEMPORARILY_DISABLED);
forms.addError(new FormMessage(Messages.INVALID_USER));
break;
case INVALID_CLIENT_SESSION:
event.error(Errors.INVALID_CODE);
forms.addError(new FormMessage(Messages.INVALID_CODE));
break;
case EXPIRED_CODE:
event.error(Errors.EXPIRED_CODE);
forms.addError(new FormMessage(Messages.EXPIRED_CODE));
break;
case DISPLAY_NOT_SUPPORTED:
event.error(Errors.DISPLAY_UNSUPPORTED);
forms.addError(new FormMessage(Messages.DISPLAY_UNSUPPORTED));
break;
case CREDENTIAL_SETUP_REQUIRED:
event.error(Errors.INVALID_USER_CREDENTIALS);
forms.addError(new FormMessage(Messages.CREDENTIAL_SETUP_REQUIRED));
break;
}
}
return forms.createErrorPage(Response.Status.BAD_REQUEST);
}
public Response handleBrowserException(Exception failure) { public Response handleBrowserException(Exception failure) {
if (failure instanceof AuthenticationFlowException) { if (failure instanceof AuthenticationFlowException) {
AuthenticationFlowException e = (AuthenticationFlowException) failure; AuthenticationFlowException e = (AuthenticationFlowException) failure;
if (e.getAfeList() != null && !e.getAfeList().isEmpty()){
return handleBrowserExceptionList(e);
}
if (e.getError() == AuthenticationFlowError.INVALID_USER) { if (e.getError() == AuthenticationFlowError.INVALID_USER) {
ServicesLogger.LOGGER.failedAuthentication(e); ServicesLogger.LOGGER.failedAuthentication(e);
@ -715,6 +791,11 @@ public class AuthenticationProcessor {
event.error(Errors.DISPLAY_UNSUPPORTED); event.error(Errors.DISPLAY_UNSUPPORTED);
if (e.getResponse() != null) return e.getResponse(); if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED); return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED);
} else if (e.getError() == AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED){
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.INVALID_USER_CREDENTIALS);
if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.CREDENTIAL_SETUP_REQUIRED);
} else { } else {
ServicesLogger.LOGGER.failedAuthentication(e); ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.INVALID_USER_CREDENTIALS); event.error(Errors.INVALID_USER_CREDENTIALS);
@ -786,7 +867,11 @@ public class AuthenticationProcessor {
AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null); AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null);
try { try {
Response challenge = authenticationFlow.processFlow(); Response challenge = authenticationFlow.processFlow();
return challenge; if (challenge != null) return challenge;
if (!authenticationFlow.isSuccessful()) {
throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR);
}
return null;
} catch (Exception e) { } catch (Exception e) {
return handleClientAuthException(e); return handleClientAuthException(e);
} }
@ -875,6 +960,9 @@ public class AuthenticationProcessor {
if (authenticationSession.getAuthenticatedUser() == null) { if (authenticationSession.getAuthenticatedUser() == null) {
throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER); throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER);
} }
if (!authenticationFlow.isSuccessful()) {
throw new AuthenticationFlowException(authenticationFlow.getFlowExceptions());
}
return authenticationComplete(); return authenticationComplete();
} }
@ -912,7 +1000,10 @@ public class AuthenticationProcessor {
if (authenticationSession.getAuthenticatedUser() == null) { if (authenticationSession.getAuthenticatedUser() == null) {
throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER); throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER);
} }
return challenge; if (!authenticationFlow.isSuccessful()) {
throw new AuthenticationFlowException(authenticationFlow.getFlowExceptions());
}
return null;
} }
// May create userSession too // May create userSession too

View file

@ -42,6 +42,8 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
AuthenticationProcessor processor; AuthenticationProcessor processor;
AuthenticationFlowModel flow; AuthenticationFlowModel flow;
private boolean success;
public ClientAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) { public ClientAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) {
this.processor = processor; this.processor = processor;
this.flow = flow; this.flow = flow;
@ -84,6 +86,8 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
if (!context.getStatus().equals(FlowStatus.SUCCESS)) { if (!context.getStatus().equals(FlowStatus.SUCCESS)) {
throw new AuthenticationFlowException("Expected success, but for an unknown reason the status was " + context.getStatus(), AuthenticationFlowError.INTERNAL_ERROR); throw new AuthenticationFlowException("Expected success, but for an unknown reason the status was " + context.getStatus(), AuthenticationFlowError.INTERNAL_ERROR);
} else {
success = true;
} }
logger.debugv("Client {0} authenticated by {1}", client.getClientId(), factory.getId()); logger.debugv("Client {0} authenticated by {1}", client.getClientId(), factory.getId());
@ -176,4 +180,9 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
return result.getChallenge(); return result.getChallenge();
} }
@Override
public boolean isSuccessful() {
return success;
}
} }

View file

@ -19,15 +19,28 @@ package org.keycloak.authentication;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.util.AuthenticationFlowHistoryHelper;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -35,19 +48,16 @@ import java.util.List;
*/ */
public class DefaultAuthenticationFlow implements AuthenticationFlow { public class DefaultAuthenticationFlow implements AuthenticationFlow {
private static final Logger logger = Logger.getLogger(DefaultAuthenticationFlow.class); private static final Logger logger = Logger.getLogger(DefaultAuthenticationFlow.class);
Response alternativeChallenge = null; private final List<AuthenticationExecutionModel> executions;
AuthenticationExecutionModel challengedAlternativeExecution = null; private final AuthenticationProcessor processor;
boolean alternativeSuccessful = false; private final AuthenticationFlowModel flow;
List<AuthenticationExecutionModel> executions; private boolean successful;
Iterator<AuthenticationExecutionModel> executionIterator; private List<AuthenticationFlowException> afeList = new ArrayList<>();
AuthenticationProcessor processor;
AuthenticationFlowModel flow;
public DefaultAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) { public DefaultAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) {
this.processor = processor; this.processor = processor;
this.flow = flow; this.flow = flow;
this.executions = processor.getRealm().getAuthenticationExecutions(flow.getId()); this.executions = processor.getRealm().getAuthenticationExecutions(flow.getId());
this.executionIterator = executions.iterator();
} }
protected boolean isProcessed(AuthenticationExecutionModel model) { protected boolean isProcessed(AuthenticationExecutionModel model) {
@ -63,7 +73,6 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
String display = processor.getAuthenticationSession().getAuthNote(OAuth2Constants.DISPLAY); String display = processor.getAuthenticationSession().getAuthNote(OAuth2Constants.DISPLAY);
if (display == null) return factory.create(processor.getSession()); if (display == null) return factory.create(processor.getSession());
if (factory instanceof DisplayTypeAuthenticatorFactory) { if (factory instanceof DisplayTypeAuthenticatorFactory) {
Authenticator authenticator = ((DisplayTypeAuthenticatorFactory) factory).createDisplay(processor.getSession(), display); Authenticator authenticator = ((DisplayTypeAuthenticatorFactory) factory).createDisplay(processor.getSession(), display);
if (authenticator != null) return authenticator; if (authenticator != null) return authenticator;
@ -73,167 +82,456 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
processor.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY); processor.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED,
ConsoleDisplayMode.browserContinue(processor.getSession(), processor.getRefreshUrl(true).toString())); ConsoleDisplayMode.browserContinue(processor.getSession(), processor.getRefreshUrl(true).toString()));
} else { } else {
return factory.create(processor.getSession()); return factory.create(processor.getSession());
} }
} }
@Override @Override
public Response processAction(String actionExecution) { public Response processAction(String actionExecution) {
logger.debugv("processAction: {0}", actionExecution); logger.debugv("processAction: {0}", actionExecution);
while (executionIterator.hasNext()) {
AuthenticationExecutionModel model = executionIterator.next(); if (actionExecution == null || actionExecution.isEmpty()) {
logger.debugv("check: {0} requirement: {1}", model.getAuthenticator(), model.getRequirement().toString()); throw new AuthenticationFlowException("action is not in current execution", AuthenticationFlowError.INTERNAL_ERROR);
if (isProcessed(model)) {
logger.debug("execution is processed");
if (!alternativeSuccessful && model.isAlternative() && processor.isSuccessful(model))
alternativeSuccessful = true;
continue;
} }
AuthenticationExecutionModel model = processor.getRealm().getAuthenticationExecutionById(actionExecution);
if (model == null) {
throw new AuthenticationFlowException("action is not in current execution", AuthenticationFlowError.INTERNAL_ERROR);
}
MultivaluedMap<String, String> inputData = processor.getRequest().getDecodedFormParameters();
String authExecId = inputData.getFirst(Constants.AUTHENTICATION_EXECUTION);
String selectedCredentialId = inputData.getFirst(Constants.CREDENTIAL_ID);
//check if the user has selected the "back" option
if (inputData.containsKey("back")) {
AuthenticationSessionModel authSession = processor.getAuthenticationSession();
AuthenticationFlowHistoryHelper history = new AuthenticationFlowHistoryHelper(processor);
if (history.hasAnyExecution()) {
String executionId = history.pullExecution();
AuthenticationExecutionModel lastActionExecution = processor.getRealm().getAuthenticationExecutionById(executionId);
logger.debugf("Moving back to authentication execution '%s'", lastActionExecution.getAuthenticator());
recursiveClearExecutionStatusOfAllExecutionsAfterOurExecutionInclusive(lastActionExecution);
Response response = processSingleFlowExecutionModel(lastActionExecution, null, false);
if (response == null) {
processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
return processFlow();
} else return response;
} else {
// This normally shouldn't happen as "back" button shouldn't be available on the form. If it is still triggered, we show "pageExpired" page
new AuthenticationFlowURLHelper(processor.getSession(), processor.getRealm(), processor.getUriInfo())
.showPageExpired(authSession);
}
}
// check if the user has switched to a new authentication execution, and if so switch to it.
if (authExecId != null && !authExecId.isEmpty()) {
List<AuthenticationSelectionOption> selectionOptions = createAuthenticationSelectionList(model);
// Check if switch to the requested authentication execution is allowed
selectionOptions.stream()
.filter(authSelectionOption -> authExecId.equals(authSelectionOption.getAuthExecId()))
.findFirst()
.orElseThrow(() -> new AuthenticationFlowException("Requested authentication execution is not allowed", AuthenticationFlowError.INTERNAL_ERROR)
);
model = processor.getRealm().getAuthenticationExecutionById(authExecId);
// In case that new execution is a flow, we will add the 1st item from the selection (preferred credential) to the history, so when later click "back", we will return to it.
if (model.isAuthenticatorFlow()) { if (model.isAuthenticatorFlow()) {
new AuthenticationFlowHistoryHelper(processor).pushExecution(selectionOptions.get(0).getAuthExecId());
}
Response response = processSingleFlowExecutionModel(model, selectedCredentialId, false);
if (response == null) {
processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
checkAndValidateParentFlow(model);
return processFlow();
} else return response;
}
//handle case where execution is a flow
if (model.isAuthenticatorFlow()) {
logger.debug("execution is flow");
AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model); AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
Response flowChallenge = null; Response flowChallenge = authenticationFlow.processAction(actionExecution);
try {
flowChallenge = authenticationFlow.processAction(actionExecution);
} catch (AuthenticationFlowException afe) {
if (model.isAlternative()) {
logger.debug("Thrown exception in alternative Subflow. Ignoring Subflow");
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
return processFlow();
} else {
throw afe;
}
}
if (flowChallenge == null) { if (flowChallenge == null) {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); checkAndValidateParentFlow(model);
if (model.isAlternative()) alternativeSuccessful = true;
return processFlow(); return processFlow();
} else { } else {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return flowChallenge; return flowChallenge;
} }
} else if (model.getId().equals(actionExecution)) {
AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator());
if (factory == null) {
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
} }
//handle normal execution case
AuthenticatorFactory factory = getAuthenticatorFactory(model);
Authenticator authenticator = createAuthenticator(factory); Authenticator authenticator = createAuthenticator(factory);
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions); AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
result.setAuthenticationSelections(createAuthenticationSelectionList(model));
result.setSelectedCredentialId(selectedCredentialId);
logger.debugv("action: {0}", model.getAuthenticator()); logger.debugv("action: {0}", model.getAuthenticator());
authenticator.action(result); authenticator.action(result);
Response response = processResult(result, true); Response response = processResult(result, true);
if (response == null) { if (response == null) {
processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
checkAndValidateParentFlow(model);
return processFlow(); return processFlow();
} else return response; } else return response;
} }
/**
* Clear execution status of targetExecution and also clear execution status of all the executions, which were triggered after this execution.
* This covers also "flow" executions and executions, which were set automatically
*
* @param targetExecution
*/
private void recursiveClearExecutionStatusOfAllExecutionsAfterOurExecutionInclusive(AuthenticationExecutionModel targetExecution) {
RealmModel realm = processor.getRealm();
AuthenticationSessionModel authSession = processor.getAuthenticationSession();
// Clear execution status of our execution
authSession.getExecutionStatus().remove(targetExecution.getId());
// Find all the "sibling" executions after target execution including target execution. For those, we can recursively remove execution status
recursiveClearExecutionStatusOfAllSiblings(targetExecution);
// Find the parent flow. If corresponding execution of this parent flow already has "executionStatus" set, we should clear it and also clear
// the status for all the siblings after that execution
while (true) {
AuthenticationFlowModel parentFlow = realm.getAuthenticationFlowById(targetExecution.getParentFlow());
if (parentFlow.isTopLevel()) {
return;
}
AuthenticationExecutionModel flowExecution = realm.getAuthenticationExecutionByFlowId(parentFlow.getId());
if (authSession.getExecutionStatus().containsKey(flowExecution.getId())) {
authSession.getExecutionStatus().remove(flowExecution.getId());
recursiveClearExecutionStatusOfAllSiblings(flowExecution);
targetExecution = flowExecution;
} else {
return;
}
}
}
/**
* Recursively removes the execution status of all "sibling" executions after targetExecution.
*
* @param targetExecution
*/
private void recursiveClearExecutionStatusOfAllSiblings(AuthenticationExecutionModel targetExecution) {
RealmModel realm = processor.getRealm();
AuthenticationFlowModel parentFlow = realm.getAuthenticationFlowById(targetExecution.getParentFlow());
logger.debugf("Recursively clearing executions in flow '%s', which are after execution '%s'", parentFlow.getAlias(), targetExecution.getId());
List<AuthenticationExecutionModel> siblingExecutions = realm.getAuthenticationExecutions(parentFlow.getId());
int index = siblingExecutions.indexOf(targetExecution);
siblingExecutions = siblingExecutions.subList(index + 1, siblingExecutions.size());
for (AuthenticationExecutionModel authExec : siblingExecutions) {
recursiveClearExecutionStatus(authExec);
}
}
/**
* Removes the execution status for an execution. If it is a flow, do the same for all sub-executions.
*
* @param execution the execution for which the status must be cleared
*/
private void recursiveClearExecutionStatus(AuthenticationExecutionModel execution) {
processor.getAuthenticationSession().getExecutionStatus().remove(execution.getId());
if (execution.isAuthenticatorFlow()) {
processor.getRealm().getAuthenticationExecutions(execution.getFlowId()).forEach(this::recursiveClearExecutionStatus);
}
}
/**
* This method makes sure that the parent flow's corresponding execution is considered successful if its contained
* executions are successful.
* The purpose is for when an execution is validated through an action, to make sure its parent flow can be successful
* when re-evaluation the flow tree.
*
* @param model An execution model.
*/
private void checkAndValidateParentFlow(AuthenticationExecutionModel model) {
List<AuthenticationExecutionModel> localExecutions = processor.getRealm().getAuthenticationExecutions(model.getParentFlow());
AuthenticationExecutionModel parentFlowModel = processor.getRealm().getAuthenticationExecutionByFlowId(model.getParentFlow());
if (parentFlowModel != null &&
((model.isRequired() && localExecutions.stream().allMatch(processor::isSuccessful)) ||
(model.isAlternative() && localExecutions.stream().anyMatch(processor::isSuccessful)))) {
processor.getAuthenticationSession().setExecutionStatus(parentFlowModel.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
} }
throw new AuthenticationFlowException("action is not in current execution", AuthenticationFlowError.INTERNAL_ERROR);
} }
@Override @Override
public Response processFlow() { public Response processFlow() {
logger.debug("processFlow"); logger.debug("processFlow");
while (executionIterator.hasNext()) {
AuthenticationExecutionModel model = executionIterator.next();
logger.debugv("check execution: {0} requirement: {1}", model.getAuthenticator(), model.getRequirement().toString());
if (isProcessed(model)) { //separate flow elements into required and alternative elements
logger.debug("execution is processed"); List<AuthenticationExecutionModel> requiredList = new ArrayList<>();
if (!alternativeSuccessful && model.isAlternative() && processor.isSuccessful(model)) List<AuthenticationExecutionModel> alternativeList = new ArrayList<>();
alternativeSuccessful = true;
for (AuthenticationExecutionModel execution : executions) {
if (isConditionalAuthenticator(execution)) {
continue;
} else if (execution.isRequired() || execution.isConditional()) {
requiredList.add(execution);
} else if (execution.isAlternative()) {
alternativeList.add(execution);
}
}
//handle required elements : all required elements need to be executed
boolean requiredElementsSuccessful = true;
Iterator<AuthenticationExecutionModel> requiredIListIterator = requiredList.listIterator();
while (requiredIListIterator.hasNext()) {
AuthenticationExecutionModel required = requiredIListIterator.next();
//Conditional flows must be considered disabled (non-existent) if their condition evaluates to false.
if (required.isConditional() && isConditionalSubflowDisabled(required)) {
requiredIListIterator.remove();
continue; continue;
} }
if (model.isAlternative() && alternativeSuccessful) { Response response = processSingleFlowExecutionModel(required, null, true);
logger.debug("Skip alternative execution"); requiredElementsSuccessful &= processor.isSuccessful(required) || isSetupRequired(required);
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); if (response != null) {
continue; return response;
}
} }
if (model.isAuthenticatorFlow()) {
logger.debug("execution is flow");
AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
Response flowChallenge = null; //Evaluate alternative elements only if there are no required elements. This may also occur if there was only condition elements
if (requiredList.isEmpty()) {
//check if an alternative is already successful, in case we are returning in the flow after an action
if (alternativeList.stream().anyMatch(alternative -> processor.isSuccessful(alternative) || isSetupRequired(alternative))) {
successful = true;
return null;
}
//handle alternative elements: the first alternative element to be satisfied is enough
for (AuthenticationExecutionModel alternative : alternativeList) {
try { try {
flowChallenge = authenticationFlow.processFlow(); Response response = processSingleFlowExecutionModel(alternative, null, true);
if (response != null) {
return response;
}
if (processor.isSuccessful(alternative) || isSetupRequired(alternative)) {
successful = true;
return null;
}
} catch (AuthenticationFlowException afe) { } catch (AuthenticationFlowException afe) {
if (model.isAlternative()) { //consuming the error is not good here from an administrative point of view, but the user, since he has alternatives, should be able to go to another alternative and continue
logger.debug("Thrown exception in alternative Subflow. Ignoring Subflow"); afeList.add(afe);
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED); processor.getAuthenticationSession().setExecutionStatus(alternative.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
continue;
} else {
throw afe;
} }
} }
} else {
successful = requiredElementsSuccessful;
}
return null;
}
if (flowChallenge == null) { /**
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); * Checks if the conditional subflow passed in parameter is disabled.
if (model.isAlternative()) alternativeSuccessful = true; * @param model
continue; * @return
} else { */
if (model.isAlternative()) { private boolean isConditionalSubflowDisabled(AuthenticationExecutionModel model) {
alternativeChallenge = flowChallenge; if (model == null || !model.isAuthenticatorFlow() || !model.isConditional()) {
challengedAlternativeExecution = model; return false;
} else if (model.isRequired()) { };
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); List<AuthenticationExecutionModel> modelList = processor.getRealm().getAuthenticationExecutions(model.getFlowId());
return flowChallenge; List<AuthenticationExecutionModel> conditionalAuthenticatorList = modelList.stream()
} else if (model.isOptional()) { .filter(this::isConditionalAuthenticator)
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); .collect(Collectors.toList());
continue; return conditionalAuthenticatorList.isEmpty() || conditionalAuthenticatorList.stream().anyMatch(m-> conditionalNotMatched(m, modelList));
} else {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue;
}
return flowChallenge;
}
} }
private boolean isConditionalAuthenticator(AuthenticationExecutionModel model) {
return !model.isAuthenticatorFlow() && model.getAuthenticator() != null && createAuthenticator(getAuthenticatorFactory(model)) instanceof ConditionalAuthenticator;
}
private AuthenticatorFactory getAuthenticatorFactory(AuthenticationExecutionModel model) {
AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator()); AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator());
if (factory == null) { if (factory == null) {
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?"); throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
} }
return factory;
}
private boolean conditionalNotMatched(AuthenticationExecutionModel model, List<AuthenticationExecutionModel> executionList) {
AuthenticatorFactory factory = getAuthenticatorFactory(model);
ConditionalAuthenticator authenticator = (ConditionalAuthenticator) createAuthenticator(factory);
AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executionList);
return !authenticator.matchCondition(context);
}
private boolean isSetupRequired(AuthenticationExecutionModel model) {
return AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED.equals(processor.getAuthenticationSession().getExecutionStatus().get(model.getId()));
}
private Response processSingleFlowExecutionModel(AuthenticationExecutionModel model, String selectedCredentialId, boolean calledFromFlow) {
logger.debugv("check execution: {0} requirement: {1}", model.getAuthenticator(), model.getRequirement());
if (isProcessed(model)) {
logger.debug("execution is processed");
return null;
}
//handle case where execution is a flow
if (model.isAuthenticatorFlow()) {
logger.debug("execution is flow");
AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
Response flowChallenge = authenticationFlow.processFlow();
if (flowChallenge == null) {
if (authenticationFlow.isSuccessful()) {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
} else {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.FAILED);
}
return null;
} else {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return flowChallenge;
}
}
//handle normal execution case
AuthenticatorFactory factory = getAuthenticatorFactory(model);
Authenticator authenticator = createAuthenticator(factory); Authenticator authenticator = createAuthenticator(factory);
logger.debugv("authenticator: {0}", factory.getId()); logger.debugv("authenticator: {0}", factory.getId());
UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser(); UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
if (authenticator.requiresUser() && authUser == null) { //If executions are alternative, get the actual execution to show based on user preference
if (alternativeChallenge != null) { List<AuthenticationSelectionOption> selectionOptions = createAuthenticationSelectionList(model);
processor.getAuthenticationSession().setExecutionStatus(challengedAlternativeExecution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); if (!selectionOptions.isEmpty() && calledFromFlow) {
return alternativeChallenge; List<AuthenticationSelectionOption> finalSelectionOptions = selectionOptions.stream().filter(aso -> !aso.getAuthenticationExecution().isAuthenticatorFlow() && !isProcessed(aso.getAuthenticationExecution())).collect(Collectors.toList());;
if (finalSelectionOptions.isEmpty()) {
//move to next
return null;
} }
model = finalSelectionOptions.get(0).getAuthenticationExecution();
factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator());
if (factory == null) {
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
}
authenticator = createAuthenticator(factory);
}
AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executions);
context.setAuthenticationSelections(selectionOptions);
if (selectedCredentialId != null) {
context.setSelectedCredentialId(selectedCredentialId);
} else if (!selectionOptions.isEmpty()) {
context.setSelectedCredentialId(selectionOptions.get(0).getCredentialId());
}
if (authenticator.requiresUser()) {
if (authUser == null) {
throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.UNKNOWN_USER); throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.UNKNOWN_USER);
} }
boolean configuredFor = false; if (!authenticator.configuredFor(processor.getSession(), processor.getRealm(), authUser)) {
if (authenticator.requiresUser() && authUser != null) { if (factory.isUserSetupAllowed() && model.isRequired() && authenticator.areRequiredActionsEnabled(processor.getSession(), processor.getRealm())) {
configuredFor = authenticator.configuredFor(processor.getSession(), processor.getRealm(), authUser); //This means that having even though the user didn't validate the
if (!configuredFor) {
if (model.isRequired()) {
if (factory.isUserSetupAllowed()) {
logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId()); logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId());
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED); processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED);
authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser()); authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser());
continue; return null;
} else { } else {
throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED); throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
}
} else if (model.isOptional()) {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue;
} }
} }
} }
// skip if action as successful already
// Response redirect = processor.checkWasSuccessfulBrowserAction();
// if (redirect != null) return redirect;
AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executions);
logger.debugv("invoke authenticator.authenticate: {0}", factory.getId()); logger.debugv("invoke authenticator.authenticate: {0}", factory.getId());
authenticator.authenticate(context); authenticator.authenticate(context);
Response response = processResult(context, false);
if (response != null) return response; return processResult(context, false);
} }
return null;
/**
* This method creates the list of authenticators that is presented to the user. For a required execution, this is
* only the credentials associated to the authenticator, and for an alternative execution, this is all other alternative
* executions in the flow, including the credentials.
* <p>
* In both cases, the credentials take precedence, with the order selected by the user (or his administrator).
*
* @param model The current execution model
* @return an ordered list of the authentication selection options to present the user.
*/
private List<AuthenticationSelectionOption> createAuthenticationSelectionList(AuthenticationExecutionModel model) {
List<AuthenticationSelectionOption> authenticationSelectionList = new ArrayList<>();
if (processor.getAuthenticationSession() != null) {
Map<String, AuthenticationExecutionModel> typeAuthExecMap = new HashMap<>();
List<AuthenticationExecutionModel> nonCredentialExecutions = new ArrayList<>();
if (model.isAlternative()) {
//get all alternative executions to be able to list their credentials
List<AuthenticationExecutionModel> alternativeExecutions = processor.getRealm().getAuthenticationExecutions(model.getParentFlow())
.stream().filter(AuthenticationExecutionModel::isAlternative).collect(Collectors.toList());
for (AuthenticationExecutionModel execution : alternativeExecutions) {
if (!execution.isAuthenticatorFlow()) {
Authenticator localAuthenticator = processor.getSession().getProvider(Authenticator.class, execution.getAuthenticator());
if (!(localAuthenticator instanceof CredentialValidator)) {
nonCredentialExecutions.add(execution);
continue;
}
CredentialValidator<?> cv = (CredentialValidator<?>) localAuthenticator;
typeAuthExecMap.put(cv.getType(processor.getSession()), execution);
} else {
nonCredentialExecutions.add(execution);
}
}
} else if (model.isRequired() && !model.isAuthenticatorFlow()) {
//only get current credentials
Authenticator authenticator = processor.getSession().getProvider(Authenticator.class, model.getAuthenticator());
if (authenticator instanceof CredentialValidator) {
typeAuthExecMap.put(((CredentialValidator<?>) authenticator).getType(processor.getSession()), model);
}
}
//add credential authenticators in order
if (processor.getAuthenticationSession().getAuthenticatedUser() != null) {
List<CredentialModel> credentials = processor.getSession().userCredentialManager()
.getStoredCredentials(processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser())
.stream()
.filter(credential -> typeAuthExecMap.containsKey(credential.getType()))
.collect(Collectors.toList());
MultivaluedMap<String, AuthenticationSelectionOption> countAuthSelections = new MultivaluedHashMap<>();
for (CredentialModel credential : credentials) {
AuthenticationSelectionOption authSel = new AuthenticationSelectionOption(typeAuthExecMap.get(credential.getType()), credential);
authenticationSelectionList.add(authSel);
countAuthSelections.add(credential.getType(), authSel);
}
for (Entry<String, List<AuthenticationSelectionOption>> entry : countAuthSelections.entrySet()) {
if (entry.getValue().size() == 1) {
entry.getValue().get(0).setShowCredentialName(false);
}
}
//don't show credential type if there's only a single type in the list
if (countAuthSelections.keySet().size() == 1 && nonCredentialExecutions.isEmpty()) {
for (AuthenticationSelectionOption so : authenticationSelectionList) {
so.setShowCredentialType(false);
}
}
}
//add all other authenticators (including flows)
for (AuthenticationExecutionModel exec : nonCredentialExecutions) {
if (exec.isAuthenticatorFlow()) {
authenticationSelectionList.add(new AuthenticationSelectionOption(exec,
processor.getRealm().getAuthenticationFlowById(exec.getFlowId())));
} else {
authenticationSelectionList.add(new AuthenticationSelectionOption(exec));
}
}
}
return authenticationSelectionList;
} }
@ -243,8 +541,11 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
switch (status) { switch (status) {
case SUCCESS: case SUCCESS:
logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator());
if (isAction) {
new AuthenticationFlowHistoryHelper(processor).pushExecution(execution.getId());
}
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
if (execution.isAlternative()) alternativeSuccessful = true;
return null; return null;
case FAILED: case FAILED:
logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator());
@ -259,26 +560,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId());
throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage());
case FORCE_CHALLENGE: case FORCE_CHALLENGE:
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
case CHALLENGE: case CHALLENGE:
logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator());
if (execution.isRequired()) {
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution); return sendChallenge(result, execution);
}
UserModel authenticatedUser = processor.getAuthenticationSession().getAuthenticatedUser();
if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), authenticatedUser)) {
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
}
if (execution.isAlternative()) {
alternativeChallenge = result.getChallenge();
challengedAlternativeExecution = execution;
} else {
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
}
return null;
case FAILURE_CHALLENGE: case FAILURE_CHALLENGE:
logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator());
processor.logFailure(); processor.logFailure();
@ -286,7 +570,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
return sendChallenge(result, execution); return sendChallenge(result, execution);
case ATTEMPTED: case ATTEMPTED:
logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator()); logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator());
if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { if (execution.isRequired()) {
throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS); throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS);
} }
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED); processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
@ -306,5 +590,13 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
return result.getChallenge(); return result.getChallenge();
} }
@Override
public boolean isSuccessful() {
return successful;
}
@Override
public List<AuthenticationFlowException> getFlowExceptions(){
return afeList;
}
} }

View file

@ -18,7 +18,6 @@
package org.keycloak.authentication; package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.LoginFormsProvider;
@ -203,7 +202,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
} else { } else {
throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED); throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
} }
} else if (formActionExecution.isOptional()) { } else if (formActionExecution.isConditional()) {
executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue; continue;
} }
@ -300,4 +299,9 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
FormContext context = new FormContextImpl(formExecution); FormContext context = new FormContextImpl(formExecution);
return formAuthenticator.render(context, form); return formAuthenticator.render(context, form);
} }
@Override
public boolean isSuccessful() {
return false;
}
} }

View file

@ -42,7 +42,7 @@ public class IdpAutoLinkAuthenticator extends AbstractIdpAuthenticator {
UserModel existingUser = getExistingUser(session, realm, authSession); UserModel existingUser = getExistingUser(session, realm, authSession);
logger.debugf("User '%s' will auto link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(), logger.debugf("User '%s' is set to authentication context when link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername()); brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
context.setUser(existingUser); context.setUser(existingUser);

View file

@ -68,10 +68,6 @@ public class IdpAutoLinkAuthenticatorFactory implements AuthenticatorFactory {
return false; return false;
} }
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override @Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
@ -80,12 +76,12 @@ public class IdpAutoLinkAuthenticatorFactory implements AuthenticatorFactory {
@Override @Override
public String getDisplayType() { public String getDisplayType() {
return "Automatically link brokered account"; return "Automatically set existing user";
} }
@Override @Override
public String getHelpText() { public String getHelpText() {
return "Automatically link brokered account without any verification"; return "Automatically set existing user to authentication context without any verification";
} }
@Override @Override

View file

@ -70,9 +70,6 @@ public class IdpConfirmLinkAuthenticatorFactory implements AuthenticatorFactory
return false; return false;
} }
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override @Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {

View file

@ -99,19 +99,20 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
// Set duplicated user, so next authenticators can deal with it // Set duplicated user, so next authenticators can deal with it
context.getAuthenticationSession().setAuthNote(EXISTING_USER_INFO, duplication.serialize()); context.getAuthenticationSession().setAuthNote(EXISTING_USER_INFO, duplication.serialize());
//Only show error message if the authenticator was required
if (context.getExecution().isRequired()) {
Response challengeResponse = context.form() Response challengeResponse = context.form()
.setError(Messages.FEDERATED_IDENTITY_EXISTS, duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue()) .setError(Messages.FEDERATED_IDENTITY_EXISTS, duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue())
.createErrorPage(Response.Status.CONFLICT); .createErrorPage(Response.Status.CONFLICT);
context.challenge(challengeResponse); context.challenge(challengeResponse);
if (context.getExecution().isRequired()) {
context.getEvent() context.getEvent()
.user(duplication.getExistingUserId()) .user(duplication.getExistingUserId())
.detail("existing_" + duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue()) .detail("existing_" + duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue())
.removeDetail(Details.AUTH_METHOD) .removeDetail(Details.AUTH_METHOD)
.removeDetail(Details.AUTH_TYPE) .removeDetail(Details.AUTH_TYPE)
.error(Errors.FEDERATED_IDENTITY_EXISTS); .error(Errors.FEDERATED_IDENTITY_EXISTS);
} else {
context.attempted();
} }
} }
} }

View file

@ -73,11 +73,6 @@ public class IdpCreateUserIfUniqueAuthenticatorFactory implements AuthenticatorF
return true; return true;
} }
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override @Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES; return REQUIREMENT_CHOICES;

View file

@ -70,10 +70,6 @@ public class IdpEmailVerificationAuthenticatorFactory implements AuthenticatorFa
return false; return false;
} }
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override @Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {

View file

@ -132,6 +132,10 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername()); logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
event.detail(Details.UPDATED_EMAIL, email); event.detail(Details.UPDATED_EMAIL, email);
// Ensure page is always shown when user later returns to it - for example with form "back" button
context.getAuthenticationSession().setAuthNote(ENFORCE_UPDATE_PROFILE, "true");
context.success(); context.success();
} }

View file

@ -75,10 +75,6 @@ public class IdpReviewProfileAuthenticatorFactory implements AuthenticatorFactor
return true; return true;
} }
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override @Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES; return REQUIREMENT_CHOICES;

View file

@ -43,7 +43,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm {
return setupForm(context, formData, existingUser) return setupForm(context, formData, existingUser)
.setStatus(Response.Status.OK) .setStatus(Response.Status.OK)
.createLogin(); .createLoginUsernamePassword();
} }
@Override @Override

View file

@ -21,7 +21,6 @@ import org.jboss.logging.Logger;
import org.keycloak.authentication.AbstractFormAuthenticator; import org.keycloak.authentication.AbstractFormAuthenticator;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.hash.PasswordHashProvider; import org.keycloak.credential.hash.PasswordHashProvider;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
@ -38,8 +37,6 @@ import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -58,14 +55,14 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
} }
protected Response challenge(AuthenticationFlowContext context, String error) { protected Response challenge(AuthenticationFlowContext context, String error) {
LoginFormsProvider form = context.form(); LoginFormsProvider form = context.form()
.setExecution(context.getExecution().getId());
if (error != null) form.setError(error); if (error != null) form.setError(error);
return createLoginForm(form); return createLoginForm(form);
} }
protected Response createLoginForm(LoginFormsProvider form) { protected Response createLoginForm(LoginFormsProvider form) {
return form.createLogin(); return form.createLoginUsernamePassword();
} }
protected String tempDisabledError() { protected String tempDisabledError() {
@ -75,7 +72,7 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) { protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
context.getEvent().error(eventError); context.getEvent().error(eventError);
Response challengeResponse = context.form() Response challengeResponse = context.form()
.setError(loginFormError).createLogin(); .setError(loginFormError).createLoginUsernamePassword();
context.failureChallenge(authenticatorError, challengeResponse); context.failureChallenge(authenticatorError, challengeResponse);
return challengeResponse; return challengeResponse;
} }
@ -103,15 +100,13 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
} }
public boolean invalidUser(AuthenticationFlowContext context, UserModel user) { public void testInvalidUser(AuthenticationFlowContext context, UserModel user) {
if (user == null) { if (user == null) {
dummyHash(context); dummyHash(context);
context.getEvent().error(Errors.USER_NOT_FOUND); context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = challenge(context, Messages.INVALID_USER); Response challengeResponse = challenge(context, Messages.INVALID_USER);
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse); context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
return true;
} }
return false;
} }
public boolean enabledUser(AuthenticationFlowContext context, UserModel user) { public boolean enabledUser(AuthenticationFlowContext context, UserModel user) {
@ -119,8 +114,6 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
context.getEvent().user(user); context.getEvent().user(user);
context.getEvent().error(Errors.USER_DISABLED); context.getEvent().error(Errors.USER_DISABLED);
Response challengeResponse = challenge(context, Messages.ACCOUNT_DISABLED); Response challengeResponse = challenge(context, Messages.ACCOUNT_DISABLED);
// this is not a failure so don't call failureChallenge.
//context.failureChallenge(AuthenticationFlowError.USER_DISABLED, challengeResponse);
context.forceChallenge(challengeResponse); context.forceChallenge(challengeResponse);
return false; return false;
} }
@ -128,13 +121,26 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
return true; return true;
} }
public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) { public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
context.clearUser();
UserModel user = getUser(context, inputData);
return user != null && validatePassword(context, user, inputData) && validateUser(context, user, inputData);
}
public boolean validateUser(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
context.clearUser();
UserModel user = getUser(context, inputData);
return user != null && validateUser(context, user, inputData);
}
private UserModel getUser(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME); String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) { if (username == null) {
context.getEvent().error(Errors.USER_NOT_FOUND); context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = challenge(context, Messages.INVALID_USER); Response challengeResponse = challenge(context, Messages.INVALID_USER);
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse); context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
return false; return null;
} }
// remove leading and trailing whitespace // remove leading and trailing whitespace
@ -155,22 +161,17 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
} else { } else {
setDuplicateUserChallenge(context, Errors.USERNAME_IN_USE, Messages.USERNAME_EXISTS, AuthenticationFlowError.INVALID_USER); setDuplicateUserChallenge(context, Errors.USERNAME_IN_USE, Messages.USERNAME_EXISTS, AuthenticationFlowError.INVALID_USER);
} }
return user;
return false;
} }
if (invalidUser(context, user)) { testInvalidUser(context, user);
return false; return user;
}
if (!validatePassword(context, user, inputData)) {
return false;
} }
private boolean validateUser(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData) {
if (!enabledUser(context, user)) { if (!enabledUser(context, user)) {
return false; return false;
} }
String rememberMe = inputData.getFirst("rememberMe"); String rememberMe = inputData.getFirst("rememberMe");
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
if (remember) { if (remember) {
@ -184,7 +185,10 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
} }
public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData) { public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData) {
List<CredentialInput> credentials = new LinkedList<>(); return validatePassword(context, user, inputData, true);
}
public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData, boolean clearUser) {
String password = inputData.getFirst(CredentialRepresentation.PASSWORD); String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
if (password == null || password.isEmpty()) { if (password == null || password.isEmpty()) {
context.getEvent().user(user); context.getEvent().user(user);
@ -197,27 +201,27 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
if (isTemporarilyDisabledByBruteForce(context, user)) return false; if (isTemporarilyDisabledByBruteForce(context, user)) return false;
credentials.add(UserCredentialModel.password(password)); if (password != null && !password.isEmpty() && context.getSession().userCredentialManager().isValid(context.getRealm(), user, UserCredentialModel.password(password))) {
if (context.getSession().userCredentialManager().isValid(context.getRealm(), user, credentials)) {
return true; return true;
} else { } else {
context.getEvent().user(user); context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = challenge(context, Messages.INVALID_USER); Response challengeResponse = challenge(context, Messages.INVALID_USER);
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
if (clearUser) {
context.clearUser(); context.clearUser();
}
return false; return false;
} }
} }
protected boolean isTemporarilyDisabledByBruteForce(AuthenticationFlowContext context, UserModel user) { protected boolean isTemporarilyDisabledByBruteForce(AuthenticationFlowContext context, UserModel user) {
if (context.getRealm().isBruteForceProtected()) { if (context.getRealm().isBruteForceProtected()) {
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) { if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) {
context.getEvent().user(user); context.getEvent().user(user);
context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED); context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
Response challengeResponse = challenge(context, tempDisabledError()); Response challengeResponse = challenge(context, tempDisabledError());
// this is not a failure so don't call failureChallenge.
//context.failureChallenge(AuthenticationFlowError.USER_TEMPORARILY_DISABLED, challengeResponse);
context.forceChallenge(challengeResponse); context.forceChallenge(challengeResponse);
return true; return true;
} }

View file

@ -23,7 +23,7 @@ import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import java.util.List; import java.util.List;
@ -52,11 +52,6 @@ public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFact
public static final ConditionalOtpFormAuthenticator SINGLETON = new ConditionalOtpFormAuthenticator(); public static final ConditionalOtpFormAuthenticator SINGLETON = new ConditionalOtpFormAuthenticator();
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.OPTIONAL,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override @Override
public Authenticator create(KeycloakSession session) { public Authenticator create(KeycloakSession session) {
return SINGLETON; return SINGLETON;
@ -84,7 +79,7 @@ public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFact
@Override @Override
public String getReferenceCategory() { public String getReferenceCategory() {
return UserCredentialModel.TOTP; return OTPCredentialModel.TYPE;
} }
@Override @Override

View file

@ -80,8 +80,6 @@ public class CookieAuthenticatorFactory implements AuthenticatorFactory, Display
return false; return false;
} }
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED};
@Override @Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES; return REQUIREMENT_CHOICES;

View file

@ -39,7 +39,7 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory { public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
protected static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { protected static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED
}; };
protected static final String DEFAULT_PROVIDER = "defaultProvider"; protected static final String DEFAULT_PROVIDER = "defaultProvider";

View file

@ -20,34 +20,43 @@ package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.CredentialValidator;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.requiredactions.UpdateTotp;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.OTPCredentialProvider;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator { public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator, CredentialValidator<OTPCredentialProvider> {
@Override @Override
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
validateOTP(context); validateOTP(context);
} }
@Override @Override
public void authenticate(AuthenticationFlowContext context) { public void authenticate(AuthenticationFlowContext context) {
Response challengeResponse = challenge(context, null); Response challengeResponse = challenge(context, null);
context.challenge(challengeResponse); context.challenge(challengeResponse);
} }
public void validateOTP(AuthenticationFlowContext context) { public void validateOTP(AuthenticationFlowContext context) {
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
if (inputData.containsKey("cancel")) { if (inputData.containsKey("cancel")) {
@ -55,20 +64,29 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
return; return;
} }
String otp = inputData.getFirst("otp");
String credentialId = context.getSelectedCredentialId();
//TODO this is lazy for when there is no clearly defined credentialId available (for example direct grant or console OTP), replace with getting the credential from the name
if (credentialId == null || credentialId.isEmpty()) {
credentialId = getCredentialProvider(context.getSession())
.getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId();
context.setSelectedCredentialId(credentialId);
}
UserModel userModel = context.getUser(); UserModel userModel = context.getUser();
if (!enabledUser(context, userModel)) { if (!enabledUser(context, userModel)) {
// error in context is set in enabledUser/isTemporarilyDisabledByBruteForce // error in context is set in enabledUser/isTemporarilyDisabledByBruteForce
return; return;
} }
String password = inputData.getFirst(CredentialRepresentation.TOTP); if (otp == null) {
if (password == null) {
Response challengeResponse = challenge(context,null); Response challengeResponse = challenge(context,null);
context.challenge(challengeResponse); context.challenge(challengeResponse);
return; return;
} }
boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), userModel, boolean valid = getCredentialProvider(context.getSession()).isValid(context.getRealm(),context.getUser(),
UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), password)); new UserCredentialModel(credentialId, getCredentialProvider(context.getSession()).getType(), otp));
if (!valid) { if (!valid) {
context.getEvent().user(userModel) context.getEvent().user(userModel)
.error(Errors.INVALID_USER_CREDENTIALS); .error(Errors.INVALID_USER_CREDENTIALS);
@ -96,7 +114,7 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
@Override @Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType()); return getCredentialProvider(session).isConfiguredFor(realm, user);
} }
@Override @Override
@ -104,11 +122,20 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
if (!user.getRequiredActions().contains(UserModel.RequiredAction.CONFIGURE_TOTP.name())) { if (!user.getRequiredActions().contains(UserModel.RequiredAction.CONFIGURE_TOTP.name())) {
user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP.name()); user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP.name());
} }
}
public List<RequiredActionFactory> getRequiredActions(KeycloakSession session) {
return Collections.singletonList((UpdateTotp)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, UserModel.RequiredAction.CONFIGURE_TOTP.name()));
} }
@Override @Override
public void close() { public void close() {
} }
@Override
public OTPCredentialProvider getCredentialProvider(KeycloakSession session) {
return (OTPCredentialProvider)session.getProvider(CredentialProvider.class, "keycloak-otp");
}
} }

View file

@ -26,7 +26,7 @@ import org.keycloak.authentication.authenticators.console.ConsoleOTPFormAuthenti
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import java.util.List; import java.util.List;
@ -74,7 +74,7 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory, Displa
@Override @Override
public String getReferenceCategory() { public String getReferenceCategory() {
return UserCredentialModel.TOTP; return OTPCredentialModel.TYPE;
} }
@Override @Override
@ -87,11 +87,6 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory, Displa
return true; return true;
} }
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.OPTIONAL,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override @Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES; return REQUIREMENT_CHOICES;

View file

@ -0,0 +1,56 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
public class PasswordForm extends UsernamePasswordForm {
protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
return validatePassword(context, context.getUser(), formData, false);
}
@Override
public void authenticate(AuthenticationFlowContext context) {
Response challengeResponse = context.form().createLoginPassword();
context.challenge(challengeResponse);
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
// never called
return getCredentialProvider(session).isConfiguredFor(realm, user, getType(session));
}
@Override
public boolean requiresUser() {
return true;
}
@Override
protected Response createLoginForm(LoginFormsProvider form) {
return form.createLoginPassword();
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
import org.keycloak.authentication.authenticators.console.ConsolePasswordAuthenticator;
import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PasswordFormFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-password-form";
public static final PasswordForm SINGLETON = new PasswordForm();
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public Authenticator createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return SINGLETON;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsolePasswordAuthenticator.SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return PasswordCredentialModel.TYPE;
}
@Override
public boolean isConfigurable() {
return false;
}
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Password Form";
}
@Override
public String getHelpText() {
return "Validates a password from login form.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}

View file

@ -51,7 +51,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En
static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.OPTIONAL, AuthenticationExecutionModel.Requirement.ALTERNATIVE,
AuthenticationExecutionModel.Requirement.DISABLED}; AuthenticationExecutionModel.Requirement.DISABLED};
static final ScriptBasedAuthenticator SINGLETON = new ScriptBasedAuthenticator(); static final ScriptBasedAuthenticator SINGLETON = new ScriptBasedAuthenticator();

Some files were not shown because too many files have changed in this diff Show more