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:
parent
e7e49c13d5
commit
4553234f64
292 changed files with 9505 additions and 4154 deletions
|
@ -25,14 +25,38 @@ package org.keycloak.common.util;
|
|||
public class Base64Url {
|
||||
public static String encode(byte[] 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('/', '_'); // 63rd char of encoding
|
||||
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
|
||||
switch (s.length() % 4) // Pad with trailing '='s
|
||||
{
|
||||
|
@ -48,12 +72,8 @@ public class Base64Url {
|
|||
throw new RuntimeException(
|
||||
"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 Base64.decode(s, Base64.DONT_GUNZIP);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -28,150 +28,164 @@ public class CredentialRepresentation {
|
|||
public static final String PASSWORD = "password";
|
||||
public static final String TOTP = "totp";
|
||||
public static final String HOTP = "hotp";
|
||||
public static final String CLIENT_CERT = "cert";
|
||||
public static final String KERBEROS = "kerberos";
|
||||
|
||||
protected String type;
|
||||
protected String device;
|
||||
|
||||
// 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 String id;
|
||||
private String type;
|
||||
private String userLabel;
|
||||
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
|
||||
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() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
public String getUserLabel() {
|
||||
return userLabel;
|
||||
}
|
||||
public void setUserLabel(String userLabel) {
|
||||
this.userLabel = userLabel;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
public String getSecretData() {
|
||||
return secretData;
|
||||
}
|
||||
public void setSecretData(String secretData) {
|
||||
this.secretData = secretData;
|
||||
}
|
||||
|
||||
public String getDevice() {
|
||||
return device;
|
||||
public String getCredentialData() {
|
||||
return credentialData;
|
||||
}
|
||||
public void setCredentialData(String credentialData) {
|
||||
this.credentialData = credentialData;
|
||||
}
|
||||
|
||||
public void setDevice(String device) {
|
||||
this.device = device;
|
||||
public Integer getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public String getHashedSaltedValue() {
|
||||
return hashedSaltedValue;
|
||||
}
|
||||
|
||||
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 void setPriority(Integer priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public Long getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
public void setCreatedDate(Long 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) {
|
||||
this.config = config;
|
||||
public Boolean isTemporary() {
|
||||
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
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
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 + ((device == null) ? 0 : device.hashCode());
|
||||
result = prime * result + ((digits == null) ? 0 : digits.hashCode());
|
||||
result = prime * result + ((hashIterations == null) ? 0 : hashIterations.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 + ((userLabel == null) ? 0 : userLabel.hashCode());
|
||||
result = prime * result + ((secretData == null) ? 0 : secretData.hashCode());
|
||||
result = prime * result + ((credentialData == null) ? 0 : credentialData.hashCode());
|
||||
result = prime * result + ((temporary == null) ? 0 : temporary.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 + ((priority == null) ? 0 : priority);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -184,55 +198,25 @@ public class CredentialRepresentation {
|
|||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
CredentialRepresentation other = (CredentialRepresentation) obj;
|
||||
if (algorithm == null) {
|
||||
if (other.algorithm != null)
|
||||
if (secretData == null) {
|
||||
if (other.secretData != null)
|
||||
return false;
|
||||
} else if (!algorithm.equals(other.algorithm))
|
||||
} else if (!secretData.equals(other.secretData))
|
||||
return false;
|
||||
if (config == null) {
|
||||
if (other.config != null)
|
||||
if (credentialData == null) {
|
||||
if (other.credentialData != null)
|
||||
return false;
|
||||
} else if (!config.equals(other.config))
|
||||
return false;
|
||||
if (counter == null) {
|
||||
if (other.counter != null)
|
||||
return false;
|
||||
} else if (!counter.equals(other.counter))
|
||||
} else if (!credentialData.equals(other.credentialData))
|
||||
return false;
|
||||
if (createdDate == null) {
|
||||
if (other.createdDate != null)
|
||||
return false;
|
||||
} else if (!createdDate.equals(other.createdDate))
|
||||
return false;
|
||||
if (device == null) {
|
||||
if (other.device != null)
|
||||
if (userLabel == null) {
|
||||
if (other.userLabel != null)
|
||||
return false;
|
||||
} else if (!device.equals(other.device))
|
||||
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))
|
||||
} else if (!userLabel.equals(other.userLabel))
|
||||
return false;
|
||||
if (temporary == null) {
|
||||
if (other.temporary != null)
|
||||
|
@ -244,11 +228,23 @@ public class CredentialRepresentation {
|
|||
return false;
|
||||
} else if (!type.equals(other.type))
|
||||
return false;
|
||||
if (id == null) {
|
||||
if (other.id != null)
|
||||
return false;
|
||||
} else if (!id.equals(other.id))
|
||||
return false;
|
||||
if (value == null) {
|
||||
if (other.value != null)
|
||||
return false;
|
||||
} else if (!value.equals(other.value))
|
||||
return false;
|
||||
if (priority == null) {
|
||||
if (other.priority != null)
|
||||
return false;
|
||||
} else if (!priority.equals(other.priority))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -30,5 +30,9 @@
|
|||
<module name="org.apache.httpcomponents"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
|
||||
<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>
|
||||
</module>
|
||||
|
|
|
@ -17,7 +17,7 @@ Example Custom Authenticator
|
|||
|
||||
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.
|
||||
Your new required action should now be displayed and enabled in the required actions list.
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<#import "select.ftl" as layout>
|
||||
<@layout.registrationLayout; section>
|
||||
<#if section = "title">
|
||||
${msg("loginTitle",realm.name)}
|
||||
|
@ -24,8 +24,9 @@
|
|||
|
||||
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
|
||||
<div class="${properties.kcFormButtonsWrapperClass!}">
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doCancel")}"/>
|
||||
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
|
||||
name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,9 @@ import org.jboss.resteasy.spi.HttpResponse;
|
|||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.CredentialValidator;
|
||||
import org.keycloak.common.util.ServerCookie;
|
||||
import org.keycloak.credential.CredentialProvider;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -38,9 +40,7 @@ import java.net.URI;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SecretQuestionAuthenticator implements Authenticator {
|
||||
|
||||
public static final String CREDENTIAL_TYPE = "secret_question";
|
||||
public class SecretQuestionAuthenticator implements Authenticator, CredentialValidator<SecretQuestionCredentialProvider> {
|
||||
|
||||
protected boolean hasCookie(AuthenticationFlowContext context) {
|
||||
Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
|
||||
|
@ -57,17 +57,13 @@ public class SecretQuestionAuthenticator implements Authenticator {
|
|||
context.success();
|
||||
return;
|
||||
}
|
||||
Response challenge = context.form().createForm("secret-question.ftl");
|
||||
Response challenge = context.form()
|
||||
.createForm("secret-question.ftl");
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
if (formData.containsKey("cancel")) {
|
||||
context.cancelLogin();
|
||||
return;
|
||||
}
|
||||
boolean validated = validateAnswer(context);
|
||||
if (!validated) {
|
||||
Response challenge = context.form()
|
||||
|
@ -107,10 +103,15 @@ public class SecretQuestionAuthenticator implements Authenticator {
|
|||
protected boolean validateAnswer(AuthenticationFlowContext context) {
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
String secret = formData.getFirst("secret_answer");
|
||||
UserCredentialModel input = new UserCredentialModel();
|
||||
input.setType(SecretQuestionCredentialProvider.SECRET_QUESTION);
|
||||
input.setValue(secret);
|
||||
return context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), input);
|
||||
String credentialId = context.getSelectedCredentialId();
|
||||
if (credentialId == null || credentialId.isEmpty()) {
|
||||
credentialId = getCredentialProvider(context.getSession())
|
||||
.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
|
||||
|
@ -120,7 +121,7 @@ public class SecretQuestionAuthenticator implements Authenticator {
|
|||
|
||||
@Override
|
||||
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
|
||||
|
@ -128,8 +129,17 @@ public class SecretQuestionAuthenticator implements Authenticator {
|
|||
user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID);
|
||||
}
|
||||
|
||||
public List<RequiredActionFactory> getRequiredActions(KeycloakSession session) {
|
||||
return Collections.singletonList((SecretQuestionRequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, SecretQuestionRequiredAction.PROVIDER_ID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretQuestionCredentialProvider getCredentialProvider(KeycloakSession session) {
|
||||
return (SecretQuestionCredentialProvider)session.getProvider(CredentialProvider.class, SecretQuestionCredentialProviderFactory.PROVIDER_ID);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
|
|||
|
||||
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED
|
||||
};
|
||||
@Override
|
||||
|
|
|
@ -16,31 +16,25 @@
|
|||
*/
|
||||
package org.keycloak.examples.authenticator;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.credential.CredentialInput;
|
||||
import org.keycloak.credential.CredentialInputUpdater;
|
||||
import org.keycloak.credential.CredentialInputValidator;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
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.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
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>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SecretQuestionCredentialProvider implements CredentialProvider, CredentialInputValidator, CredentialInputUpdater, OnUserCache {
|
||||
public static final String SECRET_QUESTION = "SECRET_QUESTION";
|
||||
public static final String CACHE_KEY = SecretQuestionCredentialProvider.class.getName() + "." + SECRET_QUESTION;
|
||||
public class SecretQuestionCredentialProvider implements CredentialProvider<SecretQuestionCredentialModel>, CredentialInputValidator {
|
||||
private static final Logger logger = Logger.getLogger(SecretQuestionCredentialProvider.class);
|
||||
|
||||
protected KeycloakSession session;
|
||||
|
||||
|
@ -48,87 +42,60 @@ public class SecretQuestionCredentialProvider implements CredentialProvider, Cre
|
|||
this.session = session;
|
||||
}
|
||||
|
||||
public CredentialModel getSecret(RealmModel realm, UserModel user) {
|
||||
CredentialModel secret = null;
|
||||
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;
|
||||
private UserCredentialStore getCredentialStore() {
|
||||
return session.userCredentialManager();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (!SECRET_QUESTION.equals(input.getType())) return false;
|
||||
if (!(input instanceof UserCredentialModel)) return false;
|
||||
UserCredentialModel credInput = (UserCredentialModel) input;
|
||||
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));
|
||||
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (!(input instanceof UserCredentialModel)) {
|
||||
logger.debug("Expected instance of UserCredentialModel for CredentialInput");
|
||||
return false;
|
||||
}
|
||||
session.userCache().evict(realm, user);
|
||||
return true;
|
||||
if (!input.getType().equals(getType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
|
||||
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());
|
||||
String challengeResponse = input.getChallengeResponse();
|
||||
if (challengeResponse == null) {
|
||||
return false;
|
||||
}
|
||||
session.userCache().evict(realm, user);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
CredentialModel credentialModel = getCredentialStore().getStoredCredentialById(realm, user, input.getCredentialId());
|
||||
SecretQuestionCredentialModel sqcm = getCredentialFromModel(credentialModel);
|
||||
return sqcm.getSecretQuestionSecretData().getAnswer().equals(challengeResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCredentialType(String credentialType) {
|
||||
return SECRET_QUESTION.equals(credentialType);
|
||||
return getType().equals(credentialType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
|
||||
if (!SECRET_QUESTION.equals(credentialType)) return false;
|
||||
return getSecret(realm, user) != null;
|
||||
if (!supportsCredentialType(credentialType)) return false;
|
||||
return !getCredentialStore().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (!SECRET_QUESTION.equals(input.getType())) return false;
|
||||
if (!(input instanceof UserCredentialModel)) return false;
|
||||
|
||||
String secret = getSecret(realm, user).getValue();
|
||||
|
||||
return secret != null && ((UserCredentialModel)input).getValue().equals(secret);
|
||||
public CredentialModel createCredential(RealmModel realm, UserModel user, SecretQuestionCredentialModel credentialModel) {
|
||||
if (credentialModel.getCreatedDate() == null) {
|
||||
credentialModel.setCreatedDate(Time.currentTimeMillis());
|
||||
}
|
||||
return getCredentialStore().createCredential(realm, user, credentialModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
|
||||
List<CredentialModel> creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION);
|
||||
if (!creds.isEmpty()) user.getCachedWith().put(CACHE_KEY, creds.get(0));
|
||||
public void deleteCredential(RealmModel realm, UserModel user, String credentialId) {
|
||||
getCredentialStore().removeStoredCredential(realm, user, credentialId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretQuestionCredentialModel getCredentialFromModel(CredentialModel model) {
|
||||
return SecretQuestionCredentialModel.createFromCredentialModel(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return SecretQuestionCredentialModel.TYPE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,9 +25,12 @@ import org.keycloak.models.KeycloakSession;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SecretQuestionCredentialProviderFactory implements CredentialProviderFactory<SecretQuestionCredentialProvider> {
|
||||
|
||||
public static final String PROVIDER_ID = "secret-question";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "secret-question";
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
|
||||
package org.keycloak.examples.authenticator;
|
||||
|
||||
import org.keycloak.authentication.CredentialRegistrator;
|
||||
import org.keycloak.authentication.RequiredActionContext;
|
||||
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;
|
||||
|
||||
|
@ -27,7 +29,7 @@ import javax.ws.rs.core.Response;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SecretQuestionRequiredAction implements RequiredActionProvider {
|
||||
public class SecretQuestionRequiredAction implements RequiredActionProvider, CredentialRegistrator {
|
||||
public static final String PROVIDER_ID = "secret_question_config";
|
||||
|
||||
@Override
|
||||
|
@ -45,10 +47,8 @@ public class SecretQuestionRequiredAction implements RequiredActionProvider {
|
|||
@Override
|
||||
public void processAction(RequiredActionContext context) {
|
||||
String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer"));
|
||||
UserCredentialModel input = new UserCredentialModel();
|
||||
input.setType(SecretQuestionCredentialProvider.SECRET_QUESTION);
|
||||
input.setValue(answer);
|
||||
context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), input);
|
||||
SecretQuestionCredentialProvider sqcp = (SecretQuestionCredentialProvider) context.getSession().getProvider(CredentialProvider.class, "secret-question");
|
||||
sqcp.createCredential(context.getRealm(), context.getUser(), SecretQuestionCredentialModel.createSecretQuestion("What is your mom's first name?", answer));
|
||||
context.success();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserManager;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
|
@ -132,7 +133,7 @@ public class KerberosFederationProvider implements UserStorageProvider,
|
|||
|
||||
@Override
|
||||
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) {
|
||||
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
|
||||
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
|
||||
public boolean supportsCredentialAuthenticationFor(String type) {
|
||||
return CredentialModel.KERBEROS.equals(type);
|
||||
return UserCredentialModel.KERBEROS.equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -167,8 +168,8 @@ public class KerberosFederationProvider implements UserStorageProvider,
|
|||
@Override
|
||||
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (!(input instanceof UserCredentialModel)) return false;
|
||||
if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) {
|
||||
return validPassword(user.getUsername(), ((UserCredentialModel)input).getValue());
|
||||
if (input.getType().equals(PasswordCredentialModel.TYPE) && !session.userCredentialManager().isConfiguredLocally(realm, user, PasswordCredentialModel.TYPE)) {
|
||||
return validPassword(user.getUsername(), input.getChallengeResponse());
|
||||
} else {
|
||||
return false; // invalid cred type
|
||||
}
|
||||
|
@ -188,7 +189,7 @@ public class KerberosFederationProvider implements UserStorageProvider,
|
|||
if (!(input instanceof UserCredentialModel)) return null;
|
||||
UserCredentialModel credential = (UserCredentialModel)input;
|
||||
if (credential.getType().equals(UserCredentialModel.KERBEROS)) {
|
||||
String spnegoToken = credential.getValue();
|
||||
String spnegoToken = credential.getChallengeResponse();
|
||||
SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig);
|
||||
|
||||
spnegoAuthenticator.authenticate();
|
||||
|
|
|
@ -40,14 +40,12 @@ import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticat
|
|||
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.cache.CachedUserModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.models.utils.DefaultRoles;
|
||||
import org.keycloak.models.utils.ReadOnlyUserModelDelegate;
|
||||
import org.keycloak.policy.PasswordPolicyManagerProvider;
|
||||
import org.keycloak.policy.PolicyError;
|
||||
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.StorageId;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
|
@ -110,7 +108,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
this.mapperManager = new LDAPStorageMapperManager(this);
|
||||
this.userManager = new LDAPStorageUserManager(this);
|
||||
|
||||
supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
|
||||
supportedCredentialTypes.add(PasswordCredentialModel.TYPE);
|
||||
if (kerberosConfig.isAllowKerberosAuthentication()) {
|
||||
supportedCredentialTypes.add(UserCredentialModel.KERBEROS);
|
||||
}
|
||||
|
@ -218,7 +216,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
|
||||
@Override
|
||||
public boolean supportsCredentialAuthenticationFor(String type) {
|
||||
return type.equals(CredentialModel.KERBEROS) && kerberosConfig.isAllowKerberosAuthentication();
|
||||
return type.equals(UserCredentialModel.KERBEROS) && kerberosConfig.isAllowKerberosAuthentication();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -613,14 +611,13 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
|
||||
@Override
|
||||
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) {
|
||||
throw new ReadOnlyException("Federated storage is not writable");
|
||||
|
||||
} else if (editMode == UserStorageProvider.EditMode.WRITABLE) {
|
||||
LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore();
|
||||
PasswordUserCredentialModel cred = (PasswordUserCredentialModel)input;
|
||||
String password = cred.getValue();
|
||||
String password = input.getChallengeResponse();
|
||||
LDAPObject ldapUser = loadAndValidateUser(realm, user);
|
||||
if (ldapIdentityStore.getConfig().isValidatePasswordPolicy()) {
|
||||
PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm, user, password);
|
||||
|
@ -629,16 +626,16 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
try {
|
||||
LDAPOperationDecorator operationDecorator = null;
|
||||
if (updater != null) {
|
||||
operationDecorator = updater.beforePasswordUpdate(user, ldapUser, cred);
|
||||
operationDecorator = updater.beforePasswordUpdate(user, ldapUser, (UserCredentialModel)input);
|
||||
}
|
||||
|
||||
ldapIdentityStore.updatePassword(ldapUser, password, operationDecorator);
|
||||
|
||||
if (updater != null) updater.passwordUpdated(user, ldapUser, cred);
|
||||
if (updater != null) updater.passwordUpdated(user, ldapUser, (UserCredentialModel)input);
|
||||
return true;
|
||||
} catch (ModelException me) {
|
||||
if (updater != null) {
|
||||
updater.passwordUpdateFailed(user, ldapUser, cred, me);
|
||||
updater.passwordUpdateFailed(user, ldapUser, (UserCredentialModel)input, me);
|
||||
return false;
|
||||
} else {
|
||||
throw me;
|
||||
|
@ -678,8 +675,8 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
@Override
|
||||
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (!(input instanceof UserCredentialModel)) return false;
|
||||
if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) {
|
||||
return validPassword(realm, user, ((UserCredentialModel)input).getValue());
|
||||
if (input.getType().equals(PasswordCredentialModel.TYPE) && !session.userCredentialManager().isConfiguredLocally(realm, user, PasswordCredentialModel.TYPE)) {
|
||||
return validPassword(realm, user, input.getChallengeResponse());
|
||||
} else {
|
||||
return false; // invalid cred type
|
||||
}
|
||||
|
@ -691,7 +688,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
|
|||
UserCredentialModel credential = (UserCredentialModel)cred;
|
||||
if (credential.getType().equals(UserCredentialModel.KERBEROS)) {
|
||||
if (kerberosConfig.isAllowKerberosAuthentication()) {
|
||||
String spnegoToken = credential.getValue();
|
||||
String spnegoToken = credential.getChallengeResponse();
|
||||
SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig);
|
||||
|
||||
spnegoAuthenticator.authenticate();
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
package org.keycloak.storage.ldap.mappers;
|
||||
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
|
||||
/**
|
||||
|
@ -27,9 +27,9 @@ import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
|||
*/
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -19,13 +19,11 @@ package org.keycloak.storage.ldap.mappers.msad;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.credential.CredentialInput;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
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.ldap.LDAPStorageProvider;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
|
@ -75,7 +73,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
|
|||
}
|
||||
|
||||
@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)
|
||||
if (password.isAdminRequest()) {
|
||||
return null;
|
||||
|
@ -86,7 +84,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
|
|||
}
|
||||
|
||||
@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());
|
||||
|
||||
// Normally it's read-only
|
||||
|
@ -106,7 +104,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
|
|||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,11 @@ package org.keycloak.storage.ldap.mappers.msadlds;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.credential.CredentialInput;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
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.ldap.LDAPStorageProvider;
|
||||
|
@ -73,12 +72,12 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
|
|||
}
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
@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());
|
||||
|
||||
// Normally it's read-only
|
||||
|
@ -96,7 +95,7 @@ public class MSADLDSUserAccountControlStorageMapper extends AbstractLDAPStorageM
|
|||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
|
|
|
@ -26,11 +26,13 @@ import org.keycloak.federation.sssd.api.Sssd;
|
|||
import org.keycloak.federation.sssd.api.Sssd.User;
|
||||
import org.keycloak.federation.sssd.impl.PAMAuthenticator;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.user.ImportedUserValidation;
|
||||
import org.keycloak.storage.user.UserLookupProvider;
|
||||
import sun.security.util.Password;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
@ -63,7 +65,7 @@ public class SSSDFederationProvider implements UserStorageProvider,
|
|||
}
|
||||
|
||||
static {
|
||||
supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
|
||||
supportedCredentialTypes.add(PasswordCredentialModel.TYPE);
|
||||
}
|
||||
|
||||
|
||||
|
@ -163,12 +165,12 @@ public class SSSDFederationProvider implements UserStorageProvider,
|
|||
|
||||
@Override
|
||||
public boolean supportsCredentialType(String credentialType) {
|
||||
return CredentialModel.PASSWORD.equals(credentialType);
|
||||
return PasswordCredentialModel.TYPE.equals(credentialType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
|
||||
return CredentialModel.PASSWORD.equals(credentialType);
|
||||
return PasswordCredentialModel.TYPE.equals(credentialType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -176,7 +178,7 @@ public class SSSDFederationProvider implements UserStorageProvider,
|
|||
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
|
||||
|
||||
UserCredentialModel cred = (UserCredentialModel)input;
|
||||
PAMAuthenticator pam = factory.createPAMAuthenticator(user.getUsername(), cred.getValue());
|
||||
PAMAuthenticator pam = factory.createPAMAuthenticator(user.getUsername(), cred.getChallengeResponse());
|
||||
return (pam.authenticate() != null);
|
||||
}
|
||||
|
||||
|
|
|
@ -83,20 +83,59 @@ public interface UserResource {
|
|||
@Path("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
|
||||
@Path("remove-totp")
|
||||
public void removeTotp();
|
||||
@Consumes(javax.ws.rs.core.MediaType.TEXT_PLAIN)
|
||||
@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.
|
||||
* Type examples "otp", "password"
|
||||
*
|
||||
* This endpoint is deprecated as it is not supported to disable credentials, just delete them
|
||||
*
|
||||
* @param credentialTypes
|
||||
*/
|
||||
@Path("disable-credential-types")
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Deprecated
|
||||
public void disableCredentialType(List<String> credentialTypes);
|
||||
|
||||
@PUT
|
||||
|
|
|
@ -1213,6 +1213,11 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
return cached.getExecutionsById().get(id);
|
||||
}
|
||||
|
||||
public AuthenticationExecutionModel getAuthenticationExecutionByFlowId(String flowId) {
|
||||
getDelegateForUpdate();
|
||||
return updated.getAuthenticationExecutionByFlowId(flowId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model) {
|
||||
getDelegateForUpdate();
|
||||
|
|
|
@ -137,7 +137,8 @@ public class ResourceAdapter extends AbstractAuthorizationModel implements Resou
|
|||
|
||||
@Override
|
||||
public ResourceServer getResourceServer() {
|
||||
return storeFactory.getResourceServerStore().findById(entity.getResourceServer().getId());
|
||||
ResourceServer temp = storeFactory.getResourceServerStore().findById(entity.getResourceServer().getId());
|
||||
return temp;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -16,23 +16,24 @@
|
|||
*/
|
||||
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.UserCredentialStore;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
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.UserEntity;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.TypedQuery;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.persistence.LockModeType;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -40,6 +41,11 @@ import javax.persistence.LockModeType;
|
|||
*/
|
||||
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;
|
||||
protected final EntityManager em;
|
||||
|
||||
|
@ -52,99 +58,23 @@ public class JpaUserCredentialStore implements UserCredentialStore {
|
|||
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
|
||||
CredentialEntity entity = em.find(CredentialEntity.class, cred.getId());
|
||||
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.setUserLabel(cred.getUserLabel());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
entity.setSecretData(cred.getSecretData());
|
||||
entity.setCredentialData(cred.getCredentialData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) {
|
||||
CredentialEntity entity = new CredentialEntity();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
CredentialEntity entity = createCredentialEntity(realm, user, cred);
|
||||
return toModel(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) {
|
||||
CredentialEntity entity = em.find(CredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
|
||||
if (entity == null) return false;
|
||||
em.remove(entity);
|
||||
return true;
|
||||
CredentialEntity entity = removeCredentialEntity(realm, user, id);
|
||||
return entity != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -155,67 +85,152 @@ public class JpaUserCredentialStore implements UserCredentialStore {
|
|||
return model;
|
||||
}
|
||||
|
||||
protected CredentialModel toModel(CredentialEntity entity) {
|
||||
CredentialModel toModel(CredentialEntity entity) {
|
||||
CredentialModel model = new CredentialModel();
|
||||
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());
|
||||
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||
model.setConfig(config);
|
||||
for (CredentialAttributeEntity attr : entity.getCredentialAttributes()) {
|
||||
config.add(attr.getName(), attr.getValue());
|
||||
model.setUserLabel(entity.getUserLabel());
|
||||
|
||||
// Backwards compatibility - users from previous version still have "salt" in the DB filled.
|
||||
// We migrate it to new secretData format on-the-fly
|
||||
if (entity.getSalt() != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
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());
|
||||
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;
|
||||
return query.getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CredentialModel> getStoredCredentialsByType(RealmModel realm, UserModel user, String type) {
|
||||
UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
|
||||
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;
|
||||
return getStoredCredentials(realm, user).stream().filter(credential -> type.equals(credential.getType())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) {
|
||||
UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
|
||||
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByNameAndType", CredentialEntity.class)
|
||||
.setParameter("type", type)
|
||||
.setParameter("device", name)
|
||||
.setParameter("user", userEntity);
|
||||
List<CredentialEntity> results = query.getResultList();
|
||||
List<CredentialModel> results = getStoredCredentials(realm, user).stream().filter(credential ->
|
||||
type.equals(credential.getType()) && name.equals(credential.getUserLabel())).collect(Collectors.toList());
|
||||
if (results.isEmpty()) return null;
|
||||
return toModel(results.get(0));
|
||||
return results.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
package org.keycloak.models.jpa;
|
||||
|
||||
import org.keycloak.authorization.jpa.entities.ResourceEntity;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
|
@ -37,7 +36,6 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.UserConsentModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
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.FederatedIdentityEntity;
|
||||
import org.keycloak.models.jpa.entities.UserConsentClientScopeEntity;
|
||||
|
@ -60,7 +58,6 @@ import javax.persistence.criteria.Subquery;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -68,12 +65,12 @@ import java.util.Set;
|
|||
import java.util.stream.Collectors;
|
||||
import javax.persistence.LockModeType;
|
||||
import javax.persistence.criteria.Expression;
|
||||
import javax.persistence.criteria.Path;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
@SuppressWarnings("JpaQueryApiInspection")
|
||||
public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
||||
|
||||
private static final String EMAIL = "email";
|
||||
|
@ -83,10 +80,12 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
|
||||
private final KeycloakSession session;
|
||||
protected EntityManager em;
|
||||
private final JpaUserCredentialStore credentialStore;
|
||||
|
||||
public JpaUserProvider(KeycloakSession session, EntityManager em) {
|
||||
this.session = session;
|
||||
this.em = em;
|
||||
credentialStore = new JpaUserCredentialStore(session, em);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -382,8 +381,6 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
num = em.createNamedQuery("deleteFederatedIdentityByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
num = em.createNamedQuery("deleteCredentialAttributeByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
num = em.createNamedQuery("deleteCredentialsByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
num = em.createNamedQuery("deleteUserAttributesByRealm")
|
||||
|
@ -408,10 +405,6 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteCredentialAttributeByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", storageProviderId)
|
||||
.executeUpdate();
|
||||
num = em.createNamedQuery("deleteCredentialsByRealmAndLink")
|
||||
.setParameter("realmId", realm.getId())
|
||||
.setParameter("link", storageProviderId)
|
||||
|
@ -858,93 +851,12 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
|
||||
@Override
|
||||
public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
|
||||
CredentialEntity entity = em.find(CredentialEntity.class, cred.getId());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
credentialStore.updateCredential(realm, user, cred);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) {
|
||||
CredentialEntity entity = new CredentialEntity();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
CredentialEntity entity = credentialStore.createCredentialEntity(realm, user, cred);
|
||||
|
||||
UserEntity userEntity = userInEntityManagerContext(user.getId());
|
||||
if (userEntity != null) {
|
||||
|
@ -955,56 +867,26 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
|
||||
@Override
|
||||
public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) {
|
||||
CredentialEntity entity = em.find(CredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
|
||||
if (entity == null) return false;
|
||||
em.remove(entity);
|
||||
CredentialEntity entity = credentialStore.removeCredentialEntity(realm, user, id);
|
||||
UserEntity userEntity = userInEntityManagerContext(user.getId());
|
||||
if (userEntity != null) {
|
||||
if (entity != null && userEntity != null) {
|
||||
userEntity.getCredentials().remove(entity);
|
||||
}
|
||||
return true;
|
||||
return entity != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) {
|
||||
CredentialEntity entity = em.find(CredentialEntity.class, id);
|
||||
if (entity == null) return null;
|
||||
CredentialModel model = toModel(entity);
|
||||
return model;
|
||||
return credentialStore.getStoredCredentialById(realm, user, id);
|
||||
}
|
||||
|
||||
protected CredentialModel toModel(CredentialEntity entity) {
|
||||
CredentialModel model = new CredentialModel();
|
||||
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;
|
||||
return credentialStore.toModel(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user) {
|
||||
UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
|
||||
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;
|
||||
return credentialStore.getStoredCredentials(realm, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1014,31 +896,25 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
|
|||
if (userEntity != null) {
|
||||
|
||||
// user already in persistence context, no need to execute a query
|
||||
results = userEntity.getCredentials().stream().filter(it -> it.getType().equals(type)).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();
|
||||
}
|
||||
results = userEntity.getCredentials().stream().filter(it -> type.equals(it.getType())).collect(Collectors.toList());
|
||||
List<CredentialModel> rtn = new LinkedList<>();
|
||||
for (CredentialEntity entity : results) {
|
||||
rtn.add(toModel(entity));
|
||||
}
|
||||
return rtn;
|
||||
} else {
|
||||
return credentialStore.getStoredCredentialsByType(realm, user, type);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) {
|
||||
UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
|
||||
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByNameAndType", CredentialEntity.class)
|
||||
.setParameter("type", type)
|
||||
.setParameter("device", name)
|
||||
.setParameter("user", userEntity);
|
||||
List<CredentialEntity> results = query.getResultList();
|
||||
if (results.isEmpty()) return null;
|
||||
return toModel(results.get(0));
|
||||
return credentialStore.getStoredCredentialByNameAndType(realm, user, name, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId) {
|
||||
return credentialStore.moveCredentialTo(realm, user, id, newPreviousCredentialId);
|
||||
}
|
||||
|
||||
// Could override this to provide a custom behavior.
|
||||
|
|
|
@ -587,6 +587,9 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
|
||||
@Override
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -1669,6 +1672,16 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
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
|
||||
public AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model) {
|
||||
AuthenticationExecutionEntity entity = new AuthenticationExecutionEntity();
|
||||
|
@ -1700,6 +1713,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
entity.setRequirement(model.getRequirement());
|
||||
entity.setAuthenticatorConfig(model.getAuthenticatorConfig());
|
||||
entity.setFlowId(model.getFlowId());
|
||||
if (model.getParentFlow() != null) {
|
||||
AuthenticationFlowEntity flow = em.find(AuthenticationFlowEntity.class, model.getParentFlow());
|
||||
entity.setParentFlow(flow);
|
||||
}
|
||||
em.flush();
|
||||
}
|
||||
|
||||
|
|
|
@ -27,12 +27,17 @@ 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 = "authenticationFlowExecution", query = "select authExec from AuthenticationExecutionEntity authExec where authExec.flowId = :flowId")
|
||||
})
|
||||
@Table(name="AUTHENTICATION_EXECUTION")
|
||||
@Entity
|
||||
public class AuthenticationExecutionEntity {
|
||||
|
|
|
@ -28,6 +28,8 @@ 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.OneToMany;
|
||||
import javax.persistence.Table;
|
||||
import java.util.ArrayList;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -25,6 +25,7 @@ import javax.persistence.Entity;
|
|||
import javax.persistence.FetchType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.JoinColumn;
|
||||
import javax.persistence.Lob;
|
||||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.NamedQueries;
|
||||
import javax.persistence.NamedQuery;
|
||||
|
@ -38,9 +39,7 @@ import java.util.Collection;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="credentialByUser", query="select cred from CredentialEntity cred where cred.user = :user"),
|
||||
@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="credentialByUser", query="select cred from CredentialEntity cred where cred.user = :user order by cred.priority"),
|
||||
@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)")
|
||||
|
||||
|
@ -55,14 +54,10 @@ public class CredentialEntity {
|
|||
|
||||
@Column(name="TYPE")
|
||||
protected String type;
|
||||
@Column(name="VALUE")
|
||||
protected String value;
|
||||
@Column(name="DEVICE")
|
||||
protected String device;
|
||||
@Column(name="SALT")
|
||||
protected byte[] salt;
|
||||
@Column(name="HASH_ITERATIONS")
|
||||
protected int hashIterations;
|
||||
|
||||
@Column(name="USER_LABEL")
|
||||
protected String userLabel;
|
||||
|
||||
@Column(name="CREATED_DATE")
|
||||
protected Long createdDate;
|
||||
|
||||
|
@ -70,121 +65,84 @@ public class CredentialEntity {
|
|||
@JoinColumn(name="USER_ID")
|
||||
protected UserEntity user;
|
||||
|
||||
@Column(name="COUNTER")
|
||||
protected int counter;
|
||||
@Column(name="SECRET_DATA")
|
||||
protected String secretData;
|
||||
|
||||
@Column(name="ALGORITHM")
|
||||
protected String algorithm;
|
||||
@Column(name="DIGITS")
|
||||
protected int digits;
|
||||
@Column(name="PERIOD")
|
||||
protected int period;
|
||||
@Column(name="CREDENTIAL_DATA")
|
||||
protected String credentialData;
|
||||
|
||||
@OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.EAGER, orphanRemoval = true, mappedBy="credential")
|
||||
protected Collection<CredentialAttributeEntity> credentialAttributes = new ArrayList<>();
|
||||
@Column(name="PRIORITY")
|
||||
protected int priority;
|
||||
|
||||
@Deprecated // Needed just for backwards compatibility when migrating old credentials
|
||||
@Column(name="SALT")
|
||||
protected byte[] salt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getDevice() {
|
||||
return device;
|
||||
public String getUserLabel() {
|
||||
return userLabel;
|
||||
}
|
||||
|
||||
public void setDevice(String device) {
|
||||
this.device = device;
|
||||
public void setUserLabel(String userLabel) {
|
||||
this.userLabel = userLabel;
|
||||
}
|
||||
|
||||
public UserEntity getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(UserEntity user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setSalt(byte[] 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 String getSecretData() {
|
||||
return secretData;
|
||||
}
|
||||
public void setSecretData(String secretData) {
|
||||
this.secretData = secretData;
|
||||
}
|
||||
|
||||
public void setCounter(int counter) {
|
||||
this.counter = counter;
|
||||
public String getCredentialData() {
|
||||
return credentialData;
|
||||
}
|
||||
public void setCredentialData(String credentialData) {
|
||||
this.credentialData = credentialData;
|
||||
}
|
||||
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
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<CredentialAttributeEntity> getCredentialAttributes() {
|
||||
return credentialAttributes;
|
||||
}
|
||||
|
||||
public void setCredentialAttributes(Collection<CredentialAttributeEntity> credentialAttributes) {
|
||||
this.credentialAttributes = credentialAttributes;
|
||||
public void setPriority(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
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.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
@ -33,6 +35,8 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserConsentModel;
|
||||
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.storage.StorageId;
|
||||
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.FederatedUserConsentClientScopeEntity;
|
||||
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.FederatedUserGroupMembershipEntity;
|
||||
import org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity;
|
||||
|
@ -55,9 +58,9 @@ import javax.persistence.TypedQuery;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Set;
|
||||
import javax.persistence.LockModeType;
|
||||
|
||||
|
@ -69,6 +72,8 @@ public class JpaUserFederatedStorageProvider implements
|
|||
UserFederatedStorageProvider,
|
||||
UserCredentialStore {
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(JpaUserFederatedStorageProvider.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
protected EntityManager em;
|
||||
|
||||
|
@ -565,53 +570,11 @@ public class JpaUserFederatedStorageProvider implements
|
|||
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, cred.getId());
|
||||
if (entity == null) return;
|
||||
createIndex(realm, userId);
|
||||
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 = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
entity.setCredentialData(cred.getCredentialData());
|
||||
entity.setSecretData(cred.getSecretData());
|
||||
cred.setUserLabel(entity.getUserLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -620,37 +583,22 @@ public class JpaUserFederatedStorageProvider implements
|
|||
FederatedUserCredentialEntity entity = new FederatedUserCredentialEntity();
|
||||
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());
|
||||
entity.setCredentialData(cred.getCredentialData());
|
||||
entity.setSecretData(cred.getSecretData());
|
||||
entity.setUserLabel(cred.getUserLabel());
|
||||
|
||||
entity.setUserId(userId);
|
||||
entity.setRealmId(realm.getId());
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -658,6 +606,18 @@ public class JpaUserFederatedStorageProvider implements
|
|||
public boolean removeStoredCredential(RealmModel realm, String userId, String id) {
|
||||
FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
@ -674,28 +634,25 @@ public class JpaUserFederatedStorageProvider implements
|
|||
CredentialModel model = new CredentialModel();
|
||||
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 (FederatedUserCredentialAttributeEntity attr : entity.getCredentialAttributes()) {
|
||||
config.add(attr.getName(), attr.getValue());
|
||||
model.setUserLabel(entity.getUserLabel());
|
||||
|
||||
// Backwards compatibility - users from previous version still have "salt" in the DB filled.
|
||||
// We migrate it to new secretData format on-the-fly
|
||||
if (entity.getSalt() != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CredentialModel> getStoredCredentials(RealmModel realm, String userId) {
|
||||
TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByUser", FederatedUserCredentialEntity.class)
|
||||
.setParameter("userId", userId);
|
||||
List<FederatedUserCredentialEntity> results = query.getResultList();
|
||||
List<FederatedUserCredentialEntity> results = getStoredCredentialEntities(userId);
|
||||
List<CredentialModel> rtn = new LinkedList<>();
|
||||
for (FederatedUserCredentialEntity entity : results) {
|
||||
rtn.add(toModel(entity));
|
||||
|
@ -703,6 +660,12 @@ public class JpaUserFederatedStorageProvider implements
|
|||
return rtn;
|
||||
}
|
||||
|
||||
private List<FederatedUserCredentialEntity> getStoredCredentialEntities(String userId) {
|
||||
TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByUser", FederatedUserCredentialEntity.class)
|
||||
.setParameter("userId", userId);
|
||||
return query.getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CredentialModel> getStoredCredentialsByType(RealmModel realm, String userId, String type) {
|
||||
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) {
|
||||
TypedQuery<FederatedUserCredentialEntity> query = em.createNamedQuery("federatedUserCredentialByNameAndType", FederatedUserCredentialEntity.class)
|
||||
.setParameter("type", type)
|
||||
.setParameter("device", name)
|
||||
.setParameter("userLabel", name)
|
||||
.setParameter("userId", userId);
|
||||
List<FederatedUserCredentialEntity> results = query.getResultList();
|
||||
if (results.isEmpty()) return null;
|
||||
|
@ -771,6 +734,60 @@ public class JpaUserFederatedStorageProvider implements
|
|||
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
|
||||
public int getStoredUsersCount(RealmModel realm) {
|
||||
Object count = em.createNamedQuery("getFederatedUserCount")
|
||||
|
@ -791,8 +808,6 @@ public class JpaUserFederatedStorageProvider implements
|
|||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
num = em.createNamedQuery("deleteBrokerLinkByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
num = em.createNamedQuery("deleteFederatedCredentialAttributeByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
num = em.createNamedQuery("deleteFederatedUserCredentialsByRealm")
|
||||
.setParameter("realmId", realm.getId()).executeUpdate();
|
||||
num = em.createNamedQuery("deleteUserFederatedAttributesByRealm")
|
||||
|
@ -862,10 +877,6 @@ public class JpaUserFederatedStorageProvider implements
|
|||
.setParameter("userId", user.getId())
|
||||
.setParameter("realmId", realm.getId())
|
||||
.executeUpdate();
|
||||
em.createNamedQuery("deleteFederatedCredentialAttributeByUser")
|
||||
.setParameter("userId", user.getId())
|
||||
.setParameter("realmId", realm.getId())
|
||||
.executeUpdate();
|
||||
em.createNamedQuery("deleteFederatedUserCredentialByUser")
|
||||
.setParameter("userId", user.getId())
|
||||
.setParameter("realmId", realm.getId())
|
||||
|
@ -905,9 +916,6 @@ public class JpaUserFederatedStorageProvider implements
|
|||
em.createNamedQuery("deleteFederatedUserConsentsByStorageProvider")
|
||||
.setParameter("storageProviderId", model.getId())
|
||||
.executeUpdate();
|
||||
em.createNamedQuery("deleteFederatedCredentialAttributeByStorageProvider")
|
||||
.setParameter("storageProviderId", model.getId())
|
||||
.executeUpdate();
|
||||
em.createNamedQuery("deleteFederatedUserCredentialsByStorageProvider")
|
||||
.setParameter("storageProviderId", model.getId())
|
||||
.executeUpdate();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package org.keycloak.storage.jpa.entity;
|
||||
|
||||
import org.keycloak.models.jpa.entities.UserEntity;
|
||||
|
||||
import javax.persistence.Access;
|
||||
import javax.persistence.AccessType;
|
||||
import javax.persistence.CascadeType;
|
||||
|
@ -24,6 +26,7 @@ import javax.persistence.Column;
|
|||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Lob;
|
||||
import javax.persistence.NamedQueries;
|
||||
import javax.persistence.NamedQuery;
|
||||
import javax.persistence.OneToMany;
|
||||
|
@ -36,12 +39,12 @@ import java.util.Collection;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
@NamedQueries({
|
||||
@NamedQuery(name="federatedUserCredentialByUser", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId"),
|
||||
@NamedQuery(name="federatedUserCredentialByUserAndType", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type"),
|
||||
@NamedQuery(name="federatedUserCredentialByNameAndType", query="select cred from FederatedUserCredentialEntity cred where cred.userId = :userId and cred.type = :type and cred.device = :device"),
|
||||
@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 order by cred.priority"),
|
||||
@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="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="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)")
|
||||
|
@ -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
|
||||
protected String id;
|
||||
|
||||
@Column(name="SECRET_DATA")
|
||||
protected String secretData;
|
||||
|
||||
@Column(name="CREDENTIAL_DATA")
|
||||
protected String credentialData;
|
||||
|
||||
@Column(name="TYPE")
|
||||
protected String type;
|
||||
@Column(name="VALUE")
|
||||
protected String value;
|
||||
@Column(name="DEVICE")
|
||||
protected String device;
|
||||
@Column(name="SALT")
|
||||
protected byte[] salt;
|
||||
@Column(name="HASH_ITERATIONS")
|
||||
protected int hashIterations;
|
||||
|
||||
@Column(name="USER_LABEL")
|
||||
protected String userLabel;
|
||||
|
||||
@Column(name="CREATED_DATE")
|
||||
protected Long createdDate;
|
||||
|
||||
|
@ -77,57 +82,62 @@ public class FederatedUserCredentialEntity {
|
|||
@Column(name = "STORAGE_PROVIDER_ID")
|
||||
protected String storageProviderId;
|
||||
|
||||
@Column(name="PRIORITY")
|
||||
protected int priority;
|
||||
|
||||
|
||||
@Column(name="COUNTER")
|
||||
protected int counter;
|
||||
|
||||
@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<>();
|
||||
@Deprecated // Needed just for backwards compatibility when migrating old credentials
|
||||
@Column(name="SALT")
|
||||
protected byte[] salt;
|
||||
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getDevice() {
|
||||
return device;
|
||||
public String getUserLabel() {
|
||||
return userLabel;
|
||||
}
|
||||
public void setUserLabel(String userLabel) {
|
||||
this.userLabel = userLabel;
|
||||
}
|
||||
|
||||
public void setDevice(String device) {
|
||||
this.device = device;
|
||||
public Long getCreatedDate() {
|
||||
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() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
@ -135,7 +145,6 @@ public class FederatedUserCredentialEntity {
|
|||
public String getRealmId() {
|
||||
return realmId;
|
||||
}
|
||||
|
||||
public void setRealmId(String realmId) {
|
||||
this.realmId = realmId;
|
||||
}
|
||||
|
@ -143,75 +152,28 @@ public class FederatedUserCredentialEntity {
|
|||
public String getStorageProviderId() {
|
||||
return storageProviderId;
|
||||
}
|
||||
|
||||
public void setStorageProviderId(String storageProviderId) {
|
||||
this.storageProviderId = storageProviderId;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setSalt(byte[] 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
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
205
model/jpa/src/main/resources/META-INF/jpa-changelog-8.0.0.xml
Normal file
205
model/jpa/src/main/resources/META-INF/jpa-changelog-8.0.0.xml
Normal 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('{"value":"', VALUE, '","salt":"__SALT__"}')"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{"hashIterations":', HASH_ITERATIONS, ',"algorithm":"', ALGORITHM, '"}')"/>
|
||||
<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('{"value":"', VALUE, '"}')"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{"subType":"totp","digits":', DIGITS, ',"period":', PERIOD, ',"algorithm":"', ALGORITHM, '"}')"/>
|
||||
<where>TYPE = 'totp'</where>
|
||||
</update>
|
||||
|
||||
<update tableName="CREDENTIAL">
|
||||
<column name="PRIORITY" value="20" />
|
||||
<column name="TYPE" value="otp" />
|
||||
<column name="SECRET_DATA" valueComputed="CONCAT('{"value":"', VALUE, '"}')"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{"subType":"hotp","digits":', DIGITS, ',"counter":', COUNTER, ',"algorithm":"', ALGORITHM, '"}')"/>
|
||||
<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('{"value":"', VALUE, '","salt":"__SALT__"}')"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{"hashIterations":', HASH_ITERATIONS, ',"algorithm":"', ALGORITHM, '"}')"/>
|
||||
<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('{"value":"', VALUE, '"}')"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{"subType":"totp","digits":', DIGITS, ',"period":', PERIOD, ',"algorithm":"', ALGORITHM, '"}')"/>
|
||||
<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('{"value":"', VALUE, '"}')"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="CONCAT('{"subType":"hotp","digits":', DIGITS, ',"counter":', COUNTER, ',"algorithm":"', ALGORITHM, '"}')"/>
|
||||
<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="'{"value":"' || VALUE || '","salt":"__SALT__"}'"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="'{"hashIterations":' || HASH_ITERATIONS || ',"algorithm":"' || ALGORITHM || '"}'"/>
|
||||
<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="'{"value":"' || VALUE || '"}'"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="'{"subType":"totp","digits":' || DIGITS || ',"period":' || PERIOD || ',"algorithm":"' || ALGORITHM || '"}'"/>
|
||||
<where>TYPE = 'totp'</where>
|
||||
</update>
|
||||
|
||||
<update tableName="CREDENTIAL">
|
||||
<column name="PRIORITY" value="20" />
|
||||
<column name="TYPE" value="otp" />
|
||||
<column name="SECRET_DATA" valueComputed="'{"value":"' || VALUE || '"}'"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="'{"subType":"hotp","digits":' || DIGITS || ',"counter":' || COUNTER || ',"algorithm":"' || ALGORITHM || '"}'"/>
|
||||
<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="'{"value":"' || VALUE || '","salt":"__SALT__"}'"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="'{"hashIterations":' || HASH_ITERATIONS || ',"algorithm":"' || ALGORITHM || '"}'"/>
|
||||
<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="'{"value":"' || VALUE || '"}'"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="'{"subType":"totp","digits":' || DIGITS || ',"period":' || PERIOD || ',"algorithm":"' || ALGORITHM || '"}'"/>
|
||||
<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="'{"value":"' || VALUE || '"}'"/>
|
||||
<column name="CREDENTIAL_DATA" valueComputed="'{"subType":"hotp","digits":' || DIGITS || ',"counter":' || COUNTER || ',"algorithm":"' || ALGORITHM || '"}'"/>
|
||||
<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>
|
|
@ -63,4 +63,5 @@
|
|||
<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-authz-7.0.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-8.0.0.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
<class>org.keycloak.models.jpa.entities.ClientEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ClientAttributeEntity</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.RealmAttributeEntity</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.FederatedUserConsentClientScopeEntity</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.FederatedUserRequiredActionEntity</class>
|
||||
<class>org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity</class>
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -154,7 +154,7 @@
|
|||
<surefire.memory.Xms>512m</surefire.memory.Xms>
|
||||
<surefire.memory.Xmx>2048m</surefire.memory.Xmx>
|
||||
<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>
|
||||
|
||||
<!-- Tomcat versions -->
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
package org.keycloak.authentication;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -30,4 +32,8 @@ public interface AuthenticationFlow {
|
|||
|
||||
Response processAction(String actionExecution);
|
||||
Response processFlow();
|
||||
boolean isSuccessful();
|
||||
default List<AuthenticationFlowException> getFlowExceptions(){
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,17 @@
|
|||
|
||||
package org.keycloak.authentication;
|
||||
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
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
|
||||
|
@ -49,6 +53,23 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -64,6 +85,11 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.authentication;
|
||||
|
||||
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.
|
||||
|
@ -28,6 +29,7 @@ import javax.ws.rs.core.Response;
|
|||
public class AuthenticationFlowException extends RuntimeException {
|
||||
private AuthenticationFlowError error;
|
||||
private Response response;
|
||||
private List<AuthenticationFlowException> afeList;
|
||||
|
||||
public AuthenticationFlowException(AuthenticationFlowError error) {
|
||||
this.error = error;
|
||||
|
@ -53,6 +55,11 @@ public class AuthenticationFlowException extends RuntimeException {
|
|||
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) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
this.error = error;
|
||||
|
@ -65,4 +72,8 @@ public class AuthenticationFlowException extends RuntimeException {
|
|||
public Response getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
public List<AuthenticationFlowException> getAfeList() {
|
||||
return afeList;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -19,9 +19,13 @@ package org.keycloak.authentication;
|
|||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
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.
|
||||
* 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);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,4 +30,5 @@ import org.keycloak.provider.ProviderFactory;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface AuthenticatorFactory extends ProviderFactory<Authenticator>, ConfigurableAuthenticatorFactory {
|
||||
|
||||
}
|
||||
|
|
|
@ -25,6 +25,12 @@ import org.keycloak.provider.ConfiguredProvider;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface ConfigurableAuthenticatorFactory extends ConfiguredProvider {
|
||||
|
||||
AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
/**
|
||||
* Friendly name for the authenticator
|
||||
*
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
package org.keycloak.authentication;
|
||||
|
||||
public interface CredentialRegistrator {
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -18,9 +18,8 @@
|
|||
package org.keycloak.credential.hash;
|
||||
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
|
@ -53,31 +52,27 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean policyCheck(PasswordPolicy policy, CredentialModel credential) {
|
||||
public boolean policyCheck(PasswordPolicy policy, PasswordCredentialModel credential) {
|
||||
int policyHashIterations = policy.getHashIterations();
|
||||
if (policyHashIterations == -1) {
|
||||
policyHashIterations = defaultIterations;
|
||||
}
|
||||
|
||||
return credential.getHashIterations() == policyHashIterations
|
||||
&& providerId.equals(credential.getAlgorithm())
|
||||
return credential.getPasswordCredentialData().getHashIterations() == policyHashIterations
|
||||
&& providerId.equals(credential.getPasswordCredentialData().getAlgorithm())
|
||||
&& derivedKeySize == keySize(credential);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(String rawPassword, int iterations, CredentialModel credential) {
|
||||
public PasswordCredentialModel encodedCredential(String rawPassword, int iterations) {
|
||||
if (iterations == -1) {
|
||||
iterations = defaultIterations;
|
||||
}
|
||||
|
||||
byte[] salt = getSalt();
|
||||
String encodedPassword = encode(rawPassword, iterations, salt, derivedKeySize);
|
||||
String encodedPassword = encodedCredential(rawPassword, iterations, salt, derivedKeySize);
|
||||
|
||||
credential.setAlgorithm(providerId);
|
||||
credential.setType(UserCredentialModel.PASSWORD);
|
||||
credential.setSalt(salt);
|
||||
credential.setHashIterations(iterations);
|
||||
credential.setValue(encodedPassword);
|
||||
return PasswordCredentialModel.createFromValues(providerId, salt, iterations, encodedPassword);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -87,17 +82,17 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
}
|
||||
|
||||
byte[] salt = getSalt();
|
||||
return encode(rawPassword, iterations, salt, derivedKeySize);
|
||||
return encodedCredential(rawPassword, iterations, salt, derivedKeySize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String rawPassword, CredentialModel credential) {
|
||||
return encode(rawPassword, credential.getHashIterations(), credential.getSalt(), keySize(credential)).equals(credential.getValue());
|
||||
public boolean verify(String rawPassword, PasswordCredentialModel credential) {
|
||||
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 {
|
||||
byte[] bytes = Base64.decode(credential.getValue());
|
||||
byte[] bytes = Base64.decode(credential.getPasswordSecretData().getValue());
|
||||
return bytes.length * 8;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Credential could not be decoded", e);
|
||||
|
@ -107,7 +102,7 @@ public class Pbkdf2PasswordHashProvider implements PasswordHashProvider {
|
|||
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);
|
||||
|
||||
try {
|
||||
|
|
|
@ -22,7 +22,7 @@ package org.keycloak.forms.login;
|
|||
*/
|
||||
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,
|
||||
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE,
|
||||
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.forms.login;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
|
@ -55,12 +56,18 @@ public interface LoginFormsProvider extends Provider {
|
|||
|
||||
String getMessage(String message, String... parameters);
|
||||
|
||||
Response createLogin();
|
||||
Response createLoginUsernamePassword();
|
||||
|
||||
Response createLoginUsername();
|
||||
|
||||
Response createLoginPassword();
|
||||
|
||||
Response createPasswordReset();
|
||||
|
||||
Response createLoginTotp();
|
||||
|
||||
Response createLoginWebAuthn();
|
||||
|
||||
Response createRegistration();
|
||||
|
||||
Response createInfoPage();
|
||||
|
@ -133,4 +140,6 @@ public interface LoginFormsProvider extends Provider {
|
|||
LoginFormsProvider setActionUri(URI requestUri);
|
||||
|
||||
LoginFormsProvider setExecution(String execution);
|
||||
|
||||
LoginFormsProvider setAuthContext(AuthenticationFlowContext context);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,10 @@
|
|||
|
||||
package org.keycloak.migration.migrators;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.migration.ModelVersion;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -26,10 +29,15 @@ import org.keycloak.representations.idm.RealmRepresentation;
|
|||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MigrateTo8_0_0 implements Migration {
|
||||
|
||||
public static final ModelVersion VERSION = new ModelVersion("8.0.0");
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MigrateTo8_0_0.class);
|
||||
|
||||
@Override
|
||||
public ModelVersion getVersion() {
|
||||
return VERSION;
|
||||
|
@ -37,15 +45,22 @@ public class MigrateTo8_0_0 implements Migration {
|
|||
|
||||
@Override
|
||||
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
|
||||
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);
|
||||
adminConsoleClient.setRootUrl(Constants.AUTH_ADMIN_URL_PROP);
|
||||
String adminConsoleBaseUrl = "/admin/" + realm.getName() + "/console/";
|
||||
|
@ -59,4 +74,54 @@ public class MigrateTo8_0_0 implements Migration {
|
|||
accountClient.setBaseUrl(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,6 +78,8 @@ public final class Constants {
|
|||
|
||||
public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST";
|
||||
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 TEMPLATE_ATTR_ACTION_URI = "actionUri";
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
|
||||
package org.keycloak.models.utils;
|
||||
|
||||
import org.keycloak.models.OTPPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -27,14 +25,17 @@ import org.keycloak.models.UserCredentialModel;
|
|||
*/
|
||||
public class CredentialValidation {
|
||||
|
||||
public static boolean validOTP(RealmModel realm, String token, String secret) {
|
||||
OTPPolicy policy = realm.getOTPPolicy();
|
||||
if (policy.getType().equals(UserCredentialModel.TOTP)) {
|
||||
TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), policy.getPeriod(), policy.getLookAheadWindow());
|
||||
return validator.validateTOTP(token, secret.getBytes());
|
||||
public static boolean validOTP(String token, OTPCredentialModel credentialModel, int lookAheadWindow) {
|
||||
if (credentialModel.getOTPCredentialData().getSubType().equals(OTPCredentialModel.TOTP)) {
|
||||
TimeBasedOTP validator = new TimeBasedOTP(credentialModel.getOTPCredentialData().getAlgorithm(),
|
||||
credentialModel.getOTPCredentialData().getDigits(), credentialModel.getOTPCredentialData().getPeriod(),
|
||||
lookAheadWindow);
|
||||
return validator.validateTOTP(token, credentialModel.getOTPSecretData().getValue().getBytes());
|
||||
} else {
|
||||
HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow());
|
||||
int c = validator.validateHOTP(token, secret, policy.getInitialCounter());
|
||||
HmacOTP validator = new HmacOTP(credentialModel.getOTPCredentialData().getDigits(),
|
||||
credentialModel.getOTPCredentialData().getAlgorithm(), lookAheadWindow);
|
||||
int c = validator.validateHOTP(token, credentialModel.getOTPSecretData().getValue(),
|
||||
credentialModel.getOTPCredentialData().getCounter());
|
||||
return c > -1;
|
||||
}
|
||||
|
||||
|
|
|
@ -143,9 +143,6 @@ public class DefaultAuthenticationFlows {
|
|||
execution.setAuthenticatorFlow(false);
|
||||
//execution.setAuthenticatorConfig(captchaConfig.getId());
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static void browserFlow(RealmModel realm) {
|
||||
|
@ -163,18 +160,18 @@ public class DefaultAuthenticationFlows {
|
|||
}
|
||||
|
||||
public static void resetCredentialsFlow(RealmModel realm) {
|
||||
AuthenticationFlowModel grant = new AuthenticationFlowModel();
|
||||
grant.setAlias(RESET_CREDENTIALS_FLOW);
|
||||
grant.setDescription("Reset credentials for a user if they forgot their password or something");
|
||||
grant.setProviderId("basic-flow");
|
||||
grant.setTopLevel(true);
|
||||
grant.setBuiltIn(true);
|
||||
grant = realm.addAuthenticationFlow(grant);
|
||||
realm.setResetCredentialsFlow(grant);
|
||||
AuthenticationFlowModel reset = new AuthenticationFlowModel();
|
||||
reset.setAlias(RESET_CREDENTIALS_FLOW);
|
||||
reset.setDescription("Reset credentials for a user if they forgot their password or something");
|
||||
reset.setProviderId("basic-flow");
|
||||
reset.setTopLevel(true);
|
||||
reset.setBuiltIn(true);
|
||||
reset = realm.addAuthenticationFlow(reset);
|
||||
realm.setResetCredentialsFlow(reset);
|
||||
|
||||
// username
|
||||
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(grant.getId());
|
||||
execution.setParentFlow(reset.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("reset-credentials-choose-user");
|
||||
execution.setPriority(10);
|
||||
|
@ -183,7 +180,7 @@ public class DefaultAuthenticationFlows {
|
|||
|
||||
// send email
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(grant.getId());
|
||||
execution.setParentFlow(reset.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("reset-credential-email");
|
||||
execution.setPriority(20);
|
||||
|
@ -192,19 +189,41 @@ public class DefaultAuthenticationFlows {
|
|||
|
||||
// password
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(grant.getId());
|
||||
execution.setParentFlow(reset.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("reset-password");
|
||||
execution.setPriority(30);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
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.setParentFlow(grant.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
|
||||
execution.setAuthenticator("reset-otp");
|
||||
execution.setParentFlow(reset.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
|
||||
execution.setFlowId(conditionalOTP.getId());
|
||||
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);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
}
|
||||
|
@ -241,14 +260,37 @@ public class DefaultAuthenticationFlows {
|
|||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
// 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.setParentFlow(grant.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
|
||||
if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) {
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
}
|
||||
execution.setAuthenticator("direct-grant-validate-otp");
|
||||
execution.setFlowId(conditionalOTP.getId());
|
||||
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);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
}
|
||||
|
@ -309,15 +351,36 @@ public class DefaultAuthenticationFlows {
|
|||
execution.setAuthenticatorFlow(false);
|
||||
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.setParentFlow(forms.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
|
||||
if (migrate && hasCredentialType(realm, RequiredCredentialModel.TOTP.getType())) {
|
||||
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.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
|
@ -432,6 +495,20 @@ public class DefaultAuthenticationFlows {
|
|||
execution.setAuthenticatorConfig(reviewProfileConfig.getId());
|
||||
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();
|
||||
createUserIfUniqueConfig.setAlias(IDP_CREATE_UNIQUE_USER_CONFIG_ALIAS);
|
||||
|
@ -441,10 +518,10 @@ public class DefaultAuthenticationFlows {
|
|||
createUserIfUniqueConfig = realm.addAuthenticatorConfig(createUserIfUniqueConfig);
|
||||
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(firstBrokerLogin.getId());
|
||||
execution.setParentFlow(uniqueOrExistingFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||
execution.setAuthenticator("idp-create-user-if-unique");
|
||||
execution.setPriority(20);
|
||||
execution.setPriority(10);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
execution.setAuthenticatorConfig(createUserIfUniqueConfig.getId());
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
@ -458,10 +535,10 @@ public class DefaultAuthenticationFlows {
|
|||
linkExistingAccountFlow.setProviderId("basic-flow");
|
||||
linkExistingAccountFlow = realm.addAuthenticationFlow(linkExistingAccountFlow);
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(firstBrokerLogin.getId());
|
||||
execution.setParentFlow(uniqueOrExistingFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||
execution.setFlowId(linkExistingAccountFlow.getId());
|
||||
execution.setPriority(30);
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(true);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
|
@ -473,11 +550,26 @@ public class DefaultAuthenticationFlows {
|
|||
execution.setAuthenticatorFlow(false);
|
||||
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.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.setAuthenticator("idp-email-verification");
|
||||
execution.setPriority(20);
|
||||
execution.setPriority(10);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
|
@ -489,10 +581,10 @@ public class DefaultAuthenticationFlows {
|
|||
verifyByReauthenticationAccountFlow.setProviderId("basic-flow");
|
||||
verifyByReauthenticationAccountFlow = realm.addAuthenticationFlow(verifyByReauthenticationAccountFlow);
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(linkExistingAccountFlow.getId());
|
||||
execution.setParentFlow(accountVerificationOptions.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||
execution.setFlowId(verifyByReauthenticationAccountFlow.getId());
|
||||
execution.setPriority(30);
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(true);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
|
@ -505,26 +597,48 @@ public class DefaultAuthenticationFlows {
|
|||
execution.setAuthenticatorFlow(false);
|
||||
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.setParentFlow(verifyByReauthenticationAccountFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
|
||||
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.CONDITIONAL);
|
||||
if (migrate) {
|
||||
// Try to read OTP requirement from browser flow
|
||||
AuthenticationFlowModel browserFlow = realm.getBrowserFlow();
|
||||
if (browserFlow == null) {
|
||||
browserFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
|
||||
}
|
||||
|
||||
List<AuthenticationExecutionModel> browserExecutions = new LinkedList<>();
|
||||
KeycloakModelUtils.deepFindAuthenticationExecutions(realm, browserFlow, 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.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.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
|
@ -591,28 +705,43 @@ public class DefaultAuthenticationFlows {
|
|||
execution.setAuthenticatorFlow(false);
|
||||
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.setParentFlow(challengeFlow.getId());
|
||||
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.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.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(challengeFlow.getId());
|
||||
execution.setParentFlow(authType.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
|
||||
execution.setAuthenticator("basic-auth-otp");
|
||||
execution.setAuthenticator("auth-spnego");
|
||||
execution.setPriority(30);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ public final class KeycloakModelUtils {
|
|||
|
||||
public static UserCredentialModel generateSecret(ClientModel client) {
|
||||
UserCredentialModel secret = UserCredentialModel.generateSecret();
|
||||
client.setSecret(secret.getValue());
|
||||
client.setSecret(secret.getChallengeResponse());
|
||||
return secret;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.events.Event;
|
|||
import org.keycloak.events.admin.AdminEvent;
|
||||
import org.keycloak.events.admin.AuthDetails;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.idm.*;
|
||||
import org.keycloak.representations.idm.authorization.*;
|
||||
|
@ -168,7 +169,7 @@ public class ModelToRepresentation {
|
|||
rep.setEmail(user.getEmail());
|
||||
rep.setEnabled(user.isEnabled());
|
||||
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.setFederationLink(user.getFederationLink());
|
||||
|
||||
|
@ -185,6 +186,7 @@ public class ModelToRepresentation {
|
|||
attrs.putAll(user.getAttributes());
|
||||
rep.setAttributes(attrs);
|
||||
}
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
|
@ -489,7 +491,18 @@ public class ModelToRepresentation {
|
|||
public static CredentialRepresentation toRepresentation(UserCredentialModel cred) {
|
||||
CredentialRepresentation rep = new CredentialRepresentation();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,15 +49,14 @@ import org.keycloak.authorization.store.ResourceServerStore;
|
|||
import org.keycloak.authorization.store.ResourceStore;
|
||||
import org.keycloak.authorization.store.ScopeStore;
|
||||
import org.keycloak.authorization.store.StoreFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.keys.KeyProvider;
|
||||
import org.keycloak.migration.MigrationProvider;
|
||||
import org.keycloak.migration.migrators.MigrateTo8_0_0;
|
||||
import org.keycloak.migration.migrators.MigrationUtils;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
|
@ -86,8 +85,11 @@ import org.keycloak.models.UserCredentialModel;
|
|||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
import org.keycloak.models.cache.UserCache;
|
||||
import org.keycloak.models.credential.PasswordUserCredentialModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
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.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.idm.ApplicationRepresentation;
|
||||
|
@ -660,8 +662,7 @@ public class RepresentationToModel {
|
|||
for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) {
|
||||
AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias());
|
||||
for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) {
|
||||
AuthenticationExecutionModel execution = toModel(newRealm, exeRep);
|
||||
execution.setParentFlow(model.getId());
|
||||
AuthenticationExecutionModel execution = toModel(newRealm, model, exeRep);
|
||||
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) {
|
||||
if (name.equals(realm.getName())) return;
|
||||
|
||||
|
@ -1677,7 +1707,11 @@ public class RepresentationToModel {
|
|||
}
|
||||
if (userRep.getRequiredActions() != null) {
|
||||
for (String requiredAction : userRep.getRequiredActions()) {
|
||||
try {
|
||||
user.addRequiredAction(UserModel.RequiredAction.valueOf(requiredAction.toUpperCase()));
|
||||
} catch (IllegalArgumentException iae) {
|
||||
user.addRequiredAction(requiredAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
convertDeprecatedCredentialsFormat(userRep);
|
||||
if (userRep.getCredentials() != null) {
|
||||
for (CredentialRepresentation cred : userRep.getCredentials()) {
|
||||
updateCredential(session, realm, user, cred, adminRequest);
|
||||
if (cred.getId() != null && session.userCredentialManager().getStoredCredentialById(realm, user, cred.getId()) != null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if (cred.getValue() != null && !cred.getValue().isEmpty()) {
|
||||
RealmModel origRealm = session.getContext().getRealm();
|
||||
try {
|
||||
session.getContext().setRealm(realm);
|
||||
session.userCredentialManager().updateCredential(realm, user, plainTextCred);
|
||||
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(cred.getValue(), false));
|
||||
} catch (ModelException ex) {
|
||||
throw new PasswordPolicyNotMetException(ex.getMessage(), user.getUsername(), ex);
|
||||
} finally {
|
||||
session.getContext().setRealm(origRealm);
|
||||
}
|
||||
} else {
|
||||
CredentialModel hashedCred = new CredentialModel();
|
||||
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);
|
||||
session.userCredentialManager().createCredentialThroughProvider(realm, user, toModel(cred));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
CredentialModel model = new CredentialModel();
|
||||
model.setHashIterations(cred.getHashIterations());
|
||||
model.setCreatedDate(cred.getCreatedDate());
|
||||
model.setType(cred.getType());
|
||||
model.setDigits(cred.getDigits());
|
||||
model.setConfig(cred.getConfig());
|
||||
model.setDevice(cred.getDevice());
|
||||
model.setAlgorithm(cred.getAlgorithm());
|
||||
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());
|
||||
}
|
||||
model.setUserLabel(cred.getUserLabel());
|
||||
model.setSecretData(cred.getSecretData());
|
||||
model.setCredentialData(cred.getCredentialData());
|
||||
model.setId(cred.getId());
|
||||
return model;
|
||||
|
||||
}
|
||||
|
||||
// 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();
|
||||
if (rep.getAuthenticatorConfig() != null) {
|
||||
AuthenticatorConfigModel config = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig());
|
||||
|
@ -1999,7 +1963,15 @@ public class RepresentationToModel {
|
|||
model.setFlowId(flow.getId());
|
||||
}
|
||||
model.setPriority(rep.getPriority());
|
||||
try {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -52,24 +53,29 @@ public class HistoryPasswordPolicyProvider implements PasswordPolicyProvider {
|
|||
PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy();
|
||||
int passwordHistoryPolicyValue = policy.getPolicyConfig(PasswordPolicy.PASSWORD_HISTORY_ID);
|
||||
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) {
|
||||
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.verify(password, cred)) {
|
||||
if (hash.verify(password, passwordCredential)) {
|
||||
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);
|
||||
for (CredentialModel cred : recentPasswordHistory) {
|
||||
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm());
|
||||
if (hash.verify(password, cred)) {
|
||||
PasswordCredentialModel passwordCredential = PasswordCredentialModel.createFromCredentialModel(cred);
|
||||
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, passwordCredential.getPasswordCredentialData().getAlgorithm());
|
||||
if (hash.verify(password, passwordCredential)) {
|
||||
return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,5 +23,7 @@ package org.keycloak.credential;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface CredentialInput {
|
||||
String getCredentialId();
|
||||
String getType();
|
||||
String getChallengeResponse();
|
||||
}
|
||||
|
|
|
@ -32,6 +32,13 @@ import java.util.List;
|
|||
public interface CredentialInputValidator {
|
||||
boolean supportsCredentialType(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);
|
||||
}
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package org.keycloak.credential;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Comparator;
|
||||
|
||||
|
@ -28,56 +26,50 @@ import java.util.Comparator;
|
|||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CredentialModel implements Serializable {
|
||||
|
||||
@Deprecated /** Use PasswordCredentialModel.TYPE instead **/
|
||||
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_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
|
||||
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 KERBEROS = "kerberos";
|
||||
public static final String OTP = "otp";
|
||||
|
||||
|
||||
|
||||
private String id;
|
||||
private String type;
|
||||
private String value;
|
||||
private String device;
|
||||
private byte[] salt;
|
||||
private int hashIterations;
|
||||
private String userLabel;
|
||||
private Long createdDate;
|
||||
|
||||
// otp stuff
|
||||
private int counter;
|
||||
private String algorithm;
|
||||
private int digits;
|
||||
private int period;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private String secretData;
|
||||
private String credentialData;
|
||||
|
||||
public CredentialModel shallowClone() {
|
||||
CredentialModel res = new CredentialModel();
|
||||
res.id = id;
|
||||
res.type = type;
|
||||
res.value = value;
|
||||
res.device = device;
|
||||
res.salt = salt;
|
||||
res.hashIterations = hashIterations;
|
||||
res.userLabel = userLabel;
|
||||
res.createdDate = createdDate;
|
||||
res.counter = counter;
|
||||
res.algorithm = algorithm;
|
||||
res.digits = digits;
|
||||
res.period = period;
|
||||
res.config = config;
|
||||
res.secretData = secretData;
|
||||
res.credentialData = credentialData;
|
||||
return res;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
@ -85,89 +77,36 @@ public class CredentialModel implements Serializable {
|
|||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
public String getUserLabel() {
|
||||
return userLabel;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
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 void setUserLabel(String userLabel) {
|
||||
this.userLabel = userLabel;
|
||||
}
|
||||
|
||||
public Long getCreatedDate() {
|
||||
return createdDate;
|
||||
}
|
||||
|
||||
public void setCreatedDate(Long createdDate) {
|
||||
this.createdDate = createdDate;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
public String getSecretData() {
|
||||
return secretData;
|
||||
}
|
||||
public void setSecretData(String secretData) {
|
||||
this.secretData = secretData;
|
||||
}
|
||||
|
||||
public void setCounter(int counter) {
|
||||
this.counter = counter;
|
||||
public String getCredentialData() {
|
||||
return credentialData;
|
||||
}
|
||||
|
||||
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 MultivaluedHashMap<String, String> getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public void setConfig(MultivaluedHashMap<String, String> config) {
|
||||
this.config = config;
|
||||
public void setCredentialData(String credentialData) {
|
||||
this.credentialData = credentialData;
|
||||
}
|
||||
|
||||
public static Comparator<CredentialModel> comparingByStartDateDesc() {
|
||||
|
|
|
@ -16,16 +16,37 @@
|
|||
*/
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface CredentialProvider extends Provider {
|
||||
public interface CredentialProvider<T extends CredentialModel> extends Provider {
|
||||
|
||||
@Override
|
||||
default
|
||||
void close() {
|
||||
default 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,4 +34,8 @@ public interface UserCredentialStore extends Provider {
|
|||
List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user);
|
||||
List<CredentialModel> getStoredCredentialsByType(RealmModel realm, UserModel user, String type);
|
||||
CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type);
|
||||
|
||||
//list operations
|
||||
boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId);
|
||||
|
||||
}
|
||||
|
|
|
@ -19,20 +19,21 @@ package org.keycloak.credential.hash;
|
|||
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
|
||||
*/
|
||||
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
|
||||
String encode(String rawPassword, int iterations) {
|
||||
return rawPassword;
|
||||
}
|
||||
|
||||
boolean verify(String rawPassword, CredentialModel credential);
|
||||
boolean verify(String rawPassword, PasswordCredentialModel credential);
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ public class AuthenticationExecutionModel implements Serializable {
|
|||
|
||||
public enum Requirement {
|
||||
REQUIRED,
|
||||
OPTIONAL,
|
||||
CONDITIONAL,
|
||||
ALTERNATIVE,
|
||||
DISABLED
|
||||
}
|
||||
|
@ -128,8 +128,8 @@ public class AuthenticationExecutionModel implements Serializable {
|
|||
public boolean isRequired() {
|
||||
return requirement == Requirement.REQUIRED;
|
||||
}
|
||||
public boolean isOptional() {
|
||||
return requirement == Requirement.OPTIONAL;
|
||||
public boolean isConditional() {
|
||||
return requirement == Requirement.CONDITIONAL;
|
||||
}
|
||||
public boolean isAlternative() {
|
||||
return requirement == Requirement.ALTERNATIVE;
|
||||
|
@ -140,4 +140,21 @@ public class AuthenticationExecutionModel implements Serializable {
|
|||
public boolean isEnabled() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.models;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.models.utils.Base32;
|
||||
import org.keycloak.models.utils.HmacOTP;
|
||||
|
||||
|
@ -66,7 +67,7 @@ public class OTPPolicy implements Serializable {
|
|||
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() {
|
||||
return algToKeyUriAlg.containsKey(algorithm) ? algToKeyUriAlg.get(algorithm) : algorithm;
|
||||
|
@ -148,9 +149,9 @@ public class OTPPolicy implements Serializable {
|
|||
+ "&algorithm=" + algToKeyUriAlg.get(algorithm) //
|
||||
+ "&issuer=" + issuerName;
|
||||
|
||||
if (type.equals(UserCredentialModel.HOTP)) {
|
||||
if (type.equals(OTPCredentialModel.HOTP)) {
|
||||
parameters += "&counter=" + initialCounter;
|
||||
} else if (type.equals(UserCredentialModel.TOTP)) {
|
||||
} else if (type.equals(OTPCredentialModel.TOTP)) {
|
||||
parameters += "&period=" + period;
|
||||
}
|
||||
|
||||
|
@ -194,11 +195,7 @@ public class OTPPolicy implements Serializable {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (policy.getType().equals("totp") && policy.getPeriod() != 30) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return policy.getType().equals("totp") && policy.getPeriod() == 30;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -298,6 +298,7 @@ public interface RealmModel extends RoleContainerModel {
|
|||
|
||||
List<AuthenticationExecutionModel> getAuthenticationExecutions(String flowId);
|
||||
AuthenticationExecutionModel getAuthenticationExecutionById(String id);
|
||||
AuthenticationExecutionModel getAuthenticationExecutionByFlowId(String flowId);
|
||||
AuthenticationExecutionModel addAuthenticatorExecution(AuthenticationExecutionModel model);
|
||||
void updateAuthenticatorExecution(AuthenticationExecutionModel model);
|
||||
void removeAuthenticatorExecution(AuthenticationExecutionModel model);
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
|
||||
package org.keycloak.models;
|
||||
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -78,7 +81,7 @@ public class RequiredCredentialModel implements Serializable {
|
|||
static {
|
||||
Map<String, RequiredCredentialModel> map = new HashMap<String, RequiredCredentialModel>();
|
||||
PASSWORD = new RequiredCredentialModel();
|
||||
PASSWORD.setType(UserCredentialModel.PASSWORD);
|
||||
PASSWORD.setType(PasswordCredentialModel.TYPE);
|
||||
PASSWORD.setInput(true);
|
||||
PASSWORD.setSecret(true);
|
||||
PASSWORD.setFormLabel("password");
|
||||
|
@ -90,7 +93,7 @@ public class RequiredCredentialModel implements Serializable {
|
|||
SECRET.setFormLabel("secret");
|
||||
map.put(SECRET.getType(), SECRET);
|
||||
TOTP = new RequiredCredentialModel();
|
||||
TOTP.setType(UserCredentialModel.TOTP);
|
||||
TOTP.setType(OTPCredentialModel.TYPE);
|
||||
TOTP.setInput(true);
|
||||
TOTP.setSecret(false);
|
||||
TOTP.setFormLabel("authenticatorCode");
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.keycloak.models;
|
||||
|
||||
import org.keycloak.credential.CredentialInput;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.credential.UserCredentialStore;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -60,6 +61,24 @@ public interface UserCredentialManager extends UserCredentialStore {
|
|||
*/
|
||||
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
|
||||
* each CredentialProvider.
|
||||
|
|
|
@ -19,10 +19,9 @@ package org.keycloak.models;
|
|||
|
||||
import org.keycloak.credential.CredentialInput;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -30,134 +29,80 @@ import java.util.UUID;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
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 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 CLIENT_CERT = CredentialModel.CLIENT_CERT;
|
||||
|
||||
protected String type;
|
||||
protected String value;
|
||||
protected String device;
|
||||
protected String algorithm;
|
||||
private final String credentialId;
|
||||
private final String type;
|
||||
private final String challengeResponse;
|
||||
private final boolean adminRequest;
|
||||
|
||||
// Additional context informations
|
||||
protected Map<String, Object> notes = new HashMap<>();
|
||||
|
||||
public UserCredentialModel() {
|
||||
public UserCredentialModel(String credentialId, String type, String challengeResponse) {
|
||||
this.credentialId = credentialId;
|
||||
this.type = type;
|
||||
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);
|
||||
}
|
||||
|
||||
public static PasswordUserCredentialModel password(String password, boolean adminRequest) {
|
||||
PasswordUserCredentialModel model = new PasswordUserCredentialModel();
|
||||
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 password(String password, boolean adminRequest) {
|
||||
return new UserCredentialModel("", PasswordCredentialModel.TYPE, password, adminRequest);
|
||||
}
|
||||
|
||||
public static UserCredentialModel secret(String password) {
|
||||
UserCredentialModel model = new UserCredentialModel();
|
||||
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;
|
||||
return new UserCredentialModel("", SECRET, password);
|
||||
}
|
||||
|
||||
public static UserCredentialModel kerberos(String token) {
|
||||
UserCredentialModel model = new UserCredentialModel();
|
||||
model.setType(KERBEROS);
|
||||
model.setValue(token);
|
||||
return model;
|
||||
return new UserCredentialModel("", KERBEROS, token);
|
||||
}
|
||||
|
||||
public static UserCredentialModel generateSecret() {
|
||||
UserCredentialModel model = new UserCredentialModel();
|
||||
model.setType(SECRET);
|
||||
model.setValue(UUID.randomUUID().toString());
|
||||
return model;
|
||||
return new UserCredentialModel("", SECRET, UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
public static boolean isOtp(String type) {
|
||||
return TOTP.equals(type) || HOTP.equals(type);
|
||||
@Override
|
||||
public String getCredentialId() {
|
||||
return credentialId;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
@Override
|
||||
public String getChallengeResponse() {
|
||||
return challengeResponse;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
public boolean isAdminRequest() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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 +
|
||||
" }";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 + '\'' +
|
||||
" }";
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* 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.
|
||||
* See the License for the specific language governing permissions and
|
||||
* 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>
|
||||
*/
|
||||
public class PasswordUserCredentialModel extends UserCredentialModel {
|
||||
public class WebAuthnSecretData {
|
||||
|
||||
// True if we have password-update request triggered by admin, not by user himself
|
||||
private static final String ADMIN_REQUEST = "adminRequest";
|
||||
|
||||
public boolean isAdminRequest() {
|
||||
Boolean b = (Boolean) this.notes.get(ADMIN_REQUEST);
|
||||
return b!=null && b;
|
||||
@JsonCreator
|
||||
public WebAuthnSecretData() {
|
||||
}
|
||||
|
||||
public void setAdminRequest(boolean adminRequest) {
|
||||
this.notes.put(ADMIN_REQUEST, adminRequest);
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WebAuthnSecretData {}";
|
||||
}
|
||||
}
|
|
@ -46,13 +46,13 @@ public interface WebAuthnConstants {
|
|||
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";
|
||||
|
||||
// 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";
|
||||
|
||||
// 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";
|
||||
|
||||
// key for storing onto AuthenticationSessionModel's Attribute challenge generated by RP(keycloak)
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAu
|
|||
import org.keycloak.authentication.authenticators.client.ClientAuthUtil;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
|
@ -240,6 +241,10 @@ public class AuthenticationProcessor {
|
|||
return request;
|
||||
}
|
||||
|
||||
public String getFlowPath() {
|
||||
return flowPath;
|
||||
}
|
||||
|
||||
public void setAutheticatedUser(UserModel user) {
|
||||
UserModel previousUser = getAuthenticationSession().getAuthenticatedUser();
|
||||
if (previousUser != null && !user.getId().equals(previousUser.getId()))
|
||||
|
@ -276,6 +281,8 @@ public class AuthenticationProcessor {
|
|||
List<AuthenticationExecutionModel> currentExecutions;
|
||||
FormMessage errorMessage;
|
||||
FormMessage successMessage;
|
||||
String selectedCredentialId;
|
||||
List<AuthenticationSelectionOption> authenticationSelections;
|
||||
|
||||
private Result(AuthenticationExecutionModel execution, Authenticator authenticator, List<AuthenticationExecutionModel> currentExecutions) {
|
||||
this.execution = execution;
|
||||
|
@ -393,6 +400,26 @@ public class AuthenticationProcessor {
|
|||
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
|
||||
public void clearUser() {
|
||||
clearAuthenticatedUser();
|
||||
|
@ -423,6 +450,11 @@ public class AuthenticationProcessor {
|
|||
return AuthenticationProcessor.this.getAuthenticationSession();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFlowPath() {
|
||||
return AuthenticationProcessor.this.getFlowPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientConnection getConnection() {
|
||||
return AuthenticationProcessor.this.getConnection();
|
||||
|
@ -483,6 +515,7 @@ public class AuthenticationProcessor {
|
|||
String accessCode = generateAccessCode();
|
||||
URI action = getActionUrl(accessCode);
|
||||
LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
|
||||
.setAuthContext(this)
|
||||
.setAuthenticationSession(getAuthenticationSession())
|
||||
.setUser(getUser())
|
||||
.setActionUri(action)
|
||||
|
@ -653,9 +686,52 @@ public class AuthenticationProcessor {
|
|||
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) {
|
||||
if (failure instanceof AuthenticationFlowException) {
|
||||
AuthenticationFlowException e = (AuthenticationFlowException) failure;
|
||||
if (e.getAfeList() != null && !e.getAfeList().isEmpty()){
|
||||
return handleBrowserExceptionList(e);
|
||||
}
|
||||
|
||||
if (e.getError() == AuthenticationFlowError.INVALID_USER) {
|
||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||
|
@ -715,6 +791,11 @@ public class AuthenticationProcessor {
|
|||
event.error(Errors.DISPLAY_UNSUPPORTED);
|
||||
if (e.getResponse() != null) return e.getResponse();
|
||||
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 {
|
||||
ServicesLogger.LOGGER.failedAuthentication(e);
|
||||
event.error(Errors.INVALID_USER_CREDENTIALS);
|
||||
|
@ -786,7 +867,11 @@ public class AuthenticationProcessor {
|
|||
AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null);
|
||||
try {
|
||||
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) {
|
||||
return handleClientAuthException(e);
|
||||
}
|
||||
|
@ -875,6 +960,9 @@ public class AuthenticationProcessor {
|
|||
if (authenticationSession.getAuthenticatedUser() == null) {
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER);
|
||||
}
|
||||
if (!authenticationFlow.isSuccessful()) {
|
||||
throw new AuthenticationFlowException(authenticationFlow.getFlowExceptions());
|
||||
}
|
||||
return authenticationComplete();
|
||||
}
|
||||
|
||||
|
@ -912,7 +1000,10 @@ public class AuthenticationProcessor {
|
|||
if (authenticationSession.getAuthenticatedUser() == null) {
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER);
|
||||
}
|
||||
return challenge;
|
||||
if (!authenticationFlow.isSuccessful()) {
|
||||
throw new AuthenticationFlowException(authenticationFlow.getFlowExceptions());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// May create userSession too
|
||||
|
|
|
@ -42,6 +42,8 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
|
|||
AuthenticationProcessor processor;
|
||||
AuthenticationFlowModel flow;
|
||||
|
||||
private boolean success;
|
||||
|
||||
public ClientAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) {
|
||||
this.processor = processor;
|
||||
this.flow = flow;
|
||||
|
@ -84,6 +86,8 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
|
|||
|
||||
if (!context.getStatus().equals(FlowStatus.SUCCESS)) {
|
||||
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());
|
||||
|
@ -176,4 +180,9 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
|
|||
|
||||
return result.getChallenge();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccessful() {
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,15 +19,28 @@ package org.keycloak.authentication;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
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.AuthenticationFlowModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.util.AuthenticationFlowHistoryHelper;
|
||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedHashMap;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
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>
|
||||
|
@ -35,19 +48,16 @@ import java.util.List;
|
|||
*/
|
||||
public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
||||
private static final Logger logger = Logger.getLogger(DefaultAuthenticationFlow.class);
|
||||
Response alternativeChallenge = null;
|
||||
AuthenticationExecutionModel challengedAlternativeExecution = null;
|
||||
boolean alternativeSuccessful = false;
|
||||
List<AuthenticationExecutionModel> executions;
|
||||
Iterator<AuthenticationExecutionModel> executionIterator;
|
||||
AuthenticationProcessor processor;
|
||||
AuthenticationFlowModel flow;
|
||||
private final List<AuthenticationExecutionModel> executions;
|
||||
private final AuthenticationProcessor processor;
|
||||
private final AuthenticationFlowModel flow;
|
||||
private boolean successful;
|
||||
private List<AuthenticationFlowException> afeList = new ArrayList<>();
|
||||
|
||||
public DefaultAuthenticationFlow(AuthenticationProcessor processor, AuthenticationFlowModel flow) {
|
||||
this.processor = processor;
|
||||
this.flow = flow;
|
||||
this.executions = processor.getRealm().getAuthenticationExecutions(flow.getId());
|
||||
this.executionIterator = executions.iterator();
|
||||
}
|
||||
|
||||
protected boolean isProcessed(AuthenticationExecutionModel model) {
|
||||
|
@ -63,7 +73,6 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
String display = processor.getAuthenticationSession().getAuthNote(OAuth2Constants.DISPLAY);
|
||||
if (display == null) return factory.create(processor.getSession());
|
||||
|
||||
|
||||
if (factory instanceof DisplayTypeAuthenticatorFactory) {
|
||||
Authenticator authenticator = ((DisplayTypeAuthenticatorFactory) factory).createDisplay(processor.getSession(), display);
|
||||
if (authenticator != null) return authenticator;
|
||||
|
@ -73,167 +82,456 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
processor.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED,
|
||||
ConsoleDisplayMode.browserContinue(processor.getSession(), processor.getRefreshUrl(true).toString()));
|
||||
|
||||
} else {
|
||||
return factory.create(processor.getSession());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Response processAction(String actionExecution) {
|
||||
logger.debugv("processAction: {0}", actionExecution);
|
||||
while (executionIterator.hasNext()) {
|
||||
AuthenticationExecutionModel model = executionIterator.next();
|
||||
logger.debugv("check: {0} requirement: {1}", model.getAuthenticator(), model.getRequirement().toString());
|
||||
if (isProcessed(model)) {
|
||||
logger.debug("execution is processed");
|
||||
if (!alternativeSuccessful && model.isAlternative() && processor.isSuccessful(model))
|
||||
alternativeSuccessful = true;
|
||||
continue;
|
||||
|
||||
if (actionExecution == null || actionExecution.isEmpty()) {
|
||||
throw new AuthenticationFlowException("action is not in current execution", AuthenticationFlowError.INTERNAL_ERROR);
|
||||
}
|
||||
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()) {
|
||||
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);
|
||||
Response flowChallenge = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
Response flowChallenge = authenticationFlow.processAction(actionExecution);
|
||||
if (flowChallenge == null) {
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
|
||||
if (model.isAlternative()) alternativeSuccessful = true;
|
||||
checkAndValidateParentFlow(model);
|
||||
return processFlow();
|
||||
} else {
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
|
||||
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);
|
||||
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
|
||||
result.setAuthenticationSelections(createAuthenticationSelectionList(model));
|
||||
|
||||
result.setSelectedCredentialId(selectedCredentialId);
|
||||
|
||||
logger.debugv("action: {0}", model.getAuthenticator());
|
||||
authenticator.action(result);
|
||||
Response response = processResult(result, true);
|
||||
if (response == null) {
|
||||
processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
|
||||
checkAndValidateParentFlow(model);
|
||||
return processFlow();
|
||||
} 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
|
||||
public Response 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)) {
|
||||
logger.debug("execution is processed");
|
||||
if (!alternativeSuccessful && model.isAlternative() && processor.isSuccessful(model))
|
||||
alternativeSuccessful = true;
|
||||
//separate flow elements into required and alternative elements
|
||||
List<AuthenticationExecutionModel> requiredList = new ArrayList<>();
|
||||
List<AuthenticationExecutionModel> alternativeList = new ArrayList<>();
|
||||
|
||||
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;
|
||||
}
|
||||
if (model.isAlternative() && alternativeSuccessful) {
|
||||
logger.debug("Skip alternative execution");
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
|
||||
continue;
|
||||
Response response = processSingleFlowExecutionModel(required, null, true);
|
||||
requiredElementsSuccessful &= processor.isSuccessful(required) || isSetupRequired(required);
|
||||
if (response != null) {
|
||||
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 {
|
||||
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) {
|
||||
if (model.isAlternative()) {
|
||||
logger.debug("Thrown exception in alternative Subflow. Ignoring Subflow");
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
|
||||
continue;
|
||||
} else {
|
||||
throw afe;
|
||||
//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
|
||||
afeList.add(afe);
|
||||
processor.getAuthenticationSession().setExecutionStatus(alternative.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
successful = requiredElementsSuccessful;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (flowChallenge == null) {
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
|
||||
if (model.isAlternative()) alternativeSuccessful = true;
|
||||
continue;
|
||||
} else {
|
||||
if (model.isAlternative()) {
|
||||
alternativeChallenge = flowChallenge;
|
||||
challengedAlternativeExecution = model;
|
||||
} else if (model.isRequired()) {
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
|
||||
return flowChallenge;
|
||||
} else if (model.isOptional()) {
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
|
||||
continue;
|
||||
} else {
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
|
||||
continue;
|
||||
}
|
||||
return flowChallenge;
|
||||
}
|
||||
/**
|
||||
* Checks if the conditional subflow passed in parameter is disabled.
|
||||
* @param model
|
||||
* @return
|
||||
*/
|
||||
private boolean isConditionalSubflowDisabled(AuthenticationExecutionModel model) {
|
||||
if (model == null || !model.isAuthenticatorFlow() || !model.isConditional()) {
|
||||
return false;
|
||||
};
|
||||
List<AuthenticationExecutionModel> modelList = processor.getRealm().getAuthenticationExecutions(model.getFlowId());
|
||||
List<AuthenticationExecutionModel> conditionalAuthenticatorList = modelList.stream()
|
||||
.filter(this::isConditionalAuthenticator)
|
||||
.collect(Collectors.toList());
|
||||
return conditionalAuthenticatorList.isEmpty() || conditionalAuthenticatorList.stream().anyMatch(m-> conditionalNotMatched(m, modelList));
|
||||
}
|
||||
|
||||
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());
|
||||
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?");
|
||||
}
|
||||
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);
|
||||
logger.debugv("authenticator: {0}", factory.getId());
|
||||
UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
|
||||
|
||||
if (authenticator.requiresUser() && authUser == null) {
|
||||
if (alternativeChallenge != null) {
|
||||
processor.getAuthenticationSession().setExecutionStatus(challengedAlternativeExecution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
|
||||
return alternativeChallenge;
|
||||
//If executions are alternative, get the actual execution to show based on user preference
|
||||
List<AuthenticationSelectionOption> selectionOptions = createAuthenticationSelectionList(model);
|
||||
if (!selectionOptions.isEmpty() && calledFromFlow) {
|
||||
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);
|
||||
}
|
||||
boolean configuredFor = false;
|
||||
if (authenticator.requiresUser() && authUser != null) {
|
||||
configuredFor = authenticator.configuredFor(processor.getSession(), processor.getRealm(), authUser);
|
||||
if (!configuredFor) {
|
||||
if (model.isRequired()) {
|
||||
if (factory.isUserSetupAllowed()) {
|
||||
if (!authenticator.configuredFor(processor.getSession(), processor.getRealm(), authUser)) {
|
||||
if (factory.isUserSetupAllowed() && model.isRequired() && authenticator.areRequiredActionsEnabled(processor.getSession(), processor.getRealm())) {
|
||||
//This means that having even though the user didn't validate the
|
||||
logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId());
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED);
|
||||
authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser());
|
||||
continue;
|
||||
return null;
|
||||
} else {
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
|
||||
}
|
||||
} else if (model.isOptional()) {
|
||||
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
|
||||
continue;
|
||||
throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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());
|
||||
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) {
|
||||
case SUCCESS:
|
||||
logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator());
|
||||
if (isAction) {
|
||||
new AuthenticationFlowHistoryHelper(processor).pushExecution(execution.getId());
|
||||
}
|
||||
|
||||
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
|
||||
if (execution.isAlternative()) alternativeSuccessful = true;
|
||||
return null;
|
||||
case FAILED:
|
||||
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());
|
||||
throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage());
|
||||
case FORCE_CHALLENGE:
|
||||
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
|
||||
return sendChallenge(result, execution);
|
||||
case CHALLENGE:
|
||||
logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator());
|
||||
if (execution.isRequired()) {
|
||||
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
|
||||
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:
|
||||
logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator());
|
||||
processor.logFailure();
|
||||
|
@ -286,7 +570,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
return sendChallenge(result, execution);
|
||||
case ATTEMPTED:
|
||||
logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator());
|
||||
if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
|
||||
if (execution.isRequired()) {
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS);
|
||||
}
|
||||
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
|
||||
|
@ -306,5 +590,13 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
return result.getChallenge();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isSuccessful() {
|
||||
return successful;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuthenticationFlowException> getFlowExceptions(){
|
||||
return afeList;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
package org.keycloak.authentication;
|
||||
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
|
@ -203,7 +202,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
|
|||
} else {
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
|
||||
}
|
||||
} else if (formActionExecution.isOptional()) {
|
||||
} else if (formActionExecution.isConditional()) {
|
||||
executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
|
||||
continue;
|
||||
}
|
||||
|
@ -300,4 +299,9 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
|
|||
FormContext context = new FormContextImpl(formExecution);
|
||||
return formAuthenticator.render(context, form);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccessful() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ public class IdpAutoLinkAuthenticator extends AbstractIdpAuthenticator {
|
|||
|
||||
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());
|
||||
|
||||
context.setUser(existingUser);
|
||||
|
|
|
@ -68,10 +68,6 @@ public class IdpAutoLinkAuthenticatorFactory implements AuthenticatorFactory {
|
|||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
|
@ -80,12 +76,12 @@ public class IdpAutoLinkAuthenticatorFactory implements AuthenticatorFactory {
|
|||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Automatically link brokered account";
|
||||
return "Automatically set existing user";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Automatically link brokered account without any verification";
|
||||
return "Automatically set existing user to authentication context without any verification";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -70,9 +70,6 @@ public class IdpConfirmLinkAuthenticatorFactory implements AuthenticatorFactory
|
|||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
|
|
|
@ -99,19 +99,20 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
|
|||
|
||||
// Set duplicated user, so next authenticators can deal with it
|
||||
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()
|
||||
.setError(Messages.FEDERATED_IDENTITY_EXISTS, duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue())
|
||||
.createErrorPage(Response.Status.CONFLICT);
|
||||
context.challenge(challengeResponse);
|
||||
|
||||
if (context.getExecution().isRequired()) {
|
||||
context.getEvent()
|
||||
.user(duplication.getExistingUserId())
|
||||
.detail("existing_" + duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue())
|
||||
.removeDetail(Details.AUTH_METHOD)
|
||||
.removeDetail(Details.AUTH_TYPE)
|
||||
.error(Errors.FEDERATED_IDENTITY_EXISTS);
|
||||
} else {
|
||||
context.attempted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,11 +73,6 @@ public class IdpCreateUserIfUniqueAuthenticatorFactory implements AuthenticatorF
|
|||
return true;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
|
|
|
@ -70,10 +70,6 @@ public class IdpEmailVerificationAuthenticatorFactory implements AuthenticatorFa
|
|||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
|
|
|
@ -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());
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -75,10 +75,6 @@ public class IdpReviewProfileAuthenticatorFactory implements AuthenticatorFactor
|
|||
return true;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
|
|
|
@ -43,7 +43,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm {
|
|||
|
||||
return setupForm(context, formData, existingUser)
|
||||
.setStatus(Response.Status.OK)
|
||||
.createLogin();
|
||||
.createLoginUsernamePassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -21,7 +21,6 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.authentication.AbstractFormAuthenticator;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.credential.CredentialInput;
|
||||
import org.keycloak.credential.hash.PasswordHashProvider;
|
||||
import org.keycloak.events.Details;
|
||||
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.Response;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
LoginFormsProvider form = context.form();
|
||||
LoginFormsProvider form = context.form()
|
||||
.setExecution(context.getExecution().getId());
|
||||
if (error != null) form.setError(error);
|
||||
|
||||
return createLoginForm(form);
|
||||
}
|
||||
|
||||
protected Response createLoginForm(LoginFormsProvider form) {
|
||||
return form.createLogin();
|
||||
return form.createLoginUsernamePassword();
|
||||
}
|
||||
|
||||
protected String tempDisabledError() {
|
||||
|
@ -75,7 +72,7 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
|||
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
|
||||
context.getEvent().error(eventError);
|
||||
Response challengeResponse = context.form()
|
||||
.setError(loginFormError).createLogin();
|
||||
.setError(loginFormError).createLoginUsernamePassword();
|
||||
context.failureChallenge(authenticatorError, 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) {
|
||||
dummyHash(context);
|
||||
context.getEvent().error(Errors.USER_NOT_FOUND);
|
||||
Response challengeResponse = challenge(context, Messages.INVALID_USER);
|
||||
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean enabledUser(AuthenticationFlowContext context, UserModel user) {
|
||||
|
@ -119,8 +114,6 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
|||
context.getEvent().user(user);
|
||||
context.getEvent().error(Errors.USER_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);
|
||||
return false;
|
||||
}
|
||||
|
@ -128,13 +121,26 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
if (username == null) {
|
||||
context.getEvent().error(Errors.USER_NOT_FOUND);
|
||||
Response challengeResponse = challenge(context, Messages.INVALID_USER);
|
||||
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove leading and trailing whitespace
|
||||
|
@ -155,22 +161,17 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
|||
} else {
|
||||
setDuplicateUserChallenge(context, Errors.USERNAME_IN_USE, Messages.USERNAME_EXISTS, AuthenticationFlowError.INVALID_USER);
|
||||
}
|
||||
|
||||
return false;
|
||||
return user;
|
||||
}
|
||||
|
||||
if (invalidUser(context, user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validatePassword(context, user, inputData)) {
|
||||
return false;
|
||||
testInvalidUser(context, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private boolean validateUser(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData) {
|
||||
if (!enabledUser(context, user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String rememberMe = inputData.getFirst("rememberMe");
|
||||
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
|
||||
if (remember) {
|
||||
|
@ -184,7 +185,10 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
|||
}
|
||||
|
||||
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);
|
||||
if (password == null || password.isEmpty()) {
|
||||
context.getEvent().user(user);
|
||||
|
@ -197,27 +201,27 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
|||
|
||||
if (isTemporarilyDisabledByBruteForce(context, user)) return false;
|
||||
|
||||
credentials.add(UserCredentialModel.password(password));
|
||||
if (context.getSession().userCredentialManager().isValid(context.getRealm(), user, credentials)) {
|
||||
if (password != null && !password.isEmpty() && context.getSession().userCredentialManager().isValid(context.getRealm(), user, UserCredentialModel.password(password))) {
|
||||
return true;
|
||||
} else {
|
||||
context.getEvent().user(user);
|
||||
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
||||
Response challengeResponse = challenge(context, Messages.INVALID_USER);
|
||||
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
|
||||
if (clearUser) {
|
||||
context.clearUser();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected boolean isTemporarilyDisabledByBruteForce(AuthenticationFlowContext context, UserModel user) {
|
||||
if (context.getRealm().isBruteForceProtected()) {
|
||||
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) {
|
||||
context.getEvent().user(user);
|
||||
context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import org.keycloak.authentication.AuthenticatorFactory;
|
|||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -52,11 +52,6 @@ public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFact
|
|||
|
||||
public static final ConditionalOtpFormAuthenticator SINGLETON = new ConditionalOtpFormAuthenticator();
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.OPTIONAL,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return SINGLETON;
|
||||
|
@ -84,7 +79,7 @@ public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFact
|
|||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return UserCredentialModel.TOTP;
|
||||
return OTPCredentialModel.TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -80,8 +80,6 @@ public class CookieAuthenticatorFactory implements AuthenticatorFactory, Display
|
|||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
|
|
|
@ -39,7 +39,7 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
|
|||
public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
|
||||
|
||||
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";
|
||||
|
|
|
@ -20,34 +20,43 @@ package org.keycloak.authentication.authenticators.browser;
|
|||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
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.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
|
||||
public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator, CredentialValidator<OTPCredentialProvider> {
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
validateOTP(context);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
Response challengeResponse = challenge(context, null);
|
||||
context.challenge(challengeResponse);
|
||||
}
|
||||
|
||||
|
||||
public void validateOTP(AuthenticationFlowContext context) {
|
||||
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
|
||||
if (inputData.containsKey("cancel")) {
|
||||
|
@ -55,20 +64,29 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
|
|||
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();
|
||||
if (!enabledUser(context, userModel)) {
|
||||
// error in context is set in enabledUser/isTemporarilyDisabledByBruteForce
|
||||
return;
|
||||
}
|
||||
|
||||
String password = inputData.getFirst(CredentialRepresentation.TOTP);
|
||||
if (password == null) {
|
||||
if (otp == null) {
|
||||
Response challengeResponse = challenge(context,null);
|
||||
context.challenge(challengeResponse);
|
||||
return;
|
||||
}
|
||||
boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), userModel,
|
||||
UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), password));
|
||||
boolean valid = getCredentialProvider(context.getSession()).isValid(context.getRealm(),context.getUser(),
|
||||
new UserCredentialModel(credentialId, getCredentialProvider(context.getSession()).getType(), otp));
|
||||
if (!valid) {
|
||||
context.getEvent().user(userModel)
|
||||
.error(Errors.INVALID_USER_CREDENTIALS);
|
||||
|
@ -96,7 +114,7 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
|
|||
|
||||
@Override
|
||||
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
|
||||
|
@ -104,11 +122,20 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
|
|||
if (!user.getRequiredActions().contains(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
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public OTPCredentialProvider getCredentialProvider(KeycloakSession session) {
|
||||
return (OTPCredentialProvider)session.getProvider(CredentialProvider.class, "keycloak-otp");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import org.keycloak.authentication.authenticators.console.ConsoleOTPFormAuthenti
|
|||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -74,7 +74,7 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory, Displa
|
|||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return UserCredentialModel.TOTP;
|
||||
return OTPCredentialModel.TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -87,11 +87,6 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory, Displa
|
|||
return true;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.OPTIONAL,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -51,7 +51,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En
|
|||
|
||||
static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.OPTIONAL,
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED};
|
||||
|
||||
static final ScriptBasedAuthenticator SINGLETON = new ScriptBasedAuthenticator();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue