options) {
+ if (options != null) {
+ Object providedEncoder = options.get(PASSWORD_ENCODER);
+
+ if (providedEncoder != null) {
+ if (PasswordEncoder.class.isInstance(providedEncoder)) {
+ this.passwordEncoder = (PasswordEncoder) providedEncoder;
+ } else {
+ throw new IllegalArgumentException("The password encoder [" + providedEncoder
+ + "] must be an instance of " + PasswordEncoder.class.getName());
+ }
+ }
+ }
+ }
+
+ public Credentials.Status validate(NoSQL noSQL, UserData user, String passwordToValidate) {
+ Credentials.Status status = Credentials.Status.INVALID;
+
+ user = noSQL.loadObject(UserData.class, user.getId());
+
+ // If the user for the provided username cannot be found we fail validation
+ if (user != null) {
+ if (user.isEnabled()) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", user.getId())
+ .build();
+ PasswordData passwordData = noSQL.loadSingleObject(PasswordData.class, query);
+
+ // If the stored hash is null we automatically fail validation
+ if (passwordData != null) {
+ // TODO: Status.INVALID should have bigger priority than Status.EXPIRED?
+ if (!isCredentialExpired(passwordData.getExpiryDate())) {
+
+ boolean matches = this.passwordEncoder.verify(saltPassword(passwordToValidate, passwordData.getSalt()), passwordData.getEncodedHash());
+
+ if (matches) {
+ status = Credentials.Status.VALID;
+ }
+ } else {
+ status = Credentials.Status.EXPIRED;
+ }
+ }
+ } else {
+ status = Credentials.Status.ACCOUNT_DISABLED;
+ }
+ }
+
+ return status;
+ }
+
+ public void update(NoSQL noSQL, UserData user, String password,
+ Date effectiveDate, Date expiryDate) {
+
+ // Delete existing password of user
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", user.getId())
+ .build();
+ noSQL.removeObjects(PasswordData.class, query);
+
+ PasswordData passwordData = new PasswordData();
+
+ String passwordSalt = generateSalt();
+
+ passwordData.setSalt(passwordSalt);
+ passwordData.setEncodedHash(this.passwordEncoder.encode(saltPassword(password, passwordSalt)));
+
+ if (effectiveDate != null) {
+ passwordData.setEffectiveDate(effectiveDate);
+ }
+
+ passwordData.setExpiryDate(expiryDate);
+
+ passwordData.setUserId(user.getId());
+
+ noSQL.saveObject(passwordData);
+ }
+
+ /**
+ *
+ * Salt the give rawPassword
with the specified salt
value.
+ *
+ *
+ * @param rawPassword
+ * @param salt
+ * @return
+ */
+ private String saltPassword(String rawPassword, String salt) {
+ return salt + rawPassword;
+ }
+
+ /**
+ *
+ * Generates a random string to be used as a salt for passwords.
+ *
+ *
+ * @return
+ */
+ private String generateSalt() {
+ // TODO: always returns same salt (See https://issues.jboss.org/browse/PLINK-258)
+ /*SecureRandom pseudoRandom = null;
+
+ try {
+ pseudoRandom = SecureRandom.getInstance(DEFAULT_SALT_ALGORITHM);
+ pseudoRandom.setSeed(1024);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Error getting SecureRandom instance: " + DEFAULT_SALT_ALGORITHM, e);
+ }
+
+ return String.valueOf(pseudoRandom.nextLong());*/
+ return UUID.randomUUID().toString();
+ }
+
+ public static boolean isCredentialExpired(Date expiryDate) {
+ return expiryDate != null && new Date().compareTo(expiryDate) > 0;
+ }
+
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/TOTPCredentialHandler.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/TOTPCredentialHandler.java
new file mode 100644
index 0000000000..b8f02e7d6e
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/TOTPCredentialHandler.java
@@ -0,0 +1,138 @@
+package org.keycloak.models.mongo.keycloak.credentials;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.keycloak.data.UserData;
+import org.keycloak.models.mongo.keycloak.data.credentials.OTPData;
+import org.picketlink.idm.credential.Credentials;
+import org.picketlink.idm.credential.util.TimeBasedOTP;
+
+import static org.picketlink.common.util.StringUtil.isNullOrEmpty;
+import static org.picketlink.idm.credential.util.TimeBasedOTP.DEFAULT_ALGORITHM;
+import static org.picketlink.idm.credential.util.TimeBasedOTP.DEFAULT_DELAY_WINDOW;
+import static org.picketlink.idm.credential.util.TimeBasedOTP.DEFAULT_INTERVAL_SECONDS;
+import static org.picketlink.idm.credential.util.TimeBasedOTP.DEFAULT_NUMBER_DIGITS;
+
+/**
+ * Defacto forked from {@link org.picketlink.idm.credential.handler.TOTPCredentialHandler}
+ *
+ * @author Marek Posolda
+ */
+public class TOTPCredentialHandler extends PasswordCredentialHandler {
+
+ public static final String ALGORITHM = "ALGORITHM";
+ public static final String INTERVAL_SECONDS = "INTERVAL_SECONDS";
+ public static final String NUMBER_DIGITS = "NUMBER_DIGITS";
+ public static final String DELAY_WINDOW = "DELAY_WINDOW";
+ public static final String DEFAULT_DEVICE = "DEFAULT_DEVICE";
+
+ private TimeBasedOTP totp;
+
+ public TOTPCredentialHandler(Map options) {
+ super(options);
+ setup(options);
+ }
+
+ private void setup(Map options) {
+ String algorithm = getConfigurationProperty(options, ALGORITHM, DEFAULT_ALGORITHM);
+ String intervalSeconds = getConfigurationProperty(options, INTERVAL_SECONDS, "" + DEFAULT_INTERVAL_SECONDS);
+ String numberDigits = getConfigurationProperty(options, NUMBER_DIGITS, "" + DEFAULT_NUMBER_DIGITS);
+ String delayWindow = getConfigurationProperty(options, DELAY_WINDOW, "" + DEFAULT_DELAY_WINDOW);
+
+ this.totp = new TimeBasedOTP(algorithm, Integer.parseInt(numberDigits), Integer.valueOf(intervalSeconds), Integer.valueOf(delayWindow));
+ }
+
+ public Credentials.Status validate(NoSQL noSQL, UserData user, String passwordToValidate, String token, String device) {
+ Credentials.Status status = super.validate(noSQL, user, passwordToValidate);
+
+ if (Credentials.Status.VALID != status) {
+ return status;
+ }
+
+ device = getDevice(device);
+
+ user = noSQL.loadObject(UserData.class, user.getId());
+
+ // If the user for the provided username cannot be found we fail validation
+ if (user != null) {
+ if (user.isEnabled()) {
+
+ // Try to find OTP based on userId and device (For now assume that this is unique combination)
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", user.getId())
+ .andCondition("device", device)
+ .build();
+ OTPData otpData = noSQL.loadSingleObject(OTPData.class, query);
+
+ // If the stored OTP is null we automatically fail validation
+ if (otpData != null) {
+ // TODO: Status.INVALID should have bigger priority than Status.EXPIRED?
+ if (!PasswordCredentialHandler.isCredentialExpired(otpData.getExpiryDate())) {
+ boolean isValid = this.totp.validate(token, otpData.getSecretKey().getBytes());
+ if (!isValid) {
+ status = Credentials.Status.INVALID;
+ }
+ } else {
+ status = Credentials.Status.EXPIRED;
+ }
+ } else {
+ status = Credentials.Status.UNVALIDATED;
+ }
+ } else {
+ status = Credentials.Status.ACCOUNT_DISABLED;
+ }
+ } else {
+ status = Credentials.Status.INVALID;
+ }
+
+ return status;
+ }
+
+ public void update(NoSQL noSQL, UserData user, String secret, String device, Date effectiveDate, Date expiryDate) {
+ device = getDevice(device);
+
+ // Try to look if user already has otp (Right now, supports just one OTP per user)
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", user.getId())
+ .andCondition("device", device)
+ .build();
+
+ OTPData otpData = noSQL.loadSingleObject(OTPData.class, query);
+ if (otpData == null) {
+ otpData = new OTPData();
+ }
+
+ otpData.setSecretKey(secret);
+ otpData.setDevice(device);
+
+ if (effectiveDate != null) {
+ otpData.setEffectiveDate(effectiveDate);
+ }
+
+ otpData.setExpiryDate(expiryDate);
+ otpData.setUserId(user.getId());
+
+ noSQL.saveObject(otpData);
+ }
+
+ private String getDevice(String device) {
+ if (isNullOrEmpty(device)) {
+ device = DEFAULT_DEVICE;
+ }
+
+ return device;
+ }
+
+ private String getConfigurationProperty(Map options, String key, String defaultValue) {
+ Object value = options.get(key);
+
+ if (value != null) {
+ return String.valueOf(value);
+ }
+
+ return defaultValue;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/ApplicationData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/ApplicationData.java
new file mode 100644
index 0000000000..5ceb788dbb
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/ApplicationData.java
@@ -0,0 +1,109 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "applications")
+public class ApplicationData implements NoSQLObject {
+
+ private String id;
+ private String name;
+ private boolean enabled;
+ private boolean surrogateAuthRequired;
+ private String managementUrl;
+ private String baseUrl;
+
+ private String resourceUserId;
+ private String realmId;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @NoSQLField
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @NoSQLField
+ public boolean isSurrogateAuthRequired() {
+ return surrogateAuthRequired;
+ }
+
+ public void setSurrogateAuthRequired(boolean surrogateAuthRequired) {
+ this.surrogateAuthRequired = surrogateAuthRequired;
+ }
+
+ @NoSQLField
+ public String getManagementUrl() {
+ return managementUrl;
+ }
+
+ public void setManagementUrl(String managementUrl) {
+ this.managementUrl = managementUrl;
+ }
+
+ @NoSQLField
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ @NoSQLField
+ public String getResourceUserId() {
+ return resourceUserId;
+ }
+
+ public void setResourceUserId(String resourceUserId) {
+ this.resourceUserId = resourceUserId;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ // Remove resourceUser of this application
+ noSQL.removeObject(UserData.class, resourceUserId);
+
+ // Remove all roles, which belongs to this application
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("applicationId", id)
+ .build();
+ noSQL.removeObjects(RoleData.class, query);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java
new file mode 100644
index 0000000000..67f74ee6a3
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java
@@ -0,0 +1,62 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "oauthClients")
+public class OAuthClientData implements NoSQLObject {
+
+ private String id;
+ private String baseUrl;
+
+ private String oauthAgentId;
+ private String realmId;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ @NoSQLField
+ public String getOauthAgentId() {
+ return oauthAgentId;
+ }
+
+ public void setOauthAgentId(String oauthUserId) {
+ this.oauthAgentId = oauthUserId;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ // Remove user of this oauthClient
+ noSQL.removeObject(UserData.class, oauthAgentId);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RealmData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RealmData.java
new file mode 100644
index 0000000000..5247d60034
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RealmData.java
@@ -0,0 +1,219 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import java.util.List;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "realms")
+public class RealmData implements NoSQLObject {
+
+ private String oid;
+
+ private String id;
+ private String name;
+ private boolean enabled;
+ private boolean sslNotRequired;
+ private boolean cookieLoginAllowed;
+ private boolean registrationAllowed;
+ private boolean verifyEmail;
+ private boolean resetPasswordAllowed;
+ private boolean social;
+ private boolean automaticRegistrationAfterSocialLogin;
+ private int tokenLifespan;
+ private int accessCodeLifespan;
+ private int accessCodeLifespanUserAction;
+ private String publicKeyPem;
+ private String privateKeyPem;
+
+ private List defaultRoles;
+ private List realmAdmins;
+
+ @NoSQLId
+ public String getOid() {
+ return oid;
+ }
+
+ public void setOid(String oid) {
+ this.oid = oid;
+ }
+
+ @NoSQLField
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String realmName) {
+ this.name = realmName;
+ }
+
+ @NoSQLField
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @NoSQLField
+ public boolean isSslNotRequired() {
+ return sslNotRequired;
+ }
+
+ public void setSslNotRequired(boolean sslNotRequired) {
+ this.sslNotRequired = sslNotRequired;
+ }
+
+ @NoSQLField
+ public boolean isCookieLoginAllowed() {
+ return cookieLoginAllowed;
+ }
+
+ public void setCookieLoginAllowed(boolean cookieLoginAllowed) {
+ this.cookieLoginAllowed = cookieLoginAllowed;
+ }
+
+ @NoSQLField
+ public boolean isRegistrationAllowed() {
+ return registrationAllowed;
+ }
+
+ public void setRegistrationAllowed(boolean registrationAllowed) {
+ this.registrationAllowed = registrationAllowed;
+ }
+
+ @NoSQLField
+ public boolean isVerifyEmail() {
+ return verifyEmail;
+ }
+
+ public void setVerifyEmail(boolean verifyEmail) {
+ this.verifyEmail = verifyEmail;
+ }
+
+ @NoSQLField
+ public boolean isResetPasswordAllowed() {
+ return resetPasswordAllowed;
+ }
+
+ public void setResetPasswordAllowed(boolean resetPasswordAllowed) {
+ this.resetPasswordAllowed = resetPasswordAllowed;
+ }
+
+ @NoSQLField
+ public boolean isSocial() {
+ return social;
+ }
+
+ public void setSocial(boolean social) {
+ this.social = social;
+ }
+
+ @NoSQLField
+ public boolean isAutomaticRegistrationAfterSocialLogin() {
+ return automaticRegistrationAfterSocialLogin;
+ }
+
+ public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
+ this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
+ }
+
+ @NoSQLField
+ public int getTokenLifespan() {
+ return tokenLifespan;
+ }
+
+ public void setTokenLifespan(int tokenLifespan) {
+ this.tokenLifespan = tokenLifespan;
+ }
+
+ @NoSQLField
+ public int getAccessCodeLifespan() {
+ return accessCodeLifespan;
+ }
+
+ public void setAccessCodeLifespan(int accessCodeLifespan) {
+ this.accessCodeLifespan = accessCodeLifespan;
+ }
+
+ @NoSQLField
+ public int getAccessCodeLifespanUserAction() {
+ return accessCodeLifespanUserAction;
+ }
+
+ public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
+ this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
+ }
+
+ @NoSQLField
+ public String getPublicKeyPem() {
+ return publicKeyPem;
+ }
+
+ public void setPublicKeyPem(String publicKeyPem) {
+ this.publicKeyPem = publicKeyPem;
+ }
+
+ @NoSQLField
+ public String getPrivateKeyPem() {
+ return privateKeyPem;
+ }
+
+ public void setPrivateKeyPem(String privateKeyPem) {
+ this.privateKeyPem = privateKeyPem;
+ }
+
+ @NoSQLField
+ public List getDefaultRoles() {
+ return defaultRoles;
+ }
+
+ public void setDefaultRoles(List defaultRoles) {
+ this.defaultRoles = defaultRoles;
+ }
+
+ @NoSQLField
+ public List getRealmAdmins() {
+ return realmAdmins;
+ }
+
+ public void setRealmAdmins(List realmAdmins) {
+ this.realmAdmins = realmAdmins;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("realmId", oid)
+ .build();
+
+ // Remove all users of this realm
+ noSQL.removeObjects(UserData.class, query);
+
+ // Remove all requiredCredentials of this realm
+ noSQL.removeObjects(RequiredCredentialData.class, query);
+
+ // Remove all roles of this realm
+ noSQL.removeObjects(RoleData.class, query);
+
+ // Remove all applications of this realm
+ noSQL.removeObjects(ApplicationData.class, query);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RequiredCredentialData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RequiredCredentialData.java
new file mode 100644
index 0000000000..e46ee9fd07
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RequiredCredentialData.java
@@ -0,0 +1,90 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "requiredCredentials")
+public class RequiredCredentialData extends AbstractNoSQLObject {
+
+ public static final int CLIENT_TYPE_USER = 1;
+ public static final int CLIENT_TYPE_RESOURCE = 2;
+ public static final int CLIENT_TYPE_OAUTH_RESOURCE = 3;
+
+ private String id;
+
+ private String type;
+ private boolean input;
+ private boolean secret;
+ private String formLabel;
+
+ private String realmId;
+ private int clientType;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @NoSQLField
+ public boolean isInput() {
+ return input;
+ }
+
+ public void setInput(boolean input) {
+ this.input = input;
+ }
+
+ @NoSQLField
+ public boolean isSecret() {
+ return secret;
+ }
+
+ public void setSecret(boolean secret) {
+ this.secret = secret;
+ }
+
+ @NoSQLField
+ public String getFormLabel() {
+ return formLabel;
+ }
+
+ public void setFormLabel(String formLabel) {
+ this.formLabel = formLabel;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @NoSQLField
+ public int getClientType() {
+ return clientType;
+ }
+
+ public void setClientType(int clientType) {
+ this.clientType = clientType;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RoleData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RoleData.java
new file mode 100644
index 0000000000..29bc1f8930
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RoleData.java
@@ -0,0 +1,98 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import java.util.List;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "roles")
+public class RoleData implements NoSQLObject {
+
+ private static final Logger logger = Logger.getLogger(RoleData.class);
+
+ private String id;
+ private String name;
+ private String description;
+
+ private String realmId;
+ private String applicationId;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @NoSQLField
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @NoSQLField
+ public String getApplicationId() {
+ return applicationId;
+ }
+
+ public void setApplicationId(String applicationId) {
+ this.applicationId = applicationId;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ // Remove this role from all users, which has it
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("roleIds", id)
+ .build();
+
+ List users = noSQL.loadObjects(UserData.class, query);
+ for (UserData user : users) {
+ logger.info("Removing role " + getName() + " from user " + user.getLoginName());
+ noSQL.pullItemFromList(user, "roleIds", getId());
+ }
+
+ // Remove this scope from all users, which has it
+ query = noSQL.createQueryBuilder()
+ .andCondition("scopeIds", id)
+ .build();
+
+ users = noSQL.loadObjects(UserData.class, query);
+ for (UserData user : users) {
+ logger.info("Removing scope " + getName() + " from user " + user.getLoginName());
+ noSQL.pullItemFromList(user, "scopeIds", getId());
+ }
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/SocialLinkData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/SocialLinkData.java
new file mode 100644
index 0000000000..37ea43d44e
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/SocialLinkData.java
@@ -0,0 +1,55 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "socialLinks")
+public class SocialLinkData extends AbstractNoSQLObject {
+
+ private String socialUsername;
+ private String socialProvider;
+ private String userId;
+ // realmId is needed to allow searching as combination socialUsername+socialProvider may not be unique
+ // (Same user could have mapped same facebook account to username "foo" in "realm1" and to username "bar" in "realm2")
+ private String realmId;
+
+ @NoSQLField
+ public String getSocialUsername() {
+ return socialUsername;
+ }
+
+ public void setSocialUsername(String socialUsername) {
+ this.socialUsername = socialUsername;
+ }
+
+ @NoSQLField
+ public String getSocialProvider() {
+ return socialProvider;
+ }
+
+ public void setSocialProvider(String socialProvider) {
+ this.socialProvider = socialProvider;
+ }
+
+ @NoSQLField
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/UserData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/UserData.java
new file mode 100644
index 0000000000..cfeb67d6d1
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/UserData.java
@@ -0,0 +1,167 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import java.util.List;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.mongo.api.AbstractAttributedNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.keycloak.data.credentials.PasswordData;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "users")
+public class UserData extends AbstractAttributedNoSQLObject {
+
+ private static final Logger logger = Logger.getLogger(UserData.class);
+
+ private String id;
+ private String loginName;
+ private String firstName;
+ private String lastName;
+ private String email;
+ private boolean emailVerified;
+ private boolean totp;
+ private boolean enabled;
+
+ private String realmId;
+
+ private List roleIds;
+ private List scopeIds;
+ private List requiredActions;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getLoginName() {
+ return loginName;
+ }
+
+ public void setLoginName(String loginName) {
+ this.loginName = loginName;
+ }
+
+ @NoSQLField
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ @NoSQLField
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @NoSQLField
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ @NoSQLField
+ public boolean isEmailVerified() {
+ return emailVerified;
+ }
+
+ public void setEmailVerified(boolean emailVerified) {
+ this.emailVerified = emailVerified;
+ }
+
+ @NoSQLField
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @NoSQLField
+ public boolean isTotp() {
+ return totp;
+ }
+
+ public void setTotp(boolean totp) {
+ this.totp = totp;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @NoSQLField
+ public List getRoleIds() {
+ return roleIds;
+ }
+
+ public void setRoleIds(List roleIds) {
+ this.roleIds = roleIds;
+ }
+
+ @NoSQLField
+ public List getScopeIds() {
+ return scopeIds;
+ }
+
+ public void setScopeIds(List scopeIds) {
+ this.scopeIds = scopeIds;
+ }
+
+ @NoSQLField
+ public List getRequiredActions() {
+ return requiredActions;
+ }
+
+ public void setRequiredActions(List requiredActions) {
+ this.requiredActions = requiredActions;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", id)
+ .build();
+
+ // Remove social links and passwords of this user
+ noSQL.removeObjects(SocialLinkData.class, query);
+ noSQL.removeObjects(PasswordData.class, query);
+
+ // Remove this user from all realms, which have him as an admin
+ NoSQLQuery realmQuery = noSQL.createQueryBuilder()
+ .andCondition("realmAdmins", id)
+ .build();
+
+ List realms = noSQL.loadObjects(RealmData.class, realmQuery);
+ for (RealmData realm : realms) {
+ logger.info("Removing admin user " + getLoginName() + " from realm " + realm.getId());
+ noSQL.pullItemFromList(realm, "realmAdmins", getId());
+ }
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java
new file mode 100644
index 0000000000..8ab31a65fc
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java
@@ -0,0 +1,66 @@
+package org.keycloak.models.mongo.keycloak.data.credentials;
+
+import java.util.Date;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "otpCredentials")
+public class OTPData extends AbstractNoSQLObject {
+
+ private Date effectiveDate = new Date();
+ private Date expiryDate;
+ private String secretKey;
+ private String device;
+
+ private String userId;
+
+ @NoSQLField
+ public Date getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public void setEffectiveDate(Date effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ }
+
+ @NoSQLField
+ public Date getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(Date expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+
+ @NoSQLField
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ @NoSQLField
+ public String getDevice() {
+ return device;
+ }
+
+ public void setDevice(String device) {
+ this.device = device;
+ }
+
+ @NoSQLField
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/PasswordData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/PasswordData.java
new file mode 100644
index 0000000000..7480e1fb87
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/PasswordData.java
@@ -0,0 +1,66 @@
+package org.keycloak.models.mongo.keycloak.data.credentials;
+
+import java.util.Date;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+
+/**
+ * @author Marek Posolda
+ */
+@NoSQLCollection(collectionName = "passwordCredentials")
+public class PasswordData extends AbstractNoSQLObject {
+
+ private Date effectiveDate = new Date();
+ private Date expiryDate;
+ private String encodedHash;
+ private String salt;
+
+ private String userId;
+
+ @NoSQLField
+ public Date getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public void setEffectiveDate(Date effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ }
+
+ @NoSQLField
+ public Date getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(Date expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+
+ @NoSQLField
+ public String getEncodedHash() {
+ return encodedHash;
+ }
+
+ public void setEncodedHash(String encodedHash) {
+ this.encodedHash = encodedHash;
+ }
+
+ @NoSQLField
+ public String getSalt() {
+ return salt;
+ }
+
+ public void setSalt(String salt) {
+ this.salt = salt;
+ }
+
+ @NoSQLField
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+}
diff --git a/model/mongo/src/test/java/org/keycloak/models/mongo/test/Address.java b/model/mongo/src/test/java/org/keycloak/models/mongo/test/Address.java
new file mode 100644
index 0000000000..8f6b6f885e
--- /dev/null
+++ b/model/mongo/src/test/java/org/keycloak/models/mongo/test/Address.java
@@ -0,0 +1,43 @@
+package org.keycloak.models.mongo.test;
+
+import java.util.List;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLField;
+
+/**
+ * @author Marek Posolda
+ */
+public class Address extends AbstractNoSQLObject {
+
+ private String street;
+ private int number;
+ private List flatNumbers;
+
+ @NoSQLField
+ public String getStreet() {
+ return street;
+ }
+
+ public void setStreet(String street) {
+ this.street = street;
+ }
+
+ @NoSQLField
+ public int getNumber() {
+ return number;
+ }
+
+ public void setNumber(int number) {
+ this.number = number;
+ }
+
+ @NoSQLField
+ public List