parent
040e52cfd7
commit
0fcf5d3936
26 changed files with 434 additions and 123 deletions
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,15 +362,22 @@ 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));
|
||||||
|
|
||||||
|
if (!reusableCodesEnabled) {
|
||||||
|
loginTotpPage.assertCurrent();
|
||||||
|
} else {
|
||||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
events.expectLogin().assertEvent();
|
events.expectLogin().assertEvent();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//KEYCLOAK-15511
|
//KEYCLOAK-15511
|
||||||
@Test
|
@Test
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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,14 +327,19 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testReauthenticationBothBrokersWithOTPRequired() throws Exception {
|
public void testReauthenticationBothBrokersWithOTPRequired() throws Exception {
|
||||||
|
final RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||||
|
final RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
|
||||||
|
|
||||||
|
try (RealmAttributeUpdater rauConsumer = new RealmAttributeUpdater(consumerRealm).setOtpPolicyCodeReusable(true).update();
|
||||||
|
RealmAttributeUpdater rauProvider = new RealmAttributeUpdater(providerRealm).setOtpPolicyCodeReusable(true).update()) {
|
||||||
|
|
||||||
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
|
KcSamlBrokerConfiguration samlBrokerConfig = KcSamlBrokerConfiguration.INSTANCE;
|
||||||
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
|
ClientRepresentation samlClient = samlBrokerConfig.createProviderClients().get(0);
|
||||||
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
|
IdentityProviderRepresentation samlBroker = samlBrokerConfig.setUpIdentityProvider();
|
||||||
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||||
adminClient.realm(bc.providerRealmName()).clients().create(samlClient);
|
providerRealm.clients().create(samlClient);
|
||||||
consumerRealm.identityProviders().create(samlBroker);
|
consumerRealm.identityProviders().create(samlBroker);
|
||||||
|
|
||||||
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
|
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
|
||||||
|
@ -364,6 +376,7 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
|
||||||
removeUserByUsername(consumerRealm, "consumer");
|
removeUserByUsername(consumerRealm, "consumer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInvalidIssuedFor() {
|
public void testInvalidIssuedFor() {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,7 +147,8 @@ public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateOTP() {
|
public void testUpdateOTP() throws IOException {
|
||||||
|
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()).setOtpPolicyCodeReusable(true).update()) {
|
||||||
// Add requiredAction to the user for update OTP
|
// Add requiredAction to the user for update OTP
|
||||||
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user");
|
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user");
|
||||||
UserRepresentation userRep = user.toRepresentation();
|
UserRepresentation userRep = user.toRepresentation();
|
||||||
|
@ -205,6 +207,7 @@ public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest {
|
||||||
appPage.logout(idTokenHint);
|
appPage.logout(idTokenHint);
|
||||||
events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent();
|
events.expectLogout(loginEvent.getSessionId()).user(userRep.getId()).assertEvent();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNormalUser() {
|
public void testNormalUser() {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue