KEYCLOAK-11745 Multi-factor authentication (#6459)

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

View file

@ -25,14 +25,38 @@ package org.keycloak.common.util;
public class Base64Url {
public 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;
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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.

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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

View file

@ -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;
}
@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());
if (!input.getType().equals(getType())) {
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;
String challengeResponse = input.getChallengeResponse();
if (challengeResponse == null) {
return false;
}
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;
}
}

View file

@ -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

View file

@ -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();
}

View file

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

View file

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

View file

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

View file

@ -34,6 +34,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.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();

View file

@ -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();

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -26,11 +26,13 @@ import org.keycloak.federation.sssd.api.Sssd;
import org.keycloak.federation.sssd.api.Sssd.User;
import org.keycloak.federation.sssd.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);
}

View file

@ -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

View file

@ -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();

View file

@ -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

View file

@ -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;
}
}

View file

@ -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)
@ -498,7 +491,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
}
return users;
}
@Override
public List<UserModel> getRoleMembers(RealmModel realm, RoleModel role) {
TypedQuery<UserEntity> query = em.createNamedQuery("usersInRole", UserEntity.class);
@ -542,11 +535,11 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
query.setParameter("email", email.toLowerCase());
query.setParameter("realmId", realm.getId());
List<UserEntity> results = query.getResultList();
if (results.isEmpty()) return null;
ensureEmailConstraint(results, realm);
return new UserAdapter(session, realm, em, results.get(0));
}
@ -659,7 +652,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
}
return users;
}
@Override
public List<UserModel> getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
TypedQuery<UserEntity> query = em.createNamedQuery("usersInRole", UserEntity.class);
@ -756,7 +749,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
Root from1 = subquery1.from(ResourceEntity.class);
List<Predicate> subs = new ArrayList<>();
Expression<String> groupId = from.get("groupId");
subs.add(builder.like(from1.get("name"), builder.concat("group.resource.", groupId)));
@ -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,53 +896,47 @@ 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());
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 {
userEntity = em.getReference(UserEntity.class, user.getId());
TypedQuery<CredentialEntity> query = em.createNamedQuery("credentialByUserAndType", CredentialEntity.class)
.setParameter("type", type)
.setParameter("user", userEntity);
results = query.getResultList();
return credentialStore.getStoredCredentialsByType(realm, user, type);
}
List<CredentialModel> rtn = new LinkedList<>();
for (CredentialEntity entity : results) {
rtn.add(toModel(entity));
}
return rtn;
}
@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.
protected void ensureEmailConstraint(List<UserEntity> users, RealmModel realm) {
UserEntity user = users.get(0);
if (users.size() > 1) {
// Realm settings have been changed from allowing duplicate emails to not allowing them
// but duplicates haven't been removed.
throw new ModelDuplicateException("Multiple users with email '" + user.getEmail() + "' exist in Keycloak.");
}
if (realm.isDuplicateEmailsAllowed()) {
return;
}
if (user.getEmail() != null && !user.getEmail().equals(user.getEmailConstraint())) {
// Realm settings have been changed from allowing duplicate emails to not allowing them.
// We need to update the email constraint to reflect this change in the user entities.
user.setEmailConstraint(user.getEmail());
em.persist(user);
}
}
}
private UserEntity userInEntityManagerContext(String id) {

View file

@ -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();
}

View file

@ -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 {

View file

@ -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;

View file

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

View file

@ -25,6 +25,7 @@ import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.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

View file

@ -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();

View file

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

View file

@ -17,6 +17,8 @@
package org.keycloak.storage.jpa.entity;
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,19 +58,21 @@ 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;
@Column(name="USER_ID")
protected String userId;
@ -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;

View file

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

View file

@ -63,4 +63,5 @@
<include file="META-INF/jpa-changelog-4.7.0.xml"/>
<include file="META-INF/jpa-changelog-4.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>

View file

@ -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>

View file

@ -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 -->

View file

@ -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();
}
}

View file

@ -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
*

View file

@ -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;
}
}

View file

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

View file

@ -19,9 +19,13 @@ package org.keycloak.authentication;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.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;
}
}

View file

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

View file

@ -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
*

View file

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

View file

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

View file

@ -18,9 +18,8 @@
package org.keycloak.credential.hash;
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 {

View file

@ -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;

View file

@ -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);
}

View file

@ -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;
public class MigrateTo8_0_0 implements Migration {
/**
* @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);
}
}
}

View file

@ -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";

View file

@ -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;
}

View file

@ -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")) {
execution.setRequirement(browserExecution.getRequirement());
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,27 +705,42 @@ 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.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("basic-auth-otp");
execution.setPriority(30);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId());
execution.setParentFlow(authType.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("auth-spnego");
execution.setPriority(40);
execution.setPriority(30);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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()) {
user.addRequiredAction(UserModel.RequiredAction.valueOf(requiredAction.toUpperCase()));
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);
}
}
}
// Detect if it is "plain-text" or "hashed" representation and update model according to it
private static void updateCredential(KeycloakSession session, RealmModel realm, UserModel user, CredentialRepresentation cred, boolean adminRequest) {
if (cred.getValue() != null) {
PasswordUserCredentialModel plainTextCred = convertCredential(cred);
plainTextCred.setAdminRequest(adminRequest);
//if called from import we need to change realm in context to load password policies from the newly created realm
RealmModel origRealm = session.getContext().getRealm();
try {
session.getContext().setRealm(realm);
session.userCredentialManager().updateCredential(realm, user, plainTextCred);
} 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");
if (cred.getId() != null && session.userCredentialManager().getStoredCredentialById(realm, user, cred.getId()) != null) {
continue;
}
if (cred.getValue() != null && !cred.getValue().isEmpty()) {
RealmModel origRealm = session.getContext().getRealm();
try {
session.getContext().setRealm(realm);
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 {
hashedCred.setAlgorithm(cred.getAlgorithm());
session.userCredentialManager().createCredentialThroughProvider(realm, user, toModel(cred));
}
} else {
if (UserCredentialModel.PASSWORD.equals(cred.getType()) || UserCredentialModel.PASSWORD_HISTORY.equals(cred.getType())) {
hashedCred.setAlgorithm("pbkdf2");
} else if (UserCredentialModel.isOtp(cred.getType())) {
hashedCred.setAlgorithm(HmacOTP.HMAC_SHA1);
}
}
if (cred.getPeriod() != null) hashedCred.setPeriod(cred.getPeriod());
if (cred.getDigits() == null && UserCredentialModel.isOtp(cred.getType())) {
hashedCred.setDigits(6);
}
if (cred.getPeriod() == null && UserCredentialModel.TOTP.equals(cred.getType())) {
hashedCred.setPeriod(30);
}
hashedCred.setCreatedDate(cred.getCreatedDate());
session.userCredentialManager().createCredential(realm, user, hashedCred);
UserCache userCache = session.userCache();
if (userCache != null) {
userCache.evict(realm, user);
}
}
}
public static PasswordUserCredentialModel convertCredential(CredentialRepresentation cred) {
PasswordUserCredentialModel credential = new PasswordUserCredentialModel();
credential.setType(cred.getType());
credential.setValue(cred.getValue());
return credential;
}
public static CredentialModel toModel(CredentialRepresentation cred) {
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());
model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement()));
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;
}

View file

@ -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,22 +53,27 @@ 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);
List<CredentialModel> recentPasswordHistory = getRecent(passwordHistory, passwordHistoryPolicyValue - 1);
for (CredentialModel cred : recentPasswordHistory) {
PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm());
if (hash.verify(password, cred)) {
return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue);
}
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) {
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;

View file

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

View file

@ -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);
}

View file

@ -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() {

View file

@ -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));
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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");

View file

@ -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.

View file

@ -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 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);
public boolean isAdminRequest() {
return adminRequest;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 {}";
}
}

View file

@ -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)

View file

@ -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

View file

@ -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;
}
}

View file

@ -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,9 +73,8 @@ 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);
Authenticator authenticator = ((DisplayTypeAuthenticatorFactory) factory).createDisplay(processor.getSession(), display);
if (authenticator != null) return authenticator;
}
// todo create a provider for handling lack of display support
@ -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 (model.isAuthenticatorFlow()) {
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;
}
}
if (flowChallenge == null) {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
if (model.isAlternative()) alternativeSuccessful = true;
return processFlow();
} else {
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?");
}
Authenticator authenticator = createAuthenticator(factory);
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
logger.debugv("action: {0}", model.getAuthenticator());
authenticator.action(result);
Response response = processResult(result, true);
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);
}
}
throw new AuthenticationFlowException("action is not in current execution", AuthenticationFlowError.INTERNAL_ERROR);
// 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 = authenticationFlow.processAction(actionExecution);
if (flowChallenge == null) {
checkAndValidateParentFlow(model);
return processFlow();
} else {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return flowChallenge;
}
}
//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);
}
}
@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;
}
}
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;
//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;
}
AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator());
/**
* 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 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 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;
}
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()) {
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());
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());
return null;
} else {
throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
}
}
}
logger.debugv("invoke authenticator.authenticate: {0}", factory.getId());
authenticator.authenticate(context);
return processResult(context, false);
}
/**
* 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;
} else {
throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
}
} else if (model.isOptional()) {
processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
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);
}
}
}
// 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;
//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 null;
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:
case 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;
}
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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

View file

@ -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() {

View file

@ -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());
Response challengeResponse = context.form()
.setError(Messages.FEDERATED_IDENTITY_EXISTS, duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue())
.createErrorPage(Response.Status.CONFLICT);
context.challenge(challengeResponse);
//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);
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();
}
}
}

View file

@ -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;

View file

@ -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() {

View file

@ -132,6 +132,10 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
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();
}

View file

@ -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;

View file

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

View file

@ -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) {
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);
context.clearUser();
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;
}

View file

@ -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

View file

@ -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;

View file

@ -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";

View file

@ -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) {
Response challengeResponse = challenge(context, 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");
}
}

View file

@ -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;

View file

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

View file

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

View file

@ -51,7 +51,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En
static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
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