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 otpPolicyLookAheadWindow;
|
||||||
protected Integer otpPolicyPeriod;
|
protected Integer otpPolicyPeriod;
|
||||||
protected List<String> otpSupportedApplications;
|
protected List<String> otpSupportedApplications;
|
||||||
|
|
||||||
|
// WebAuthn 2-factor properties below
|
||||||
|
|
||||||
protected String webAuthnPolicyRpEntityName;
|
protected String webAuthnPolicyRpEntityName;
|
||||||
protected List<String> webAuthnPolicySignatureAlgorithms;
|
protected List<String> webAuthnPolicySignatureAlgorithms;
|
||||||
protected String webAuthnPolicyRpId;
|
protected String webAuthnPolicyRpId;
|
||||||
|
@ -117,6 +120,19 @@ public class RealmRepresentation {
|
||||||
protected Boolean webAuthnPolicyAvoidSameAuthenticatorRegister;
|
protected Boolean webAuthnPolicyAvoidSameAuthenticatorRegister;
|
||||||
protected List<String> webAuthnPolicyAcceptableAaguids;
|
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> users;
|
||||||
protected List<UserRepresentation> federatedUsers;
|
protected List<UserRepresentation> federatedUsers;
|
||||||
protected List<ScopeMappingRepresentation> scopeMappings;
|
protected List<ScopeMappingRepresentation> scopeMappings;
|
||||||
|
@ -926,6 +942,8 @@ public class RealmRepresentation {
|
||||||
this.otpSupportedApplications = otpSupportedApplications;
|
this.otpSupportedApplications = otpSupportedApplications;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebAuthn 2-factor properties below
|
||||||
|
|
||||||
public String getWebAuthnPolicyRpEntityName() {
|
public String getWebAuthnPolicyRpEntityName() {
|
||||||
return webAuthnPolicyRpEntityName;
|
return webAuthnPolicyRpEntityName;
|
||||||
}
|
}
|
||||||
|
@ -1006,6 +1024,89 @@ public class RealmRepresentation {
|
||||||
this.webAuthnPolicyAcceptableAaguids = webAuthnPolicyAcceptableAaguids;
|
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() {
|
public String getBrowserFlow() {
|
||||||
return browserFlow;
|
return browserFlow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -633,6 +633,18 @@ public class RealmAdapter implements CachedRealmModel {
|
||||||
updated.setWebAuthnPolicy(policy);
|
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
|
@Override
|
||||||
public RoleModel getRoleById(String id) {
|
public RoleModel getRoleById(String id) {
|
||||||
if (isUpdated()) return updated.getRoleById(id);
|
if (isUpdated()) return updated.getRoleById(id);
|
||||||
|
|
|
@ -97,6 +97,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
protected PasswordPolicy passwordPolicy;
|
protected PasswordPolicy passwordPolicy;
|
||||||
protected OTPPolicy otpPolicy;
|
protected OTPPolicy otpPolicy;
|
||||||
protected WebAuthnPolicy webAuthnPolicy;
|
protected WebAuthnPolicy webAuthnPolicy;
|
||||||
|
protected WebAuthnPolicy webAuthnPasswordlessPolicy;
|
||||||
|
|
||||||
protected String loginTheme;
|
protected String loginTheme;
|
||||||
protected String accountTheme;
|
protected String accountTheme;
|
||||||
|
@ -207,6 +208,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
passwordPolicy = model.getPasswordPolicy();
|
passwordPolicy = model.getPasswordPolicy();
|
||||||
otpPolicy = model.getOTPPolicy();
|
otpPolicy = model.getOTPPolicy();
|
||||||
webAuthnPolicy = model.getWebAuthnPolicy();
|
webAuthnPolicy = model.getWebAuthnPolicy();
|
||||||
|
webAuthnPasswordlessPolicy = model.getWebAuthnPolicyPasswordless();
|
||||||
|
|
||||||
loginTheme = model.getLoginTheme();
|
loginTheme = model.getLoginTheme();
|
||||||
accountTheme = model.getAccountTheme();
|
accountTheme = model.getAccountTheme();
|
||||||
|
@ -613,6 +615,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
return webAuthnPolicy;
|
return webAuthnPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WebAuthnPolicy getWebAuthnPasswordlessPolicy() {
|
||||||
|
return webAuthnPasswordlessPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
public AuthenticationFlowModel getBrowserFlow() {
|
public AuthenticationFlowModel getBrowserFlow() {
|
||||||
return browserFlow;
|
return browserFlow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -941,55 +941,80 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||||
em.flush();
|
em.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// WebAuthn
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WebAuthnPolicy getWebAuthnPolicy() {
|
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();
|
WebAuthnPolicy policy = new WebAuthnPolicy();
|
||||||
|
|
||||||
// mandatory parameters
|
// 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())
|
if (rpEntityName == null || rpEntityName.isEmpty())
|
||||||
rpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
rpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
|
||||||
policy.setRpEntityName(rpEntityName);
|
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())
|
if (signatureAlgorithmsString == null || signatureAlgorithmsString.isEmpty())
|
||||||
signatureAlgorithmsString = Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS;
|
signatureAlgorithmsString = Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS;
|
||||||
List<String> signatureAlgorithms = Arrays.asList(signatureAlgorithmsString.split(","));
|
List<String> signatureAlgorithms = Arrays.asList(signatureAlgorithmsString.split(","));
|
||||||
policy.setSignatureAlgorithm(signatureAlgorithms);
|
policy.setSignatureAlgorithm(signatureAlgorithms);
|
||||||
|
|
||||||
// optional parameters
|
// 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 = "";
|
if (rpId == null || rpId.isEmpty()) rpId = "";
|
||||||
policy.setRpId(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())
|
if (attestationConveyancePreference == null || attestationConveyancePreference.isEmpty())
|
||||||
attestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
attestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
policy.setAttestationConveyancePreference(attestationConveyancePreference);
|
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())
|
if (authenticatorAttachment == null || authenticatorAttachment.isEmpty())
|
||||||
authenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
authenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
policy.setAuthenticatorAttachment(authenticatorAttachment);
|
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())
|
if (requireResidentKey == null || requireResidentKey.isEmpty())
|
||||||
requireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
requireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
policy.setRequireResidentKey(requireResidentKey);
|
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())
|
if (userVerificationRequirement == null || userVerificationRequirement.isEmpty())
|
||||||
userVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
userVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
|
||||||
policy.setUserVerificationRequirement(userVerificationRequirement);
|
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));
|
if (createTime != null) policy.setCreateTimeout(Integer.parseInt(createTime));
|
||||||
else policy.setCreateTimeout(0);
|
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));
|
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<>();
|
List<String> acceptableAaguids = new ArrayList<>();
|
||||||
if (acceptableAaguidsString != null && !acceptableAaguidsString.isEmpty())
|
if (acceptableAaguidsString != null && !acceptableAaguidsString.isEmpty())
|
||||||
acceptableAaguids = Arrays.asList(acceptableAaguidsString.split(","));
|
acceptableAaguids = Arrays.asList(acceptableAaguidsString.split(","));
|
||||||
|
@ -998,44 +1023,45 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setWebAuthnPolicy(WebAuthnPolicy policy) {
|
|
||||||
|
private void setWebAuthnPolicy(WebAuthnPolicy policy, String attributePrefix) {
|
||||||
// mandatory parameters
|
// mandatory parameters
|
||||||
String rpEntityName = policy.getRpEntityName();
|
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();
|
List<String> signatureAlgorithms = policy.getSignatureAlgorithm();
|
||||||
String signatureAlgorithmsString = String.join(",", signatureAlgorithms);
|
String signatureAlgorithmsString = String.join(",", signatureAlgorithms);
|
||||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS, signatureAlgorithmsString);
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS + attributePrefix, signatureAlgorithmsString);
|
||||||
|
|
||||||
// optional parameters
|
// optional parameters
|
||||||
String rpId = policy.getRpId();
|
String rpId = policy.getRpId();
|
||||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID, rpId);
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID + attributePrefix, rpId);
|
||||||
|
|
||||||
String attestationConveyancePreference = policy.getAttestationConveyancePreference();
|
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();
|
String authenticatorAttachment = policy.getAuthenticatorAttachment();
|
||||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT, authenticatorAttachment);
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT + attributePrefix, authenticatorAttachment);
|
||||||
|
|
||||||
String requireResidentKey = policy.getRequireResidentKey();
|
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();
|
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();
|
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();
|
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();
|
List<String> acceptableAaguids = policy.getAcceptableAaguids();
|
||||||
if (acceptableAaguids != null && !acceptableAaguids.isEmpty()) {
|
if (acceptableAaguids != null && !acceptableAaguids.isEmpty()) {
|
||||||
String acceptableAaguidsString = String.join(",", acceptableAaguids);
|
String acceptableAaguidsString = String.join(",", acceptableAaguids);
|
||||||
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS, acceptableAaguidsString);
|
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix, acceptableAaguidsString);
|
||||||
} else {
|
} 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_SERIAL_NUMBER = "x509_cert_serial_number";
|
||||||
String X509_CERTIFICATE_SUBJECT_DISTINGUISHED_NAME = "x509_cert_subject_distinguished_name";
|
String X509_CERTIFICATE_SUBJECT_DISTINGUISHED_NAME = "x509_cert_subject_distinguished_name";
|
||||||
String X509_CERTIFICATE_ISSUER_DISTINGUISHED_NAME = "x509_cert_issuer_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
|
// it stands for optional parameter not specified in WebAuthn
|
||||||
public static final String DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED = "not specified";
|
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_KEY = "VERIFY_EMAIL_KEY";
|
||||||
public static final String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
|
public static final String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
|
||||||
public static final String EXECUTION = "execution";
|
public static final String EXECUTION = "execution";
|
||||||
|
|
|
@ -343,6 +343,7 @@ public class ModelToRepresentation {
|
||||||
rep.setOtpPolicyType(otpPolicy.getType());
|
rep.setOtpPolicyType(otpPolicy.getType());
|
||||||
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
|
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
|
||||||
rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
|
rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
|
||||||
|
|
||||||
WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy();
|
WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy();
|
||||||
rep.setWebAuthnPolicyRpEntityName(webAuthnPolicy.getRpEntityName());
|
rep.setWebAuthnPolicyRpEntityName(webAuthnPolicy.getRpEntityName());
|
||||||
rep.setWebAuthnPolicySignatureAlgorithms(webAuthnPolicy.getSignatureAlgorithm());
|
rep.setWebAuthnPolicySignatureAlgorithms(webAuthnPolicy.getSignatureAlgorithm());
|
||||||
|
@ -354,6 +355,19 @@ public class ModelToRepresentation {
|
||||||
rep.setWebAuthnPolicyCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
rep.setWebAuthnPolicyCreateTimeout(webAuthnPolicy.getCreateTimeout());
|
||||||
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
|
||||||
rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
|
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.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
|
||||||
if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
|
if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
|
||||||
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().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));
|
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
|
||||||
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
|
||||||
|
|
||||||
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
|
WebAuthnPolicy webAuthnPolicy = getWebAuthnPolicyTwoFactor(rep);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
newRealm.setWebAuthnPolicy(webAuthnPolicy);
|
newRealm.setWebAuthnPolicy(webAuthnPolicy);
|
||||||
|
|
||||||
|
webAuthnPolicy = getWebAuthnPolicyPasswordless(rep);
|
||||||
|
newRealm.setWebAuthnPolicyPasswordless(webAuthnPolicy);
|
||||||
|
|
||||||
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
|
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
|
||||||
if (rep.getRequiredActions() != null) {
|
if (rep.getRequiredActions() != null) {
|
||||||
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
|
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) {
|
public static void importUserFederationProvidersAndMappers(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm) {
|
||||||
// providers to convert to component model
|
// providers to convert to component model
|
||||||
Set<String> convertSet = new HashSet<>();
|
Set<String> convertSet = new HashSet<>();
|
||||||
|
@ -1042,55 +1103,12 @@ public class RepresentationToModel {
|
||||||
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
|
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
|
WebAuthnPolicy webAuthnPolicy = getWebAuthnPolicyTwoFactor(rep);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
realm.setWebAuthnPolicy(webAuthnPolicy);
|
realm.setWebAuthnPolicy(webAuthnPolicy);
|
||||||
|
|
||||||
|
webAuthnPolicy = getWebAuthnPolicyPasswordless(rep);
|
||||||
|
realm.setWebAuthnPolicyPasswordless(webAuthnPolicy);
|
||||||
|
|
||||||
if (rep.getSmtpServer() != null) {
|
if (rep.getSmtpServer() != null) {
|
||||||
Map<String, String> config = new HashMap(rep.getSmtpServer());
|
Map<String, String> config = new HashMap(rep.getSmtpServer());
|
||||||
if (rep.getSmtpServer().containsKey("password") && ComponentRepresentation.SECRET_VALUE.equals(rep.getSmtpServer().get("password"))) {
|
if (rep.getSmtpServer().containsKey("password") && ComponentRepresentation.SECRET_VALUE.equals(rep.getSmtpServer().get("password"))) {
|
||||||
|
|
|
@ -240,9 +240,30 @@ public interface RealmModel extends RoleContainerModel {
|
||||||
OTPPolicy getOTPPolicy();
|
OTPPolicy getOTPPolicy();
|
||||||
void setOTPPolicy(OTPPolicy policy);
|
void setOTPPolicy(OTPPolicy policy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return WebAuthn policy for 2-factor authentication
|
||||||
|
*/
|
||||||
WebAuthnPolicy getWebAuthnPolicy();
|
WebAuthnPolicy getWebAuthnPolicy();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set WebAuthn policy for 2-factor authentication
|
||||||
|
*
|
||||||
|
* @param policy
|
||||||
|
*/
|
||||||
void setWebAuthnPolicy(WebAuthnPolicy 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);
|
RoleModel getRoleById(String id);
|
||||||
|
|
||||||
List<GroupModel> getDefaultGroups();
|
List<GroupModel> getDefaultGroups();
|
||||||
|
|
|
@ -31,22 +31,28 @@ import org.keycloak.util.JsonSerialization;
|
||||||
*/
|
*/
|
||||||
public class WebAuthnCredentialModel extends CredentialModel {
|
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 WebAuthnCredentialData credentialData;
|
||||||
private final WebAuthnSecretData secretData;
|
private final WebAuthnSecretData secretData;
|
||||||
|
|
||||||
private WebAuthnCredentialModel(WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
|
private WebAuthnCredentialModel(String credentialType, WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
|
||||||
this.credentialData = credentialData;
|
this.credentialData = credentialData;
|
||||||
this.secretData = secretData;
|
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) {
|
String attestationStatement, String credentialPublicKey, long counter) {
|
||||||
WebAuthnCredentialData credentialData = new WebAuthnCredentialData(aaguid, credentialId, counter, attestationStatement, credentialPublicKey);
|
WebAuthnCredentialData credentialData = new WebAuthnCredentialData(aaguid, credentialId, counter, attestationStatement, credentialPublicKey);
|
||||||
WebAuthnSecretData secretData = new WebAuthnSecretData();
|
WebAuthnSecretData secretData = new WebAuthnSecretData();
|
||||||
|
|
||||||
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialData, secretData);
|
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialType, credentialData, secretData);
|
||||||
credentialModel.fillCredentialModelFields();
|
credentialModel.fillCredentialModelFields();
|
||||||
credentialModel.setUserLabel(userLabel);
|
credentialModel.setUserLabel(userLabel);
|
||||||
return credentialModel;
|
return credentialModel;
|
||||||
|
@ -58,10 +64,10 @@ public class WebAuthnCredentialModel extends CredentialModel {
|
||||||
WebAuthnCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), WebAuthnCredentialData.class);
|
WebAuthnCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), WebAuthnCredentialData.class);
|
||||||
WebAuthnSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), WebAuthnSecretData.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.setUserLabel(credentialModel.getUserLabel());
|
||||||
webAuthnCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
|
webAuthnCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
|
||||||
webAuthnCredentialModel.setType(TYPE);
|
webAuthnCredentialModel.setType(credentialModel.getType());
|
||||||
webAuthnCredentialModel.setId(credentialModel.getId());
|
webAuthnCredentialModel.setId(credentialModel.getId());
|
||||||
webAuthnCredentialModel.setSecretData(credentialModel.getSecretData());
|
webAuthnCredentialModel.setSecretData(credentialModel.getSecretData());
|
||||||
webAuthnCredentialModel.setCredentialData(credentialModel.getCredentialData());
|
webAuthnCredentialModel.setCredentialData(credentialModel.getCredentialData());
|
||||||
|
@ -95,7 +101,6 @@ public class WebAuthnCredentialModel extends CredentialModel {
|
||||||
try {
|
try {
|
||||||
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
|
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
|
||||||
setSecretData(JsonSerialization.writeValueAsString(secretData));
|
setSecretData(JsonSerialization.writeValueAsString(secretData));
|
||||||
setType(TYPE);
|
|
||||||
setCreatedDate(Time.currentTimeMillis());
|
setCreatedDate(Time.currentTimeMillis());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
@ -106,7 +111,8 @@ public class WebAuthnCredentialModel extends CredentialModel {
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "WebAuthnCredentialModel { " +
|
return "WebAuthnCredentialModel { " +
|
||||||
credentialData +
|
getType() +
|
||||||
|
", " + credentialData +
|
||||||
", " + secretData +
|
", " + secretData +
|
||||||
" }";
|
" }";
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,16 +35,17 @@ import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.common.util.UriUtils;
|
import org.keycloak.common.util.UriUtils;
|
||||||
import org.keycloak.credential.CredentialProvider;
|
import org.keycloak.credential.CredentialProvider;
|
||||||
import org.keycloak.credential.OTPCredentialProvider;
|
|
||||||
import org.keycloak.credential.WebAuthnCredentialModelInput;
|
import org.keycloak.credential.WebAuthnCredentialModelInput;
|
||||||
import org.keycloak.credential.WebAuthnCredentialProvider;
|
import org.keycloak.credential.WebAuthnCredentialProvider;
|
||||||
import org.keycloak.credential.WebAuthnCredentialProviderFactory;
|
import org.keycloak.credential.WebAuthnCredentialProviderFactory;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.forms.login.freemarker.model.WebAuthnAuthenticatorsBean;
|
import org.keycloak.forms.login.freemarker.model.WebAuthnAuthenticatorsBean;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.WebAuthnPolicy;
|
||||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
@ -52,6 +53,9 @@ import javax.ws.rs.core.Response;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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> {
|
public class WebAuthnAuthenticator implements Authenticator, CredentialValidator<WebAuthnCredentialProvider> {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(WebAuthnAuthenticator.class);
|
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);
|
context.getAuthenticationSession().setAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE, challengeValue);
|
||||||
form.setAttribute(WebAuthnConstants.CHALLENGE, 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();
|
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
|
||||||
form.setAttribute(WebAuthnConstants.RP_ID, rpId);
|
form.setAttribute(WebAuthnConstants.RP_ID, rpId);
|
||||||
|
|
||||||
|
@ -77,7 +82,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
||||||
boolean isUserIdentified = false;
|
boolean isUserIdentified = false;
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
// in 2 Factor Scenario where the user has already been identified
|
// 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()) {
|
if (authenticators.getAuthenticators().isEmpty()) {
|
||||||
// require the user to register webauthn authenticator
|
// require the user to register webauthn authenticator
|
||||||
return;
|
return;
|
||||||
|
@ -91,15 +96,26 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
||||||
form.setAttribute(WebAuthnConstants.IS_USER_IDENTIFIED, Boolean.toString(isUserIdentified));
|
form.setAttribute(WebAuthnConstants.IS_USER_IDENTIFIED, Boolean.toString(isUserIdentified));
|
||||||
|
|
||||||
// read options from policy
|
// read options from policy
|
||||||
String userVerificationRequirement = context.getRealm().getWebAuthnPolicy().getUserVerificationRequirement();
|
String userVerificationRequirement = policy.getUserVerificationRequirement();
|
||||||
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
|
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
|
||||||
|
|
||||||
context.challenge(form.createLoginWebAuthn());
|
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) {
|
public void action(AuthenticationFlowContext context) {
|
||||||
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
|
||||||
|
context.getEvent().detail(Details.CREDENTIAL_TYPE, getCredentialType());
|
||||||
|
|
||||||
// receive error from navigator.credentials.get()
|
// receive error from navigator.credentials.get()
|
||||||
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
|
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
|
||||||
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
|
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
|
||||||
|
@ -121,7 +137,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
||||||
|
|
||||||
String userId = params.getFirst(WebAuthnConstants.USER_HANDLE);
|
String userId = params.getFirst(WebAuthnConstants.USER_HANDLE);
|
||||||
boolean isUVFlagChecked = false;
|
boolean isUVFlagChecked = false;
|
||||||
String userVerificationRequirement = context.getRealm().getWebAuthnPolicy().getUserVerificationRequirement();
|
String userVerificationRequirement = getWebAuthnPolicy(context).getUserVerificationRequirement();
|
||||||
if (WebAuthnConstants.OPTION_REQUIRED.equals(userVerificationRequirement)) isUVFlagChecked = true;
|
if (WebAuthnConstants.OPTION_REQUIRED.equals(userVerificationRequirement)) isUVFlagChecked = true;
|
||||||
|
|
||||||
// existing User Handle means that the authenticator used Resident Key supported public key credential
|
// 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
|
isUVFlagChecked
|
||||||
);
|
);
|
||||||
|
|
||||||
WebAuthnCredentialModelInput cred = new WebAuthnCredentialModelInput();
|
WebAuthnCredentialModelInput cred = new WebAuthnCredentialModelInput(getCredentialType());
|
||||||
cred.setAuthenticationContext(authenticationContext);
|
cred.setAuthenticationContext(authenticationContext);
|
||||||
|
|
||||||
boolean result = false;
|
boolean result = false;
|
||||||
|
@ -171,7 +187,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
|
||||||
String encodedCredentialID = Base64Url.encode(credentialId);
|
String encodedCredentialID = Base64Url.encode(credentialId);
|
||||||
if (result) {
|
if (result) {
|
||||||
String isUVChecked = Boolean.toString(isUVFlagChecked);
|
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.setUser(user);
|
||||||
context.getEvent()
|
context.getEvent()
|
||||||
.detail("web_authn_authenticator_user_verification_checked", isUVChecked)
|
.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) {
|
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) {
|
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_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_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_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_VERIFICATION_FAIL = "WebAuthn 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_AUTHENTICATED_USER_NOT_FOUND = "Unknown user authenticated by the WebAuthn Authenticator";
|
||||||
|
|
||||||
private void setErrorResponse(AuthenticationFlowContext context, final String errorCase, final String errorMessage) {
|
private void setErrorResponse(AuthenticationFlowContext context, final String errorCase, final String errorMessage) {
|
||||||
Response errorResponse = null;
|
Response errorResponse = null;
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getHelpText() {
|
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.WebAuthnCredentialProvider;
|
||||||
import org.keycloak.credential.WebAuthnCredentialProviderFactory;
|
import org.keycloak.credential.WebAuthnCredentialProviderFactory;
|
||||||
import org.keycloak.crypto.Algorithm;
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.UserModel;
|
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 com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
|
||||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required action for register WebAuthn 2-factor credential for the user
|
||||||
|
*/
|
||||||
public class WebAuthnRegister implements RequiredActionProvider, CredentialRegistrator {
|
public class WebAuthnRegister implements RequiredActionProvider, CredentialRegistrator {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(WebAuthnRegister.class);
|
private static final Logger logger = Logger.getLogger(WebAuthnRegister.class);
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
private CertPathTrustworthinessValidator certPathtrustValidator;
|
private CertPathTrustworthinessValidator certPathtrustValidator;
|
||||||
|
|
||||||
public WebAuthnRegister(KeycloakSession session) {
|
|
||||||
this.session = session;
|
|
||||||
this.certPathtrustValidator = new NullCertPathTrustworthinessValidator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public WebAuthnRegister(KeycloakSession session, CertPathTrustworthinessValidator certPathtrustValidator) {
|
public WebAuthnRegister(KeycloakSession session, CertPathTrustworthinessValidator certPathtrustValidator) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.certPathtrustValidator = certPathtrustValidator;
|
this.certPathtrustValidator = certPathtrustValidator;
|
||||||
|
@ -95,7 +94,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
||||||
// construct parameters for calling WebAuthn API navigator.credential.create()
|
// construct parameters for calling WebAuthn API navigator.credential.create()
|
||||||
|
|
||||||
// mandatory
|
// mandatory
|
||||||
WebAuthnPolicy policy = context.getRealm().getWebAuthnPolicy();
|
WebAuthnPolicy policy = getWebAuthnPolicy(context);
|
||||||
List<String> signatureAlgorithmsList = policy.getSignatureAlgorithm();
|
List<String> signatureAlgorithmsList = policy.getSignatureAlgorithm();
|
||||||
String signatureAlgorithms = stringifySignatureAlgorithms(signatureAlgorithmsList);
|
String signatureAlgorithms = stringifySignatureAlgorithms(signatureAlgorithmsList);
|
||||||
String rpEntityName = policy.getRpEntityName();
|
String rpEntityName = policy.getRpEntityName();
|
||||||
|
@ -112,7 +111,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
||||||
|
|
||||||
String excludeCredentialIds = "";
|
String excludeCredentialIds = "";
|
||||||
if (avoidSameAuthenticatorRegister) {
|
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 -> {
|
List<String> webAuthnCredentialPubKeyIds = webAuthnCredentials.stream().map(credentialModel -> {
|
||||||
|
|
||||||
WebAuthnCredentialModel credModel = WebAuthnCredentialModel.createFromCredentialModel(credentialModel);
|
WebAuthnCredentialModel credModel = WebAuthnCredentialModel.createFromCredentialModel(credentialModel);
|
||||||
|
@ -140,11 +139,25 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
||||||
context.challenge(form);
|
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
|
@Override
|
||||||
public void processAction(RequiredActionContext context) {
|
public void processAction(RequiredActionContext context) {
|
||||||
|
|
||||||
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
|
||||||
|
context.getEvent().detail(Details.CREDENTIAL_TYPE, getCredentialType());
|
||||||
|
|
||||||
// receive error from navigator.credentials.create()
|
// receive error from navigator.credentials.create()
|
||||||
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
|
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
|
||||||
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
|
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
|
||||||
|
@ -152,7 +165,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
WebAuthnPolicy policy = context.getRealm().getWebAuthnPolicy();
|
WebAuthnPolicy policy = getWebAuthnPolicy(context);
|
||||||
String rpId = policy.getRpId();
|
String rpId = policy.getRpId();
|
||||||
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
|
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
|
||||||
String label = params.getFirst(WebAuthnConstants.AUTHENTICATOR_LABEL);
|
String label = params.getFirst(WebAuthnConstants.AUTHENTICATOR_LABEL);
|
||||||
|
@ -176,20 +189,20 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
|
||||||
|
|
||||||
checkAcceptedAuthenticator(response, policy);
|
checkAcceptedAuthenticator(response, policy);
|
||||||
|
|
||||||
WebAuthnCredentialModelInput credential = new WebAuthnCredentialModelInput();
|
WebAuthnCredentialModelInput credential = new WebAuthnCredentialModelInput(getCredentialType());
|
||||||
|
|
||||||
credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
|
credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
|
||||||
credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
|
credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
|
||||||
|
|
||||||
// Save new webAuthn credential
|
// 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);
|
WebAuthnCredentialModel newCredentialModel = webAuthnCredProvider.getCredentialModelFromCredentialInput(credential, label);
|
||||||
|
|
||||||
webAuthnCredProvider.createCredential(context.getRealm(), context.getUser(), newCredentialModel);
|
webAuthnCredProvider.createCredential(context.getRealm(), context.getUser(), newCredentialModel);
|
||||||
|
|
||||||
String aaguid = newCredentialModel.getWebAuthnCredentialData().getAaguid();
|
String aaguid = newCredentialModel.getWebAuthnCredentialData().getAaguid();
|
||||||
logger.debugv("WebAuthn credential registration success for user {0}. publicKeyCredentialId = {1}, publicKeyCredentialLabel = {2}, publicKeyCredentialAAGUID = {3}",
|
logger.debugv("WebAuthn credential registration success for user {0}. credentialType = {1}, publicKeyCredentialId = {2}, publicKeyCredentialLabel = {3}, publicKeyCredentialAAGUID = {4}",
|
||||||
context.getUser().getUsername(), publicKeyCredentialId, label, aaguid);
|
context.getUser().getUsername(), getCredentialType(), publicKeyCredentialId, label, aaguid);
|
||||||
webAuthnCredProvider.dumpCredentialModel(newCredentialModel, credential);
|
webAuthnCredProvider.dumpCredentialModel(newCredentialModel, credential);
|
||||||
|
|
||||||
context.getEvent()
|
context.getEvent()
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.keycloak.authentication.requiredactions;
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
|
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.Config.Scope;
|
import org.keycloak.Config.Scope;
|
||||||
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
||||||
|
@ -39,17 +40,21 @@ public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTy
|
||||||
WebAuthnRegister webAuthnRegister = null;
|
WebAuthnRegister webAuthnRegister = null;
|
||||||
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
|
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
|
||||||
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
|
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
|
||||||
webAuthnRegister = new WebAuthnRegister(session, new NullCertPathTrustworthinessValidator());
|
webAuthnRegister = createProvider(session, new NullCertPathTrustworthinessValidator());
|
||||||
} else {
|
} else {
|
||||||
KeyStoreTrustAnchorsProvider trustAnchorsProvider = new KeyStoreTrustAnchorsProvider();
|
KeyStoreTrustAnchorsProvider trustAnchorsProvider = new KeyStoreTrustAnchorsProvider();
|
||||||
trustAnchorsProvider.setKeyStore(truststoreProvider.getTruststore());
|
trustAnchorsProvider.setKeyStore(truststoreProvider.getTruststore());
|
||||||
TrustAnchorsResolverImpl resolverImpl = new TrustAnchorsResolverImpl(trustAnchorsProvider);
|
TrustAnchorsResolverImpl resolverImpl = new TrustAnchorsResolverImpl(trustAnchorsProvider);
|
||||||
TrustAnchorCertPathTrustworthinessValidator trustValidator = new TrustAnchorCertPathTrustworthinessValidator(resolverImpl);
|
TrustAnchorCertPathTrustworthinessValidator trustValidator = new TrustAnchorCertPathTrustworthinessValidator(resolverImpl);
|
||||||
webAuthnRegister = new WebAuthnRegister(session, trustValidator);
|
webAuthnRegister = createProvider(session, trustValidator);
|
||||||
}
|
}
|
||||||
return webAuthnRegister;
|
return webAuthnRegister;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected WebAuthnRegister createProvider(KeycloakSession session, CertPathTrustworthinessValidator trustValidator) {
|
||||||
|
return new WebAuthnRegister(session, trustValidator);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Scope config) {
|
public void init(Scope config) {
|
||||||
// NOP
|
// NOP
|
||||||
|
|
|
@ -26,13 +26,16 @@ import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
|
|
||||||
public class WebAuthnCredentialModelInput implements CredentialInput {
|
public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||||
|
|
||||||
public static final String WEBAUTHN_CREDENTIAL_TYPE = WebAuthnCredentialModel.TYPE;
|
|
||||||
|
|
||||||
private AttestedCredentialData attestedCredentialData;
|
private AttestedCredentialData attestedCredentialData;
|
||||||
private AttestationStatement attestationStatement;
|
private AttestationStatement attestationStatement;
|
||||||
private WebAuthnAuthenticationContext authenticationContext;
|
private WebAuthnAuthenticationContext authenticationContext;
|
||||||
private long count;
|
private long count;
|
||||||
private String credentialDBId;
|
private String credentialDBId;
|
||||||
|
private final String credentialType;
|
||||||
|
|
||||||
|
public WebAuthnCredentialModelInput(String credentialType) {
|
||||||
|
this.credentialType = credentialType;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCredentialId() {
|
public String getCredentialId() {
|
||||||
|
@ -46,12 +49,9 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return WEBAUTHN_CREDENTIAL_TYPE;
|
return credentialType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebAuthnCredentialModelInput() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public AttestedCredentialData getAttestedCredentialData() {
|
public AttestedCredentialData getAttestedCredentialData() {
|
||||||
return attestedCredentialData;
|
return attestedCredentialData;
|
||||||
|
@ -93,8 +93,12 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
|
||||||
this.credentialDBId = credentialDBId;
|
this.credentialDBId = credentialDBId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCredentialType() {
|
||||||
|
return credentialType;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder("Credential Type = " + credentialType + ",");
|
||||||
if (credentialDBId != null)
|
if (credentialDBId != null)
|
||||||
sb.append("Credential DB Id = ")
|
sb.append("Credential DB Id = ")
|
||||||
.append(credentialDBId)
|
.append(credentialDBId)
|
||||||
|
|
|
@ -40,6 +40,9 @@ import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
|
||||||
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
||||||
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credential provider for WebAuthn 2-factor credential of the user
|
||||||
|
*/
|
||||||
public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCredentialModel>, CredentialInputValidator {
|
public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCredentialModel>, CredentialInputValidator {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class);
|
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());
|
String credentialPublicKey = credentialPublicKeyConverter.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCOSEKey());
|
||||||
long counter = webAuthnModel.getCount();
|
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());
|
model.setId(webAuthnModel.getCredentialDBId());
|
||||||
|
|
||||||
|
@ -114,7 +117,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
||||||
|
|
||||||
WebAuthnCredentialData credData = webAuthnCredential.getWebAuthnCredentialData();
|
WebAuthnCredentialData credData = webAuthnCredential.getWebAuthnCredentialData();
|
||||||
|
|
||||||
WebAuthnCredentialModelInput auth = new WebAuthnCredentialModelInput();
|
WebAuthnCredentialModelInput auth = new WebAuthnCredentialModelInput(getType());
|
||||||
|
|
||||||
byte[] credentialId = null;
|
byte[] credentialId = null;
|
||||||
try {
|
try {
|
||||||
|
@ -142,7 +145,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsCredentialType(String credentialType) {
|
public boolean supportsCredentialType(String credentialType) {
|
||||||
return WebAuthnCredentialModelInput.WEBAUTHN_CREDENTIAL_TYPE.equals(credentialType);
|
return getType().equals(credentialType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -204,12 +207,12 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return WebAuthnCredentialModel.TYPE;
|
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private List<WebAuthnCredentialModelInput> getWebAuthnCredentialModelList(RealmModel realm, UserModel user) {
|
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()
|
return credentialModels.stream()
|
||||||
.map(this::getCredentialInputFromCredentialModel)
|
.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 {
|
public class WebAuthnAuthenticatorsBean {
|
||||||
private List<WebAuthnAuthenticatorBean> authenticators = new LinkedList<WebAuthnAuthenticatorBean>();
|
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.
|
// 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);
|
WebAuthnCredentialModel webAuthnCredential = WebAuthnCredentialModel.createFromCredentialModel(credential);
|
||||||
|
|
||||||
String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId());
|
String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId());
|
||||||
|
|
|
@ -48,3 +48,4 @@ org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFacto
|
||||||
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
|
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
|
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory
|
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory
|
||||||
|
org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory
|
|
@ -21,3 +21,4 @@ org.keycloak.authentication.requiredactions.UpdateTotp
|
||||||
org.keycloak.authentication.requiredactions.VerifyEmail
|
org.keycloak.authentication.requiredactions.VerifyEmail
|
||||||
org.keycloak.authentication.requiredactions.TermsAndConditions
|
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.OTPCredentialProviderFactory
|
||||||
org.keycloak.credential.PasswordCredentialProviderFactory
|
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;
|
package org.keycloak.testsuite.pages.webauthn;
|
||||||
|
|
||||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
|
||||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
|
||||||
import org.openqa.selenium.By;
|
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 {
|
public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
|
||||||
|
|
||||||
@ArquillianResource
|
// After click the button, the "navigator.credentials.get" will be called on the browser side, which should automatically
|
||||||
protected OAuthClient oauth;
|
// login user with the chrome testing API
|
||||||
|
public void confirmWebAuthnLogin() {
|
||||||
@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();
|
|
||||||
|
|
||||||
driver.findElement(By.cssSelector("input[type=\"button\"]")).click();
|
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() {
|
public boolean isCurrent() {
|
||||||
String realm = "test";
|
return driver.getPageSource().contains("navigator.credentials.get");
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void open() {
|
public void open() {
|
||||||
oauth.openLoginForm();
|
throw new UnsupportedOperationException();
|
||||||
assertCurrent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,169 +19,37 @@ package org.keycloak.testsuite.pages.webauthn;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.keycloak.testsuite.pages.AbstractPage;
|
import org.keycloak.testsuite.pages.AbstractPage;
|
||||||
import org.keycloak.testsuite.pages.PageUtils;
|
|
||||||
import org.openqa.selenium.Alert;
|
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.ExpectedConditions;
|
||||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
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 {
|
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()
|
// label edit after registering authenicator by .create()
|
||||||
WebDriverWait wait = new WebDriverWait(driver, 60);
|
WebDriverWait wait = new WebDriverWait(driver, 60);
|
||||||
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
|
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.sendKeys(authenticatorLabel);
|
||||||
promptDialog.accept();
|
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() {
|
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
|
@Override
|
||||||
public void open() {
|
public void open() {
|
||||||
throw new UnsupportedOperationException();
|
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");
|
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
||||||
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
|
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
|
||||||
"Testsuite Username authenticator. Username parameter sets username");
|
"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",
|
addProviderInfo(result, "auth-username-form", "Username Form",
|
||||||
"Selects a user from his username.");
|
"Selects a user from his username.");
|
||||||
|
|
|
@ -77,9 +77,9 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
|
||||||
public void testCRUDRequiredAction() {
|
public void testCRUDRequiredAction() {
|
||||||
int lastPriority = authMgmtResource.getRequiredActions().get(authMgmtResource.getRequiredActions().size() - 1).getPriority();
|
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();
|
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
|
||||||
Assert.assertEquals(2, result.size());
|
Assert.assertEquals(3, result.size());
|
||||||
RequiredActionProviderSimpleRepresentation action = result.get(0);
|
RequiredActionProviderSimpleRepresentation action = result.get(0);
|
||||||
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
|
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
|
||||||
Assert.assertEquals("Dummy Action", action.getName());
|
Assert.assertEquals("Dummy Action", action.getName());
|
||||||
|
|
|
@ -23,15 +23,21 @@ import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.WebAuthnConstants;
|
import org.keycloak.WebAuthnConstants;
|
||||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||||
|
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
|
||||||
import org.keycloak.common.util.RandomString;
|
import org.keycloak.common.util.RandomString;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventType;
|
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.EventRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
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.WebAuthnLoginPage;
|
||||||
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
|
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
|
||||||
import org.keycloak.testsuite.WebAuthnAssume;
|
import org.keycloak.testsuite.WebAuthnAssume;
|
||||||
|
@ -55,10 +61,16 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
|
||||||
protected AppPage appPage;
|
protected AppPage appPage;
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
protected WebAuthnLoginPage loginPage;
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
@Page
|
@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";
|
private static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
@ -115,7 +127,10 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
|
||||||
registerPage.assertCurrent();
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
String authenticatorLabel = RandomString.randomCode(24);
|
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();
|
appPage.assertCurrent();
|
||||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
@ -153,6 +168,10 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
loginPage.login(username, password);
|
loginPage.login(username, password);
|
||||||
|
|
||||||
|
// Confirm login on the WebAuthn login page
|
||||||
|
webAuthnLoginPage.assertCurrent();
|
||||||
|
webAuthnLoginPage.confirmWebAuthnLogin();
|
||||||
|
|
||||||
appPage.assertCurrent();
|
appPage.assertCurrent();
|
||||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
appPage.openAccount();
|
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) {
|
private void assertUserRegistered(String userId, String username, String email) {
|
||||||
UserRepresentation user = getUser(userId);
|
UserRepresentation user = getUser(userId);
|
||||||
Assert.assertNotNull(user);
|
Assert.assertNotNull(user);
|
||||||
Assert.assertNotNull(user.getCreatedTimestamp());
|
Assert.assertNotNull(user.getCreatedTimestamp());
|
||||||
// test that timestamp is current with 10s tollerance
|
// test that timestamp is current with 60s tollerance
|
||||||
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
|
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 60000);
|
||||||
// test user info is set from form
|
// test user info is set from form
|
||||||
assertEquals(username.toLowerCase(), user.getUsername());
|
assertEquals(username.toLowerCase(), user.getUsername());
|
||||||
assertEquals(email.toLowerCase(), user.getEmail());
|
assertEquals(email.toLowerCase(), user.getEmail());
|
||||||
|
|
|
@ -26,17 +26,6 @@
|
||||||
"00000000-0000-0000-0000-000000000000",
|
"00000000-0000-0000-0000-000000000000",
|
||||||
"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73"
|
"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": {
|
"smtpServer": {
|
||||||
"from": "auto@keycloak.org",
|
"from": "auto@keycloak.org",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
|
@ -574,7 +563,7 @@
|
||||||
|
|
||||||
"authenticationFlows": [
|
"authenticationFlows": [
|
||||||
{
|
{
|
||||||
"alias": "Copy of browser",
|
"alias": "browser-webauthn",
|
||||||
"description": "browser based authentication",
|
"description": "browser based authentication",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": true,
|
"topLevel": true,
|
||||||
|
@ -604,14 +593,14 @@
|
||||||
{
|
{
|
||||||
"requirement": "ALTERNATIVE",
|
"requirement": "ALTERNATIVE",
|
||||||
"priority": 30,
|
"priority": 30,
|
||||||
"flowAlias": "Copy of browser forms",
|
"flowAlias": "browser-webauthn-forms",
|
||||||
"userSetupAllowed": false,
|
"userSetupAllowed": false,
|
||||||
"autheticatorFlow": true
|
"autheticatorFlow": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"alias": "Copy of browser forms",
|
"alias": "browser-webauthn-forms",
|
||||||
"description": "Username, password, otp and other auth forms.",
|
"description": "Username, password, otp and other auth forms.",
|
||||||
"providerId": "basic-flow",
|
"providerId": "basic-flow",
|
||||||
"topLevel": false,
|
"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",
|
"alias": "Handle Existing Account",
|
||||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||||
|
@ -1083,9 +1125,18 @@
|
||||||
"defaultAction": true,
|
"defaultAction": true,
|
||||||
"priority": 51,
|
"priority": 51,
|
||||||
"config": {}
|
"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,
|
"internationalizationEnabled": true,
|
||||||
"supportedLocales": ["en", "de"],
|
"supportedLocales": ["en", "de"],
|
||||||
|
|
|
@ -1063,6 +1063,9 @@ add-policy.placeholder=Add policy...
|
||||||
policy-type=Policy Type
|
policy-type=Policy Type
|
||||||
policy-value=Policy Value
|
policy-value=Policy Value
|
||||||
webauthn-policy=WebAuthn Policy
|
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=Relying Party Entity Name
|
||||||
webauthn-rp-entity-name.tooltip=Human-readable server name as WebAuthn Relying Party
|
webauthn-rp-entity-name.tooltip=Human-readable server name as WebAuthn Relying Party
|
||||||
webauthn-signature-algorithms=Signature Algorithms
|
webauthn-signature-algorithms=Signature Algorithms
|
||||||
|
|
|
@ -2017,6 +2017,18 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'RealmWebAuthnPolicyCtrl'
|
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', {
|
.when('/realms/:realm/authentication/flows/:flow/config/:provider/:config', {
|
||||||
templateUrl : resourceUrl + '/partials/authenticator-config.html',
|
templateUrl : resourceUrl + '/partials/authenticator-config.html',
|
||||||
resolve : {
|
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");
|
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) {
|
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");
|
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] == '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] == '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] == '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>
|
</ul>
|
|
@ -339,5 +339,6 @@ auth-password-form=Password
|
||||||
auth-username-form=Username
|
auth-username-form=Username
|
||||||
auth-username-password-form=Username and password
|
auth-username-password-form=Username and password
|
||||||
webauthn-authenticator=WebAuthn
|
webauthn-authenticator=WebAuthn
|
||||||
|
webauthn-authenticator-passwordless=WebAuthn Passwordless
|
||||||
identity-provider-redirector=Connect with another Identity Provider
|
identity-provider-redirector=Connect with another Identity Provider
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue