Details
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 301b76f..4866834 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
@@ -17,6 +17,9 @@
<column name="DIGITS" type="INT" defaultValueNumeric="6">
<constraints nullable="true"/>
</column>
+ <column name="PERIOD" type="INT" defaultValueNumeric="30">
+ <constraints nullable="true"/>
+ </column>
<column name="ALGORITHM" type="VARCHAR(36)" defaultValue="HmacSHA1">
<constraints nullable="true"/>
</column>
@@ -28,6 +31,9 @@
<column name="OTP_POLICY_WINDOW" type="INT" defaultValueNumeric="1">
<constraints nullable="true"/>
</column>
+ <column name="OTP_POLICY_PERIOD" type="INT" defaultValueNumeric="30">
+ <constraints nullable="true"/>
+ </column>
<column name="OTP_POLICY_DIGITS" type="INT" defaultValueNumeric="6">
<constraints nullable="true"/>
</column>
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 cf81d81..1e57dbc 100755
--- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
@@ -26,6 +26,7 @@ public class CredentialRepresentation {
protected Integer counter;
private String algorithm;
private Integer digits;
+ private Integer period;
// only used when updating a credential. Might set required action
protected boolean temporary;
@@ -109,4 +110,12 @@ public class CredentialRepresentation {
public void setDigits(Integer digits) {
this.digits = digits;
}
+
+ public Integer getPeriod() {
+ return period;
+ }
+
+ public void setPeriod(Integer period) {
+ this.period = period;
+ }
}
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 ee412a5..d93a0d6 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -54,6 +54,7 @@ public class RealmRepresentation {
protected Integer otpPolicyInitialCounter;
protected Integer otpPolicyDigits;
protected Integer otpPolicyLookAheadWindow;
+ protected Integer otpPolicyPeriod;
protected List<UserRepresentation> users;
protected List<ScopeMappingRepresentation> scopeMappings;
@@ -699,4 +700,12 @@ public class RealmRepresentation {
public void setOtpPolicyLookAheadWindow(Integer otpPolicyLookAheadWindow) {
this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow;
}
+
+ public Integer getOtpPolicyPeriod() {
+ return otpPolicyPeriod;
+ }
+
+ public void setOtpPolicyPeriod(Integer otpPolicyPeriod) {
+ this.otpPolicyPeriod = otpPolicyPeriod;
+ }
}
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 699d04a..22818a5 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
@@ -16,6 +16,7 @@ public class CredentialEntity {
private int counter;
private String algorithm;
private int digits;
+ private int period;
public String getId() {
@@ -105,4 +106,12 @@ public class CredentialEntity {
public void setDigits(int digits) {
this.digits = digits;
}
+
+ public int getPeriod() {
+ return period;
+ }
+
+ public void setPeriod(int period) {
+ this.period = period;
+ }
}
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 199d612..64228e4 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
@@ -25,6 +25,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
protected int otpPolicyInitialCounter;
protected int otpPolicyDigits;
protected int otpPolicyLookAheadWindow;
+ protected int otpPolicyPeriod;
private boolean editUsernameAllowed;
@@ -557,6 +558,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
public void setOtpPolicyLookAheadWindow(int otpPolicyLookAheadWindow) {
this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow;
}
+
+ public int getOtpPolicyPeriod() {
+ return otpPolicyPeriod;
+ }
+
+ public void setOtpPolicyPeriod(int otpPolicyPeriod) {
+ this.otpPolicyPeriod = otpPolicyPeriod;
+ }
}
diff --git a/model/api/src/main/java/org/keycloak/models/OTPPolicy.java b/model/api/src/main/java/org/keycloak/models/OTPPolicy.java
index 4e70335..4ea52ac 100755
--- a/model/api/src/main/java/org/keycloak/models/OTPPolicy.java
+++ b/model/api/src/main/java/org/keycloak/models/OTPPolicy.java
@@ -18,6 +18,7 @@ public class OTPPolicy {
protected int initialCounter;
protected int digits;
protected int lookAheadWindow;
+ protected int period;
private static final Map<String, String> algToKeyUriAlg = new HashMap<>();
@@ -30,15 +31,16 @@ public class OTPPolicy {
public OTPPolicy() {
}
- public OTPPolicy(String type, String algorithm, int initialCounter, int digits, int lookAheadWindow) {
+ public OTPPolicy(String type, String algorithm, int initialCounter, int digits, int lookAheadWindow, int period) {
this.type = type;
this.algorithm = algorithm;
this.initialCounter = initialCounter;
this.digits = digits;
this.lookAheadWindow = lookAheadWindow;
+ this.period = period;
}
- public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(UserCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1);
+ public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(UserCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1, 30);
public String getType() {
return type;
@@ -80,12 +82,23 @@ public class OTPPolicy {
this.lookAheadWindow = lookAheadWindow;
}
+ public int getPeriod() {
+ return period;
+ }
+
+ public void setPeriod(int period) {
+ this.period = period;
+ }
+
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;
}
+ if (type.equals(UserCredentialModel.TOTP)) {
+ uri += "&period=" + period;
+ }
return uri;
}
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 42fc4ca..202afe5 100755
--- a/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserCredentialValueModel.java
@@ -20,6 +20,7 @@ public class UserCredentialValueModel implements Serializable {
private int counter;
private String algorithm;
private int digits;
+ private int period;
public String getType() {
@@ -93,4 +94,12 @@ public class UserCredentialValueModel implements Serializable {
public void setDigits(int digits) {
this.digits = digits;
}
+
+ public int getPeriod() {
+ return period;
+ }
+
+ public void setPeriod(int period) {
+ this.period = period;
+ }
}
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 c17c016..fd4569b 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -424,7 +424,9 @@ public class UserFederationManager implements UserProvider {
|| cred.getDigits() != otpPolicy.getDigits()) {
return false;
}
-
+ if (type.equals(UserCredentialModel.TOTP) && cred.getPeriod() != otpPolicy.getPeriod()) {
+ return false;
+ }
}
return true;
}
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 b2eaf72..d1eef51 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
@@ -105,7 +105,7 @@ public class CredentialValidation {
public static boolean validOTP(RealmModel realm, String token, String secret) {
OTPPolicy policy = realm.getOTPPolicy();
if (policy.getType().equals(UserCredentialModel.TOTP)) {
- TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), 30, policy.getLookAheadWindow());
+ TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), policy.getPeriod(), policy.getLookAheadWindow());
return validator.validateTOTP(token, secret.getBytes());
} else {
HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow());
@@ -118,7 +118,7 @@ public class CredentialValidation {
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());
+ TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), policy.getPeriod(), policy.getLookAheadWindow());
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
if (cred.getType().equals(UserCredentialModel.TOTP)) {
if (validator.validateTOTP(otp, cred.getValue().getBytes())) {
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
index 7770755..210f82b 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java
@@ -59,8 +59,8 @@ public class HmacOTP {
public int validateHOTP(String token, String key, int counter) {
int newCounter = counter;
- for (newCounter = counter; newCounter < counter + lookAheadWindow; newCounter++) {
- String candidate = generateHOTP(key, counter);
+ for (newCounter = counter; newCounter <= counter + lookAheadWindow; newCounter++) {
+ String candidate = generateHOTP(key, newCounter);
if (candidate.equals(token)) {
return newCounter + 1;
}
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 5761256..39a936d 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
@@ -155,6 +155,7 @@ public class ModelToRepresentation {
}
OTPPolicy otpPolicy = realm.getOTPPolicy();
rep.setOtpPolicyAlgorithm(otpPolicy.getAlgorithm());
+ rep.setOtpPolicyPeriod(otpPolicy.getPeriod());
rep.setOtpPolicyDigits(otpPolicy.getDigits());
rep.setOtpPolicyInitialCounter(otpPolicy.getInitialCounter());
rep.setOtpPolicyType(otpPolicy.getType());
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 33d640b..f90e7b9 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
@@ -71,6 +71,7 @@ public class RepresentationToModel {
policy.setInitialCounter(rep.getOtpPolicyInitialCounter());
policy.setAlgorithm(rep.getOtpPolicyAlgorithm());
policy.setDigits(rep.getOtpPolicyDigits());
+ policy.setPeriod(rep.getOtpPolicyPeriod());
return policy;
}
@@ -945,12 +946,16 @@ public class RepresentationToModel {
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.getPeriod() != null) hashedCred.setPeriod(cred.getPeriod());
if (cred.getDigits() == null && UserCredentialModel.isOtp(cred.getType())) {
hashedCred.setDigits(6);
}
if (cred.getAlgorithm() == null && UserCredentialModel.isOtp(cred.getType())) {
hashedCred.setAlgorithm(HmacOTP.HMAC_SHA1);
}
+ if (cred.getPeriod() == null && UserCredentialModel.TOTP.equals(cred.getType())) {
+ hashedCred.setPeriod(30);
+ }
user.updateCredentialDirectly(hashedCred);
}
}
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 17692b0..d4b3916 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
@@ -298,6 +298,7 @@ public class RealmAdapter implements RealmModel {
otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter());
otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow());
otpPolicy.setType(realm.getOtpPolicyType());
+ otpPolicy.setPeriod(realm.getOtpPolicyPeriod());
}
return otpPolicy;
}
@@ -309,6 +310,7 @@ public class RealmAdapter implements RealmModel {
realm.setOtpPolicyInitialCounter(policy.getInitialCounter());
realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow());
realm.setOtpPolicyType(policy.getType());
+ realm.setOtpPolicyPeriod(policy.getPeriod());
}
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 f145141..7461cbd 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
@@ -297,6 +297,7 @@ public class UserAdapter implements UserModel, Comparable {
credentialEntity.setAlgorithm(otpPolicy.getAlgorithm());
credentialEntity.setDigits(otpPolicy.getDigits());
credentialEntity.setCounter(otpPolicy.getInitialCounter());
+ credentialEntity.setPeriod(otpPolicy.getPeriod());
user.getCredentials().add(credentialEntity);
} else {
credentialEntity.setValue(cred.getValue());
@@ -304,6 +305,7 @@ public class UserAdapter implements UserModel, Comparable {
credentialEntity.setDigits(policy.getDigits());
credentialEntity.setCounter(policy.getInitialCounter());
credentialEntity.setAlgorithm(policy.getAlgorithm());
+ credentialEntity.setPeriod(policy.getPeriod());
}
}
@@ -415,9 +417,28 @@ 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());
+ if (UserCredentialModel.isOtp(credEntity.getType())) {
+ credModel.setCounter(credEntity.getCounter());
+ if (credEntity.getAlgorithm() == null) {
+ // for migration where these values would be null
+ credModel.setAlgorithm(realm.getOTPPolicy().getAlgorithm());
+ } else {
+ credModel.setAlgorithm(credEntity.getAlgorithm());
+ }
+ if (credEntity.getDigits() == 0) {
+ // for migration where these values would be 0
+ credModel.setDigits(realm.getOTPPolicy().getDigits());
+ } else {
+ credModel.setDigits(credEntity.getDigits());
+ }
+
+ if (credEntity.getPeriod() == 0) {
+ // for migration where these values would be 0
+ credModel.setPeriod(realm.getOTPPolicy().getPeriod());
+ } else {
+ credModel.setPeriod(credEntity.getPeriod());
+ }
+ }
result.add(credModel);
}
@@ -445,6 +466,7 @@ public class UserAdapter implements UserModel, Comparable {
credentialEntity.setCounter(credModel.getCounter());
credentialEntity.setAlgorithm(credModel.getAlgorithm());
credentialEntity.setDigits(credModel.getDigits());
+ credentialEntity.setPeriod(credModel.getPeriod());
}
@Override
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 fc36f47..387d0b3 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
@@ -51,6 +51,8 @@ public class CredentialEntity {
protected String algorithm;
@Column(name="DIGITS")
protected int digits;
+ @Column(name="PERIOD")
+ protected int period;
public String getId() {
@@ -140,4 +142,12 @@ public class CredentialEntity {
public void setDigits(int digits) {
this.digits = digits;
}
+
+ public int getPeriod() {
+ return period;
+ }
+
+ public void setPeriod(int period) {
+ this.period = period;
+ }
}
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 f187cec..67ba5a6 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
@@ -69,6 +69,8 @@ public class RealmEntity {
protected int otpPolicyDigits;
@Column(name="OTP_POLICY_WINDOW")
protected int otpPolicyLookAheadWindow;
+ @Column(name="OTP_POLICY_PERIOD")
+ protected int otpPolicyPeriod;
@Column(name="EDIT_USERNAME_ALLOWED")
@@ -633,5 +635,13 @@ public class RealmEntity {
public void setOtpPolicyLookAheadWindow(int otpPolicyLookAheadWindow) {
this.otpPolicyLookAheadWindow = otpPolicyLookAheadWindow;
}
+
+ public int getOtpPolicyPeriod() {
+ return otpPolicyPeriod;
+ }
+
+ public void setOtpPolicyPeriod(int otpPolicyPeriod) {
+ this.otpPolicyPeriod = otpPolicyPeriod;
+ }
}
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 f9e5d01..9706393 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
@@ -1028,6 +1028,7 @@ public class RealmAdapter implements RealmModel {
otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter());
otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow());
otpPolicy.setType(realm.getOtpPolicyType());
+ otpPolicy.setPeriod(realm.getOtpPolicyPeriod());
}
return otpPolicy;
}
@@ -1039,6 +1040,7 @@ public class RealmAdapter implements RealmModel {
realm.setOtpPolicyInitialCounter(policy.getInitialCounter());
realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow());
realm.setOtpPolicyType(policy.getType());
+ realm.setOtpPolicyPeriod(policy.getPeriod());
em.flush();
}
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 ae5c66c..67def6b 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
@@ -321,6 +321,7 @@ public class UserAdapter implements UserModel {
credentialEntity.setAlgorithm(otpPolicy.getAlgorithm());
credentialEntity.setDigits(otpPolicy.getDigits());
credentialEntity.setCounter(otpPolicy.getInitialCounter());
+ credentialEntity.setPeriod(otpPolicy.getPeriod());
em.persist(credentialEntity);
user.getCredentials().add(credentialEntity);
} else {
@@ -329,6 +330,7 @@ public class UserAdapter implements UserModel {
credentialEntity.setCounter(policy.getInitialCounter());
credentialEntity.setAlgorithm(policy.getAlgorithm());
credentialEntity.setValue(cred.getValue());
+ credentialEntity.setPeriod(policy.getPeriod());
}
}
@@ -450,6 +452,7 @@ public class UserAdapter implements UserModel {
credModel.setCounter(credEntity.getCounter());
credModel.setAlgorithm(credEntity.getAlgorithm());
credModel.setDigits(credEntity.getDigits());
+ credModel.setPeriod(credEntity.getPeriod());
result.add(credModel);
}
@@ -479,6 +482,7 @@ public class UserAdapter implements UserModel {
credentialEntity.setCounter(credModel.getCounter());
credentialEntity.setAlgorithm(credModel.getAlgorithm());
credentialEntity.setDigits(credModel.getDigits());
+ credentialEntity.setPeriod(credModel.getPeriod());
em.flush();
}
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 19f2601..d5d31eb 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
@@ -283,6 +283,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
otpPolicy.setInitialCounter(realm.getOtpPolicyInitialCounter());
otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow());
otpPolicy.setType(realm.getOtpPolicyType());
+ otpPolicy.setPeriod(realm.getOtpPolicyPeriod());
}
return otpPolicy;
}
@@ -294,6 +295,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
realm.setOtpPolicyInitialCounter(policy.getInitialCounter());
realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow());
realm.setOtpPolicyType(policy.getType());
+ realm.setOtpPolicyPeriod(policy.getPeriod());
updateRealm();
}
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 be5c45d..8130ff5 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
@@ -24,6 +24,7 @@ import org.keycloak.models.entities.UserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils;
+import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.util.Time;
@@ -269,6 +270,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
credentialEntity.setAlgorithm(otpPolicy.getAlgorithm());
credentialEntity.setDigits(otpPolicy.getDigits());
credentialEntity.setCounter(otpPolicy.getInitialCounter());
+ credentialEntity.setPeriod(otpPolicy.getPeriod());
user.getCredentials().add(credentialEntity);
} else {
credentialEntity.setValue(cred.getValue());
@@ -276,6 +278,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
credentialEntity.setDigits(policy.getDigits());
credentialEntity.setCounter(policy.getInitialCounter());
credentialEntity.setAlgorithm(policy.getAlgorithm());
+ credentialEntity.setPeriod(policy.getPeriod());
}
}
@@ -386,9 +389,28 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> 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());
+ if (UserCredentialModel.isOtp(credEntity.getType())) {
+ credModel.setCounter(credEntity.getCounter());
+ if (credEntity.getAlgorithm() == null) {
+ // for migration where these values would be null
+ credModel.setAlgorithm(realm.getOTPPolicy().getAlgorithm());
+ } else {
+ credModel.setAlgorithm(credEntity.getAlgorithm());
+ }
+ if (credEntity.getDigits() == 0) {
+ // for migration where these values would be 0
+ credModel.setDigits(realm.getOTPPolicy().getDigits());
+ } else {
+ credModel.setDigits(credEntity.getDigits());
+ }
+
+ if (credEntity.getPeriod() == 0) {
+ // for migration where these values would be 0
+ credModel.setPeriod(realm.getOTPPolicy().getPeriod());
+ } else {
+ credModel.setPeriod(credEntity.getPeriod());
+ }
+ }
result.add(credModel);
}
@@ -414,6 +436,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
credentialEntity.setCounter(credModel.getCounter());
credentialEntity.setAlgorithm(credModel.getAlgorithm());
credentialEntity.setDigits(credModel.getDigits());
+ credentialEntity.setPeriod(credModel.getPeriod());
getMongoStore().updateEntity(user, invocationContext);
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 bcaa98e..95d644e 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);
}
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 cb5c721..9279e8c 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,13 +25,21 @@ import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.constants.KerberosConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.OTPPolicy;
+import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
@@ -55,6 +63,8 @@ import org.openqa.selenium.WebDriver;
*/
public class RequiredActionTotpSetupTest {
+ private static OTPPolicy originalPolicy;
+
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
@@ -66,6 +76,7 @@ public class RequiredActionTotpSetupTest {
requiredAction.setDefaultAction(true);
appRealm.updateRequiredActionProvider(requiredAction);
appRealm.setResetPasswordAllowed(true);
+ originalPolicy = appRealm.getOTPPolicy();
}
});
@@ -152,6 +163,8 @@ public class RequiredActionTotpSetupTest {
events.expectLogin().assertEvent();
}
+
+
@Test
public void setupTotpRegisteredAfterTotpRemoval() {
// Register new user
@@ -221,4 +234,164 @@ public class RequiredActionTotpSetupTest {
events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setuptotp2").assertEvent();
}
+ @Test
+ public void setupOtpPolicyChangedTotp8Digits() {
+ // set policy to 8 digits
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ OTPPolicy newPolicy = new OTPPolicy();
+ newPolicy.setLookAheadWindow(1);
+ newPolicy.setDigits(8);
+ newPolicy.setPeriod(30);
+ newPolicy.setType(UserCredentialModel.TOTP);
+ newPolicy.setAlgorithm(HmacOTP.HMAC_SHA1);
+ newPolicy.setInitialCounter(0);
+ appRealm.setOTPPolicy(newPolicy);
+ }
+
+ });
+
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ totpPage.assertCurrent();
+
+ String totpSecret = totpPage.getTotpSecret();
+
+ TimeBasedOTP timeBased = new TimeBasedOTP(HmacOTP.HMAC_SHA1, 8, 30, 1);
+ totpPage.configure(timeBased.generateTOTP(totpSecret));
+
+ String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId();
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ Event loginEvent = events.expectLogin().session(sessionId).assertEvent();
+
+ oauth.openLogout();
+
+ events.expectLogout(loginEvent.getSessionId()).assertEvent();
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ String src = driver.getPageSource();
+ String token = timeBased.generateTOTP(totpSecret);
+ Assert.assertEquals(8, token.length());
+ loginTotpPage.login(token);
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ events.expectLogin().assertEvent();
+
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setOTPPolicy(originalPolicy);
+ }
+
+ });
+
+ }
+
+ @Test
+ public void setupOtpPolicyChangedHotp() {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ OTPPolicy newPolicy = new OTPPolicy();
+ newPolicy.setLookAheadWindow(0);
+ newPolicy.setDigits(6);
+ newPolicy.setPeriod(30);
+ newPolicy.setType(UserCredentialModel.HOTP);
+ newPolicy.setAlgorithm(HmacOTP.HMAC_SHA1);
+ newPolicy.setInitialCounter(0);
+ appRealm.setOTPPolicy(newPolicy);
+ }
+
+ });
+
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ totpPage.assertCurrent();
+
+ String totpSecret = totpPage.getTotpSecret();
+
+ HmacOTP otpgen = new HmacOTP(6, HmacOTP.HMAC_SHA1, 1);
+ totpPage.configure(otpgen.generateHOTP(totpSecret, 0));
+ String uri = driver.getCurrentUrl();
+ String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId();
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ Event loginEvent = events.expectLogin().session(sessionId).assertEvent();
+
+ oauth.openLogout();
+
+ events.expectLogout(loginEvent.getSessionId()).assertEvent();
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ String token = otpgen.generateHOTP(totpSecret, 1);
+ loginTotpPage.login(token);
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ events.expectLogin().assertEvent();
+
+ oauth.openLogout();
+ events.expectLogout(null).session(AssertEvents.isUUID()).assertEvent();
+
+ // test lookAheadWindow
+
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ OTPPolicy newPolicy = new OTPPolicy();
+ newPolicy.setLookAheadWindow(5);
+ newPolicy.setDigits(6);
+ newPolicy.setPeriod(30);
+ newPolicy.setType(UserCredentialModel.HOTP);
+ newPolicy.setAlgorithm(HmacOTP.HMAC_SHA1);
+ newPolicy.setInitialCounter(0);
+ appRealm.setOTPPolicy(newPolicy);
+ }
+
+ });
+
+
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ token = otpgen.generateHOTP(totpSecret, 4);
+ loginTotpPage.assertCurrent();
+ loginTotpPage.login(token);
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ events.expectLogin().assertEvent();
+
+
+
+
+
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setOTPPolicy(originalPolicy);
+ }
+
+ });
+
+
+ }
+
+
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginHotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginHotpTest.java
new file mode 100755
index 0000000..3721c11
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginHotpTest.java
@@ -0,0 +1,178 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.forms;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.models.OTPPolicy;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.HmacOTP;
+import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginTotpPage;
+import org.keycloak.testsuite.rule.GreenMailRule;
+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.openqa.selenium.WebDriver;
+
+import java.net.MalformedURLException;
+import java.util.Collections;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class LoginHotpTest {
+
+ public static OTPPolicy policy;
+ @ClassRule
+ public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
+ UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
+ policy = appRealm.getOTPPolicy();
+ policy.setType(UserCredentialModel.HOTP);
+ policy.setLookAheadWindow(2);
+ appRealm.setOTPPolicy(policy);
+
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(CredentialRepresentation.HOTP);
+ credentials.setValue("hotpSecret");
+ user.updateCredential(credentials);
+
+ user.setOtpEnabled(true);
+ appRealm.setEventsListeners(Collections.singleton("dummy"));
+ }
+
+ });
+
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @Rule
+ public GreenMailRule greenMail = new GreenMailRule();
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected AppPage appPage;
+
+ @WebResource
+ protected LoginPage loginPage;
+
+ @WebResource
+ protected LoginTotpPage loginTotpPage;
+
+ private HmacOTP otp = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow());
+
+ private int lifespan;
+
+ private static int counter = 0;
+
+ @Before
+ public void before() throws MalformedURLException {
+ otp = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow());
+ }
+
+ @Test
+ public void loginWithHotpFailure() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ loginTotpPage.assertCurrent();
+
+ loginTotpPage.login("123456");
+ loginTotpPage.assertCurrent();
+ Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
+
+ //loginPage.assertCurrent(); // Invalid authenticator code.
+ //Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ events.expectLogin().error("invalid_user_credentials").session((String) null)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ }
+
+ @Test
+ public void loginWithMissingHotp() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ loginTotpPage.assertCurrent();
+
+ loginTotpPage.login(null);
+ loginTotpPage.assertCurrent();
+ Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
+
+ //loginPage.assertCurrent(); // Invalid authenticator code.
+ //Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ events.expectLogin().error("invalid_user_credentials").session((String) null)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ }
+
+ @Test
+ public void loginWithHotpSuccess() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ loginTotpPage.assertCurrent();
+
+ loginTotpPage.login(otp.generateHOTP("hotpSecret", counter++));
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ events.expectLogin().assertEvent();
+ }
+
+ @Test
+ public void loginWithHotpInvalidPassword() throws Exception {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "invalid");
+
+ loginPage.assertCurrent();
+
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ events.expectLogin().error("invalid_user_credentials").session((String) null)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ }
+}