Reuse of token in TOTP is possible

Fixes #13607
This commit is contained in:
Martin Bartoš 2022-08-24 16:48:26 +02:00 committed by Pedro Igor
parent 040e52cfd7
commit 0fcf5d3936
26 changed files with 434 additions and 123 deletions

View file

@ -24,7 +24,6 @@ import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -122,6 +121,7 @@ public class RealmRepresentation {
protected Integer otpPolicyDigits; protected Integer otpPolicyDigits;
protected Integer otpPolicyLookAheadWindow; protected Integer otpPolicyLookAheadWindow;
protected Integer otpPolicyPeriod; protected Integer otpPolicyPeriod;
protected Boolean otpPolicyCodeReusable;
protected List<String> otpSupportedApplications; protected List<String> otpSupportedApplications;
// WebAuthn 2-factor properties below // WebAuthn 2-factor properties below
@ -1025,6 +1025,14 @@ public class RealmRepresentation {
this.otpSupportedApplications = otpSupportedApplications; this.otpSupportedApplications = otpSupportedApplications;
} }
public Boolean isOtpPolicyCodeReusable() {
return otpPolicyCodeReusable;
}
public void setOtpPolicyCodeReusable(Boolean isCodeReusable) {
this.otpPolicyCodeReusable = isCodeReusable;
}
// WebAuthn 2-factor properties below // WebAuthn 2-factor properties below
public String getWebAuthnPolicyRpEntityName() { public String getWebAuthnPolicyRpEntityName() {

View file

@ -894,6 +894,7 @@ public class RealmAdapter implements LegacyRealmModel, JpaModel<RealmEntity> {
otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow()); otpPolicy.setLookAheadWindow(realm.getOtpPolicyLookAheadWindow());
otpPolicy.setType(realm.getOtpPolicyType()); otpPolicy.setType(realm.getOtpPolicyType());
otpPolicy.setPeriod(realm.getOtpPolicyPeriod()); otpPolicy.setPeriod(realm.getOtpPolicyPeriod());
otpPolicy.setCodeReusable(getAttribute(OTPPolicy.REALM_REUSABLE_CODE_ATTRIBUTE, OTPPolicy.DEFAULT_IS_REUSABLE));
} }
return otpPolicy; return otpPolicy;
} }
@ -906,6 +907,7 @@ public class RealmAdapter implements LegacyRealmModel, JpaModel<RealmEntity> {
realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow()); realm.setOtpPolicyLookAheadWindow(policy.getLookAheadWindow());
realm.setOtpPolicyType(policy.getType()); realm.setOtpPolicyType(policy.getType());
realm.setOtpPolicyPeriod(policy.getPeriod()); realm.setOtpPolicyPeriod(policy.getPeriod());
setAttribute(OTPPolicy.REALM_REUSABLE_CODE_ATTRIBUTE, policy.isCodeReusable());
em.flush(); em.flush();
} }

View file

@ -1374,8 +1374,8 @@ public class LegacyExportImportManager implements ExportImportManager {
if (rep.getOtpPolicyAlgorithm() != null) policy.setAlgorithm(rep.getOtpPolicyAlgorithm()); if (rep.getOtpPolicyAlgorithm() != null) policy.setAlgorithm(rep.getOtpPolicyAlgorithm());
if (rep.getOtpPolicyDigits() != null) policy.setDigits(rep.getOtpPolicyDigits()); if (rep.getOtpPolicyDigits() != null) policy.setDigits(rep.getOtpPolicyDigits());
if (rep.getOtpPolicyPeriod() != null) policy.setPeriod(rep.getOtpPolicyPeriod()); if (rep.getOtpPolicyPeriod() != null) policy.setPeriod(rep.getOtpPolicyPeriod());
if (rep.isOtpPolicyCodeReusable() != null) policy.setCodeReusable(rep.isOtpPolicyCodeReusable());
return policy; return policy;
} }

View file

@ -20,6 +20,8 @@ public class HotRodOTPPolicyEntity extends AbstractHotRodEntity {
public String otpPolicyAlgorithm; public String otpPolicyAlgorithm;
@ProtoField(number = 6) @ProtoField(number = 6)
public String otpPolicyType; public String otpPolicyType;
@ProtoField(number = 7)
public Boolean otpPolicyCodeReusable;
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
return HotRodOTPPolicyEntityDelegate.entityEquals(this, o); return HotRodOTPPolicyEntityDelegate.entityEquals(this, o);

View file

@ -1280,8 +1280,8 @@ public class MapExportImportManager implements ExportImportManager {
if (rep.getOtpPolicyAlgorithm() != null) policy.setAlgorithm(rep.getOtpPolicyAlgorithm()); if (rep.getOtpPolicyAlgorithm() != null) policy.setAlgorithm(rep.getOtpPolicyAlgorithm());
if (rep.getOtpPolicyDigits() != null) policy.setDigits(rep.getOtpPolicyDigits()); if (rep.getOtpPolicyDigits() != null) policy.setDigits(rep.getOtpPolicyDigits());
if (rep.getOtpPolicyPeriod() != null) policy.setPeriod(rep.getOtpPolicyPeriod()); if (rep.getOtpPolicyPeriod() != null) policy.setPeriod(rep.getOtpPolicyPeriod());
if (rep.isOtpPolicyCodeReusable() != null) policy.setCodeReusable(rep.isOtpPolicyCodeReusable());
return policy; return policy;
} }

View file

@ -34,22 +34,31 @@ public interface MapOTPPolicyEntity extends UpdatableEntity {
entity.setOtpPolicyLookAheadWindow(model.getLookAheadWindow()); entity.setOtpPolicyLookAheadWindow(model.getLookAheadWindow());
entity.setOtpPolicyType(model.getType()); entity.setOtpPolicyType(model.getType());
entity.setOtpPolicyPeriod(model.getPeriod()); entity.setOtpPolicyPeriod(model.getPeriod());
entity.setOtpPolicyCodeReusable(model.isCodeReusable());
return entity; return entity;
} }
static OTPPolicy toModel(MapOTPPolicyEntity entity) { static OTPPolicy toModel(MapOTPPolicyEntity entity) {
if (entity == null) return null; if (entity == null) return null;
OTPPolicy model = new OTPPolicy(); OTPPolicy model = new OTPPolicy();
Integer otpPolicyDigits = entity.getOtpPolicyDigits(); Integer otpPolicyDigits = entity.getOtpPolicyDigits();
model.setDigits(otpPolicyDigits == null ? 0 : otpPolicyDigits); model.setDigits(otpPolicyDigits == null ? 0 : otpPolicyDigits);
model.setAlgorithm(entity.getOtpPolicyAlgorithm()); model.setAlgorithm(entity.getOtpPolicyAlgorithm());
Integer otpPolicyInitialCounter = entity.getOtpPolicyInitialCounter(); Integer otpPolicyInitialCounter = entity.getOtpPolicyInitialCounter();
model.setInitialCounter(otpPolicyInitialCounter == null ? 0 : otpPolicyInitialCounter); model.setInitialCounter(otpPolicyInitialCounter == null ? 0 : otpPolicyInitialCounter);
Integer otpPolicyLookAheadWindow = entity.getOtpPolicyLookAheadWindow(); Integer otpPolicyLookAheadWindow = entity.getOtpPolicyLookAheadWindow();
model.setLookAheadWindow(otpPolicyLookAheadWindow == null ? 0 : otpPolicyLookAheadWindow); model.setLookAheadWindow(otpPolicyLookAheadWindow == null ? 0 : otpPolicyLookAheadWindow);
model.setType(entity.getOtpPolicyType()); model.setType(entity.getOtpPolicyType());
Integer otpPolicyPeriod = entity.getOtpPolicyPeriod(); Integer otpPolicyPeriod = entity.getOtpPolicyPeriod();
model.setPeriod(otpPolicyPeriod == null ? 0 : otpPolicyPeriod); model.setPeriod(otpPolicyPeriod == null ? 0 : otpPolicyPeriod);
Boolean isOtpPolicyReusable = entity.isOtpPolicyCodeReusable();
model.setCodeReusable(isOtpPolicyReusable == null ? OTPPolicy.DEFAULT_IS_REUSABLE : isOtpPolicyReusable);
return model; return model;
} }
@ -70,4 +79,7 @@ public interface MapOTPPolicyEntity extends UpdatableEntity {
String getOtpPolicyAlgorithm(); String getOtpPolicyAlgorithm();
void setOtpPolicyAlgorithm(String otpPolicyAlgorithm); void setOtpPolicyAlgorithm(String otpPolicyAlgorithm);
Boolean isOtpPolicyCodeReusable();
void setOtpPolicyCodeReusable(Boolean isOtpPolicyCodeReusable);
} }

View file

@ -418,6 +418,7 @@ public class ModelToRepresentation {
rep.setOtpPolicyType(otpPolicy.getType()); rep.setOtpPolicyType(otpPolicy.getType());
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow()); rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications()); rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
rep.setOtpPolicyCodeReusable(otpPolicy.isCodeReusable());
WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy(); WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy();
rep.setWebAuthnPolicyRpEntityName(webAuthnPolicy.getRpEntityName()); rep.setWebAuthnPolicyRpEntityName(webAuthnPolicy.getRpEntityName());

View file

@ -44,6 +44,7 @@ public class OTPPolicy implements Serializable {
protected int digits; protected int digits;
protected int lookAheadWindow; protected int lookAheadWindow;
protected int period; protected int period;
protected boolean isCodeReusable;
private static final Map<String, String> algToKeyUriAlg = new HashMap<>(); private static final Map<String, String> algToKeyUriAlg = new HashMap<>();
@ -59,15 +60,24 @@ public class OTPPolicy implements Serializable {
} }
public OTPPolicy(String type, String algorithm, int initialCounter, int digits, int lookAheadWindow, int period) { public OTPPolicy(String type, String algorithm, int initialCounter, int digits, int lookAheadWindow, int period) {
this(type, algorithm, initialCounter, digits, lookAheadWindow, period, DEFAULT_IS_REUSABLE);
}
public OTPPolicy(String type, String algorithm, int initialCounter, int digits, int lookAheadWindow, int period, boolean isCodeReusable) {
this.type = type; this.type = type;
this.algorithm = algorithm; this.algorithm = algorithm;
this.initialCounter = initialCounter; this.initialCounter = initialCounter;
this.digits = digits; this.digits = digits;
this.lookAheadWindow = lookAheadWindow; this.lookAheadWindow = lookAheadWindow;
this.period = period; this.period = period;
this.isCodeReusable = isCodeReusable;
} }
public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(OTPCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1, 30); public static OTPPolicy DEFAULT_POLICY = new OTPPolicy(OTPCredentialModel.TOTP, HmacOTP.HMAC_SHA1, 0, 6, 1, 30);
public static final boolean DEFAULT_IS_REUSABLE = false;
// Realm attributes
public static final String REALM_REUSABLE_CODE_ATTRIBUTE = "realmReusableOtpCode";
public String getAlgorithmKey() { public String getAlgorithmKey() {
return algToKeyUriAlg.containsKey(algorithm) ? algToKeyUriAlg.get(algorithm) : algorithm; return algToKeyUriAlg.containsKey(algorithm) ? algToKeyUriAlg.get(algorithm) : algorithm;
@ -121,8 +131,17 @@ public class OTPPolicy implements Serializable {
this.period = period; this.period = period;
} }
public boolean isCodeReusable() {
return isCodeReusable;
}
public void setCodeReusable(boolean isReusable) {
isCodeReusable = isReusable;
}
/** /**
* Constructs the <code>otpauth://</code> URI based on the <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key-Uri-Format</a>. * Constructs the <code>otpauth://</code> URI based on the <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key-Uri-Format</a>.
*
* @param realm * @param realm
* @param user * @param user
* @param secret * @param secret

View file

@ -19,14 +19,15 @@ package org.keycloak.credential;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.ObjectUtil;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.dto.OTPCredentialData;
import org.keycloak.models.credential.dto.OTPSecretData;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.dto.OTPCredentialData;
import org.keycloak.models.credential.dto.OTPSecretData;
import org.keycloak.models.utils.HmacOTP; import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
@ -99,6 +100,7 @@ public class OTPCredentialProvider implements CredentialProvider<OTPCredentialMo
OTPSecretData secretData = otpCredentialModel.getOTPSecretData(); OTPSecretData secretData = otpCredentialModel.getOTPSecretData();
OTPCredentialData credentialData = otpCredentialModel.getOTPCredentialData(); OTPCredentialData credentialData = otpCredentialModel.getOTPCredentialData();
OTPPolicy policy = realm.getOTPPolicy(); OTPPolicy policy = realm.getOTPPolicy();
if (OTPCredentialModel.HOTP.equals(credentialData.getSubType())) { if (OTPCredentialModel.HOTP.equals(credentialData.getSubType())) {
HmacOTP validator = new HmacOTP(credentialData.getDigits(), credentialData.getAlgorithm(), policy.getLookAheadWindow()); HmacOTP validator = new HmacOTP(credentialData.getDigits(), credentialData.getAlgorithm(), policy.getLookAheadWindow());
int counter = validator.validateHOTP(challengeResponse, secretData.getValue(), credentialData.getCounter()); int counter = validator.validateHOTP(challengeResponse, secretData.getValue(), credentialData.getCounter());
@ -110,7 +112,17 @@ public class OTPCredentialProvider implements CredentialProvider<OTPCredentialMo
return true; return true;
} else if (OTPCredentialModel.TOTP.equals(credentialData.getSubType())) { } else if (OTPCredentialModel.TOTP.equals(credentialData.getSubType())) {
TimeBasedOTP validator = new TimeBasedOTP(credentialData.getAlgorithm(), credentialData.getDigits(), credentialData.getPeriod(), policy.getLookAheadWindow()); TimeBasedOTP validator = new TimeBasedOTP(credentialData.getAlgorithm(), credentialData.getDigits(), credentialData.getPeriod(), policy.getLookAheadWindow());
return validator.validateTOTP(challengeResponse, secretData.getValue().getBytes(StandardCharsets.UTF_8)); final boolean isValid = validator.validateTOTP(challengeResponse, secretData.getValue().getBytes(StandardCharsets.UTF_8));
if (isValid) {
if (policy.isCodeReusable()) return true;
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
final long validLifespan = (long) credentialData.getPeriod() * (2L * policy.getLookAheadWindow() + 1);
final String searchKey = credential.getId() + "." + challengeResponse;
return singleUseStore.putIfAbsent(searchKey, validLifespan);
}
} }
return false; return false;
} }

View file

@ -2,6 +2,7 @@ package org.keycloak.testsuite.updaters;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -117,4 +118,40 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
rep.setInternationalizationEnabled(internationalizationEnabled); rep.setInternationalizationEnabled(internationalizationEnabled);
return this; return this;
} }
// OTP Policy
public RealmAttributeUpdater setOtpPolicyAlgorithm(String otpPolicyAlgorithm) {
rep.setOtpPolicyAlgorithm(otpPolicyAlgorithm);
return this;
}
public RealmAttributeUpdater setOtpPolicyDigits(Integer otpPolicyDigits) {
rep.setOtpPolicyDigits(otpPolicyDigits);
return this;
}
public RealmAttributeUpdater setOtpPolicyInitialCounter(Integer otpPolicyInitialCounter) {
rep.setOtpPolicyInitialCounter(otpPolicyInitialCounter);
return this;
}
public RealmAttributeUpdater setOtpPolicyPeriod(Integer otpPolicyPeriod) {
rep.setOtpPolicyPeriod(otpPolicyPeriod);
return this;
}
public RealmAttributeUpdater setOtpPolicyType(String otpPolicyType) {
rep.setOtpPolicyType(otpPolicyType);
return this;
}
public RealmAttributeUpdater setOtpPolicyLookAheadWindow(Integer otpPolicyLookAheadWindow) {
rep.setOtpPolicyLookAheadWindow(otpPolicyLookAheadWindow);
return this;
}
public RealmAttributeUpdater setOtpPolicyCodeReusable(Boolean isCodeReusable) {
rep.setOtpPolicyCodeReusable(isCodeReusable);
return this;
}
} }

View file

@ -40,6 +40,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.RealmProvider; import org.keycloak.models.RealmProvider;
import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache; import org.keycloak.models.cache.UserCache;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -69,7 +70,6 @@ import org.openqa.selenium.WebDriver;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.PipedInputStream; import java.io.PipedInputStream;
@ -85,20 +85,25 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.*; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.keycloak.testsuite.admin.Users.setPasswordFor; import static org.keycloak.testsuite.admin.Users.setPasswordFor;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_HOST; import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_HOST;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT; import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SCHEME; import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SCHEME;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED; import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
import static org.keycloak.testsuite.util.URLUtils.navigateToUri;
import static org.keycloak.testsuite.util.ServerURLs.removeDefaultPorts; import static org.keycloak.testsuite.util.ServerURLs.removeDefaultPorts;
import static org.keycloak.testsuite.util.URLUtils.navigateToUri;
/** /**
* *
@ -654,6 +659,13 @@ public abstract class AbstractKeycloakTest {
log.debugv("Reset time offset, response {0}", response); log.debugv("Reset time offset, response {0}", response);
} }
public void setOtpTimeOffset(int offsetSeconds, TimeBasedOTP otp) {
setTimeOffset(offsetSeconds);
final Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, offsetSeconds);
otp.setCalendar(calendar);
}
public int getCurrentTime() { public int getCurrentTime() {
return Time.currentTime(); return Time.currentTime();
} }

View file

@ -31,19 +31,32 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.admin.Users; import org.keycloak.testsuite.admin.Users;
import org.keycloak.testsuite.auth.page.login.OneTimeCode; import org.keycloak.testsuite.auth.page.login.OneTimeCode;
import org.keycloak.testsuite.pages.LoginConfigTotpPage; import org.keycloak.testsuite.pages.LoginConfigTotpPage;
import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.pages.PageUtils; import org.keycloak.testsuite.pages.PageUtils;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.*; import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.DEFAULT_OTP_OUTCOME;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.FORCE;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.FORCE_OTP_FOR_HTTP_HEADER;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.FORCE_OTP_ROLE;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.OTP_CONTROL_USER_ATTRIBUTE;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.SKIP;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.SKIP_OTP_FOR_HTTP_HEADER;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.SKIP_OTP_ROLE;
import static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP; import static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP;
import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT; import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT;
@ -65,6 +78,9 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
@Page @Page
private LoginConfigTotpPage loginConfigTotpPage; private LoginConfigTotpPage loginConfigTotpPage;
@Page
private LoginTotpPage loginTotpPage;
@Override @Override
public void setDefaultPageUriParameters() { public void setDefaultPageUriParameters() {
super.setDefaultPageUriParameters(); super.setDefaultPageUriParameters();
@ -119,6 +135,58 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
assertCurrentUrlStartsWith(testLoginOneTimeCodePage); assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
} }
@Test
public void reuseExistingOTP() {
reuseExistingOtp(true);
}
@Test
public void notReuseExistingOTP() {
reuseExistingOtp(false);
}
private void reuseExistingOtp(boolean allowReusingExistingOtp) {
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealmResource())
.setBrowserFlow("browser")
.setOtpPolicyCodeReusable(allowReusingExistingOtp)
.update()) {
//update realm browser flow
RealmRepresentation realm = testRealmResource().toRepresentation();
realm.setBrowserFlow("browser");
testRealmResource().update(realm);
updateRequirement("browser", Requirement.REQUIRED, (authExec) -> authExec.getDisplayName().equals("Browser - Conditional OTP"));
testRealmAccountManagementPage.navigateTo();
testRealmLoginPage.form().login(testUser);
assertTrue(loginConfigTotpPage.isCurrent());
//configure OTP for test user
testRealmAccountManagementPage.navigateTo();
testRealmLoginPage.form().login(testUser);
final String totpSecret = testRealmLoginPage.form().totpForm().getTotpSecret();
assertThat(totpSecret, notNullValue());
final String generatedOtp = totp.generateTOTP(totpSecret);
assertThat(generatedOtp, notNullValue());
testRealmLoginPage.form().totpForm().setTotp(generatedOtp);
testRealmLoginPage.form().totpForm().submit();
testRealmAccountManagementPage.signOut();
testRealmAccountManagementPage.navigateTo();
testRealmLoginPage.form().login(testUser);
loginTotpPage.assertCurrent();
loginTotpPage.login(generatedOtp);
assertThat(loginTotpPage.isCurrent(), is(!allowReusingExistingOtp));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test @Test
@AuthServerContainerExclude(AuthServer.REMOTE) @AuthServerContainerExclude(AuthServer.REMOTE)
public void conditionalOTPNoDefault() { public void conditionalOTPNoDefault() {

View file

@ -363,6 +363,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
events.expectLogout(authSessionId).assertEvent(); events.expectLogout(authSessionId).assertEvent();
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
loginPage.open(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
@ -412,6 +414,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
assertTrue(loginPage.isCurrent()); assertTrue(loginPage.isCurrent());
Assert.assertFalse(totpPage.isCurrent()); Assert.assertFalse(totpPage.isCurrent());
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
// Login with one-time password // Login with one-time password
loginTotpPage.login(totp.generateTOTP(totpCode)); loginTotpPage.login(totp.generateTOTP(totpCode));
@ -472,6 +476,8 @@ public class AppInitiatedActionTotpSetupTest extends AbstractAppInitiatedActionT
events.expectLogout(loginEvent.getSessionId()).assertEvent(); events.expectLogout(loginEvent.getSessionId()).assertEvent();
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, timeBased);
loginPage.open(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
String src = driver.getPageSource(); String src = driver.getPageSource();

View file

@ -47,14 +47,15 @@ import org.keycloak.testsuite.pages.LoginConfigTotpPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginTotpPage; import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -281,6 +282,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
String customOtpLabel = "my-custom-otp-label"; String customOtpLabel = "my-custom-otp-label";
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
// Set OTP label to a custom value // Set OTP label to a custom value
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()), customOtpLabel); totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()), customOtpLabel);
@ -325,7 +328,18 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
} }
@Test @Test
public void setupTotpExisting() { public void setupTotpExistingReusableCodeEnabled() throws IOException {
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setOtpPolicyCodeReusable(true).update()) {
setupTotpExisting(true);
}
}
@Test
public void setupTotpExistingReusableCodeDisabled() {
setupTotpExisting(false); // Default value
}
public void setupTotpExisting(boolean reusableCodesEnabled) {
loginPage.open(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
@ -348,14 +362,21 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
events.expectLogout(authSessionId).assertEvent(); events.expectLogout(authSessionId).assertEvent();
if (!reusableCodesEnabled) {
setTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS);
}
loginPage.open(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
String src = driver.getPageSource(); String src = driver.getPageSource();
loginTotpPage.login(totp.generateTOTP(totpSecret)); loginTotpPage.login(totp.generateTOTP(totpSecret));
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); if (!reusableCodesEnabled) {
loginTotpPage.assertCurrent();
events.expectLogin().assertEvent(); } else {
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().assertEvent();
}
} }
//KEYCLOAK-15511 //KEYCLOAK-15511
@ -409,6 +430,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
oauth.idTokenHint(tokenResponse.getIdToken()).openLogout(); oauth.idTokenHint(tokenResponse.getIdToken()).openLogout();
events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent(); events.expectLogout(loginEvent.getSessionId()).user(userId).assertEvent();
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
// Try to login after logout // Try to login after logout
loginPage.open(); loginPage.open();
loginPage.login("setupTotp2", "password2"); loginPage.login("setupTotp2", "password2");
@ -437,6 +460,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
accountTotpPage.logout(); accountTotpPage.logout();
events.expectLogout(loginEvent.getSessionId()).user(userId).detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/totp").assertEvent(); events.expectLogout(loginEvent.getSessionId()).user(userId).detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/totp").assertEvent();
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
// Try to login // Try to login
loginPage.open(); loginPage.open();
loginPage.login("setupTotp2", "password2"); loginPage.login("setupTotp2", "password2");
@ -489,6 +514,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
events.expectLogout(loginEvent.getSessionId()).assertEvent(); events.expectLogout(loginEvent.getSessionId()).assertEvent();
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, timeBased);
loginPage.open(); loginPage.open();
loginPage.login("test-user@localhost", "password"); loginPage.login("test-user@localhost", "password");
String src = driver.getPageSource(); String src = driver.getPageSource();

View file

@ -17,7 +17,9 @@
package org.keycloak.testsuite.admin.realm; package org.keycloak.testsuite.admin.realm;
import com.google.common.collect.Sets;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Rule; import org.junit.Rule;
@ -35,6 +37,7 @@ import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory;
import org.keycloak.models.CibaConfig; import org.keycloak.models.CibaConfig;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.OAuth2DeviceConfig; import org.keycloak.models.OAuth2DeviceConfig;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.ParConfig; import org.keycloak.models.ParConfig;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -59,6 +62,7 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.client.KeycloakTestingClient; import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.events.TestEventsListenerProviderFactory; import org.keycloak.testsuite.events.TestEventsListenerProviderFactory;
import org.keycloak.testsuite.model.StoreProvider;
import org.keycloak.testsuite.runonserver.RunHelpers; import org.keycloak.testsuite.runonserver.RunHelpers;
import org.keycloak.testsuite.updaters.Creator; import org.keycloak.testsuite.updaters.Creator;
import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AdminEventPaths;
@ -74,16 +78,24 @@ import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
@ -192,11 +204,23 @@ public class RealmTest extends AbstractAdminTest {
CibaConfig.CIBA_AUTH_REQUESTED_USER_HINT).stream().forEach(i -> rep2.getAttributes().remove(i)); CibaConfig.CIBA_AUTH_REQUESTED_USER_HINT).stream().forEach(i -> rep2.getAttributes().remove(i));
} }
Map<String, String> attributes = rep2.getAttributes(); Set<String> attributesKeys = rep2.getAttributes().keySet();
assertTrue("Attributes expected to be present oauth2DeviceCodeLifespan, oauth2DevicePollingInterval, found: " + String.join(", ", attributes.keySet()),
attributes.size() == 3 && attributes.containsKey(OAuth2DeviceConfig.OAUTH2_DEVICE_CODE_LIFESPAN) int expectedAttributesCount = 3;
&& attributes.containsKey(OAuth2DeviceConfig.OAUTH2_DEVICE_POLLING_INTERVAL) final Set<String> expectedAttributes = Sets.newHashSet(
&& attributes.containsKey(ParConfig.PAR_REQUEST_URI_LIFESPAN)); OAuth2DeviceConfig.OAUTH2_DEVICE_CODE_LIFESPAN,
OAuth2DeviceConfig.OAUTH2_DEVICE_POLLING_INTERVAL,
ParConfig.PAR_REQUEST_URI_LIFESPAN
);
// This attribute is represented in Legacy store as attribute and for Map store as a field
if (!StoreProvider.getCurrentProvider().isMapStore()) {
expectedAttributes.add(OTPPolicy.REALM_REUSABLE_CODE_ATTRIBUTE);
expectedAttributesCount++;
}
assertThat(attributesKeys.size(), CoreMatchers.is(expectedAttributesCount));
assertThat(attributesKeys, CoreMatchers.is(expectedAttributes));
} finally { } finally {
adminClient.realm("attributes").remove(); adminClient.realm("attributes").remove();
} }

View file

@ -8,6 +8,8 @@ import org.keycloak.common.Profile;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.models.IdentityProviderMapperSyncMode; import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
@ -473,6 +475,8 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest {
assertNumFederatedIdentities(realm.users().search(bc.getUserLogin()).get(0).getId(), 1); assertNumFederatedIdentities(realm.users().search(bc.getUserLogin()).get(0).getId(), 1);
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName()); logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
logInWithBroker(bc); logInWithBroker(bc);
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
@ -512,6 +516,8 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest {
assertNumFederatedIdentities(realm.users().search(bc.getUserLogin()).get(0).getId(), 1); assertNumFederatedIdentities(realm.users().search(bc.getUserLogin()).get(0).getId(), 1);
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName()); logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
logInWithBroker(bc); logInWithBroker(bc);
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
@ -531,6 +537,8 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest {
String userId = ApiUtil.findUserByUsername(realm, bc.getUserLogin()).getId(); String userId = ApiUtil.findUserByUsername(realm, bc.getUserLogin()).getId();
realm.attackDetection().clearBruteForceForUser(userId); realm.attackDetection().clearBruteForceForUser(userId);
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
loginTotpPage.login(totp.generateTOTP(totpSecret)); loginTotpPage.login(totp.generateTOTP(totpSecret));
waitForAccountManagementTitle(); waitForAccountManagementTitle();
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName()); logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());

View file

@ -5,6 +5,7 @@ import com.google.common.collect.Lists;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.ClientsResource;
@ -21,20 +22,18 @@ import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderMapperSyncMode; import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.Urls;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
@ -51,6 +50,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.keycloak.models.utils.TimeBasedOTP.DEFAULT_INTERVAL_SECONDS;
import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername; import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername;
import static org.keycloak.testsuite.broker.BrokerRunOnServerUtil.configurePostBrokerLoginWithOTP; import static org.keycloak.testsuite.broker.BrokerRunOnServerUtil.configurePostBrokerLoginWithOTP;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME; import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
@ -69,12 +69,17 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
return KcOidcBrokerConfiguration.INSTANCE; return KcOidcBrokerConfiguration.INSTANCE;
} }
@Before
public void setUpTotp() {
totp = new TimeBasedOTP();
}
@Override @Override
protected Iterable<IdentityProviderMapperRepresentation> createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode) { protected Iterable<IdentityProviderMapperRepresentation> createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode) {
IdentityProviderMapperRepresentation attrMapper1 = new IdentityProviderMapperRepresentation(); IdentityProviderMapperRepresentation attrMapper1 = new IdentityProviderMapperRepresentation();
attrMapper1.setName("manager-role-mapper"); attrMapper1.setName("manager-role-mapper");
attrMapper1.setIdentityProviderMapper(ExternalKeycloakRoleToRoleMapper.PROVIDER_ID); attrMapper1.setIdentityProviderMapper(ExternalKeycloakRoleToRoleMapper.PROVIDER_ID);
attrMapper1.setConfig(ImmutableMap.<String,String>builder() attrMapper1.setConfig(ImmutableMap.<String, String>builder()
.put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString())
.put("external.role", ROLE_MANAGER) .put("external.role", ROLE_MANAGER)
.put("role", ROLE_MANAGER) .put("role", ROLE_MANAGER)
@ -258,6 +263,8 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
totpPage.configure(totp.generateTOTP(totpSecret)); totpPage.configure(totp.generateTOTP(totpSecret));
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName()); logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
setOtpTimeOffset(DEFAULT_INTERVAL_SECONDS, totp);
logInWithBroker(bc); logInWithBroker(bc);
waitForPage(driver, "account already exists", false); waitForPage(driver, "account already exists", false);
@ -320,48 +327,54 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
*/ */
@Test @Test
public void testReauthenticationBothBrokersWithOTPRequired() throws Exception { public void testReauthenticationBothBrokersWithOTPRequired() throws Exception {
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE; final RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0); final RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
try { try (RealmAttributeUpdater rauConsumer = new RealmAttributeUpdater(consumerRealm).setOtpPolicyCodeReusable(true).update();
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin); RealmAttributeUpdater rauProvider = new RealmAttributeUpdater(providerRealm).setOtpPolicyCodeReusable(true).update()) {
adminClient.realm(bc.providerRealmName()).clients().create(samlClient);
consumerRealm.identityProviders().create(samlBroker);
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName())); KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(samlBrokerConfig.getIDPAlias())); ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
logInWithBroker(samlBrokerConfig); IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
totpPage.assertCurrent();
String totpSecret = totpPage.getTotpSecret();
totpPage.configure(totp.generateTOTP(totpSecret));
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(bc.getIDPAlias())); try {
logInWithBroker(bc); updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
providerRealm.clients().create(samlClient);
consumerRealm.identityProviders().create(samlBroker);
waitForPage(driver, "account already exists", false); driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
idpConfirmLinkPage.assertCurrent(); testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(samlBrokerConfig.getIDPAlias()));
idpConfirmLinkPage.clickLinkAccount(); logInWithBroker(samlBrokerConfig);
loginPage.clickSocial(samlBrokerConfig.getIDPAlias()); totpPage.assertCurrent();
String totpSecret = totpPage.getTotpSecret();
totpPage.configure(totp.generateTOTP(totpSecret));
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
loginTotpPage.assertCurrent(); testingClient.server(bc.consumerRealmName()).run(configurePostBrokerLoginWithOTP(bc.getIDPAlias()));
loginTotpPage.login(totp.generateTOTP(totpSecret)); logInWithBroker(bc);
logoutFromRealm(getProviderRoot(), bc.providerRealmName());
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
logInWithBroker(bc); waitForPage(driver, "account already exists", false);
idpConfirmLinkPage.assertCurrent();
idpConfirmLinkPage.clickLinkAccount();
loginPage.clickSocial(samlBrokerConfig.getIDPAlias());
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
loginTotpPage.login(totp.generateTOTP(totpSecret)); loginTotpPage.login(totp.generateTOTP(totpSecret));
waitForAccountManagementTitle(); logoutFromRealm(getProviderRoot(), bc.providerRealmName());
accountUpdateProfilePage.assertCurrent(); logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
assertNumFederatedIdentities(consumerRealm.users().search(samlBrokerConfig.getUserLogin()).get(0).getId(), 2); logInWithBroker(bc);
} finally {
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin); loginTotpPage.assertCurrent();
removeUserByUsername(consumerRealm, "consumer"); loginTotpPage.login(totp.generateTOTP(totpSecret));
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
assertNumFederatedIdentities(consumerRealm.users().search(samlBrokerConfig.getUserLogin()).get(0).getId(), 2);
} finally {
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
removeUserByUsername(consumerRealm, "consumer");
}
} }
} }

