diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml index 4f33cf7cfa..301b76f171 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.5.0.xml @@ -10,5 +10,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java index 4b2ad4e6a7..cf81d8119f 100755 --- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java @@ -9,6 +9,7 @@ public class CredentialRepresentation { public static final String PASSWORD = "password"; public static final String PASSWORD_TOKEN = "password-token"; 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"; @@ -22,6 +23,10 @@ public class CredentialRepresentation { protected String hashedSaltedValue; protected String salt; protected Integer hashIterations; + protected Integer counter; + private String algorithm; + private Integer digits; + // only used when updating a credential. Might set required action protected boolean temporary; @@ -80,4 +85,28 @@ public class CredentialRepresentation { 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; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index ceb22a87c5..ee412a56d6 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -49,6 +49,12 @@ public class RealmRepresentation { @Deprecated protected Set requiredCredentials; protected String passwordPolicy; + protected String otpPolicyType; + protected String otpPolicyAlgorithm; + protected Integer otpPolicyInitialCounter; + protected Integer otpPolicyDigits; + protected Integer otpPolicyLookAheadWindow; + protected List users; protected List scopeMappings; protected Map> clientScopeMappings; @@ -653,4 +659,44 @@ public class RealmRepresentation { public void setRequiredActions(List requiredActions) { this.requiredActions = requiredActions; } + + public String getOtpPolicyType() { + return otpPolicyType; + } + + public void setOtpPolicyType(String otpPolicyType) { + this.otpPolicyType = otpPolicyType; + } + + public String getOtpPolicyAlgorithm() { + return otpPolicyAlgorithm; + } + + public void setOtpPolicyAlgorithm(String otpPolicyAlgorithm) { + this.otpPolicyAlgorithm = otpPolicyAlgorithm; + } + + public Integer getOtpPolicyInitialCounter() { + return otpPolicyInitialCounter; + } + + public void setOtpPolicyInitialCounter(Integer otpPolicyInitialCounter) { + this.otpPolicyInitialCounter = otpPolicyInitialCounter; + } + + public Integer getOtpPolicyDigits() { + return otpPolicyDigits; + } + + public void setOtpPolicyDigits(Integer otpPolicyDigits) { + this.otpPolicyDigits = otpPolicyDigits; + } + + public Integer getOtpPolicyLookAheadWindow() { + return otpPolicyLookAheadWindow; + } + + public void setOtpPolicyLookAheadWindow(Integer otpPolicyLookAheadWindow) { + this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow; + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index ea20afc708..d724fd7453 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -150,15 +150,6 @@ public class UserRepresentation { this.credentials = credentials; } - public UserRepresentation credential(String type, String value) { - if (this.credentials == null) credentials = new ArrayList(); - CredentialRepresentation cred = new CredentialRepresentation(); - cred.setType(type); - cred.setValue(value); - credentials.add(cred); - return this; - } - public List getRequiredActions() { return requiredActions; } diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 601ddaf9fa..5db42fce47 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -313,6 +313,9 @@ public class ExportUtils { credRep.setHashedSaltedValue(userCred.getValue()); if (userCred.getSalt() != null) credRep.setSalt(Base64.encodeBytes(userCred.getSalt())); credRep.setHashIterations(userCred.getHashIterations()); + credRep.setCounter(userCred.getCounter()); + credRep.setAlgorithm(userCred.getAlgorithm()); + credRep.setDigits(userCred.getDigits()); return credRep; } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java index 1ce00c22f5..35081ce7b9 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java @@ -174,7 +174,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { switch (page) { case TOTP: - attributes.put("totp", new TotpBean(realm, user, baseUri)); + attributes.put("totp", new TotpBean(session, realm, user, baseUri)); break; case FEDERATED_IDENTITY: attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker)); diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java index 33542e07c8..b7d4df3df7 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java @@ -21,6 +21,7 @@ */ package org.keycloak.account.freemarker.model; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.Base32; @@ -41,14 +42,16 @@ public class TotpBean { private final boolean enabled; private final String contextUrl; private final String realmName; + private final String keyUri; - public TotpBean(RealmModel realm, UserModel user, URI baseUri) { + public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri) { this.realmName = realm.getName(); - this.enabled = user.isTotp(); + this.enabled = session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user); this.contextUrl = baseUri.getPath(); this.totpSecret = randomString(20); this.totpSecretEncoded = Base32.encode(totpSecret.getBytes()); + this.keyUri = realm.getOTPPolicy().getKeyURI(realm, this.totpSecret); } private static String randomString(int length) { @@ -89,7 +92,7 @@ public class TotpBean { } public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException { - String contents = URLEncoder.encode("otpauth://totp/" + realmName + "?secret=" + totpSecretEncoded, "utf-8"); + String contents = URLEncoder.encode(keyUri, "utf-8"); return contextUrl + "qrcode" + "?size=246x246&contents=" + contents; } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index fbaefd18d2..878f13da57 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1162,6 +1162,18 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'RealmPasswordPolicyCtrl' }) + .when('/realms/:realm/authentication/otp-policy', { + templateUrl : resourceUrl + '/partials/otp-policy.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + serverInfo : function(ServerInfo) { + return ServerInfo.delay; + } + }, + controller : 'RealmOtpPolicyCtrl' + }) .when('/realms/:realm/authentication/config/:provider/:config', { templateUrl : resourceUrl + '/partials/authenticator-config.html', resolve : { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index c3adc0835a..0ed6fa4667 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -372,6 +372,10 @@ module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, rea genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/login-settings"); }); +module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) { + genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy"); +}); + module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) { genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings"); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html new file mode 100755 index 0000000000..90e76b5119 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html @@ -0,0 +1,73 @@ +
+

Authentication

+ + + +
+
+ +
+
+ +
+
+ totp is Time-Based One Time Password. 'hotp' is a counter base one time password in which the server keeps a counter to hash against. +
+
+ +
+
+ +
+
+ What hashing algorithm should be used to generate the OTP. +
+ +
+ +
+
+ +
+
+ How many digits should the OTP have? +
+ +
+ +
+ +
+ How far ahead should the server look just in case the token generator and server are out of time sync or counter sync? +
+ +
+ +
+ +
+ What should the initial counter value be? +
+ +
+
+ + +
+
+
+ +
+ + + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html index c9e2cbb770..214a9ad084 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-authentication.html @@ -2,4 +2,5 @@
  • Flows
  • Required Actions
  • Password Policy
  • +
  • OTP Policy
  • \ No newline at end of file diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java index 7ac58644a9..6f891670c8 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java @@ -24,11 +24,11 @@ package org.keycloak.login.freemarker.model; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.Base32; +import org.keycloak.models.utils.HmacOTP; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; -import java.util.Random; /** * @author Stian Thorgersen @@ -40,25 +40,16 @@ public class TotpBean { private final boolean enabled; private final String contextUrl; private final String realmName; + private final String keyUri; public TotpBean(RealmModel realm, UserModel user, URI baseUri) { this.realmName = realm.getName(); - this.enabled = user.isTotp(); + this.enabled = user.isOtpEnabled(); this.contextUrl = baseUri.getPath(); - this.totpSecret = randomString(20); + this.totpSecret = HmacOTP.generateSecret(20); this.totpSecretEncoded = Base32.encode(totpSecret.getBytes()); - } - - private static String randomString(int length) { - String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890"; - Random r = new Random(); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < length; i++) { - char c = chars.charAt(r.nextInt(chars.length())); - sb.append(c); - } - return sb.toString(); + this.keyUri = realm.getOTPPolicy().getKeyURI(realm, this.totpSecret); } public boolean isEnabled() { @@ -81,7 +72,7 @@ public class TotpBean { } public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException { - String contents = URLEncoder.encode("otpauth://totp/" + realmName + "?secret=" + totpSecretEncoded, "utf-8"); + String contents = URLEncoder.encode(keyUri, "utf-8"); return contextUrl + "qrcode" + "?size=246x246&contents=" + contents; } diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModel.java b/model/api/src/main/java/org/keycloak/migration/MigrationModel.java index f0d0ea4099..792822e731 100755 --- a/model/api/src/main/java/org/keycloak/migration/MigrationModel.java +++ b/model/api/src/main/java/org/keycloak/migration/MigrationModel.java @@ -11,7 +11,7 @@ public interface MigrationModel { /** * Must have the form of major.minor.micro as the version is parsed and numbers are compared */ - public static final String LATEST_VERSION = "1.4.0"; + public static final String LATEST_VERSION = "1.5.0"; String getStoredVersion(); void setStoredVersion(String version); diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java index 05289b271d..09239afb7d 100755 --- a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java +++ b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java @@ -3,6 +3,7 @@ package org.keycloak.migration; import org.jboss.logging.Logger; import org.keycloak.migration.migrators.MigrateTo1_3_0; import org.keycloak.migration.migrators.MigrateTo1_4_0; +import org.keycloak.migration.migrators.MigrateTo1_5_0; import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1; import org.keycloak.models.KeycloakSession; @@ -40,6 +41,12 @@ public class MigrationModelManager { } new MigrateTo1_4_0().migrate(session); } + if (stored == null || stored.lessThan(MigrateTo1_5_0.VERSION)) { + if (stored != null) { + logger.debug("Migrating older model to 1.5.0 updates"); + } + new MigrateTo1_4_0().migrate(session); + } model.setStoredVersion(MigrationModel.LATEST_VERSION); } diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java new file mode 100755 index 0000000000..2b35150592 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java @@ -0,0 +1,31 @@ +package org.keycloak.migration.migrators; + +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.ImpersonationConstants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OTPPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.models.utils.DefaultRequiredActions; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class MigrateTo1_5_0 { + public static final ModelVersion VERSION = new ModelVersion("1.5.0"); + + public void migrate(KeycloakSession session) { + List realms = session.realms().getRealms(); + for (RealmModel realm : realms) { + realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY); + } + + } +} diff --git a/model/api/src/main/java/org/keycloak/models/OTPPolicy.java b/model/api/src/main/java/org/keycloak/models/OTPPolicy.java new file mode 100755 index 0000000000..4e703355bf --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/OTPPolicy.java @@ -0,0 +1,92 @@ +package org.keycloak.models; + +import org.keycloak.models.utils.Base32; +import org.keycloak.models.utils.HmacOTP; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class OTPPolicy { + + + protected String type; + protected String algorithm; + protected int initialCounter; + protected int digits; + protected int lookAheadWindow; + + private static final Map algToKeyUriAlg = new HashMap<>(); + + static { + algToKeyUriAlg.put(HmacOTP.HMAC_SHA1, "SHA1"); + algToKeyUriAlg.put(HmacOTP.HMAC_SHA256, "SHA256"); + algToKeyUriAlg.put(HmacOTP.HMAC_SHA512, "SHA512"); + } + + public OTPPolicy() { + } + + public OTPPolicy(String type, String algorithm, int initialCounter, int digits, int lookAheadWindow) { + this.type = type; + this.algorithm = algorithm; + this.initialCounter = initialCounter; + this.digits = digits; + this.lookAheadWindow = lookAheadWindow; + } + + public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(UserCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1); + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public int getInitialCounter() { + return initialCounter; + } + + public void setInitialCounter(int initialCounter) { + this.initialCounter = initialCounter; + } + + public int getDigits() { + return digits; + } + + public void setDigits(int digits) { + this.digits = digits; + } + + public int getLookAheadWindow() { + return lookAheadWindow; + } + + public void setLookAheadWindow(int lookAheadWindow) { + this.lookAheadWindow = lookAheadWindow; + } + + public String getKeyURI(RealmModel realm, String secret) { + + String uri = "otpauth://" + type + "/" + realm.getName() + "?secret=" + Base32.encode(secret.getBytes()) + "&digits=" + digits + "&algorithm=" + algToKeyUriAlg.get(algorithm); + if (type.equals(UserCredentialModel.HOTP)) { + uri += "&counter=" + initialCounter; + } + return uri; + + } +} diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index d5c7fafba7..a9b81a64cf 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -148,6 +148,9 @@ public interface RealmModel extends RoleContainerModel { void setPasswordPolicy(PasswordPolicy policy); + OTPPolicy getOTPPolicy(); + void setOTPPolicy(OTPPolicy policy); + RoleModel getRoleById(String id); List getDefaultRoles(); diff --git a/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java b/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java index 97f6b347d2..7e2bfb4658 100755 --- a/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserCredentialModel.java @@ -14,6 +14,7 @@ public class UserCredentialModel { // 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"; @@ -44,6 +45,12 @@ public class UserCredentialModel { 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); @@ -51,6 +58,13 @@ public class UserCredentialModel { return model; } + public static UserCredentialModel hotp(String key) { + UserCredentialModel model = new UserCredentialModel(); + model.setType(HOTP); + model.setValue(key); + return model; + } + public static UserCredentialModel kerberos(String token) { UserCredentialModel model = new UserCredentialModel(); model.setType(KERBEROS); @@ -65,6 +79,10 @@ public class UserCredentialModel { return model; } + public static boolean isOtp(String type) { + return TOTP.equals(type) || HOTP.equals(type); + } + public String getType() { return type; diff --git a/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java b/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java index 988f6a62f7..42fc4ca80a 100755 --- a/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java @@ -16,6 +16,12 @@ public class UserCredentialValueModel implements Serializable { private int hashIterations; private Long createdDate; + // otp stuff + private int counter; + private String algorithm; + private int digits; + + public String getType() { return type; } @@ -63,5 +69,28 @@ public class UserCredentialValueModel implements Serializable { 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; + } } diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index ee18d798fa..c17c01659f 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -411,9 +411,23 @@ public class UserFederationManager implements UserProvider { Set supportedCredentialTypes = link.getSupportedCredentialTypes(user); if (supportedCredentialTypes.contains(type)) return true; } + if (UserCredentialModel.isOtp(type)) { + if (!user.isOtpEnabled()) return false; + } + List creds = user.getCredentialsDirectly(); for (UserCredentialValueModel cred : creds) { - if (cred.getType().equals(type)) return true; + if (cred.getType().equals(type)) { + if (UserCredentialModel.isOtp(type)) { + OTPPolicy otpPolicy = realm.getOTPPolicy(); + if (!cred.getAlgorithm().equals(otpPolicy.getAlgorithm()) + || cred.getDigits() != otpPolicy.getDigits()) { + return false; + } + + } + return true; + } } return false; } diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index 94c2ffcdfd..3282e61955 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -30,7 +30,7 @@ public interface UserModel { boolean isEnabled(); - boolean isTotp(); + boolean isOtpEnabled(); void setEnabled(boolean enabled); @@ -86,7 +86,7 @@ public interface UserModel { void setEmailVerified(boolean verified); - void setTotp(boolean totp); + void setOtpEnabled(boolean totp); void updateCredential(UserCredentialModel cred); diff --git a/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java b/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java index 08f8f90277..699d04a2a9 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/CredentialEntity.java @@ -13,7 +13,10 @@ public class CredentialEntity { private int hashIterations; private Long createdDate; private UserEntity user; - + private int counter; + private String algorithm; + private int digits; + public String getId() { return id; @@ -78,5 +81,28 @@ public class CredentialEntity { public void setUser(UserEntity user) { this.user = user; } - + + 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; + } } diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index c8bcecdffd..199d612159 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -19,6 +19,14 @@ public class RealmEntity extends AbstractIdentifiableEntity { private boolean verifyEmail; private boolean resetPasswordAllowed; private String passwordPolicy; + + protected String otpPolicyType; + protected String otpPolicyAlgorithm; + protected int otpPolicyInitialCounter; + protected int otpPolicyDigits; + protected int otpPolicyLookAheadWindow; + + private boolean editUsernameAllowed; //--- brute force settings private boolean bruteForceProtected; @@ -509,6 +517,46 @@ public class RealmEntity extends AbstractIdentifiableEntity { public void setRequiredActionProviders(List requiredActionProviders) { this.requiredActionProviders = requiredActionProviders; } + + public String getOtpPolicyType() { + return otpPolicyType; + } + + public void setOtpPolicyType(String otpPolicyType) { + this.otpPolicyType = otpPolicyType; + } + + public String getOtpPolicyAlgorithm() { + return otpPolicyAlgorithm; + } + + public void setOtpPolicyAlgorithm(String otpPolicyAlgorithm) { + this.otpPolicyAlgorithm = otpPolicyAlgorithm; + } + + public int getOtpPolicyInitialCounter() { + return otpPolicyInitialCounter; + } + + public void setOtpPolicyInitialCounter(int otpPolicyInitialCounter) { + this.otpPolicyInitialCounter = otpPolicyInitialCounter; + } + + public int getOtpPolicyDigits() { + return otpPolicyDigits; + } + + public void setOtpPolicyDigits(int otpPolicyDigits) { + this.otpPolicyDigits = otpPolicyDigits; + } + + public int getOtpPolicyLookAheadWindow() { + return otpPolicyLookAheadWindow; + } + + public void setOtpPolicyLookAheadWindow(int otpPolicyLookAheadWindow) { + this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java b/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java index cefd304e40..b2eaf7282d 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java @@ -2,6 +2,7 @@ package org.keycloak.models.utils; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; @@ -84,11 +85,43 @@ public class CredentialValidation { } } + public static boolean validHOTP(RealmModel realm, UserModel user, String otp) { + UserCredentialValueModel passwordCred = null; + OTPPolicy policy = realm.getOTPPolicy(); + HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow()); + for (UserCredentialValueModel cred : user.getCredentialsDirectly()) { + if (cred.getType().equals(UserCredentialModel.HOTP)) { + int counter = validator.validateHOTP(otp, cred.getValue(), cred.getCounter()); + if (counter < 0) return false; + cred.setCounter(counter); + user.updateCredentialDirectly(cred); + return true; + } + } + return false; + + } + + 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(), 30, policy.getLookAheadWindow()); + return validator.validateTOTP(token, secret.getBytes()); + } else { + HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow()); + int c = validator.validateHOTP(token, secret, policy.getInitialCounter()); + return c > -1; + } + + } + public static boolean validTOTP(RealmModel realm, UserModel user, String otp) { UserCredentialValueModel passwordCred = null; + OTPPolicy policy = realm.getOTPPolicy(); + TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), 30, policy.getLookAheadWindow()); for (UserCredentialValueModel cred : user.getCredentialsDirectly()) { if (cred.getType().equals(UserCredentialModel.TOTP)) { - if (new TimeBasedOTP().validate(otp, cred.getValue().getBytes())) { + if (validator.validateTOTP(otp, cred.getValue().getBytes())) { return true; } } @@ -149,6 +182,10 @@ public class CredentialValidation { if (!validTOTP(realm, user, credential.getValue())) { return false; } + } else if (credential.getType().equals(UserCredentialModel.HOTP)) { + if (!validHOTP(realm, user, credential.getValue())) { + return false; + } } else if (credential.getType().equals(UserCredentialModel.SECRET)) { if (!validSecret(realm, user, credential.getValue())) { return false; diff --git a/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java b/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java new file mode 100755 index 0000000000..77707553c8 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java @@ -0,0 +1,164 @@ +package org.keycloak.models.utils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.math.BigInteger; +import java.util.Random; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class HmacOTP { + public static final String HMAC_SHA1 = "HmacSHA1"; + public static final String HMAC_SHA256 = "HmacSHA256"; + public static final String HMAC_SHA512 = "HmacSHA512"; + public static final String DEFAULT_ALGORITHM = HMAC_SHA1; + public static final int DEFAULT_NUMBER_DIGITS = 6; + // 0 1 2 3 4 5 6 7 8 + private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; + protected final String algorithm; + protected final int numberDigits; + protected final int lookAheadWindow; + + public HmacOTP(int numberDigits, String algorithm, int delayWindow) { + this.numberDigits = numberDigits; + this.algorithm = algorithm; + this.lookAheadWindow = delayWindow; + } + + public static String generateSecret(int length) { + String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890"; + Random r = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + char c = chars.charAt(r.nextInt(chars.length())); + sb.append(c); + } + return sb.toString(); + } + + public String generateHOTP(String key, int counter) { + String steps = Integer.toHexString(counter).toUpperCase(); + + // Just get a 16 digit string + while (steps.length() < 16) + steps = "0" + steps; + + return generateOTP(key, steps, numberDigits, algorithm); + + } + + /** + * + * @param token + * @param key + * @param counter + * @return -1 if not a match. A positive number means successful validation. This positive number is also the new value of the counter + */ + public int validateHOTP(String token, String key, int counter) { + + int newCounter = counter; + for (newCounter = counter; newCounter < counter + lookAheadWindow; newCounter++) { + String candidate = generateHOTP(key, counter); + if (candidate.equals(token)) { + return newCounter + 1; + } + + } + return -1; + } + + /** + * This method generates an OTP value for the given set of parameters. + * + * @param key the shared secret, HEX encoded + * @param counter a value that reflects a time + * @param returnDigits number of digits to return + * @param crypto the crypto function to use + * @return A numeric String in base 10 that includes return digits + * @throws java.security.GeneralSecurityException + * + */ + public String generateOTP(String key, String counter, int returnDigits, String crypto) { + String result = null; + byte[] hash; + + // Using the counter + // First 8 bytes are for the movingFactor + // Complaint with base RFC 4226 (HOTP) + while (counter.length() < 16) + counter = "0" + counter; + + // Get the HEX in a Byte[] + byte[] msg = hexStr2Bytes(counter); + + // Adding one byte to get the right conversion + // byte[] k = hexStr2Bytes(key); + byte[] k = key.getBytes(); + + hash = hmac_sha1(crypto, k, msg); + + // put selected bytes into result int + int offset = hash[hash.length - 1] & 0xf; + + int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) + | (hash[offset + 3] & 0xff); + + int otp = binary % DIGITS_POWER[returnDigits]; + + result = Integer.toString(otp); + + while (result.length() < returnDigits) { + result = "0" + result; + } + return result; + } + + /** + * This method uses the JCE to provide the crypto algorithm. HMAC computes a Hashed Message Authentication Code with the + * crypto hash algorithm as a parameter. + * + * @param crypto the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512) + * @param keyBytes the bytes to use for the HMAC key + * @param text the message or text to be authenticated. + * @throws java.security.NoSuchAlgorithmException + * + * @throws java.security.InvalidKeyException + * + */ + private byte[] hmac_sha1(String crypto, byte[] keyBytes, byte[] text) { + byte[] value; + + try { + Mac hmac = Mac.getInstance(crypto); + SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW"); + + hmac.init(macKey); + + value = hmac.doFinal(text); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return value; + } + + /** + * This method converts HEX string to Byte[] + * + * @param hex the HEX string + * @return A byte array + */ + private byte[] hexStr2Bytes(String hex) { + // Adding one byte to get the right conversion + // values starting with "0" can be converted + byte[] bArray = new BigInteger("10" + hex, 16).toByteArray(); + + // Copy all the REAL bytes, not the "first" + byte[] ret = new byte[bArray.length - 1]; + for (int i = 0; i < ret.length; i++) + ret[i] = bArray[i + 1]; + return ret; + } +} diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 3fcde28996..5761256b90 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -9,6 +9,7 @@ import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.ModelException; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -64,7 +65,7 @@ public class ModelToRepresentation { rep.setEmail(user.getEmail()); rep.setEnabled(user.isEnabled()); rep.setEmailVerified(user.isEmailVerified()); - rep.setTotp(user.isTotp()); + rep.setTotp(user.isOtpEnabled()); rep.setFederationLink(user.getFederationLink()); List reqActions = new ArrayList(); @@ -152,6 +153,12 @@ public class ModelToRepresentation { if (realm.getPasswordPolicy() != null) { rep.setPasswordPolicy(realm.getPasswordPolicy().toString()); } + OTPPolicy otpPolicy = realm.getOTPPolicy(); + rep.setOtpPolicyAlgorithm(otpPolicy.getAlgorithm()); + rep.setOtpPolicyDigits(otpPolicy.getDigits()); + rep.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter()); + rep.setOtpPolicyType(otpPolicy.getType()); + rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow()); List defaultRoles = realm.getDefaultRoles(); if (!defaultRoles.isEmpty()) { diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 92060b01ef..33d640b578 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -15,6 +15,7 @@ import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; @@ -63,7 +64,16 @@ import java.util.TreeSet; public class RepresentationToModel { private static Logger logger = Logger.getLogger(RepresentationToModel.class); + public static OTPPolicy toPolicy(RealmRepresentation rep) { + OTPPolicy policy = new OTPPolicy(); + policy.setType(rep.getOtpPolicyType()); + policy.setLookAheadWindow(rep.getOtpPolicyLookAheadWindow()); + policy.setInitialCounter(rep.getOtpPolicyInitialCounter()); + policy.setAlgorithm(rep.getOtpPolicyAlgorithm()); + policy.setDigits(rep.getOtpPolicyDigits()); + return policy; + } public static void importRealm(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm) { convertDeprecatedSocialProviders(rep); convertDeprecatedApplications(session, rep); @@ -144,6 +154,8 @@ public class RepresentationToModel { } if (rep.getPasswordPolicy() != null) newRealm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy())); + if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep)); + else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY); importIdentityProviders(rep, newRealm); importIdentityProviderMappers(rep, newRealm); @@ -497,6 +509,7 @@ public class RepresentationToModel { if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy())); + if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep)); if (rep.getDefaultRoles() != null) { realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()])); @@ -847,7 +860,7 @@ public class RepresentationToModel { user.setFirstName(userRep.getFirstName()); user.setLastName(userRep.getLastName()); user.setFederationLink(userRep.getFederationLink()); - user.setTotp(userRep.isTotp()); + user.setOtpEnabled(userRep.isTotp()); if (userRep.getAttributes() != null) { for (Map.Entry entry : userRep.getAttributes().entrySet()) { Object value = entry.getValue(); @@ -922,13 +935,22 @@ public class RepresentationToModel { UserCredentialValueModel hashedCred = new UserCredentialValueModel(); hashedCred.setType(cred.getType()); hashedCred.setDevice(cred.getDevice()); - hashedCred.setHashIterations(cred.getHashIterations()); + 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) hashedCred.setAlgorithm(cred.getAlgorithm()); + if (cred.getDigits() == null && UserCredentialModel.isOtp(cred.getType())) { + hashedCred.setDigits(6); + } + if (cred.getAlgorithm() == null && UserCredentialModel.isOtp(cred.getType())) { + hashedCred.setAlgorithm(HmacOTP.HMAC_SHA1); + } user.updateCredentialDirectly(hashedCred); } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/TimeBasedOTP.java b/model/api/src/main/java/org/keycloak/models/utils/TimeBasedOTP.java index 5f21d389db..a9bf73c0b6 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/TimeBasedOTP.java +++ b/model/api/src/main/java/org/keycloak/models/utils/TimeBasedOTP.java @@ -1,8 +1,5 @@ package org.keycloak.models.utils; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.math.BigInteger; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.TimeZone; @@ -13,24 +10,12 @@ import java.util.TimeZone; * @author anil saldhana * @since Sep 20, 2010 */ -public class TimeBasedOTP { +public class TimeBasedOTP extends HmacOTP { - public static final String HMAC_SHA1 = "HmacSHA1"; - public static final String HMAC_SHA256 = "HmacSHA256"; - public static final String HMAC_SHA512 = "HmacSHA512"; - - public static final String DEFAULT_ALGORITHM = HMAC_SHA1; - public static final int DEFAULT_NUMBER_DIGITS = 6; public static final int DEFAULT_INTERVAL_SECONDS = 30; public static final int DEFAULT_DELAY_WINDOW = 1; - // 0 1 2 3 4 5 6 7 8 - private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; - private Clock clock; - private final String algorithm; - private final int numberDigits; - private final int delayWindow; public TimeBasedOTP() { this(DEFAULT_ALGORITHM, DEFAULT_NUMBER_DIGITS, DEFAULT_INTERVAL_SECONDS, DEFAULT_DELAY_WINDOW); @@ -40,13 +25,11 @@ public class TimeBasedOTP { * @param algorithm the encryption algorithm * @param numberDigits the number of digits for tokens * @param timeIntervalInSeconds the number of seconds a token is valid - * @param delayWindow the number of previous intervals that should be used to validate tokens. + * @param lookAheadWindow the number of previous intervals that should be used to validate tokens. */ - public TimeBasedOTP(String algorithm, int numberDigits, int timeIntervalInSeconds, int delayWindow) { - this.algorithm = algorithm; - this.numberDigits = numberDigits; + public TimeBasedOTP(String algorithm, int numberDigits, int timeIntervalInSeconds, int lookAheadWindow) { + super(numberDigits, algorithm, lookAheadWindow); this.clock = new Clock(timeIntervalInSeconds); - this.delayWindow = delayWindow; } /** @@ -54,7 +37,7 @@ public class TimeBasedOTP { * * @param secretKey the secret key to derive the token from. */ - public String generate(String secretKey) { + public String generateTOTP(String secretKey) { long T = this.clock.getCurrentInterval(); String steps = Long.toHexString(T).toUpperCase(); @@ -63,53 +46,7 @@ public class TimeBasedOTP { while (steps.length() < 16) steps = "0" + steps; - return generateTOTP(secretKey, steps, this.numberDigits, this.algorithm); - } - - /** - * This method generates an TOTP value for the given set of parameters. - * - * @param key the shared secret, HEX encoded - * @param time a value that reflects a time - * @param returnDigits number of digits to return - * @param crypto the crypto function to use - * @return A numeric String in base 10 that includes {@link truncationDigits} digits - * @throws java.security.GeneralSecurityException - * - */ - public String generateTOTP(String key, String time, int returnDigits, String crypto) { - String result = null; - byte[] hash; - - // Using the counter - // First 8 bytes are for the movingFactor - // Complaint with base RFC 4226 (HOTP) - while (time.length() < 16) - time = "0" + time; - - // Get the HEX in a Byte[] - byte[] msg = hexStr2Bytes(time); - - // Adding one byte to get the right conversion - // byte[] k = hexStr2Bytes(key); - byte[] k = key.getBytes(); - - hash = hmac_sha1(crypto, k, msg); - - // put selected bytes into result int - int offset = hash[hash.length - 1] & 0xf; - - int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) - | (hash[offset + 3] & 0xff); - - int otp = binary % DIGITS_POWER[returnDigits]; - - result = Integer.toString(otp); - - while (result.length() < returnDigits) { - result = "0" + result; - } - return result; + return generateOTP(secretKey, steps, this.numberDigits, this.algorithm); } /** @@ -119,17 +56,17 @@ public class TimeBasedOTP { * @param secret Shared secret * @return */ - public boolean validate(String token, byte[] secret) { + public boolean validateTOTP(String token, byte[] secret) { long currentInterval = this.clock.getCurrentInterval(); - for (int i = this.delayWindow; i >= 0; --i) { + for (int i = this.lookAheadWindow; i >= 0; --i) { String steps = Long.toHexString(currentInterval - i).toUpperCase(); // Just get a 16 digit string while (steps.length() < 16) steps = "0" + steps; - String candidate = generateTOTP(new String(secret), steps, this.numberDigits, this.algorithm); + String candidate = generateOTP(new String(secret), steps, this.numberDigits, this.algorithm); if (candidate.equals(token)) { return true; @@ -143,53 +80,6 @@ public class TimeBasedOTP { this.clock.setCalendar(calendar); } - /** - * This method uses the JCE to provide the crypto algorithm. HMAC computes a Hashed Message Authentication Code with the - * crypto hash algorithm as a parameter. - * - * @param crypto the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512) - * @param keyBytes the bytes to use for the HMAC key - * @param text the message or text to be authenticated. - * @throws java.security.NoSuchAlgorithmException - * - * @throws java.security.InvalidKeyException - * - */ - private byte[] hmac_sha1(String crypto, byte[] keyBytes, byte[] text) { - byte[] value; - - try { - Mac hmac = Mac.getInstance(crypto); - SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW"); - - hmac.init(macKey); - - value = hmac.doFinal(text); - } catch (Exception e) { - throw new RuntimeException(e); - } - - return value; - } - - /** - * This method converts HEX string to Byte[] - * - * @param hex the HEX string - * @return A byte array - */ - private byte[] hexStr2Bytes(String hex) { - // Adding one byte to get the right conversion - // values starting with "0" can be converted - byte[] bArray = new BigInteger("10" + hex, 16).toByteArray(); - - // Copy all the REAL bytes, not the "first" - byte[] ret = new byte[bArray.length - 1]; - for (int i = 0; i < ret.length; i++) - ret[i] = bArray[i + 1]; - return ret; - } - private class Clock { private final int interval; diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index 3c1edb3797..4cd162bce6 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -43,8 +43,8 @@ public class UserModelDelegate implements UserModel { } @Override - public boolean isTotp() { - return delegate.isTotp(); + public boolean isOtpEnabled() { + return delegate.isOtpEnabled(); } @Override @@ -148,8 +148,8 @@ public class UserModelDelegate implements UserModel { } @Override - public void setTotp(boolean totp) { - delegate.setTotp(totp); + public void setOtpEnabled(boolean totp) { + delegate.setOtpEnabled(totp); } @Override diff --git a/model/api/src/test/java/org/keycloak/models/HmacTest.java b/model/api/src/test/java/org/keycloak/models/HmacTest.java new file mode 100755 index 0000000000..0cda696e7e --- /dev/null +++ b/model/api/src/test/java/org/keycloak/models/HmacTest.java @@ -0,0 +1,32 @@ +package org.keycloak.models; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.models.utils.Base32; +import org.keycloak.models.utils.HmacOTP; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.BufferedReader; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Scanner; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class HmacTest { + + @Test + public void testHmac() throws Exception { + HmacOTP hmacOTP = new HmacOTP(6, HmacOTP.HMAC_SHA1, 10); + String secret = "JNSVMMTEKZCUGSKJIVGHMNSQOZBDA5JT"; + String decoded = new String(Base32.decode(secret)); + System.out.println(hmacOTP.generateHOTP(decoded, 0)); + System.out.println(hmacOTP.validateHOTP("550233", decoded, 0)); + Assert.assertEquals(1, hmacOTP.validateHOTP("550233", decoded, 0)); + } +} diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index c2d85d3ba0..17692b09e0 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -26,6 +26,7 @@ import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -81,6 +82,7 @@ public class RealmAdapter implements RealmModel { protected volatile transient Key codeSecretKey; private volatile transient PasswordPolicy passwordPolicy; + private volatile transient OTPPolicy otpPolicy; private volatile transient KeycloakSession session; private final Map allApps = new HashMap(); @@ -287,6 +289,29 @@ public class RealmAdapter implements RealmModel { realm.setPasswordPolicy(policy.toString()); } + @Override + public OTPPolicy getOTPPolicy() { + if (otpPolicy == null) { + otpPolicy = new OTPPolicy(); + otpPolicy.setDigits(realm.getOtpPolicyDigits()); + otpPolicy.setAlgorithm(realm.getOtpPolicyAlgorithm()); + otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter()); + otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow()); + otpPolicy.setType(realm.getOtpPolicyType()); + } + return otpPolicy; + } + + @Override + public void setOTPPolicy(OTPPolicy policy) { + realm.setOtpPolicyAlgorithm(policy.getAlgorithm()); + realm.setOtpPolicyDigits(policy.getDigits()); + realm.setOtpPolicyInitialCounter(policy.getInitialCounter()); + realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow()); + realm.setOtpPolicyType(policy.getType()); + + } + @Override public int getNotBefore() { return realm.getNotBefore(); diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index 98e4254ef2..f1451415f2 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -22,6 +22,7 @@ import org.keycloak.models.ClientModel; import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt; import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.UserConsentModel; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; @@ -256,12 +257,12 @@ public class UserAdapter implements UserModel, Comparable { } @Override - public boolean isTotp() { + public boolean isOtpEnabled() { return user.isTotp(); } @Override - public void setTotp(boolean totp) { + public void setOtpEnabled(boolean totp) { user.setTotp(totp); } @@ -270,7 +271,10 @@ public class UserAdapter implements UserModel, Comparable { if (cred.getType().equals(UserCredentialModel.PASSWORD)) { updatePasswordCredential(cred); - } else { + } else if (UserCredentialModel.isOtp(cred.getType())){ + updateOtpCredential(cred); + + }else { CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); if (credentialEntity == null) { @@ -283,6 +287,27 @@ public class UserAdapter implements UserModel, Comparable { } } + private void updateOtpCredential(UserCredentialModel cred) { + CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); + + if (credentialEntity == null) { + credentialEntity = setCredentials(user, cred); + credentialEntity.setValue(cred.getValue()); + OTPPolicy otpPolicy = realm.getOTPPolicy(); + credentialEntity.setAlgorithm(otpPolicy.getAlgorithm()); + credentialEntity.setDigits(otpPolicy.getDigits()); + credentialEntity.setCounter(otpPolicy.getInitialCounter()); + user.getCredentials().add(credentialEntity); + } else { + credentialEntity.setValue(cred.getValue()); + OTPPolicy policy = realm.getOTPPolicy(); + credentialEntity.setDigits(policy.getDigits()); + credentialEntity.setCounter(policy.getInitialCounter()); + credentialEntity.setAlgorithm(policy.getAlgorithm()); + } + } + + private void updatePasswordCredential(UserCredentialModel cred) { CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); @@ -390,6 +415,9 @@ public class UserAdapter implements UserModel, Comparable { credModel.setValue(credEntity.getValue()); credModel.setSalt(credEntity.getSalt()); credModel.setHashIterations(credEntity.getHashIterations()); + credModel.setCounter(credEntity.getCounter()); + credModel.setAlgorithm(credEntity.getAlgorithm()); + credModel.setDigits(credEntity.getDigits()); result.add(credModel); } @@ -414,6 +442,9 @@ public class UserAdapter implements UserModel, Comparable { credentialEntity.setSalt(credModel.getSalt()); credentialEntity.setDevice(credModel.getDevice()); credentialEntity.setHashIterations(credModel.getHashIterations()); + credentialEntity.setCounter(credModel.getCounter()); + credentialEntity.setAlgorithm(credModel.getAlgorithm()); + credentialEntity.setDigits(credModel.getDigits()); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index 84938bfd34..cb38a53b8b 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -8,6 +8,7 @@ import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -452,6 +453,19 @@ public class RealmAdapter implements RealmModel { updated.setPasswordPolicy(policy); } + @Override + public OTPPolicy getOTPPolicy() { + if (updated != null) return updated.getOTPPolicy(); + return cached.getOtpPolicy(); + } + + @Override + public void setOTPPolicy(OTPPolicy policy) { + getDelegateForUpdate(); + updated.setOTPPolicy(policy); + + } + @Override public RoleModel getRoleById(String id) { if (updated != null) return updated.getRoleById(id); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java index f2b5e33da7..a4af54e2b7 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java @@ -80,8 +80,8 @@ public class UserAdapter implements UserModel { } @Override - public boolean isTotp() { - if (updated != null) return updated.isTotp(); + public boolean isOtpEnabled() { + if (updated != null) return updated.isOtpEnabled(); return cached.isTotp(); } @@ -208,9 +208,9 @@ public class UserAdapter implements UserModel { } @Override - public void setTotp(boolean totp) { + public void setOtpEnabled(boolean totp) { getDelegateForUpdate(); - updated.setTotp(totp); + updated.setOtpEnabled(totp); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index f4b1e35d24..e93d4159e7 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -7,6 +7,7 @@ import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; @@ -63,6 +64,7 @@ public class CachedRealm implements Serializable { private int accessCodeLifespanLogin; private int notBefore; private PasswordPolicy passwordPolicy; + private OTPPolicy otpPolicy; private String publicKeyPem; private String privateKeyPem; @@ -137,6 +139,7 @@ public class CachedRealm implements Serializable { accessCodeLifespanLogin = model.getAccessCodeLifespanLogin(); notBefore = model.getNotBefore(); passwordPolicy = model.getPasswordPolicy(); + otpPolicy = model.getOTPPolicy(); publicKeyPem = model.getPublicKeyPem(); privateKeyPem = model.getPrivateKeyPem(); @@ -456,4 +459,8 @@ public class CachedRealm implements Serializable { public Map getRequiredActionProvidersByAlias() { return requiredActionProvidersByAlias; } + + public OTPPolicy getOtpPolicy() { + return otpPolicy; + } } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java index 853677b6d6..9757c630cd 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java @@ -7,11 +7,9 @@ import org.keycloak.models.UserModel; import org.keycloak.util.MultivaluedHashMap; import java.io.Serializable; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -48,7 +46,7 @@ public class CachedUser implements Serializable { this.emailVerified = user.isEmailVerified(); this.credentials.addAll(user.getCredentialsDirectly()); this.enabled = user.isEnabled(); - this.totp = user.isTotp(); + this.totp = user.isOtpEnabled(); this.federationLink = user.getFederationLink(); this.serviceAccountClientLink = user.getServiceAccountClientLink(); this.requiredActions.addAll(user.getRequiredActions()); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index c7dcf5aef3..f9e5d01e4e 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -9,6 +9,7 @@ import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; @@ -65,6 +66,7 @@ public class RealmAdapter implements RealmModel { protected volatile transient Key codeSecretKey; protected KeycloakSession session; private PasswordPolicy passwordPolicy; + private OTPPolicy otpPolicy; public RealmAdapter(KeycloakSession session, EntityManager em, RealmEntity realm) { this.session = session; @@ -1017,6 +1019,30 @@ public class RealmAdapter implements RealmModel { em.flush(); } + @Override + public OTPPolicy getOTPPolicy() { + if (otpPolicy == null) { + otpPolicy = new OTPPolicy(); + otpPolicy.setDigits(realm.getOtpPolicyDigits()); + otpPolicy.setAlgorithm(realm.getOtpPolicyAlgorithm()); + otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter()); + otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow()); + otpPolicy.setType(realm.getOtpPolicyType()); + } + return otpPolicy; + } + + @Override + public void setOTPPolicy(OTPPolicy policy) { + realm.setOtpPolicyAlgorithm(policy.getAlgorithm()); + realm.setOtpPolicyDigits(policy.getDigits()); + realm.setOtpPolicyInitialCounter(policy.getInitialCounter()); + realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow()); + realm.setOtpPolicyType(policy.getType()); + em.flush(); + } + + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index e60377746c..ae5c66c099 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -1,6 +1,7 @@ package org.keycloak.models.jpa; import org.keycloak.models.ClientModel; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.ModelDuplicateException; @@ -94,7 +95,7 @@ public class UserAdapter implements UserModel { } @Override - public boolean isTotp() { + public boolean isOtpEnabled() { return user.isTotp(); } @@ -282,7 +283,7 @@ public class UserAdapter implements UserModel { } @Override - public void setTotp(boolean totp) { + public void setOtpEnabled(boolean totp) { user.setTotp(totp); } @@ -291,6 +292,9 @@ public class UserAdapter implements UserModel { if (cred.getType().equals(UserCredentialModel.PASSWORD)) { updatePasswordCredential(cred); + } else if (UserCredentialModel.isOtp(cred.getType())){ + updateOtpCredential(cred); + } else { CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); @@ -306,6 +310,31 @@ public class UserAdapter implements UserModel { em.flush(); } + private void updateOtpCredential(UserCredentialModel cred) { + CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); + + if (credentialEntity == null) { + credentialEntity = setCredentials(user, cred); + + credentialEntity.setValue(cred.getValue()); + OTPPolicy otpPolicy = realm.getOTPPolicy(); + credentialEntity.setAlgorithm(otpPolicy.getAlgorithm()); + credentialEntity.setDigits(otpPolicy.getDigits()); + credentialEntity.setCounter(otpPolicy.getInitialCounter()); + em.persist(credentialEntity); + user.getCredentials().add(credentialEntity); + } else { + OTPPolicy policy = realm.getOTPPolicy(); + credentialEntity.setDigits(policy.getDigits()); + credentialEntity.setCounter(policy.getInitialCounter()); + credentialEntity.setAlgorithm(policy.getAlgorithm()); + credentialEntity.setValue(cred.getValue()); + } + } + + + + private void updatePasswordCredential(UserCredentialModel cred) { CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); @@ -418,6 +447,9 @@ public class UserAdapter implements UserModel { credModel.setCreatedDate(credEntity.getCreatedDate()); credModel.setSalt(credEntity.getSalt()); credModel.setHashIterations(credEntity.getHashIterations()); + credModel.setCounter(credEntity.getCounter()); + credModel.setAlgorithm(credEntity.getAlgorithm()); + credModel.setDigits(credEntity.getDigits()); result.add(credModel); } @@ -444,6 +476,9 @@ public class UserAdapter implements UserModel { credentialEntity.setSalt(credModel.getSalt()); credentialEntity.setDevice(credModel.getDevice()); credentialEntity.setHashIterations(credModel.getHashIterations()); + credentialEntity.setCounter(credModel.getCounter()); + credentialEntity.setAlgorithm(credModel.getAlgorithm()); + credentialEntity.setDigits(credModel.getDigits()); em.flush(); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java index f247f0100d..fc36f476ae 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/CredentialEntity.java @@ -44,6 +44,15 @@ public class CredentialEntity { @JoinColumn(name="USER_ID") protected UserEntity user; + @Column(name="COUNTER") + protected int counter; + + @Column(name="ALGORITHM") + protected String algorithm; + @Column(name="DIGITS") + protected int digits; + + public String getId() { return id; } @@ -107,5 +116,28 @@ public class CredentialEntity { 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; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index fe9b10d9b3..f187cecde3 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -55,8 +55,22 @@ public class RealmEntity { protected boolean resetPasswordAllowed; @Column(name="REMEMBER_ME") protected boolean rememberMe; + @Column(name="PASSWORD_POLICY") protected String passwordPolicy; + + @Column(name="OTP_POLICY_TYPE") + protected String otpPolicyType; + @Column(name="OTP_POLICY_ALG") + protected String otpPolicyAlgorithm; + @Column(name="OTP_POLICY_COUNTER") + protected int otpPolicyInitialCounter; + @Column(name="OTP_POLICY_DIGITS") + protected int otpPolicyDigits; + @Column(name="OTP_POLICY_WINDOW") + protected int otpPolicyLookAheadWindow; + + @Column(name="EDIT_USERNAME_ALLOWED") protected boolean editUsernameAllowed; @@ -580,5 +594,44 @@ public class RealmEntity { this.authenticationFlows = authenticationFlows; } + public String getOtpPolicyType() { + return otpPolicyType; + } + + public void setOtpPolicyType(String otpPolicyType) { + this.otpPolicyType = otpPolicyType; + } + + public String getOtpPolicyAlgorithm() { + return otpPolicyAlgorithm; + } + + public void setOtpPolicyAlgorithm(String otpPolicyAlgorithm) { + this.otpPolicyAlgorithm = otpPolicyAlgorithm; + } + + public int getOtpPolicyInitialCounter() { + return otpPolicyInitialCounter; + } + + public void setOtpPolicyInitialCounter(int otpPolicyInitialCounter) { + this.otpPolicyInitialCounter = otpPolicyInitialCounter; + } + + public int getOtpPolicyDigits() { + return otpPolicyDigits; + } + + public void setOtpPolicyDigits(int otpPolicyDigits) { + this.otpPolicyDigits = otpPolicyDigits; + } + + public int getOtpPolicyLookAheadWindow() { + return otpPolicyLookAheadWindow; + } + + public void setOtpPolicyLookAheadWindow(int otpPolicyLookAheadWindow) { + this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 795ccbf29d..19f2601fcf 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -13,6 +13,7 @@ import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; @@ -66,6 +67,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme protected volatile transient X509Certificate certificate; protected volatile transient Key codeSecretKey; + private volatile transient OTPPolicy otpPolicy; private volatile transient PasswordPolicy passwordPolicy; private volatile transient KeycloakSession session; @@ -272,6 +274,30 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateRealm(); } + @Override + public OTPPolicy getOTPPolicy() { + if (otpPolicy == null) { + otpPolicy = new OTPPolicy(); + otpPolicy.setDigits(realm.getOtpPolicyDigits()); + otpPolicy.setAlgorithm(realm.getOtpPolicyAlgorithm()); + otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter()); + otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow()); + otpPolicy.setType(realm.getOtpPolicyType()); + } + return otpPolicy; + } + + @Override + public void setOTPPolicy(OTPPolicy policy) { + realm.setOtpPolicyAlgorithm(policy.getAlgorithm()); + realm.setOtpPolicyDigits(policy.getDigits()); + realm.setOtpPolicyInitialCounter(policy.getInitialCounter()); + realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow()); + realm.setOtpPolicyType(policy.getType()); + updateRealm(); + } + + @Override public int getNotBefore() { return realm.getNotBefore(); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index 6dae14b371..be5c45d13d 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -7,6 +7,7 @@ import com.mongodb.QueryBuilder; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.KeycloakSession; @@ -227,12 +228,12 @@ public class UserAdapter extends AbstractMongoAdapter implement } @Override - public boolean isTotp() { + public boolean isOtpEnabled() { return user.isTotp(); } @Override - public void setTotp(boolean totp) { + public void setOtpEnabled(boolean totp) { user.setTotp(totp); updateUser(); } @@ -242,6 +243,8 @@ public class UserAdapter extends AbstractMongoAdapter implement if (cred.getType().equals(UserCredentialModel.PASSWORD)) { updatePasswordCredential(cred); + } else if (UserCredentialModel.isOtp(cred.getType())){ + updateOtpCredential(cred); } else { CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); @@ -256,6 +259,27 @@ public class UserAdapter extends AbstractMongoAdapter implement getMongoStore().updateEntity(user, invocationContext); } + private void updateOtpCredential(UserCredentialModel cred) { + CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); + + if (credentialEntity == null) { + credentialEntity = setCredentials(user, cred); + credentialEntity.setValue(cred.getValue()); + OTPPolicy otpPolicy = realm.getOTPPolicy(); + credentialEntity.setAlgorithm(otpPolicy.getAlgorithm()); + credentialEntity.setDigits(otpPolicy.getDigits()); + credentialEntity.setCounter(otpPolicy.getInitialCounter()); + user.getCredentials().add(credentialEntity); + } else { + credentialEntity.setValue(cred.getValue()); + OTPPolicy policy = realm.getOTPPolicy(); + credentialEntity.setDigits(policy.getDigits()); + credentialEntity.setCounter(policy.getInitialCounter()); + credentialEntity.setAlgorithm(policy.getAlgorithm()); + } + } + + private void updatePasswordCredential(UserCredentialModel cred) { CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); @@ -362,6 +386,9 @@ public class UserAdapter extends AbstractMongoAdapter implement credModel.setValue(credEntity.getValue()); credModel.setSalt(credEntity.getSalt()); credModel.setHashIterations(credEntity.getHashIterations()); + credModel.setCounter(credEntity.getCounter()); + credModel.setAlgorithm(credEntity.getAlgorithm()); + credModel.setDigits(credEntity.getDigits()); result.add(credModel); } @@ -384,6 +411,9 @@ public class UserAdapter extends AbstractMongoAdapter implement credentialEntity.setSalt(credModel.getSalt()); credentialEntity.setDevice(credModel.getDevice()); credentialEntity.setHashIterations(credModel.getHashIterations()); + credentialEntity.setCounter(credModel.getCounter()); + credentialEntity.setAlgorithm(credModel.getAlgorithm()); + credentialEntity.setDigits(credModel.getDigits()); getMongoStore().updateEntity(user, invocationContext); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java index c6036865a3..6c3b09adf1 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java @@ -45,7 +45,7 @@ public class OTPFormAuthenticator extends AbstractFormAuthenticator implements A context.challenge(challengeResponse); return; } - credentials.add(UserCredentialModel.totp(password)); + credentials.add(UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), password)); boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials); if (!valid) { context.getEvent().user(context.getUser()) @@ -75,7 +75,7 @@ public class OTPFormAuthenticator extends AbstractFormAuthenticator implements A @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp(); + return session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java index 7d4da65165..9c9846b5b5 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java @@ -50,7 +50,7 @@ public class ValidateOTP extends AbstractDirectGrantAuthenticator { context.failure(AuthenticationProcessor.Error.INVALID_USER, challengeResponse); return; } - credentials.add(UserCredentialModel.totp(otp)); + credentials.add(UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), otp)); boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials); if (!valid) { context.getEvent().user(context.getUser()); @@ -74,7 +74,7 @@ public class ValidateOTP extends AbstractDirectGrantAuthenticator { } private boolean isConfigured(KeycloakSession session, RealmModel realm, UserModel user) { - return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp(); + return session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user); } @Override diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index c011551923..a44511388d 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -11,6 +11,7 @@ import org.keycloak.models.BrowserSecurityHeaders; import org.keycloak.models.Constants; import org.keycloak.models.ImpersonationConstants; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OTPPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; @@ -150,6 +151,7 @@ public class RealmManager { realm.setMaxDeltaTimeSeconds(60 * 60 * 12); // 12 hours realm.setFailureFactor(30); realm.setSslRequired(SslRequired.EXTERNAL); + realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY); realm.setEventsListeners(Collections.singleton("jboss-logging")); } diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index bd702d267c..1756f12cab 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -22,11 +22,6 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; -import org.jboss.resteasy.spi.BadRequestException; -import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.AbstractOAuthClient; -import org.keycloak.ClientConnection; -import org.keycloak.OAuth2Constants; import org.keycloak.account.AccountPages; import org.keycloak.account.AccountProvider; import org.keycloak.events.Details; @@ -38,10 +33,8 @@ import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.AccountRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.Constants; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; import org.keycloak.models.ModelReadOnlyException; @@ -50,11 +43,11 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.CredentialValidation; import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -65,7 +58,6 @@ import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; -import org.keycloak.services.util.CookieHelper; import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.validation.Validation; import org.keycloak.util.UriUtils; @@ -76,12 +68,8 @@ import javax.ws.rs.OPTIONS; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Cookie; -import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; @@ -441,7 +429,7 @@ public class AccountService extends AbstractSecuredLocalService { csrfCheck(stateChecker); UserModel user = auth.getUser(); - user.setTotp(false); + user.setOtpEnabled(false); event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); @@ -552,17 +540,23 @@ public class AccountService extends AbstractSecuredLocalService { if (Validation.isBlank(totp)) { setReferrerOnPage(); return account.setError(Messages.MISSING_TOTP).createResponse(AccountPages.TOTP); - } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { + } else if (!CredentialValidation.validOTP(realm, totp, totpSecret)) { setReferrerOnPage(); return account.setError(Messages.INVALID_TOTP).createResponse(AccountPages.TOTP); } UserCredentialModel credentials = new UserCredentialModel(); - credentials.setType(CredentialRepresentation.TOTP); + credentials.setType(realm.getOTPPolicy().getType()); credentials.setValue(totpSecret); session.users().updateCredential(realm, user, credentials); - user.setTotp(true); + user.setOtpEnabled(true); + + // to update counter + UserCredentialModel cred = new UserCredentialModel(); + cred.setType(realm.getOTPPolicy().getType()); + cred.setValue(totp); + session.users().validCredentials(realm, user, cred); event.event(EventType.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 14f81d04a6..c6b77b561f 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -47,6 +47,7 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.CredentialValidation; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.TimeBasedOTP; @@ -563,18 +564,25 @@ public class LoginActionsService { return loginForms.setError(Messages.MISSING_TOTP) .setClientSessionCode(accessCode.getCode()) .createResponse(RequiredAction.CONFIGURE_TOTP); - } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { + } else if (!CredentialValidation.validOTP(realm, totp, totpSecret)) { return loginForms.setError(Messages.INVALID_TOTP) .setClientSessionCode(accessCode.getCode()) .createResponse(RequiredAction.CONFIGURE_TOTP); } UserCredentialModel credentials = new UserCredentialModel(); - credentials.setType(CredentialRepresentation.TOTP); + credentials.setType(realm.getOTPPolicy().getType()); credentials.setValue(totpSecret); session.users().updateCredential(realm, user, credentials); - user.setTotp(true); + + // if type is HOTP, to update counter we execute validation based on supplied token + UserCredentialModel cred = new UserCredentialModel(); + cred.setType(realm.getOTPPolicy().getType()); + cred.setValue(totp); + session.users().validCredentials(realm, user, cred); + + user.setOtpEnabled(true); user.removeRequiredAction(RequiredAction.CONFIGURE_TOTP); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 1a1aa29a5c..245a3d332b 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -69,7 +69,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -213,7 +212,7 @@ public class UsersResource { user.setLastName(rep.getLastName()); user.setEnabled(rep.isEnabled()); - user.setTotp(rep.isTotp()); + user.setOtpEnabled(rep.isTotp()); user.setEmailVerified(rep.isEmailVerified()); List reqActions = rep.getRequiredActions(); @@ -821,7 +820,7 @@ public class UsersResource { throw new NotFoundException("User not found"); } - user.setTotp(false); + user.setOtpEnabled(false); adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/TotpGenerator.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/TotpGenerator.java index 1e2449b842..6126602dd3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/TotpGenerator.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/TotpGenerator.java @@ -43,7 +43,7 @@ public class TotpGenerator { public void run() { String google = new String(Base32.decode(secret)); TimeBasedOTP otp = new TimeBasedOTP(); - System.out.println(otp.generate(google)); + System.out.println(otp.generateTOTP(google)); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 7faacda3df..bcaa98e257 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -167,7 +167,7 @@ public class AccountTest { }); } - //@Test + @Test public void ideTesting() throws Exception { Thread.sleep(100000000); } @@ -517,11 +517,11 @@ public class AccountTest { Assert.assertFalse(driver.getPageSource().contains("Remove Google")); // Error with false code - totpPage.configure(totp.generate(totpPage.getTotpSecret() + "123")); + totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret() + "123")); Assert.assertEquals("Invalid authenticator code.", profilePage.getError()); - totpPage.configure(totp.generate(totpPage.getTotpSecret())); + totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); Assert.assertEquals("Mobile authenticator configured.", profilePage.getSuccess()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java index c448a927ea..cb5c7212d3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java @@ -25,7 +25,6 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; -import org.keycloak.authentication.requiredactions.UpdateTotp; import org.keycloak.events.Details; import org.keycloak.events.Event; import org.keycloak.events.EventType; @@ -113,7 +112,7 @@ public class RequiredActionTotpSetupTest { totpPage.assertCurrent(); - totpPage.configure(totp.generate(totpPage.getTotpSecret())); + totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent().getSessionId(); @@ -131,7 +130,7 @@ public class RequiredActionTotpSetupTest { String totpSecret = totpPage.getTotpSecret(); - totpPage.configure(totp.generate(totpSecret)); + totpPage.configure(totp.generateTOTP(totpSecret)); String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId(); @@ -146,7 +145,7 @@ public class RequiredActionTotpSetupTest { loginPage.open(); loginPage.login("test-user@localhost", "password"); String src = driver.getPageSource(); - loginTotpPage.login(totp.generate(totpSecret)); + loginTotpPage.login(totp.generateTOTP(totpSecret)); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -166,7 +165,7 @@ public class RequiredActionTotpSetupTest { totpPage.assertCurrent(); String totpCode = totpPage.getTotpSecret(); - totpPage.configure(totp.generate(totpCode)); + totpPage.configure(totp.generateTOTP(totpCode)); // After totp config, user should be on the app page Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -184,11 +183,13 @@ public class RequiredActionTotpSetupTest { loginPage.login("setupTotp2", "password2"); // Totp is already configured, thus one-time password is needed, login page should be loaded + String uri = driver.getCurrentUrl(); + String src = driver.getPageSource(); Assert.assertTrue(loginPage.isCurrent()); Assert.assertFalse(totpPage.isCurrent()); // Login with one-time password - loginTotpPage.login(totp.generate(totpCode)); + loginTotpPage.login(totp.generateTOTP(totpCode)); loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent(); @@ -211,7 +212,7 @@ public class RequiredActionTotpSetupTest { // Since the authentificator was removed, it has to be set up again totpPage.assertCurrent(); - totpPage.configure(totp.generate(totpPage.getTotpSecret())); + totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent().getSessionId(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java index 19cbbf06a7..8f0778cfe4 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java @@ -72,7 +72,7 @@ public class BruteForceTest { credentials.setValue("totpSecret"); user.updateCredential(credentials); - user.setTotp(true); + user.setOtpEnabled(true); appRealm.setEventsListeners(Collections.singleton("dummy")); appRealm.setBruteForceProtected(true); @@ -158,14 +158,14 @@ public class BruteForceTest { @Test public void testGrantInvalidPassword() throws Exception { { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNotNull(response.getAccessToken()); Assert.assertNull(response.getError()); events.clear(); } { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret); Assert.assertNull(response.getAccessToken()); Assert.assertEquals(response.getError(), "invalid_grant"); @@ -173,7 +173,7 @@ public class BruteForceTest { events.clear(); } { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("invalid", totpSecret); Assert.assertNull(response.getAccessToken()); Assert.assertEquals(response.getError(), "invalid_grant"); @@ -181,7 +181,7 @@ public class BruteForceTest { events.clear(); } { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNull(response.getAccessToken()); Assert.assertNotNull(response.getError()); @@ -191,7 +191,7 @@ public class BruteForceTest { } clearUserFailures(); { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNotNull(response.getAccessToken()); Assert.assertNull(response.getError()); @@ -203,7 +203,7 @@ public class BruteForceTest { @Test public void testGrantInvalidOtp() throws Exception { { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNotNull(response.getAccessToken()); Assert.assertNull(response.getError()); @@ -224,7 +224,7 @@ public class BruteForceTest { events.clear(); } { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNull(response.getAccessToken()); Assert.assertNotNull(response.getError()); @@ -234,7 +234,7 @@ public class BruteForceTest { } clearUserFailures(); { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNotNull(response.getAccessToken()); Assert.assertNull(response.getError()); @@ -244,7 +244,7 @@ public class BruteForceTest { } @Test public void testGrantMissingOtp() throws Exception { { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNotNull(response.getAccessToken()); Assert.assertNull(response.getError()); @@ -265,7 +265,7 @@ public class BruteForceTest { events.clear(); } { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNull(response.getAccessToken()); Assert.assertNotNull(response.getError()); @@ -275,7 +275,7 @@ public class BruteForceTest { } clearUserFailures(); { - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); Assert.assertNotNull(response.getAccessToken()); Assert.assertNull(response.getError()); @@ -353,7 +353,7 @@ public class BruteForceTest { loginTotpPage.assertCurrent(); - String totpSecret = totp.generate("totpSecret"); + String totpSecret = totp.generateTOTP("totpSecret"); loginTotpPage.login(totpSecret); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java index 03747150d3..b37401ca94 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java @@ -43,7 +43,6 @@ import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; -import org.keycloak.util.Time; import org.openqa.selenium.WebDriver; import java.net.MalformedURLException; @@ -66,7 +65,7 @@ public class LoginTotpTest { credentials.setValue("totpSecret"); user.updateCredential(credentials); - user.setTotp(true); + user.setOtpEnabled(true); appRealm.setEventsListeners(Collections.singleton("dummy")); } @@ -147,7 +146,7 @@ public class LoginTotpTest { loginTotpPage.assertCurrent(); - loginTotpPage.login(totp.generate("totpSecret")); + loginTotpPage.login(totp.generateTOTP("totpSecret")); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());