KEYCLOAK-12174 WebAuthn: create authenticator, requiredAction and policy for passwordless (#6649)
This commit is contained in:
parent
1a53110bb6
commit
d46620569a
38 changed files with 1152 additions and 491 deletions
|
@ -106,6 +106,9 @@ public class RealmRepresentation {
|
|||
protected Integer otpPolicyLookAheadWindow;
|
||||
protected Integer otpPolicyPeriod;
|
||||
protected List<String> otpSupportedApplications;
|
||||
|
||||
// WebAuthn 2-factor properties below
|
||||
|
||||
protected String webAuthnPolicyRpEntityName;
|
||||
protected List<String> webAuthnPolicySignatureAlgorithms;
|
||||
protected String webAuthnPolicyRpId;
|
||||
|
@ -117,6 +120,19 @@ public class RealmRepresentation {
|
|||
protected Boolean webAuthnPolicyAvoidSameAuthenticatorRegister;
|
||||
protected List<String> webAuthnPolicyAcceptableAaguids;
|
||||
|
||||
// WebAuthn passwordless properties below
|
||||
|
||||
protected String webAuthnPolicyPasswordlessRpEntityName;
|
||||
protected List<String> webAuthnPolicyPasswordlessSignatureAlgorithms;
|
||||
protected String webAuthnPolicyPasswordlessRpId;
|
||||
protected String webAuthnPolicyPasswordlessAttestationConveyancePreference;
|
||||
protected String webAuthnPolicyPasswordlessAuthenticatorAttachment;
|
||||
protected String webAuthnPolicyPasswordlessRequireResidentKey;
|
||||
protected String webAuthnPolicyPasswordlessUserVerificationRequirement;
|
||||
protected Integer webAuthnPolicyPasswordlessCreateTimeout;
|
||||
protected Boolean webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister;
|
||||
protected List<String> webAuthnPolicyPasswordlessAcceptableAaguids;
|
||||
|
||||
protected List<UserRepresentation> users;
|
||||
protected List<UserRepresentation> federatedUsers;
|
||||
protected List<ScopeMappingRepresentation> scopeMappings;
|
||||
|
@ -926,6 +942,8 @@ public class RealmRepresentation {
|
|||
this.otpSupportedApplications = otpSupportedApplications;
|
||||
}
|
||||
|
||||
// WebAuthn 2-factor properties below
|
||||
|
||||
public String getWebAuthnPolicyRpEntityName() {
|
||||
return webAuthnPolicyRpEntityName;
|
||||
}
|
||||
|
@ -1006,6 +1024,89 @@ public class RealmRepresentation {
|
|||
this.webAuthnPolicyAcceptableAaguids = webAuthnPolicyAcceptableAaguids;
|
||||
}
|
||||
|
||||
// WebAuthn passwordless properties below
|
||||
|
||||
|
||||
public String getWebAuthnPolicyPasswordlessRpEntityName() {
|
||||
return webAuthnPolicyPasswordlessRpEntityName;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessRpEntityName(String webAuthnPolicyPasswordlessRpEntityName) {
|
||||
this.webAuthnPolicyPasswordlessRpEntityName = webAuthnPolicyPasswordlessRpEntityName;
|
||||
}
|
||||
|
||||
public List<String> getWebAuthnPolicyPasswordlessSignatureAlgorithms() {
|
||||
return webAuthnPolicyPasswordlessSignatureAlgorithms;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessSignatureAlgorithms(List<String> webAuthnPolicyPasswordlessSignatureAlgorithms) {
|
||||
this.webAuthnPolicyPasswordlessSignatureAlgorithms = webAuthnPolicyPasswordlessSignatureAlgorithms;
|
||||
}
|
||||
|
||||
public String getWebAuthnPolicyPasswordlessRpId() {
|
||||
return webAuthnPolicyPasswordlessRpId;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessRpId(String webAuthnPolicyPasswordlessRpId) {
|
||||
this.webAuthnPolicyPasswordlessRpId = webAuthnPolicyPasswordlessRpId;
|
||||
}
|
||||
|
||||
public String getWebAuthnPolicyPasswordlessAttestationConveyancePreference() {
|
||||
return webAuthnPolicyPasswordlessAttestationConveyancePreference;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessAttestationConveyancePreference(String webAuthnPolicyPasswordlessAttestationConveyancePreference) {
|
||||
this.webAuthnPolicyPasswordlessAttestationConveyancePreference = webAuthnPolicyPasswordlessAttestationConveyancePreference;
|
||||
}
|
||||
|
||||
public String getWebAuthnPolicyPasswordlessAuthenticatorAttachment() {
|
||||
return webAuthnPolicyPasswordlessAuthenticatorAttachment;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessAuthenticatorAttachment(String webAuthnPolicyPasswordlessAuthenticatorAttachment) {
|
||||
this.webAuthnPolicyPasswordlessAuthenticatorAttachment = webAuthnPolicyPasswordlessAuthenticatorAttachment;
|
||||
}
|
||||
|
||||
public String getWebAuthnPolicyPasswordlessRequireResidentKey() {
|
||||
return webAuthnPolicyPasswordlessRequireResidentKey;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessRequireResidentKey(String webAuthnPolicyPasswordlessRequireResidentKey) {
|
||||
this.webAuthnPolicyPasswordlessRequireResidentKey = webAuthnPolicyPasswordlessRequireResidentKey;
|
||||
}
|
||||
|
||||
public String getWebAuthnPolicyPasswordlessUserVerificationRequirement() {
|
||||
return webAuthnPolicyPasswordlessUserVerificationRequirement;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessUserVerificationRequirement(String webAuthnPolicyPasswordlessUserVerificationRequirement) {
|
||||
this.webAuthnPolicyPasswordlessUserVerificationRequirement = webAuthnPolicyPasswordlessUserVerificationRequirement;
|
||||
}
|
||||
|
||||
public Integer getWebAuthnPolicyPasswordlessCreateTimeout() {
|
||||
return webAuthnPolicyPasswordlessCreateTimeout;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessCreateTimeout(Integer webAuthnPolicyPasswordlessCreateTimeout) {
|
||||
this.webAuthnPolicyPasswordlessCreateTimeout = webAuthnPolicyPasswordlessCreateTimeout;
|
||||
}
|
||||
|
||||
public Boolean isWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister() {
|
||||
return webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(Boolean webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister) {
|
||||
this.webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister = webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister;
|
||||
}
|
||||
|
||||
public List<String> getWebAuthnPolicyPasswordlessAcceptableAaguids() {
|
||||
return webAuthnPolicyPasswordlessAcceptableAaguids;
|
||||
}
|
||||
|
||||
public void setWebAuthnPolicyPasswordlessAcceptableAaguids(List<String> webAuthnPolicyPasswordlessAcceptableAaguids) {
|
||||
this.webAuthnPolicyPasswordlessAcceptableAaguids = webAuthnPolicyPasswordlessAcceptableAaguids;
|
||||
}
|
||||
|
||||
public String getBrowserFlow() {
|
||||
return browserFlow;
|
||||
}
|
||||
|
|
|
@ -633,6 +633,18 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
updated.setWebAuthnPolicy(policy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthnPolicy getWebAuthnPolicyPasswordless() {
|
||||
if (isUpdated()) return updated.getWebAuthnPolicyPasswordless();
|
||||
return cached.getWebAuthnPasswordlessPolicy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWebAuthnPolicyPasswordless(WebAuthnPolicy policy) {
|
||||
getDelegateForUpdate();
|
||||
updated.setWebAuthnPolicyPasswordless(policy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RoleModel getRoleById(String id) {
|
||||
if (isUpdated()) return updated.getRoleById(id);
|
||||
|
|
|
@ -97,6 +97,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
protected PasswordPolicy passwordPolicy;
|
||||
protected OTPPolicy otpPolicy;
|
||||
protected WebAuthnPolicy webAuthnPolicy;
|
||||
protected WebAuthnPolicy webAuthnPasswordlessPolicy;
|
||||
|
||||
protected String loginTheme;
|
||||
protected String accountTheme;
|
||||
|
@ -207,6 +208,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
passwordPolicy = model.getPasswordPolicy();
|
||||
otpPolicy = model.getOTPPolicy();
|
||||
webAuthnPolicy = model.getWebAuthnPolicy();
|
||||
webAuthnPasswordlessPolicy = model.getWebAuthnPolicyPasswordless();
|
||||
|
||||
loginTheme = model.getLoginTheme();
|
||||
accountTheme = model.getAccountTheme();
|
||||
|
@ -613,6 +615,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
return webAuthnPolicy;
|
||||
}
|
||||
|
||||
public WebAuthnPolicy getWebAuthnPasswordlessPolicy() {
|
||||
return webAuthnPasswordlessPolicy;
|
||||
}
|
||||
|
||||
public AuthenticationFlowModel getBrowserFlow() {
|
||||
return browserFlow;
|
||||
}
|
||||
|
|
|
@ -941,55 +941,80 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
em.flush();
|
||||
}
|
||||
|
||||
|
||||
// WebAuthn
|
||||
|
||||
@Override
|
||||
public WebAuthnPolicy getWebAuthnPolicy() {
|
||||
return getWebAuthnPolicy("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWebAuthnPolicy(WebAuthnPolicy policy) {
|
||||
setWebAuthnPolicy(policy, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebAuthnPolicy getWebAuthnPolicyPasswordless() {
|
||||
// We will use some prefix for attributes related to passwordless WebAuthn policy
|
||||
return getWebAuthnPolicy(Constants.WEBAUTHN_PASSWORDLESS_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWebAuthnPolicyPasswordless(WebAuthnPolicy policy) {
|
||||
// We will use some prefix for attributes related to passwordless WebAuthn policy
|
||||
setWebAuthnPolicy(policy, Constants.WEBAUTHN_PASSWORDLESS_PREFIX);
|
||||
}
|
||||
|
||||
|
||||
private WebAuthnPolicy getWebAuthnPolicy(String attributePrefix) {
|
||||
WebAuthnPolicy policy = new WebAuthnPolicy();
|
||||
|
||||
// mandatory parameters
|
||||
String rpEntityName = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME);
|
||||
String rpEntityName = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME + attributePrefix);
|
||||
if (rpEntityName == null || rpEntityName.isEmpty())
|
||||
rpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||
policy.setRpEntityName(rpEntityName);
|
||||
|
||||
String signatureAlgorithmsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS);
|
||||
String signatureAlgorithmsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS + attributePrefix);
|
||||
if (signatureAlgorithmsString == null || signatureAlgorithmsString.isEmpty())
|
||||
signatureAlgorithmsString = Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS;
|
||||
List<String> signatureAlgorithms = Arrays.asList(signatureAlgorithmsString.split(","));
|
||||
policy.setSignatureAlgorithm(signatureAlgorithms);
|
||||
|
||||
// optional parameters
|
||||
String rpId = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID);
|
||||
String rpId = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID + attributePrefix);
|
||||
if (rpId == null || rpId.isEmpty()) rpId = "";
|
||||
policy.setRpId(rpId);
|
||||
|
||||
String attestationConveyancePreference = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE);
|
||||
String attestationConveyancePreference = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE + attributePrefix);
|
||||
if (attestationConveyancePreference == null || attestationConveyancePreference.isEmpty())
|
||||
attestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
policy.setAttestationConveyancePreference(attestationConveyancePreference);
|
||||
|
||||
String authenticatorAttachment = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT);
|
||||
String authenticatorAttachment = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT + attributePrefix);
|
||||
if (authenticatorAttachment == null || authenticatorAttachment.isEmpty())
|
||||
authenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
policy.setAuthenticatorAttachment(authenticatorAttachment);
|
||||
|
||||
String requireResidentKey = getAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY);
|
||||
String requireResidentKey = getAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY + attributePrefix);
|
||||
if (requireResidentKey == null || requireResidentKey.isEmpty())
|
||||
requireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
policy.setRequireResidentKey(requireResidentKey);
|
||||
|
||||
String userVerificationRequirement = getAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT);
|
||||
String userVerificationRequirement = getAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT + attributePrefix);
|
||||
if (userVerificationRequirement == null || userVerificationRequirement.isEmpty())
|
||||
userVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
policy.setUserVerificationRequirement(userVerificationRequirement);
|
||||
|
||||
String createTime = getAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT);
|
||||
String createTime = getAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT + attributePrefix);
|
||||
if (createTime != null) policy.setCreateTimeout(Integer.parseInt(createTime));
|
||||
else policy.setCreateTimeout(0);
|
||||
|
||||
String avoidSameAuthenticatorRegister = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER);
|
||||
String avoidSameAuthenticatorRegister = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER + attributePrefix);
|
||||
if (avoidSameAuthenticatorRegister != null) policy.setAvoidSameAuthenticatorRegister(Boolean.parseBoolean(avoidSameAuthenticatorRegister));
|
||||
|
||||
String acceptableAaguidsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS);
|
||||
String acceptableAaguidsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix);
|
||||
List<String> acceptableAaguids = new ArrayList<>();
|
||||
if (acceptableAaguidsString != null && !acceptableAaguidsString.isEmpty())
|
||||
acceptableAaguids = Arrays.asList(acceptableAaguidsString.split(","));
|
||||
|
@ -998,44 +1023,45 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
return policy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWebAuthnPolicy(WebAuthnPolicy policy) {
|
||||
|
||||
|
||||
private void setWebAuthnPolicy(WebAuthnPolicy policy, String attributePrefix) {
|
||||
// mandatory parameters
|
||||
String rpEntityName = policy.getRpEntityName();
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME, rpEntityName);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME + attributePrefix, rpEntityName);
|
||||
|
||||
List<String> signatureAlgorithms = policy.getSignatureAlgorithm();
|
||||
String signatureAlgorithmsString = String.join(",", signatureAlgorithms);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS, signatureAlgorithmsString);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS + attributePrefix, signatureAlgorithmsString);
|
||||
|
||||
// optional parameters
|
||||
String rpId = policy.getRpId();
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID, rpId);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID + attributePrefix, rpId);
|
||||
|
||||
String attestationConveyancePreference = policy.getAttestationConveyancePreference();
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE, attestationConveyancePreference);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE + attributePrefix, attestationConveyancePreference);
|
||||
|
||||
String authenticatorAttachment = policy.getAuthenticatorAttachment();
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT, authenticatorAttachment);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT + attributePrefix, authenticatorAttachment);
|
||||
|
||||
String requireResidentKey = policy.getRequireResidentKey();
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY, requireResidentKey);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY + attributePrefix, requireResidentKey);
|
||||
|
||||
String userVerificationRequirement = policy.getUserVerificationRequirement();
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT, userVerificationRequirement);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT + attributePrefix, userVerificationRequirement);
|
||||
|
||||
int createTime = policy.getCreateTimeout();
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT, Integer.toString(createTime));
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT + attributePrefix, Integer.toString(createTime));
|
||||
|
||||
boolean avoidSameAuthenticatorRegister = policy.isAvoidSameAuthenticatorRegister();
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER, Boolean.toString(avoidSameAuthenticatorRegister));
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER + attributePrefix, Boolean.toString(avoidSameAuthenticatorRegister));
|
||||
|
||||
List<String> acceptableAaguids = policy.getAcceptableAaguids();
|
||||
if (acceptableAaguids != null && !acceptableAaguids.isEmpty()) {
|
||||
String acceptableAaguidsString = String.join(",", acceptableAaguids);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS, acceptableAaguidsString);
|
||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix, acceptableAaguidsString);
|
||||
} else {
|
||||
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS);
|
||||
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -73,4 +73,6 @@ public interface Details {
|
|||
String X509_CERTIFICATE_SERIAL_NUMBER = "x509_cert_serial_number";
|
||||
String X509_CERTIFICATE_SUBJECT_DISTINGUISHED_NAME = "x509_cert_subject_distinguished_name";
|
||||
String X509_CERTIFICATE_ISSUER_DISTINGUISHED_NAME = "x509_cert_issuer_distinguished_name";
|
||||
|
||||
String CREDENTIAL_TYPE = "credential_type";
|
||||
}
|
||||
|
|
|
@ -65,6 +65,9 @@ public final class Constants {
|
|||
// it stands for optional parameter not specified in WebAuthn
|
||||
public static final String DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED = "not specified";
|
||||
|
||||
// Prefix used for the realm attributes and other places
|
||||
public static final String WEBAUTHN_PASSWORDLESS_PREFIX = "Passwordless";
|
||||
|
||||
public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
||||
public static final String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
|
||||
public static final String EXECUTION = "execution";
|
||||
|
|
|
@ -343,6 +343,7 @@ public class ModelToRepresentation {
|
|||
rep.setOtpPolicyType(otpPolicy.getType());
|
||||
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
|
||||
rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
|
||||
|
||||
WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy();
|
||||
rep.setWebAuthnPolicyRpEntityName(webAuthnPolicy.getRpEntityName());
|
||||
rep.setWebAuthnPolicySignatureAlgorithms(webAuthnPolicy.getSignatureAlgorithm());
|
||||
|
@ -354,6 +355,19 @@ public class ModelToRepresentation {
|
|||
rep.setWebAuthnPolicyCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
||||
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
||||
rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
|
||||
|
||||
webAuthnPolicy = realm.getWebAuthnPolicyPasswordless();
|
||||
rep.setWebAuthnPolicyPasswordlessRpEntityName(webAuthnPolicy.getRpEntityName());
|
||||
rep.setWebAuthnPolicyPasswordlessSignatureAlgorithms(webAuthnPolicy.getSignatureAlgorithm());
|
||||
rep.setWebAuthnPolicyPasswordlessRpId(webAuthnPolicy.getRpId());
|
||||
rep.setWebAuthnPolicyPasswordlessAttestationConveyancePreference(webAuthnPolicy.getAttestationConveyancePreference());
|
||||
rep.setWebAuthnPolicyPasswordlessAuthenticatorAttachment(webAuthnPolicy.getAuthenticatorAttachment());
|
||||
rep.setWebAuthnPolicyPasswordlessRequireResidentKey(webAuthnPolicy.getRequireResidentKey());
|
||||
rep.setWebAuthnPolicyPasswordlessUserVerificationRequirement(webAuthnPolicy.getUserVerificationRequirement());
|
||||
rep.setWebAuthnPolicyPasswordlessCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
||||
rep.setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
||||
rep.setWebAuthnPolicyPasswordlessAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
|
||||
|
||||
if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
|
||||
if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
|
||||
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
|
||||
|
|
|
@ -265,55 +265,12 @@ public class RepresentationToModel {
|
|||
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
|
||||
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
||||
|
||||
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
|
||||
|
||||
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyRpEntityName();
|
||||
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
|
||||
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
|
||||
|
||||
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
|
||||
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
|
||||
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
|
||||
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
|
||||
|
||||
String webAuthnPolicyRpId = rep.getWebAuthnPolicyRpId();
|
||||
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
|
||||
webAuthnPolicyRpId = "";
|
||||
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
|
||||
|
||||
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
|
||||
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
|
||||
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
|
||||
|
||||
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
|
||||
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
|
||||
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
|
||||
|
||||
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
|
||||
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
|
||||
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
|
||||
|
||||
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
|
||||
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
|
||||
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
|
||||
|
||||
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyCreateTimeout();
|
||||
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
|
||||
else webAuthnPolicy.setCreateTimeout(0);
|
||||
|
||||
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
|
||||
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||
|
||||
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
||||
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||
|
||||
WebAuthnPolicy webAuthnPolicy = getWebAuthnPolicyTwoFactor(rep);
|
||||
newRealm.setWebAuthnPolicy(webAuthnPolicy);
|
||||
|
||||
webAuthnPolicy = getWebAuthnPolicyPasswordless(rep);
|
||||
newRealm.setWebAuthnPolicyPasswordless(webAuthnPolicy);
|
||||
|
||||
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
|
||||
if (rep.getRequiredActions() != null) {
|
||||
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
|
||||
|
@ -483,6 +440,110 @@ public class RepresentationToModel {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private static WebAuthnPolicy getWebAuthnPolicyTwoFactor(RealmRepresentation rep) {
|
||||
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
|
||||
|
||||
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyRpEntityName();
|
||||
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
|
||||
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
|
||||
|
||||
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
|
||||
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
|
||||
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
|
||||
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
|
||||
|
||||
String webAuthnPolicyRpId = rep.getWebAuthnPolicyRpId();
|
||||
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
|
||||
webAuthnPolicyRpId = "";
|
||||
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
|
||||
|
||||
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
|
||||
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
|
||||
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
|
||||
|
||||
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
|
||||
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
|
||||
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
|
||||
|
||||
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
|
||||
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
|
||||
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
|
||||
|
||||
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
|
||||
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
|
||||
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
|
||||
|
||||
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyCreateTimeout();
|
||||
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
|
||||
else webAuthnPolicy.setCreateTimeout(0);
|
||||
|
||||
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
|
||||
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||
|
||||
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
||||
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||
|
||||
return webAuthnPolicy;
|
||||
}
|
||||
|
||||
|
||||
private static WebAuthnPolicy getWebAuthnPolicyPasswordless(RealmRepresentation rep) {
|
||||
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
|
||||
|
||||
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyPasswordlessRpEntityName();
|
||||
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
|
||||
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
|
||||
|
||||
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicyPasswordlessSignatureAlgorithms();
|
||||
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
|
||||
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
|
||||
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
|
||||
|
||||
String webAuthnPolicyRpId = rep.getWebAuthnPolicyPasswordlessRpId();
|
||||
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
|
||||
webAuthnPolicyRpId = "";
|
||||
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
|
||||
|
||||
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyPasswordlessAttestationConveyancePreference();
|
||||
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
|
||||
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
|
||||
|
||||
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyPasswordlessAuthenticatorAttachment();
|
||||
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
|
||||
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
|
||||
|
||||
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyPasswordlessRequireResidentKey();
|
||||
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
|
||||
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
|
||||
|
||||
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyPasswordlessUserVerificationRequirement();
|
||||
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
|
||||
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
|
||||
|
||||
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyPasswordlessCreateTimeout();
|
||||
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
|
||||
else webAuthnPolicy.setCreateTimeout(0);
|
||||
|
||||
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister();
|
||||
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||
|
||||
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyPasswordlessAcceptableAaguids();
|
||||
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||
|
||||
return webAuthnPolicy;
|
||||
}
|
||||
|
||||
public static void importUserFederationProvidersAndMappers(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm) {
|
||||
// providers to convert to component model
|
||||
Set<String> convertSet = new HashSet<>();
|
||||
|
@ -1042,55 +1103,12 @@ public class RepresentationToModel {
|
|||
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
|
||||
}
|
||||
|
||||
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
|
||||
|
||||
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyRpEntityName();
|
||||
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
|
||||
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
|
||||
|
||||
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
|
||||
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
|
||||
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
|
||||
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
|
||||
|
||||
String webAuthnPolicyRpId = rep.getWebAuthnPolicyRpId();
|
||||
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
|
||||
webAuthnPolicyRpId = "";
|
||||
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
|
||||
|
||||
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
|
||||
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
|
||||
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
|
||||
|
||||
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
|
||||
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
|
||||
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
|
||||
|
||||
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
|
||||
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
|
||||
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
|
||||
|
||||
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
|
||||
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
|
||||
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
|
||||
|
||||
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyCreateTimeout();
|
||||
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
|
||||
else webAuthnPolicy.setCreateTimeout(0);
|
||||
|
||||
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
|
||||
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
|
||||
|
||||
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
|
||||
webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
|
||||
|
||||
WebAuthnPolicy webAuthnPolicy = getWebAuthnPolicyTwoFactor(rep);
|
||||
realm.setWebAuthnPolicy(webAuthnPolicy);
|
||||
|
||||
webAuthnPolicy = getWebAuthnPolicyPasswordless(rep);
|
||||
realm.setWebAuthnPolicyPasswordless(webAuthnPolicy);
|
||||
|
||||
if (rep.getSmtpServer() != null) {
|
||||
Map<String, String> config = new HashMap(rep.getSmtpServer());
|
||||
if (rep.getSmtpServer().containsKey("password") && ComponentRepresentation.SECRET_VALUE.equals(rep.getSmtpServer().get("password"))) {
|
||||
|
|
|
@ -240,9 +240,30 @@ public interface RealmModel extends RoleContainerModel {
|
|||
OTPPolicy getOTPPolicy();
|
||||
void setOTPPolicy(OTPPolicy policy);
|
||||
|
||||
/**
|
||||
* @return WebAuthn policy for 2-factor authentication
|
||||
*/
|
||||
WebAuthnPolicy getWebAuthnPolicy();
|
||||
|
||||
/**
|
||||
* Set WebAuthn policy for 2-factor authentication
|
||||
*
|
||||
* @param policy
|
||||
*/
|
||||
void setWebAuthnPolicy(WebAuthnPolicy policy);
|
||||
|
||||
/**
|
||||
*
|
||||
* @return WebAuthn passwordless policy below. This is temporary and will be removed later.
|
||||
*/
|
||||
WebAuthnPolicy getWebAuthnPolicyPasswordless();
|
||||
|
||||
/**
|
||||
* Set WebAuthn passwordless policy below. This is temporary and will be removed later.
|
||||
* @param policy
|
||||
*/
|
||||
void setWebAuthnPolicyPasswordless(WebAuthnPolicy policy);
|
||||
|
||||
RoleModel getRoleById(String id);
|
||||
|
||||
List<GroupModel> getDefaultGroups();
|
||||
|
|
|
@ -31,22 +31,28 @@ import org.keycloak.util.JsonSerialization;
|
|||
*/
|
||||
public class WebAuthnCredentialModel extends CredentialModel {
|
||||
|
||||
public static final String TYPE = "webauthn";
|
||||
// Credential type used for WebAuthn two factor credentials
|
||||
public static final String TYPE_TWOFACTOR = "webauthn";
|
||||
|
||||
// Credential type used for WebAuthn passwordless credentials
|
||||
public static final String TYPE_PASSWORDLESS = "webauthn-passwordless";
|
||||
|
||||
// Either
|
||||
private final WebAuthnCredentialData credentialData;
|
||||
private final WebAuthnSecretData secretData;
|
||||
|
||||
private WebAuthnCredentialModel(WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
|
||||
private WebAuthnCredentialModel(String credentialType, WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
|
||||
this.credentialData = credentialData;
|
||||
this.secretData = secretData;
|
||||
setType(credentialType);
|
||||
}
|
||||
|
||||
public static WebAuthnCredentialModel create(String userLabel, String aaguid, String credentialId,
|
||||
public static WebAuthnCredentialModel create(String credentialType, String userLabel, String aaguid, String credentialId,
|
||||
String attestationStatement, String credentialPublicKey, long counter) {
|
||||
WebAuthnCredentialData credentialData = new WebAuthnCredentialData(aaguid, credentialId, counter, attestationStatement, credentialPublicKey);
|
||||
WebAuthnSecretData secretData = new WebAuthnSecretData();
|
||||
|
||||
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialData, secretData);
|
||||
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialType, credentialData, secretData);
|
||||
credentialModel.fillCredentialModelFields();
|
||||
credentialModel.setUserLabel(userLabel);
|
||||
return credentialModel;
|
||||
|
@ -58,10 +64,10 @@ public class WebAuthnCredentialModel extends CredentialModel {
|
|||
WebAuthnCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), WebAuthnCredentialData.class);
|
||||
WebAuthnSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), WebAuthnSecretData.class);
|
||||
|
||||
WebAuthnCredentialModel webAuthnCredentialModel = new WebAuthnCredentialModel(credentialData, secretData);
|
||||
WebAuthnCredentialModel webAuthnCredentialModel = new WebAuthnCredentialModel(credentialModel.getType(), credentialData, secretData);
|
||||
webAuthnCredentialModel.setUserLabel(credentialModel.getUserLabel());
|
||||
webAuthnCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
|
||||
webAuthnCredentialModel.setType(TYPE);
|
||||
webAuthnCredentialModel.setType(credentialModel.getType());
|
||||
webAuthnCredentialModel.setId(credentialModel.getId());
|
||||
webAuthnCredentialModel.setSecretData(credentialModel.getSecretData());
|
||||
webAuthnCredentialModel.setCredentialData(credentialModel.getCredentialData());
|
||||
|
@ -95,7 +101,6 @@ public class WebAuthnCredentialModel extends CredentialModel {
|
|||
try {
|
||||
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
|
||||
setSecretData(JsonSerialization.writeValueAsString(secretData));
|
||||
setType(TYPE);
|
||||
setCreatedDate(Time.currentTimeMillis());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
@ -106,7 +111,8 @@ public class WebAuthnCredentialModel extends CredentialModel {
|
|||
@Override
|
||||
public String toString() {
|
||||
return "WebAuthnCredentialModel { " +
|
||||
credentialData +
|
||||
getType() +
|
||||
", " + credentialData +
|
||||
", " + secretData +
|
||||
" }";
|
||||
}
|
||||
|
|
|
@ -35,16 +35,17 @@ import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
|||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.credential.CredentialProvider;
|
||||
import org.keycloak.credential.OTPCredentialProvider;
|
||||
import org.keycloak.credential.WebAuthnCredentialModelInput;
|
||||
import org.keycloak.credential.WebAuthnCredentialProvider;
|
||||
import org.keycloak.credential.WebAuthnCredentialProviderFactory;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.forms.login.freemarker.model.WebAuthnAuthenticatorsBean;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
@ -52,6 +53,9 @@ import javax.ws.rs.core.Response;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Authenticator for WebAuthn authentication, which will be typically used when WebAuthn is used as second factor.
|
||||
*/
|
||||
public class WebAuthnAuthenticator implements Authenticator, CredentialValidator<WebAuthnCredentialProvider> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(WebAuthnAuthenticator.class);
|
||||
|
@ -69,7 +73,8 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
context.getAuthenticationSession().setAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE, challengeValue);
|
||||
form.setAttribute(WebAuthnConstants.CHALLENGE, challengeValue);
|
||||
|
||||
String rpId = context.getRealm().getWebAuthnPolicy().getRpId();
|
||||
WebAuthnPolicy policy = getWebAuthnPolicy(context);
|
||||
String rpId = policy.getRpId();
|
||||
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
|
||||
form.setAttribute(WebAuthnConstants.RP_ID, rpId);
|
||||
|
||||
|
@ -77,7 +82,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
boolean isUserIdentified = false;
|
||||
if (user != null) {
|
||||
// in 2 Factor Scenario where the user has already been identified
|
||||
WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user);
|
||||
WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user, getCredentialType());
|
||||
if (authenticators.getAuthenticators().isEmpty()) {
|
||||
// require the user to register webauthn authenticator
|
||||
return;
|
||||
|
@ -91,15 +96,26 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
form.setAttribute(WebAuthnConstants.IS_USER_IDENTIFIED, Boolean.toString(isUserIdentified));
|
||||
|
||||
// read options from policy
|
||||
String userVerificationRequirement = context.getRealm().getWebAuthnPolicy().getUserVerificationRequirement();
|
||||
String userVerificationRequirement = policy.getUserVerificationRequirement();
|
||||
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
|
||||
|
||||
context.challenge(form.createLoginWebAuthn());
|
||||
}
|
||||
|
||||
protected WebAuthnPolicy getWebAuthnPolicy(AuthenticationFlowContext context) {
|
||||
return context.getRealm().getWebAuthnPolicy();
|
||||
}
|
||||
|
||||
protected String getCredentialType() {
|
||||
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
|
||||
}
|
||||
|
||||
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
||||
|
||||
context.getEvent().detail(Details.CREDENTIAL_TYPE, getCredentialType());
|
||||
|
||||
// receive error from navigator.credentials.get()
|
||||
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
|
||||
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
|
||||
|
@ -121,7 +137,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
|
||||
String userId = params.getFirst(WebAuthnConstants.USER_HANDLE);
|
||||
boolean isUVFlagChecked = false;
|
||||
String userVerificationRequirement = context.getRealm().getWebAuthnPolicy().getUserVerificationRequirement();
|
||||
String userVerificationRequirement = getWebAuthnPolicy(context).getUserVerificationRequirement();
|
||||
if (WebAuthnConstants.OPTION_REQUIRED.equals(userVerificationRequirement)) isUVFlagChecked = true;
|
||||
|
||||
// existing User Handle means that the authenticator used Resident Key supported public key credential
|
||||
|
@ -158,7 +174,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
isUVFlagChecked
|
||||
);
|
||||
|
||||
WebAuthnCredentialModelInput cred = new WebAuthnCredentialModelInput();
|
||||
WebAuthnCredentialModelInput cred = new WebAuthnCredentialModelInput(getCredentialType());
|
||||
cred.setAuthenticationContext(authenticationContext);
|
||||
|
||||
boolean result = false;
|
||||
|
@ -171,7 +187,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
String encodedCredentialID = Base64Url.encode(credentialId);
|
||||
if (result) {
|
||||
String isUVChecked = Boolean.toString(isUVFlagChecked);
|
||||
logger.infov("WebAuthn Authentication successed. isUserVerificationChecked = {0}, PublicKeyCredentialID = {1}", isUVChecked, encodedCredentialID);
|
||||
logger.debugv("WebAuthn Authentication successed. isUserVerificationChecked = {0}, PublicKeyCredentialID = {1}", isUVChecked, encodedCredentialID);
|
||||
context.setUser(user);
|
||||
context.getEvent()
|
||||
.detail("web_authn_authenticator_user_verification_checked", isUVChecked)
|
||||
|
@ -191,7 +207,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
}
|
||||
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return session.userCredentialManager().isConfiguredFor(realm, user, WebAuthnCredentialModel.TYPE);
|
||||
return session.userCredentialManager().isConfiguredFor(realm, user, getCredentialType());
|
||||
}
|
||||
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
|
@ -219,8 +235,8 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
|||
private static final String ERR_NO_AUTHENTICATORS_REGISTERED = "No WebAuthn Authenticator registered.";
|
||||
private static final String ERR_WEBAUTHN_API_GET = "Failed to authenticate by the WebAuthn Authenticator";
|
||||
private static final String ERR_DIFFERENT_USER_AUTHENTICATED = "First authenticated user is not the one authenticated by the WebAuthn authenticator.";
|
||||
private static final String ERR_WEBAUTHN_VERIFICATION_FAIL = "WetAuthn Authentication result is invalid.";
|
||||
private static final String ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND = "Unknown user authenticated by the WebAuthen Authenticator";
|
||||
private static final String ERR_WEBAUTHN_VERIFICATION_FAIL = "WebAuthn Authentication result is invalid.";
|
||||
private static final String ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND = "Unknown user authenticated by the WebAuthn Authenticator";
|
||||
|
||||
private void setErrorResponse(AuthenticationFlowContext context, final String errorCase, final String errorMessage) {
|
||||
Response errorResponse = null;
|
||||
|
|
|
@ -57,7 +57,7 @@ public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory {
|
|||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Authenticator for WebAuthn";
|
||||
return "Authenticator for WebAuthn. Usually used for WebAuthn two-factor authentication";
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.browser;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.RequiredActionFactory;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||
import org.keycloak.credential.CredentialProvider;
|
||||
import org.keycloak.credential.WebAuthnPasswordlessCredentialProvider;
|
||||
import org.keycloak.credential.WebAuthnPasswordlessCredentialProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
|
||||
/**
|
||||
* Authenticator for WebAuthn authentication with passwordless credential. This class is temporary and will be likely
|
||||
* removed in the future during future improvements in authentication SPI
|
||||
*/
|
||||
public class WebAuthnPasswordlessAuthenticator extends WebAuthnAuthenticator {
|
||||
|
||||
public WebAuthnPasswordlessAuthenticator(KeycloakSession session) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WebAuthnPolicy getWebAuthnPolicy(AuthenticationFlowContext context) {
|
||||
return context.getRealm().getWebAuthnPolicyPasswordless();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCredentialType() {
|
||||
return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
// ask the user to do required action to register webauthn authenticator
|
||||
if (!user.getRequiredActions().contains(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)) {
|
||||
user.addRequiredAction(WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RequiredActionFactory> getRequiredActions(KeycloakSession session) {
|
||||
return Collections.singletonList((WebAuthnPasswordlessRegisterFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public WebAuthnPasswordlessCredentialProvider getCredentialProvider(KeycloakSession session) {
|
||||
return (WebAuthnPasswordlessCredentialProvider)session.getProvider(CredentialProvider.class, WebAuthnPasswordlessCredentialProviderFactory.PROVIDER_ID);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.browser;
|
||||
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class WebAuthnPasswordlessAuthenticatorFactory extends WebAuthnAuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "webauthn-authenticator-passwordless";
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "WebAuthn Passwordless Authenticator";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Authenticator for Passwordless WebAuthn authentication";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return new WebAuthnPasswordlessAuthenticator(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.requiredactions;
|
||||
|
||||
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
|
||||
import org.keycloak.authentication.RequiredActionContext;
|
||||
import org.keycloak.credential.WebAuthnPasswordlessCredentialProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.WebAuthnPolicy;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
|
||||
/**
|
||||
* Required action for register WebAuthn passwordless credential for the user. This class is temporary and will be likely
|
||||
* removed in the future during future improvements in authentication SPI
|
||||
*
|
||||
*/
|
||||
public class WebAuthnPasswordlessRegister extends WebAuthnRegister {
|
||||
|
||||
public WebAuthnPasswordlessRegister(KeycloakSession session, CertPathTrustworthinessValidator certPathtrustValidator) {
|
||||
super(session, certPathtrustValidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WebAuthnPolicy getWebAuthnPolicy(RequiredActionContext context) {
|
||||
return context.getRealm().getWebAuthnPolicyPasswordless();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCredentialType() {
|
||||
return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCredentialProviderId() {
|
||||
return WebAuthnPasswordlessCredentialProviderFactory.PROVIDER_ID;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.requiredactions;
|
||||
|
||||
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class WebAuthnPasswordlessRegisterFactory extends WebAuthnRegisterFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "webauthn-register-passwordless";
|
||||
|
||||
@Override
|
||||
protected WebAuthnRegister createProvider(KeycloakSession session, CertPathTrustworthinessValidator trustValidator) {
|
||||
return new WebAuthnPasswordlessRegister(session, trustValidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
return "Webauthn Register Passwordless";
|
||||
}
|
||||
|
||||
}
|
|
@ -37,6 +37,7 @@ import org.keycloak.credential.WebAuthnCredentialModelInput;
|
|||
import org.keycloak.credential.WebAuthnCredentialProvider;
|
||||
import org.keycloak.credential.WebAuthnCredentialProviderFactory;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
@ -67,17 +68,15 @@ import com.webauthn4j.validator.attestation.trustworthiness.ecdaa.DefaultECDAATr
|
|||
import com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
|
||||
/**
|
||||
* Required action for register WebAuthn 2-factor credential for the user
|
||||
*/
|
||||
public class WebAuthnRegister implements RequiredActionProvider, CredentialRegistrator {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(WebAuthnRegister.class);
|
||||
private KeycloakSession session;
|
||||
private CertPathTrustworthinessValidator certPathtrustValidator;
|
||||
|
||||
public WebAuthnRegister(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.certPathtrustValidator = new NullCertPathTrustworthinessValidator();
|
||||
}
|
||||
|
||||
public WebAuthnRegister(KeycloakSession session, CertPathTrustworthinessValidator certPathtrustValidator) {
|
||||
this.session = session;
|
||||
this.certPathtrustValidator = certPathtrustValidator;
|
||||
|
@ -95,7 +94,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
// construct parameters for calling WebAuthn API navigator.credential.create()
|
||||
|
||||
// mandatory
|
||||
WebAuthnPolicy policy = context.getRealm().getWebAuthnPolicy();
|
||||
WebAuthnPolicy policy = getWebAuthnPolicy(context);
|
||||
List<String> signatureAlgorithmsList = policy.getSignatureAlgorithm();
|
||||
String signatureAlgorithms = stringifySignatureAlgorithms(signatureAlgorithmsList);
|
||||
String rpEntityName = policy.getRpEntityName();
|
||||
|
@ -112,7 +111,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
|
||||
String excludeCredentialIds = "";
|
||||
if (avoidSameAuthenticatorRegister) {
|
||||
List<CredentialModel> webAuthnCredentials = session.userCredentialManager().getStoredCredentialsByType(context.getRealm(), userModel, WebAuthnCredentialModel.TYPE);
|
||||
List<CredentialModel> webAuthnCredentials = session.userCredentialManager().getStoredCredentialsByType(context.getRealm(), userModel, getCredentialType());
|
||||
List<String> webAuthnCredentialPubKeyIds = webAuthnCredentials.stream().map(credentialModel -> {
|
||||
|
||||
WebAuthnCredentialModel credModel = WebAuthnCredentialModel.createFromCredentialModel(credentialModel);
|
||||
|
@ -140,11 +139,25 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
context.challenge(form);
|
||||
}
|
||||
|
||||
protected WebAuthnPolicy getWebAuthnPolicy(RequiredActionContext context) {
|
||||
return context.getRealm().getWebAuthnPolicy();
|
||||
}
|
||||
|
||||
protected String getCredentialType() {
|
||||
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
|
||||
}
|
||||
|
||||
protected String getCredentialProviderId() {
|
||||
return WebAuthnCredentialProviderFactory.PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processAction(RequiredActionContext context) {
|
||||
|
||||
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
||||
|
||||
context.getEvent().detail(Details.CREDENTIAL_TYPE, getCredentialType());
|
||||
|
||||
// receive error from navigator.credentials.create()
|
||||
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
|
||||
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
|
||||
|
@ -152,7 +165,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
return;
|
||||
}
|
||||
|
||||
WebAuthnPolicy policy = context.getRealm().getWebAuthnPolicy();
|
||||
WebAuthnPolicy policy = getWebAuthnPolicy(context);
|
||||
String rpId = policy.getRpId();
|
||||
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
|
||||
String label = params.getFirst(WebAuthnConstants.AUTHENTICATOR_LABEL);
|
||||
|
@ -176,20 +189,20 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
|||
|
||||
checkAcceptedAuthenticator(response, policy);
|
||||
|
||||
WebAuthnCredentialModelInput credential = new WebAuthnCredentialModelInput();
|
||||
WebAuthnCredentialModelInput credential = new WebAuthnCredentialModelInput(getCredentialType());
|
||||
|
||||
credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
|
||||
credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
|
||||
|
||||
// Save new webAuthn credential
|
||||
WebAuthnCredentialProvider webAuthnCredProvider = (WebAuthnCredentialProvider) this.session.getProvider(CredentialProvider.class, WebAuthnCredentialProviderFactory.PROVIDER_ID);
|
||||
WebAuthnCredentialProvider webAuthnCredProvider = (WebAuthnCredentialProvider) this.session.getProvider(CredentialProvider.class, getCredentialProviderId());
|
||||
WebAuthnCredentialModel newCredentialModel = webAuthnCredProvider.getCredentialModelFromCredentialInput(credential, label);
|
||||
|
||||
webAuthnCredProvider.createCredential(context.getRealm(), context.getUser(), newCredentialModel);
|
||||
|
||||
String aaguid = newCredentialModel.getWebAuthnCredentialData().getAaguid();
|
||||
logger.debugv("WebAuthn credential registration success for user {0}. publicKeyCredentialId = {1}, publicKeyCredentialLabel = {2}, publicKeyCredentialAAGUID = {3}",
|
||||
context.getUser().getUsername(), publicKeyCredentialId, label, aaguid);
|
||||
logger.debugv("WebAuthn credential registration success for user {0}. credentialType = {1}, publicKeyCredentialId = {2}, publicKeyCredentialLabel = {3}, publicKeyCredentialAAGUID = {4}",
|
||||
context.getUser().getUsername(), getCredentialType(), publicKeyCredentialId, label, aaguid);
|
||||
webAuthnCredProvider.dumpCredentialModel(newCredentialModel, credential);
|
||||
|
||||
context.getEvent()
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.keycloak.authentication.requiredactions;
|
||||
|
||||
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
||||
|
@ -39,17 +40,21 @@ public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTy
|
|||
WebAuthnRegister webAuthnRegister = null;
|
||||
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
|
||||
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
|
||||
webAuthnRegister = new WebAuthnRegister(session, new NullCertPathTrustworthinessValidator());
|
||||
webAuthnRegister = createProvider(session, new NullCertPathTrustworthinessValidator());
|
||||
} else {
|
||||
KeyStoreTrustAnchorsProvider trustAnchorsProvider = new KeyStoreTrustAnchorsProvider();
|
||||
trustAnchorsProvider.setKeyStore(truststoreProvider.getTruststore());
|
||||
TrustAnchorsResolverImpl resolverImpl = new TrustAnchorsResolverImpl(trustAnchorsProvider);
|
||||
TrustAnchorCertPathTrustworthinessValidator trustValidator = new TrustAnchorCertPathTrustworthinessValidator(resolverImpl);
|
||||
webAuthnRegister = new WebAuthnRegister(session, trustValidator);
|
||||
webAuthnRegister = createProvider(session, trustValidator);
|
||||
}
|
||||
return webAuthnRegister;
|
||||
}
|
||||
|
||||
protected WebAuthnRegister createProvider(KeycloakSession session, CertPathTrustworthinessValidator trustValidator) {
|
||||
return new WebAuthnRegister(session, trustValidator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Scope config) {
|
||||
// NOP
|
||||
|
|
|
@ -26,13 +26,16 @@ import org.keycloak.models.credential.WebAuthnCredentialModel;
|
|||
|
||||
public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||
|
||||
public static final String WEBAUTHN_CREDENTIAL_TYPE = WebAuthnCredentialModel.TYPE;
|
||||
|
||||
private AttestedCredentialData attestedCredentialData;
|
||||
private AttestationStatement attestationStatement;
|
||||
private WebAuthnAuthenticationContext authenticationContext;
|
||||
private long count;
|
||||
private String credentialDBId;
|
||||
private final String credentialType;
|
||||
|
||||
public WebAuthnCredentialModelInput(String credentialType) {
|
||||
this.credentialType = credentialType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCredentialId() {
|
||||
|
@ -46,12 +49,9 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
|||
|
||||
@Override
|
||||
public String getType() {
|
||||
return WEBAUTHN_CREDENTIAL_TYPE;
|
||||
return credentialType;
|
||||
}
|
||||
|
||||
public WebAuthnCredentialModelInput() {
|
||||
|
||||
}
|
||||
|
||||
public AttestedCredentialData getAttestedCredentialData() {
|
||||
return attestedCredentialData;
|
||||
|
@ -93,8 +93,12 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
|||
this.credentialDBId = credentialDBId;
|
||||
}
|
||||
|
||||
public String getCredentialType() {
|
||||
return credentialType;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
StringBuilder sb = new StringBuilder("Credential Type = " + credentialType + ",");
|
||||
if (credentialDBId != null)
|
||||
sb.append("Credential DB Id = ")
|
||||
.append(credentialDBId)
|
||||
|
|
|
@ -40,6 +40,9 @@ import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
|
|||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
||||
|
||||
/**
|
||||
* Credential provider for WebAuthn 2-factor credential of the user
|
||||
*/
|
||||
public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCredentialModel>, CredentialInputValidator {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class);
|
||||
|
@ -98,7 +101,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
|||
String credentialPublicKey = credentialPublicKeyConverter.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCOSEKey());
|
||||
long counter = webAuthnModel.getCount();
|
||||
|
||||
WebAuthnCredentialModel model = WebAuthnCredentialModel.create(userLabel, aaguid, credentialId, null, credentialPublicKey, counter);
|
||||
WebAuthnCredentialModel model = WebAuthnCredentialModel.create(getType(), userLabel, aaguid, credentialId, null, credentialPublicKey, counter);
|
||||
|
||||
model.setId(webAuthnModel.getCredentialDBId());
|
||||
|
||||
|
@ -114,7 +117,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
|||
|
||||
WebAuthnCredentialData credData = webAuthnCredential.getWebAuthnCredentialData();
|
||||
|
||||
WebAuthnCredentialModelInput auth = new WebAuthnCredentialModelInput();
|
||||
WebAuthnCredentialModelInput auth = new WebAuthnCredentialModelInput(getType());
|
||||
|
||||
byte[] credentialId = null;
|
||||
try {
|
||||
|
@ -142,7 +145,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
|||
|
||||
@Override
|
||||
public boolean supportsCredentialType(String credentialType) {
|
||||
return WebAuthnCredentialModelInput.WEBAUTHN_CREDENTIAL_TYPE.equals(credentialType);
|
||||
return getType().equals(credentialType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -204,12 +207,12 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
|||
|
||||
@Override
|
||||
public String getType() {
|
||||
return WebAuthnCredentialModel.TYPE;
|
||||
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
|
||||
}
|
||||
|
||||
|
||||
private List<WebAuthnCredentialModelInput> getWebAuthnCredentialModelList(RealmModel realm, UserModel user) {
|
||||
List<CredentialModel> credentialModels = session.userCredentialManager().getStoredCredentialsByType(realm, user, WebAuthnCredentialModel.TYPE);
|
||||
List<CredentialModel> credentialModels = session.userCredentialManager().getStoredCredentialsByType(realm, user, getType());
|
||||
|
||||
return credentialModels.stream()
|
||||
.map(this::getCredentialInputFromCredentialModel)
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.credential;
|
||||
|
||||
import com.webauthn4j.converter.util.CborConverter;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
|
||||
/**
|
||||
* Credential provider for WebAuthn passwordless credential of the user
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class WebAuthnPasswordlessCredentialProvider extends WebAuthnCredentialProvider {
|
||||
|
||||
public WebAuthnPasswordlessCredentialProvider(KeycloakSession session, CborConverter converter) {
|
||||
super(session, converter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return WebAuthnCredentialModel.TYPE_PASSWORDLESS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.credential;
|
||||
|
||||
import com.webauthn4j.converter.util.CborConverter;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class WebAuthnPasswordlessCredentialProviderFactory implements CredentialProviderFactory<WebAuthnPasswordlessCredentialProvider> {
|
||||
|
||||
public static final String PROVIDER_ID = "keycloak-webauthn-passwordless";
|
||||
|
||||
private static CborConverter converter = new CborConverter();
|
||||
|
||||
@Override
|
||||
public CredentialProvider create(KeycloakSession session) {
|
||||
return new WebAuthnPasswordlessCredentialProvider(session, converter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -28,9 +28,9 @@ import org.keycloak.models.credential.WebAuthnCredentialModel;
|
|||
public class WebAuthnAuthenticatorsBean {
|
||||
private List<WebAuthnAuthenticatorBean> authenticators = new LinkedList<WebAuthnAuthenticatorBean>();
|
||||
|
||||
public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user, String credentialType) {
|
||||
// should consider multiple credentials in the future, but only single credential supported now.
|
||||
for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, WebAuthnCredentialModel.TYPE)) {
|
||||
for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType)) {
|
||||
WebAuthnCredentialModel webAuthnCredential = WebAuthnCredentialModel.createFromCredentialModel(credential);
|
||||
|
||||
String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId());
|
||||
|
|
|
@ -47,4 +47,5 @@ org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthen
|
|||
org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory
|
|
@ -20,4 +20,5 @@ org.keycloak.authentication.requiredactions.UpdateProfile
|
|||
org.keycloak.authentication.requiredactions.UpdateTotp
|
||||
org.keycloak.authentication.requiredactions.VerifyEmail
|
||||
org.keycloak.authentication.requiredactions.TermsAndConditions
|
||||
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
||||
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
||||
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory
|
|
@ -1,3 +1,4 @@
|
|||
org.keycloak.credential.OTPCredentialProviderFactory
|
||||
org.keycloak.credential.PasswordCredentialProviderFactory
|
||||
org.keycloak.credential.WebAuthnCredentialProviderFactory
|
||||
org.keycloak.credential.WebAuthnCredentialProviderFactory
|
||||
org.keycloak.credential.WebAuthnPasswordlessCredentialProviderFactory
|
|
@ -17,174 +17,28 @@
|
|||
|
||||
package org.keycloak.testsuite.pages.webauthn;
|
||||
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
import static org.keycloak.testsuite.util.UIUtils.clickLink;
|
||||
|
||||
/**
|
||||
* Page shown during WebAuthn login. Page is useful with Chrome testing API
|
||||
*/
|
||||
public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
||||
|
||||
@ArquillianResource
|
||||
protected OAuthClient oauth;
|
||||
|
||||
@FindBy(id = "username")
|
||||
private WebElement usernameInput;
|
||||
|
||||
@FindBy(id = "password")
|
||||
private WebElement passwordInput;
|
||||
|
||||
@FindBy(id = "totp")
|
||||
private WebElement totp;
|
||||
|
||||
@FindBy(id = "rememberMe")
|
||||
private WebElement rememberMe;
|
||||
|
||||
@FindBy(name = "login")
|
||||
private WebElement submitButton;
|
||||
|
||||
@FindBy(name = "cancel")
|
||||
private WebElement cancelButton;
|
||||
|
||||
@FindBy(linkText = "Register")
|
||||
private WebElement registerLink;
|
||||
|
||||
@FindBy(linkText = "Forgot Password?")
|
||||
private WebElement resetPasswordLink;
|
||||
|
||||
@FindBy(linkText = "Username")
|
||||
private WebElement recoverUsernameLink;
|
||||
|
||||
@FindBy(className = "alert-error")
|
||||
private WebElement loginErrorMessage;
|
||||
|
||||
@FindBy(className = "alert-warning")
|
||||
private WebElement loginWarningMessage;
|
||||
|
||||
@FindBy(className = "alert-success")
|
||||
private WebElement loginSuccessMessage;
|
||||
|
||||
|
||||
@FindBy(className = "alert-info")
|
||||
private WebElement loginInfoMessage;
|
||||
|
||||
@FindBy(className = "instruction")
|
||||
private WebElement instruction;
|
||||
|
||||
|
||||
public void login(String username, String password) {
|
||||
driver.findElement(By.id("username")).clear();
|
||||
driver.findElement(By.id("username")).sendKeys(username);
|
||||
|
||||
driver.findElement(By.id("password")).clear();
|
||||
driver.findElement(By.id("password")).sendKeys(password);
|
||||
|
||||
driver.findElement(By.name("login")).click();
|
||||
|
||||
// After click the button, the "navigator.credentials.get" will be called on the browser side, which should automatically
|
||||
// login user with the chrome testing API
|
||||
public void confirmWebAuthnLogin() {
|
||||
driver.findElement(By.cssSelector("input[type=\"button\"]")).click();
|
||||
}
|
||||
|
||||
public void login(String password) {
|
||||
driver.findElement(By.id("password")).clear();
|
||||
driver.findElement(By.id("password")).sendKeys(password);
|
||||
|
||||
driver.findElement(By.name("login")).click();
|
||||
}
|
||||
|
||||
public void missingPassword(String username) {
|
||||
driver.findElement(By.id("username")).clear();
|
||||
driver.findElement(By.id("username")).sendKeys(username);
|
||||
driver.findElement(By.id("password")).clear();
|
||||
driver.findElement(By.name("login")).click();
|
||||
|
||||
}
|
||||
public void missingUsername() {
|
||||
driver.findElement(By.id("username")).clear();
|
||||
driver.findElement(By.name("login")).click();
|
||||
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return driver.findElement(By.id("username")).getAttribute("value");
|
||||
}
|
||||
|
||||
public boolean isUsernameInputEnabled() {
|
||||
return driver.findElement(By.id("username")).isEnabled();
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return driver.findElement(By.id("password")).getAttribute("value");
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
driver.findElement(By.name("cancel")).click();
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||
}
|
||||
|
||||
public String getInstruction() {
|
||||
return instruction != null ? instruction.getText() : null;
|
||||
}
|
||||
|
||||
public String getSuccessMessage() {
|
||||
return loginSuccessMessage != null ? loginSuccessMessage.getText() : null;
|
||||
}
|
||||
public String getInfoMessage() {
|
||||
return loginInfoMessage != null ? loginInfoMessage.getText() : null;
|
||||
}
|
||||
|
||||
|
||||
public boolean isCurrent() {
|
||||
String realm = "test";
|
||||
return isCurrent(realm);
|
||||
}
|
||||
|
||||
public boolean isCurrent(String realm) {
|
||||
return driver.getTitle().equals("Log in to " + realm) || driver.getTitle().equals("Anmeldung bei " + realm);
|
||||
}
|
||||
|
||||
public void clickRegister() {
|
||||
driver.findElement(By.linkText("Register")).click();
|
||||
}
|
||||
|
||||
public void clickSocial(String providerId) {
|
||||
WebElement socialButton = findSocialButton(providerId);
|
||||
clickLink(socialButton);
|
||||
}
|
||||
|
||||
public WebElement findSocialButton(String providerId) {
|
||||
String id = "zocial-" + providerId;
|
||||
return this.driver.findElement(By.id(id));
|
||||
}
|
||||
|
||||
public void resetPassword() {
|
||||
resetPasswordLink.click();
|
||||
}
|
||||
|
||||
public void recoverUsername() {
|
||||
recoverUsernameLink.click();
|
||||
}
|
||||
|
||||
public void setRememberMe(boolean enable) {
|
||||
boolean current = rememberMe.isSelected();
|
||||
if (current != enable) {
|
||||
rememberMe.click();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRememberMeChecked() {
|
||||
return rememberMe.isSelected();
|
||||
return driver.getPageSource().contains("navigator.credentials.get");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open() {
|
||||
oauth.openLoginForm();
|
||||
assertCurrent();
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,172 +19,40 @@ package org.keycloak.testsuite.pages.webauthn;
|
|||
|
||||
import org.junit.Assert;
|
||||
import org.keycloak.testsuite.pages.AbstractPage;
|
||||
import org.keycloak.testsuite.pages.PageUtils;
|
||||
import org.openqa.selenium.Alert;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
|
||||
/**
|
||||
* WebAuthnRegisterPage, which is displayed when WebAuthnRegister required action is triggered. It is useful with Chrome testing API.
|
||||
*
|
||||
* Page will be displayed after successful JS call of "navigator.credentials.create", which will register WebAuthn credential
|
||||
* with the browser
|
||||
*/
|
||||
public class WebAuthnRegisterPage extends AbstractPage {
|
||||
|
||||
@FindBy(id = "firstName")
|
||||
private WebElement firstNameInput;
|
||||
|
||||
@FindBy(id = "lastName")
|
||||
private WebElement lastNameInput;
|
||||
|
||||
@FindBy(id = "email")
|
||||
private WebElement emailInput;
|
||||
|
||||
@FindBy(id = "username")
|
||||
private WebElement usernameInput;
|
||||
|
||||
@FindBy(id = "password")
|
||||
private WebElement passwordInput;
|
||||
|
||||
@FindBy(id = "password-confirm")
|
||||
private WebElement passwordConfirmInput;
|
||||
|
||||
@FindBy(css = "input[type=\"submit\"]")
|
||||
private WebElement submitButton;
|
||||
|
||||
@FindBy(className = "alert-error")
|
||||
private WebElement loginErrorMessage;
|
||||
|
||||
@FindBy(className = "instruction")
|
||||
private WebElement loginInstructionMessage;
|
||||
|
||||
@FindBy(linkText = "« Back to Login")
|
||||
private WebElement backToLoginLink;
|
||||
|
||||
|
||||
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, String authenticatorLabel) {
|
||||
driver.findElement(By.id("firstName")).clear();
|
||||
if (firstName != null) {
|
||||
driver.findElement(By.id("firstName")).sendKeys(firstName);
|
||||
}
|
||||
|
||||
driver.findElement(By.id("lastName")).clear();
|
||||
if (lastName != null) {
|
||||
driver.findElement(By.id("lastName")).sendKeys(lastName);
|
||||
}
|
||||
|
||||
driver.findElement(By.id("email")).clear();
|
||||
if (email != null) {
|
||||
driver.findElement(By.id("email")).sendKeys(email);
|
||||
}
|
||||
|
||||
driver.findElement(By.id("username")).clear();
|
||||
if (username != null) {
|
||||
driver.findElement(By.id("username")).sendKeys(username);
|
||||
}
|
||||
|
||||
driver.findElement(By.id("password")).clear();
|
||||
if (password != null) {
|
||||
driver.findElement(By.id("password")).sendKeys(password);
|
||||
}
|
||||
|
||||
driver.findElement(By.id("password-confirm")).clear();
|
||||
if (passwordConfirm != null) {
|
||||
driver.findElement(By.id("password-confirm")).sendKeys(passwordConfirm);
|
||||
}
|
||||
|
||||
driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
|
||||
|
||||
public void registerWebAuthnCredential(String authenticatorLabel) {
|
||||
// label edit after registering authenicator by .create()
|
||||
WebDriverWait wait = new WebDriverWait(driver, 60);
|
||||
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
|
||||
//Alert promptDialog = driver.switchTo().alert();
|
||||
|
||||
Assert.assertEquals("Please input your registered authenticator's label", promptDialog.getText());
|
||||
|
||||
promptDialog.sendKeys(authenticatorLabel);
|
||||
promptDialog.accept();
|
||||
}
|
||||
|
||||
public void registerWithEmailAsUsername(String firstName, String lastName, String email, String password, String passwordConfirm) {
|
||||
driver.findElement(By.id("firstName")).clear();
|
||||
if (firstName != null) {
|
||||
driver.findElement(By.id("firstName")).sendKeys(firstName);
|
||||
}
|
||||
|
||||
driver.findElement(By.id("lastName")).clear();
|
||||
if (lastName != null) {
|
||||
driver.findElement(By.id("lastName")).sendKeys(lastName);
|
||||
}
|
||||
|
||||
driver.findElement(By.id("email")).clear();
|
||||
if (email != null) {
|
||||
driver.findElement(By.id("email")).sendKeys(email);
|
||||
}
|
||||
|
||||
try {
|
||||
driver.findElement(By.id("username")).clear();
|
||||
Assert.fail("Form must be without username field");
|
||||
} catch (NoSuchElementException e) {
|
||||
// OK
|
||||
}
|
||||
|
||||
driver.findElement(By.id("password")).clear();
|
||||
if (password != null) {
|
||||
driver.findElement(By.id("password")).sendKeys(password);
|
||||
}
|
||||
|
||||
driver.findElement(By.id("password-confirm")).clear();
|
||||
if (passwordConfirm != null) {
|
||||
driver.findElement(By.id("password-confirm")).sendKeys(passwordConfirm);
|
||||
}
|
||||
driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
|
||||
}
|
||||
|
||||
public void clickBackToLogin() {
|
||||
backToLoginLink.click();
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
|
||||
}
|
||||
|
||||
public String getInstruction() {
|
||||
try {
|
||||
return loginInstructionMessage != null ? loginInstructionMessage.getText() : null;
|
||||
} catch (NoSuchElementException e){
|
||||
// OK
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return driver.findElement(By.id("firstName")).getAttribute("value");
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return driver.findElement(By.id("lastName")).getAttribute("value");
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return driver.findElement(By.id("email")).getAttribute("value");
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return driver.findElement(By.id("username")).getAttribute("value");
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return driver.findElement(By.id("password")).getAttribute("value");
|
||||
}
|
||||
|
||||
public String getPasswordConfirm() {
|
||||
return driver.findElement(By.id("password-confirm")).getAttribute("value");
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return PageUtils.getPageTitle(driver).equals("Register");
|
||||
// Cant verify the page in case that prompt is shown. Prompt is shown immediately when WebAuthnRegisterPage is displayed
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void open() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,7 +198,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
|||
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
||||
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
|
||||
"Testsuite Username authenticator. Username parameter sets username");
|
||||
addProviderInfo(result, "webauthn-authenticator", "WebAuthn Authenticator", "Authenticator for WebAuthn");
|
||||
addProviderInfo(result, "webauthn-authenticator", "WebAuthn Authenticator", "Authenticator for WebAuthn. Usually used for WebAuthn two-factor authentication");
|
||||
addProviderInfo(result, "webauthn-authenticator-passwordless", "WebAuthn Passwordless Authenticator", "Authenticator for Passwordless WebAuthn authentication");
|
||||
|
||||
addProviderInfo(result, "auth-username-form", "Username Form",
|
||||
"Selects a user from his username.");
|
||||
|
|
|
@ -77,9 +77,9 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
|
|||
public void testCRUDRequiredAction() {
|
||||
int lastPriority = authMgmtResource.getRequiredActions().get(authMgmtResource.getRequiredActions().size() - 1).getPriority();
|
||||
|
||||
// Just Dummy RequiredAction is not registered in the realm
|
||||
// Dummy RequiredAction is not registered in the realm and WebAuthn actions
|
||||
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
|
||||
Assert.assertEquals(2, result.size());
|
||||
Assert.assertEquals(3, result.size());
|
||||
RequiredActionProviderSimpleRepresentation action = result.get(0);
|
||||
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
|
||||
Assert.assertEquals("Dummy Action", action.getName());
|
||||
|
|
|
@ -23,15 +23,21 @@ import org.junit.Rule;
|
|||
import org.junit.Test;
|
||||
import org.keycloak.WebAuthnConstants;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||
import org.keycloak.common.util.RandomString;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.RegisterPage;
|
||||
import org.keycloak.testsuite.pages.webauthn.WebAuthnLoginPage;
|
||||
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
|
||||
import org.keycloak.testsuite.WebAuthnAssume;
|
||||
|
@ -55,10 +61,16 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
|
|||
protected AppPage appPage;
|
||||
|
||||
@Page
|
||||
protected WebAuthnLoginPage loginPage;
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected WebAuthnRegisterPage registerPage;
|
||||
protected WebAuthnLoginPage webAuthnLoginPage;
|
||||
|
||||
@Page
|
||||
protected RegisterPage registerPage;
|
||||
|
||||
@Page
|
||||
protected WebAuthnRegisterPage webAuthnRegisterPage;
|
||||
|
||||
private static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
||||
|
||||
|
@ -115,7 +127,10 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
|
|||
registerPage.assertCurrent();
|
||||
|
||||
String authenticatorLabel = RandomString.randomCode(24);
|
||||
registerPage.register("firstName", "lastName", email, username, password, password, authenticatorLabel);
|
||||
registerPage.register("firstName", "lastName", email, username, password, password);
|
||||
|
||||
// User was registered. Now he needs to register WebAuthn credential
|
||||
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
|
@ -153,6 +168,10 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
|
|||
loginPage.open();
|
||||
loginPage.login(username, password);
|
||||
|
||||
// Confirm login on the WebAuthn login page
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
webAuthnLoginPage.confirmWebAuthnLogin();
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
appPage.openAccount();
|
||||
|
@ -174,12 +193,105 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testWebAuthnTwoFactorAndWebAuthnPasswordlessTogether() {
|
||||
|
||||
// Change binding to browser-webauthn-passwordless. This is flow, which contains both "webauthn" and "webauthn-passwordless" authenticator
|
||||
RealmRepresentation realmRep = testRealm().toRepresentation();
|
||||
realmRep.setBrowserFlow("browser-webauthn-passwordless");
|
||||
testRealm().update(realmRep);
|
||||
|
||||
//WaitUtils.pause(10000000);
|
||||
|
||||
try {
|
||||
String userId = ApiUtil.findUserByUsername(testRealm(), "test-user@localhost").getId();
|
||||
|
||||
// Login as test-user@localhost with password
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
// Register first requiredAction is needed. Use label "Label1"
|
||||
webAuthnRegisterPage.registerWebAuthnCredential("label1");
|
||||
|
||||
// Register second requiredAction is needed. Use label "Label2". This will be for passwordless WebAuthn credential
|
||||
webAuthnRegisterPage.registerWebAuthnCredential("label2");
|
||||
|
||||
appPage.assertCurrent();
|
||||
|
||||
// Assert user is logged and WebAuthn credentials were registered
|
||||
EventRepresentation eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "label1")
|
||||
.assertEvent();
|
||||
String regPubKeyCredentialId1 = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||
|
||||
eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
|
||||
.user(userId)
|
||||
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
|
||||
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "label2")
|
||||
.assertEvent();
|
||||
String regPubKeyCredentialId2 = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
|
||||
|
||||
String sessionId = events.expectLogin()
|
||||
.user(userId)
|
||||
.assertEvent().getSessionId();
|
||||
|
||||
// Logout
|
||||
appPage.logout();
|
||||
events.expectLogout(sessionId)
|
||||
.user(userId)
|
||||
.assertEvent();
|
||||
|
||||
// Assert user has 2 webauthn credentials. One of type "webauthn" and the other of type "webauthn-passwordless".
|
||||
List<CredentialRepresentation> rep = testRealm().users().get(userId).credentials();
|
||||
|
||||
CredentialRepresentation webAuthnCredential1 = rep.stream()
|
||||
.filter(credential -> WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(credential.getType()))
|
||||
.findFirst().get();
|
||||
Assert.assertEquals("label1", webAuthnCredential1.getUserLabel());
|
||||
|
||||
CredentialRepresentation webAuthnCredential2 = rep.stream()
|
||||
.filter(credential -> WebAuthnCredentialModel.TYPE_PASSWORDLESS.equals(credential.getType()))
|
||||
.findFirst().get();
|
||||
Assert.assertEquals("label2", webAuthnCredential2.getUserLabel());
|
||||
|
||||
// Assert user needs to authenticate first with "webauthn" during login
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
Assert.assertTrue(driver.getPageSource().contains(regPubKeyCredentialId1));
|
||||
webAuthnLoginPage.confirmWebAuthnLogin();
|
||||
|
||||
// Assert user needs to authenticate also with "webauthn-passwordless"
|
||||
webAuthnLoginPage.assertCurrent();
|
||||
Assert.assertTrue(driver.getPageSource().contains(regPubKeyCredentialId2));
|
||||
webAuthnLoginPage.confirmWebAuthnLogin();
|
||||
|
||||
// Assert user logged now
|
||||
appPage.assertCurrent();
|
||||
events.expectLogin()
|
||||
.user(userId)
|
||||
.assertEvent();
|
||||
|
||||
// Remove webauthn credentials from the user
|
||||
testRealm().users().get(userId).removeCredential(webAuthnCredential1.getId());
|
||||
testRealm().users().get(userId).removeCredential(webAuthnCredential2.getId());
|
||||
} finally {
|
||||
// Revert binding to browser-webauthn
|
||||
realmRep.setBrowserFlow("browser-webauthn");
|
||||
testRealm().update(realmRep);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertUserRegistered(String userId, String username, String email) {
|
||||
UserRepresentation user = getUser(userId);
|
||||
Assert.assertNotNull(user);
|
||||
Assert.assertNotNull(user.getCreatedTimestamp());
|
||||
// test that timestamp is current with 10s tollerance
|
||||
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
|
||||
// test that timestamp is current with 60s tollerance
|
||||
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 60000);
|
||||
// test user info is set from form
|
||||
assertEquals(username.toLowerCase(), user.getUsername());
|
||||
assertEquals(email.toLowerCase(), user.getEmail());
|
||||
|
|
|
@ -26,17 +26,6 @@
|
|||
"00000000-0000-0000-0000-000000000000",
|
||||
"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73"
|
||||
],
|
||||
"attributes": {
|
||||
"webAuthnPolicyAcceptableAaguids": "00000000-0000-0000-0000-000000000000,6d44ba9b-f6ec-2e49-b930-0c8fe920cb73",
|
||||
"webAuthnPolicyAuthenticatorAttachment": "not specified",
|
||||
"webAuthnPolicyRpEntityName": "keycloak-webauthn-2FA",
|
||||
"webAuthnPolicySignatureAlgorithms": "ES256,RS256,RS1",
|
||||
"webAuthnPolicyUserVerificationRequirement": "not specified",
|
||||
"webAuthnPolicyCreateTimeout": "60",
|
||||
"webAuthnPolicyRequireResidentKey": "not specified",
|
||||
"webAuthnPolicyAttestationConveyancePreference": "not specified",
|
||||
"webAuthnPolicyAvoidSameAuthenticatorRegister": "true"
|
||||
},
|
||||
"smtpServer": {
|
||||
"from": "auto@keycloak.org",
|
||||
"host": "localhost",
|
||||
|
@ -574,7 +563,7 @@
|
|||
|
||||
"authenticationFlows": [
|
||||
{
|
||||
"alias": "Copy of browser",
|
||||
"alias": "browser-webauthn",
|
||||
"description": "browser based authentication",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": true,
|
||||
|
@ -604,14 +593,14 @@
|
|||
{
|
||||
"requirement": "ALTERNATIVE",
|
||||
"priority": 30,
|
||||
"flowAlias": "Copy of browser forms",
|
||||
"flowAlias": "browser-webauthn-forms",
|
||||
"userSetupAllowed": false,
|
||||
"autheticatorFlow": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"alias": "Copy of browser forms",
|
||||
"alias": "browser-webauthn-forms",
|
||||
"description": "Username, password, otp and other auth forms.",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": false,
|
||||
|
@ -640,6 +629,59 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"alias": "browser-webauthn-passwordless",
|
||||
"description": "browser based authentication",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": true,
|
||||
"builtIn": false,
|
||||
"authenticationExecutions": [
|
||||
{
|
||||
"authenticator": "auth-cookie",
|
||||
"requirement": "ALTERNATIVE",
|
||||
"priority": 10,
|
||||
"userSetupAllowed": false,
|
||||
"autheticatorFlow": false
|
||||
},
|
||||
{
|
||||
"requirement": "ALTERNATIVE",
|
||||
"priority": 30,
|
||||
"flowAlias": "browser-webauthn-passwordless-forms",
|
||||
"userSetupAllowed": false,
|
||||
"autheticatorFlow": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"alias": "browser-webauthn-passwordless-forms",
|
||||
"description": "Username, password, otp and other auth forms.",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": false,
|
||||
"builtIn": false,
|
||||
"authenticationExecutions": [
|
||||
{
|
||||
"authenticator": "auth-username-password-form",
|
||||
"requirement": "REQUIRED",
|
||||
"priority": 10,
|
||||
"userSetupAllowed": false,
|
||||
"autheticatorFlow": false
|
||||
},
|
||||
{
|
||||
"authenticator": "webauthn-authenticator",
|
||||
"requirement": "REQUIRED",
|
||||
"priority": 20,
|
||||
"userSetupAllowed": false,
|
||||
"autheticatorFlow": false
|
||||
},
|
||||
{
|
||||
"authenticator": "webauthn-authenticator-passwordless",
|
||||
"requirement": "REQUIRED",
|
||||
"priority": 30,
|
||||
"userSetupAllowed": false,
|
||||
"autheticatorFlow": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"alias": "Handle Existing Account",
|
||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||
|
@ -1083,9 +1125,18 @@
|
|||
"defaultAction": true,
|
||||
"priority": 51,
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"alias": "webauthn-register-passwordless",
|
||||
"name": "Webauthn Register Passwordless",
|
||||
"providerId": "webauthn-register-passwordless",
|
||||
"enabled": true,
|
||||
"defaultAction": false,
|
||||
"priority": 52,
|
||||
"config": {}
|
||||
}
|
||||
],
|
||||
"browserFlow": "Copy of browser",
|
||||
"browserFlow": "browser-webauthn",
|
||||
|
||||
"internationalizationEnabled": true,
|
||||
"supportedLocales": ["en", "de"],
|
||||
|
|
|
@ -1063,6 +1063,9 @@ add-policy.placeholder=Add policy...
|
|||
policy-type=Policy Type
|
||||
policy-value=Policy Value
|
||||
webauthn-policy=WebAuthn Policy
|
||||
webauthn-policy.tooltip=Policy for WebAuthn authentication. This one will be used by 'WebAuthn Register' required action and 'WebAuthn Authenticator' authenticator. Typical usage is, when WebAuthn will be used for the two-factor authentication.
|
||||
webauthn-policy-passwordless=WebAuthn Passwordless Policy
|
||||
webauthn-policy-passwordless.tooltip=Policy for passwordless WebAuthn authentication. This one will be used by 'Webauthn Register Passwordless' required action and 'WebAuthn Passwordless Authenticator' authenticator. Typical usage is, when WebAuthn will be used as first-factor authentication. Having both 'WebAuthn Policy' and 'WebAuthn Passwordless Policy' allows to use WebAuthn as both first factor and second factor authenticator in the same realm.
|
||||
webauthn-rp-entity-name=Relying Party Entity Name
|
||||
webauthn-rp-entity-name.tooltip=Human-readable server name as WebAuthn Relying Party
|
||||
webauthn-signature-algorithms=Signature Algorithms
|
||||
|
|
|
@ -2017,6 +2017,18 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
controller : 'RealmWebAuthnPolicyCtrl'
|
||||
})
|
||||
.when('/realms/:realm/authentication/webauthn-policy-passwordless', {
|
||||
templateUrl : resourceUrl + '/partials/webauthn-policy-passwordless.html',
|
||||
resolve : {
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
},
|
||||
serverInfo : function(ServerInfo) {
|
||||
return ServerInfo.delay;
|
||||
}
|
||||
},
|
||||
controller : 'RealmWebAuthnPasswordlessPolicyCtrl'
|
||||
})
|
||||
.when('/realms/:realm/authentication/flows/:flow/config/:provider/:config', {
|
||||
templateUrl : resourceUrl + '/partials/authenticator-config.html',
|
||||
resolve : {
|
||||
|
|
|
@ -412,6 +412,20 @@ module.controller('RealmWebAuthnPolicyCtrl', function($scope, Current, Realm, re
|
|||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy");
|
||||
});
|
||||
|
||||
module.controller('RealmWebAuthnPasswordlessPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
||||
|
||||
$scope.deleteAcceptableAaguid = function(index) {
|
||||
$scope.realm.webAuthnPolicyPasswordlessAcceptableAaguids.splice(index, 1);
|
||||
}
|
||||
|
||||
$scope.addAcceptableAaguid = function() {
|
||||
$scope.realm.webAuthnPolicyPasswordlessAcceptableAaguids.push($scope.newAcceptableAaguid);
|
||||
$scope.newAcceptableAaguid = "";
|
||||
}
|
||||
|
||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy-passwordless");
|
||||
});
|
||||
|
||||
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
|
||||
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");
|
||||
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
<!--
|
||||
~ Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
~
|
||||
-->
|
||||
|
||||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<h1>{{:: 'authentication' | translate}}</h1>
|
||||
|
||||
<kc-tabs-authentication></kc-tabs-authentication>
|
||||
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-md-2 control-label"><span class="required">*</span> {{:: 'webauthn-rp-entity-name' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<input id="name" type="text" ng-model="realm.webAuthnPolicyPasswordlessRpEntityName" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-rp-entity-name.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sigalg" class="col-md-2 control-label">{{:: 'webauthn-signature-algorithms' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<select id="sigalg" ng-model="realm.webAuthnPolicyPasswordlessSignatureAlgorithms" class="form-control" multiple>
|
||||
<option value="ES256">ES256</option>
|
||||
<option value="ES384">ES384</option>
|
||||
<option value="ES512">ES512</option>
|
||||
<option value="RS256">RS256</option>
|
||||
<option value="RS384">RS384</option>
|
||||
<option value="RS512">RS512</option>
|
||||
<option value="RS1">RS1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-signature-algorithms.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="rpid" class="col-md-2 control-label">{{:: 'webauthn-rp-id' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<input id="rpid" type="text" ng-model="realm.webAuthnPolicyPasswordlessRpId" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-rp-id.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="attpref" class="col-md-2 control-label">{{:: 'webauthn-attestation-conveyance-preference' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<select id="attpref" ng-model="realm.webAuthnPolicyPasswordlessAttestationConveyancePreference" class="form-control">
|
||||
<option value="not specified"></option>
|
||||
<option value="none">none</option>
|
||||
<option value="indirect">indirect</option>
|
||||
<option value="direct">direct</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-attestation-conveyance-preference.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="authnatt" class="col-md-2 control-label">{{:: 'webauthn-authenticator-attachment' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<select id="authnatt" ng-model="realm.webAuthnPolicyPasswordlessAuthenticatorAttachment" class="form-control">
|
||||
<option value="not specified"></option>
|
||||
<option value="platform">platform</option>
|
||||
<option value="cross-platform">cross-platform</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-authenticator-attachment.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reqresident" class="col-md-2 control-label">{{:: 'webauthn-require-resident-key' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<select id="reqresident" ng-model="realm.webAuthnPolicyPasswordlessRequireResidentKey" class="form-control">
|
||||
<option value="not specified"></option>
|
||||
<option value="Yes">Yes</option>
|
||||
<option value="No">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-require-resident-key.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="usrverify" class="col-md-2 control-label">{{:: 'webauthn-user-verification-requirement' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<select id="usrverify" ng-model="realm.webAuthnPolicyPasswordlessUserVerificationRequirement" class="form-control">
|
||||
<option value="not specified"></option>
|
||||
<option value="required">required</option>
|
||||
<option value="preferred">preferred</option>
|
||||
<option value="discouraged">discouraged</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-user-verification-requirement.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="timeout" class="col-md-2 control-label">{{:: 'webauthn-create-timeout' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<input id="timeout" type="number" min="0" max="31536" ng-model="realm.webAuthnPolicyPasswordlessCreateTimeout" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-create-timeout.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="avoidsame" class="col-md-2 control-label">{{:: 'webauthn-avoid-same-authenticator-register' | translate}}</label>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<input id="avoidsame" ng-model="realm.webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-avoid-same-authenticator-register.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="type" class="col-md-2 control-label">{{:: 'webauthn-acceptable-aaguids' | translate}}</label>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group" ng-repeat="(i, acceptableAaguid) in realm.webAuthnPolicyPasswordlessAcceptableAaguids track by $index">
|
||||
<input class="form-control" ng-model="realm.webAuthnPolicyPasswordlessAcceptableAaguids[i]">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" data-ng-click="deleteAcceptableAaguid($index)">
|
||||
<span class="fa fa-minus"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class = "input-group">
|
||||
<input class="form-control" ng-model="newAcceptableAaguid" id="newAcceptableAaguid">
|
||||
<div class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" data-ng-click="newAcceptableAaguid.length > 0 && addAcceptableAaguid()">
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'webauthn-acceptable-aaguids.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group" data-ng-show="access.manageRealm">
|
||||
<div class="col-md-10 col-md-offset-2">
|
||||
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<kc-menu></kc-menu>
|
|
@ -4,5 +4,12 @@
|
|||
<li ng-class="{active: path[3] == 'required-actions'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/required-actions">{{:: 'required-actions' | translate}}</a></li>
|
||||
<li ng-class="{active: path[3] == 'password-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/password-policy">{{:: 'password-policy' | translate}}</a></li>
|
||||
<li ng-class="{active: path[3] == 'otp-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/otp-policy">{{:: 'otp-policy' | translate}}</a></li>
|
||||
<li ng-class="{active: path[3] == 'webauthn-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/webauthn-policy">{{:: 'webauthn-policy' | translate}}</a></li>
|
||||
<li ng-class="{active: path[3] == 'webauthn-policy'}" data-ng-show="access.viewRealm">
|
||||
<a href="#/realms/{{realm.realm}}/authentication/webauthn-policy">{{:: 'webauthn-policy' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'webauthn-policy.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
<li ng-class="{active: path[3] == 'webauthn-policy-passwordless'}" data-ng-show="access.viewRealm">
|
||||
<a href="#/realms/{{realm.realm}}/authentication/webauthn-policy-passwordless">{{:: 'webauthn-policy-passwordless' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'webauthn-policy-passwordless.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
</ul>
|
|
@ -339,5 +339,6 @@ auth-password-form=Password
|
|||
auth-username-form=Username
|
||||
auth-username-password-form=Username and password
|
||||
webauthn-authenticator=WebAuthn
|
||||
webauthn-authenticator-passwordless=WebAuthn Passwordless
|
||||
identity-provider-redirector=Connect with another Identity Provider
|
||||
|
||||
|
|
Loading…
Reference in a new issue