View file

@ -6,6 +6,7 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
@ -81,6 +82,8 @@ public class KcOidcFirstBrokerLoginNewAuthTest extends AbstractInitializedBaseBr
String consumerRealmUserId = createUser("consumer"); String consumerRealmUserId = createUser("consumer");
String totpSecret = addTOTPToUser("consumer"); String totpSecret = addTOTPToUser("consumer");
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
loginWithBrokerAndConfirmLinkAccount(); loginWithBrokerAndConfirmLinkAccount();
// Login with password // Login with password
@ -164,6 +167,8 @@ public class KcOidcFirstBrokerLoginNewAuthTest extends AbstractInitializedBaseBr
String consumerRealmUserId = createUser("consumer"); String consumerRealmUserId = createUser("consumer");
String totpSecret = addTOTPToUser("consumer"); String totpSecret = addTOTPToUser("consumer");
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
loginWithBrokerAndConfirmLinkAccount(); loginWithBrokerAndConfirmLinkAccount();
// Assert that user can see credentials combobox. Password and OTP are available credentials. Password should be selected. // Assert that user can see credentials combobox. Password and OTP are available credentials. Password should be selected.

View file

@ -53,6 +53,7 @@ import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginConfigTotpPage; import org.keycloak.testsuite.pages.LoginConfigTotpPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginTotpPage; import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED; import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
@ -146,64 +147,66 @@ public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void testUpdateOTP() { public void testUpdateOTP() throws IOException {
// Add requiredAction to the user for update OTP try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setOtpPolicyCodeReusable(true).update()) {
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user"); // Add requiredAction to the user for update OTP
UserRepresentation userRep = user.toRepresentation(); UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user");
userRep.setRequiredActions(Collections.singletonList(UserModel.RequiredAction.CONFIGURE_TOTP.toString())); UserRepresentation userRep = user.toRepresentation();
user.update(userRep); userRep.setRequiredActions(Collections.singletonList(UserModel.RequiredAction.CONFIGURE_TOTP.toString()));
user.update(userRep);
// Authenticate as the user // Authenticate as the user
loginPage.open(); loginPage.open();
loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD); loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD);
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
loginTotpPage.login(DummyUserFederationProvider.HARDCODED_OTP); loginTotpPage.login(DummyUserFederationProvider.HARDCODED_OTP);
// User should be required to update OTP // User should be required to update OTP
loginConfigTotpPage.assertCurrent(); loginConfigTotpPage.assertCurrent();
// Dummy OTP code won't work when configure new OTP // Dummy OTP code won't work when configure new OTP
loginConfigTotpPage.configure(DummyUserFederationProvider.HARDCODED_OTP); loginConfigTotpPage.configure(DummyUserFederationProvider.HARDCODED_OTP);
Assert.assertEquals("Invalid authenticator code.", loginConfigTotpPage.getInputCodeError()); Assert.assertEquals("Invalid authenticator code.", loginConfigTotpPage.getInputCodeError());
// This will save the credential to the local DB // This will save the credential to the local DB
String totpSecret = loginConfigTotpPage.getTotpSecret(); String totpSecret = loginConfigTotpPage.getTotpSecret();
log.infof("Totp Secret: %s", totpSecret); log.infof("Totp Secret: %s", totpSecret);
String totpCode = totp.generateTOTP(totpSecret); String totpCode = totp.generateTOTP(totpSecret);
loginConfigTotpPage.configure(totpCode); loginConfigTotpPage.configure(totpCode);
appPage.assertCurrent(); appPage.assertCurrent();
// Logout // Logout
events.expect(EventType.UPDATE_TOTP).user(userRep.getId()).assertEvent(); //remove the UPDATE_TOTP event events.expect(EventType.UPDATE_TOTP).user(userRep.getId()).assertEvent(); //remove the UPDATE_TOTP event
EventRepresentation loginEvent = events.expectLogin().user(userRep.getId()).assertEvent(); EventRepresentation loginEvent = events.expectLogin().user(userRep.getId()).assertEvent();
String idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken(); String idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken();
appPage.logout(idTokenHint); appPage.logout(idTokenHint);
events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent(); events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent();
// Authenticate as the user again with the dummy OTP should still work // Authenticate as the user again with the dummy OTP should still work
loginPage.open(); loginPage.open();
loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD); loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD);
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
loginTotpPage.login(DummyUserFederationProvider.HARDCODED_OTP); loginTotpPage.login(DummyUserFederationProvider.HARDCODED_OTP);
appPage.assertCurrent(); appPage.assertCurrent();
loginEvent = events.expectLogin().user(userRep.getId()).assertEvent(); loginEvent = events.expectLogin().user(userRep.getId()).assertEvent();
idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken(); idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken();
appPage.logout(idTokenHint); appPage.logout(idTokenHint);
events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent(); events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent();
// Authenticate with the new OTP code should work as well // Authenticate with the new OTP code should work as well
loginPage.open(); loginPage.open();
loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD); loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD);
loginTotpPage.assertCurrent(); loginTotpPage.assertCurrent();
loginTotpPage.login(totp.generateTOTP(totpSecret)); loginTotpPage.login(totp.generateTOTP(totpSecret));
appPage.assertCurrent(); appPage.assertCurrent();
loginEvent = events.expectLogin().user(userRep.getId()).assertEvent(); loginEvent = events.expectLogin().user(userRep.getId()).assertEvent();
idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken(); idTokenHint = sendTokenRequestAndGetResponse(loginEvent).getIdToken();
appPage.logout(idTokenHint); appPage.logout(idTokenHint);
events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent(); events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent();
}
} }
@Test @Test

View file

@ -53,6 +53,7 @@ import org.keycloak.testsuite.util.UserBuilder;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
@ -112,6 +113,7 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
testRealm.setMaxDeltaTimeSeconds(20); testRealm.setMaxDeltaTimeSeconds(20);
testRealm.setMaxFailureWaitSeconds(100); testRealm.setMaxFailureWaitSeconds(100);
testRealm.setWaitIncrementSeconds(5); testRealm.setWaitIncrementSeconds(5);
testRealm.setOtpPolicyCodeReusable(true);
//testRealm.setQuickLoginCheckMilliSeconds(0L); //testRealm.setQuickLoginCheckMilliSeconds(0L);
userId = user.getId(); userId = user.getId();
@ -130,6 +132,7 @@ public class BruteForceTest extends AbstractTestRealmKeycloakTest {
realm.setMaxDeltaTimeSeconds(20); realm.setMaxDeltaTimeSeconds(20);
realm.setMaxFailureWaitSeconds(100); realm.setMaxFailureWaitSeconds(100);
realm.setWaitIncrementSeconds(5); realm.setWaitIncrementSeconds(5);
realm.setOtpPolicyCodeReusable(true);
adminClient.realm("test").update(realm); adminClient.realm("test").update(realm);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);

View file

@ -27,6 +27,7 @@ import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@ -62,11 +63,13 @@ import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginTotpPage; import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.pages.PushTheButtonPage; import org.keycloak.testsuite.pages.PushTheButtonPage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.FlowUtil; import org.keycloak.testsuite.util.FlowUtil;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RealmRepUtil; import org.keycloak.testsuite.util.RealmRepUtil;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
@ -100,6 +103,7 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
@Override @Override
public void configureTestRealm(RealmRepresentation testRealm) { public void configureTestRealm(RealmRepresentation testRealm) {
try { try {
testRealm.setOtpPolicyCodeReusable(true);
findTestApp(testRealm).setAttributes(Collections.singletonMap(Constants.ACR_LOA_MAP, getAcrToLoaMappingForClient())); findTestApp(testRealm).setAttributes(Collections.singletonMap(Constants.ACR_LOA_MAP, getAcrToLoaMappingForClient()));
UserRepresentation user = RealmRepUtil.findUser(testRealm, "test-user@localhost"); UserRepresentation user = RealmRepUtil.findUser(testRealm, "test-user@localhost");
UserBuilder.edit(user) UserBuilder.edit(user)
@ -121,6 +125,19 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
@Before @Before
public void setupFlow() { public void setupFlow() {
configureStepUpFlow(testingClient); configureStepUpFlow(testingClient);
canBeOtpCodesReusable(true);
}
@After
public void tearDown() {
canBeOtpCodesReusable(false);
}
// Fixing this test with not reusable OTP codes would bring additional unwanted workarounds; not scope of this test
private void canBeOtpCodesReusable(boolean state) {
new RealmAttributeUpdater(testRealm())
.setOtpPolicyCodeReusable(state)
.update();
} }
public static void configureStepUpFlow(KeycloakTestingClient testingClient) { public static void configureStepUpFlow(KeycloakTestingClient testingClient) {

View file

@ -24,6 +24,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
@ -33,11 +34,13 @@ import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginTotpPage; import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmRepUtil; import org.keycloak.testsuite.util.RealmRepUtil;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.WaitUtils;
import javax.ws.rs.client.Client; import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity; import javax.ws.rs.client.Entity;
@ -132,6 +135,8 @@ public class LoginTotpTest extends AbstractTestRealmKeycloakTest {
Assert.assertTrue(loginTotpPage.isCurrent()); Assert.assertTrue(loginTotpPage.isCurrent());
setOtpTimeOffset(TimeBasedOTP.DEFAULT_INTERVAL_SECONDS, totp);
loginTotpPage.login(totp.generateTOTP("totpSecret")); loginTotpPage.login(totp.generateTOTP("totpSecret"));
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@ -199,7 +204,7 @@ public class LoginTotpTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void loginWithTotp_getToken_checkCompatibilityCLI() throws IOException { public void loginWithTotp_getToken_checkCompatibilityCLI() throws IOException {
Client httpClient = AdminClientUtil.createResteasyClient(); Client httpClient = AdminClientUtil.createResteasyClient();
try { try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setOtpPolicyCodeReusable(true).update()) {
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT) WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
.path("/realms") .path("/realms")
.path(TEST) .path(TEST)

View file

@ -21,6 +21,8 @@
*/ */
package org.keycloak.testsuite.console.page.authentication.otppolicy; package org.keycloak.testsuite.console.page.authentication.otppolicy;
import org.keycloak.models.OTPPolicy;
import org.keycloak.testsuite.console.page.fragment.OnOffSwitch;
import org.keycloak.testsuite.page.Form; import org.keycloak.testsuite.page.Form;
import org.keycloak.testsuite.util.UIUtils; import org.keycloak.testsuite.util.UIUtils;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
@ -54,7 +56,14 @@ public class OTPPolicyForm extends Form {
@FindBy(id = "counter") @FindBy(id = "counter")
private WebElement counter; private WebElement counter;
@FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='reusableCode']]")
private OnOffSwitch reusableCode;
public void setValues(OTPType otpType, OTPHashAlg otpHashAlg, Digits digits, String lookAheadOrAround, String periodOrCounter) { public void setValues(OTPType otpType, OTPHashAlg otpHashAlg, Digits digits, String lookAheadOrAround, String periodOrCounter) {
setValues(otpType, otpHashAlg, digits, lookAheadOrAround, periodOrCounter, OTPPolicy.DEFAULT_IS_REUSABLE);
}
public void setValues(OTPType otpType, OTPHashAlg otpHashAlg, Digits digits, String lookAheadOrAround, String periodOrCounter, boolean isReusableCode) {
this.otpType.selectByValue(otpType.getName()); this.otpType.selectByValue(otpType.getName());
this.otpHashAlg.selectByValue(otpHashAlg.getName()); this.otpHashAlg.selectByValue(otpHashAlg.getName());
this.digits.selectByVisibleText("" + digits.getName()); this.digits.selectByVisibleText("" + digits.getName());
@ -63,6 +72,7 @@ public class OTPPolicyForm extends Form {
case TIME_BASED: case TIME_BASED:
UIUtils.setTextInputValue(this.lookAround, lookAheadOrAround); UIUtils.setTextInputValue(this.lookAround, lookAheadOrAround);
UIUtils.setTextInputValue(period, periodOrCounter); UIUtils.setTextInputValue(period, periodOrCounter);
reusableCode.setOn(isReusableCode);
break; break;
case COUNTER_BASED: case COUNTER_BASED:
UIUtils.setTextInputValue(this.lookAhead, lookAheadOrAround); UIUtils.setTextInputValue(this.lookAhead, lookAheadOrAround);

View file

@ -32,7 +32,9 @@ import org.keycloak.testsuite.console.page.authentication.otppolicy.OTPPolicyFor
import org.keycloak.testsuite.console.page.authentication.otppolicy.OTPPolicyForm.OTPType; import org.keycloak.testsuite.console.page.authentication.otppolicy.OTPPolicyForm.OTPType;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
import static org.junit.Assert.*; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
/** /**
* *
@ -60,13 +62,16 @@ public class OTPPolicyTest extends AbstractConsoleTest {
assertEquals(Integer.valueOf(8), realm.getOtpPolicyDigits()); assertEquals(Integer.valueOf(8), realm.getOtpPolicyDigits());
assertEquals(Integer.valueOf(10), realm.getOtpPolicyLookAheadWindow()); assertEquals(Integer.valueOf(10), realm.getOtpPolicyLookAheadWindow());
assertEquals(Integer.valueOf(50), realm.getOtpPolicyInitialCounter()); assertEquals(Integer.valueOf(50), realm.getOtpPolicyInitialCounter());
assertThat(realm.isOtpPolicyCodeReusable(), is(org.keycloak.models.OTPPolicy.DEFAULT_IS_REUSABLE));
otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA512, Digits.EIGHT, "10", "40"); otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA512, Digits.EIGHT, "10", "40", false);
assertAlertSuccess(); assertAlertSuccess();
realm = testRealmResource().toRepresentation(); realm = testRealmResource().toRepresentation();
assertEquals("totp", realm.getOtpPolicyType()); assertEquals("totp", realm.getOtpPolicyType());
assertEquals(Integer.valueOf(40), realm.getOtpPolicyPeriod()); assertEquals(Integer.valueOf(40), realm.getOtpPolicyPeriod());
assertThat(realm.isOtpPolicyCodeReusable(), is(false));
} }
@Test @Test

View file

@ -1371,6 +1371,8 @@ otp-token-period=OTP Token Period
otp-token-period.tooltip=How many seconds should an OTP token be valid? Defaults to 30 seconds. otp-token-period.tooltip=How many seconds should an OTP token be valid? Defaults to 30 seconds.
otp-supported-applications=Supported Applications otp-supported-applications=Supported Applications
otp-supported-applications.tooltip=Applications that are known to work with the current OTP policy otp-supported-applications.tooltip=Applications that are known to work with the current OTP policy
otp-reusable-code=Reusable token
otp-reusable-code.tooltip=Possibility to use the same OTP code again after successful authentication.
loa-level=Level of Authentication loa-level=Level of Authentication
loa-level.tooltip=Sets the Level of Authentication to the specified value. loa-level.tooltip=Sets the Level of Authentication to the specified value.
loa-max-age=Max Age loa-max-age=Max Age

View file

@ -81,6 +81,16 @@
<kc-tooltip>{{:: 'otp-supported-applications.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'otp-supported-applications.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group" data-ng-if="realm.otpPolicyType == 'totp'">
<label for="reusableCode" class="col-md-2 control-label">{{:: 'otp-reusable-code' | translate}}</label>
<div class="col-md-2">
<div>
<input id="reusableCode" ng-model="realm.otpPolicyCodeReusable" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
<kc-tooltip>{{:: 'otp-reusable-code.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="access.manageRealm"> <div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2"> <div class="col-md-10 col-md-offset-2">