KEYCLOAK-12174 WebAuthn: create authenticator, requiredAction and policy for passwordless (#6649)

This commit is contained in:
Marek Posolda 2020-01-29 09:33:45 +01:00 committed by GitHub
parent 1a53110bb6
commit d46620569a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1152 additions and 491 deletions

View file

@ -106,6 +106,9 @@ public class RealmRepresentation {
protected Integer otpPolicyLookAheadWindow;
protected Integer otpPolicyPeriod;
protected List<String> otpSupportedApplications;
// WebAuthn 2-factor properties below
protected String webAuthnPolicyRpEntityName;
protected List<String> webAuthnPolicySignatureAlgorithms;
protected String webAuthnPolicyRpId;
@ -117,6 +120,19 @@ public class RealmRepresentation {
protected Boolean webAuthnPolicyAvoidSameAuthenticatorRegister;
protected List<String> webAuthnPolicyAcceptableAaguids;
// WebAuthn passwordless properties below
protected String webAuthnPolicyPasswordlessRpEntityName;
protected List<String> webAuthnPolicyPasswordlessSignatureAlgorithms;
protected String webAuthnPolicyPasswordlessRpId;
protected String webAuthnPolicyPasswordlessAttestationConveyancePreference;
protected String webAuthnPolicyPasswordlessAuthenticatorAttachment;
protected String webAuthnPolicyPasswordlessRequireResidentKey;
protected String webAuthnPolicyPasswordlessUserVerificationRequirement;
protected Integer webAuthnPolicyPasswordlessCreateTimeout;
protected Boolean webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister;
protected List<String> webAuthnPolicyPasswordlessAcceptableAaguids;
protected List<UserRepresentation> users;
protected List<UserRepresentation> federatedUsers;
protected List<ScopeMappingRepresentation> scopeMappings;
@ -926,6 +942,8 @@ public class RealmRepresentation {
this.otpSupportedApplications = otpSupportedApplications;
}
// WebAuthn 2-factor properties below
public String getWebAuthnPolicyRpEntityName() {
return webAuthnPolicyRpEntityName;
}
@ -1006,6 +1024,89 @@ public class RealmRepresentation {
this.webAuthnPolicyAcceptableAaguids = webAuthnPolicyAcceptableAaguids;
}
// WebAuthn passwordless properties below
public String getWebAuthnPolicyPasswordlessRpEntityName() {
return webAuthnPolicyPasswordlessRpEntityName;
}
public void setWebAuthnPolicyPasswordlessRpEntityName(String webAuthnPolicyPasswordlessRpEntityName) {
this.webAuthnPolicyPasswordlessRpEntityName = webAuthnPolicyPasswordlessRpEntityName;
}
public List<String> getWebAuthnPolicyPasswordlessSignatureAlgorithms() {
return webAuthnPolicyPasswordlessSignatureAlgorithms;
}
public void setWebAuthnPolicyPasswordlessSignatureAlgorithms(List<String> webAuthnPolicyPasswordlessSignatureAlgorithms) {
this.webAuthnPolicyPasswordlessSignatureAlgorithms = webAuthnPolicyPasswordlessSignatureAlgorithms;
}
public String getWebAuthnPolicyPasswordlessRpId() {
return webAuthnPolicyPasswordlessRpId;
}
public void setWebAuthnPolicyPasswordlessRpId(String webAuthnPolicyPasswordlessRpId) {
this.webAuthnPolicyPasswordlessRpId = webAuthnPolicyPasswordlessRpId;
}
public String getWebAuthnPolicyPasswordlessAttestationConveyancePreference() {
return webAuthnPolicyPasswordlessAttestationConveyancePreference;
}
public void setWebAuthnPolicyPasswordlessAttestationConveyancePreference(String webAuthnPolicyPasswordlessAttestationConveyancePreference) {
this.webAuthnPolicyPasswordlessAttestationConveyancePreference = webAuthnPolicyPasswordlessAttestationConveyancePreference;
}
public String getWebAuthnPolicyPasswordlessAuthenticatorAttachment() {
return webAuthnPolicyPasswordlessAuthenticatorAttachment;
}
public void setWebAuthnPolicyPasswordlessAuthenticatorAttachment(String webAuthnPolicyPasswordlessAuthenticatorAttachment) {
this.webAuthnPolicyPasswordlessAuthenticatorAttachment = webAuthnPolicyPasswordlessAuthenticatorAttachment;
}
public String getWebAuthnPolicyPasswordlessRequireResidentKey() {
return webAuthnPolicyPasswordlessRequireResidentKey;
}
public void setWebAuthnPolicyPasswordlessRequireResidentKey(String webAuthnPolicyPasswordlessRequireResidentKey) {
this.webAuthnPolicyPasswordlessRequireResidentKey = webAuthnPolicyPasswordlessRequireResidentKey;
}
public String getWebAuthnPolicyPasswordlessUserVerificationRequirement() {
return webAuthnPolicyPasswordlessUserVerificationRequirement;
}
public void setWebAuthnPolicyPasswordlessUserVerificationRequirement(String webAuthnPolicyPasswordlessUserVerificationRequirement) {
this.webAuthnPolicyPasswordlessUserVerificationRequirement = webAuthnPolicyPasswordlessUserVerificationRequirement;
}
public Integer getWebAuthnPolicyPasswordlessCreateTimeout() {
return webAuthnPolicyPasswordlessCreateTimeout;
}
public void setWebAuthnPolicyPasswordlessCreateTimeout(Integer webAuthnPolicyPasswordlessCreateTimeout) {
this.webAuthnPolicyPasswordlessCreateTimeout = webAuthnPolicyPasswordlessCreateTimeout;
}
public Boolean isWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister() {
return webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister;
}
public void setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(Boolean webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister) {
this.webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister = webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister;
}
public List<String> getWebAuthnPolicyPasswordlessAcceptableAaguids() {
return webAuthnPolicyPasswordlessAcceptableAaguids;
}
public void setWebAuthnPolicyPasswordlessAcceptableAaguids(List<String> webAuthnPolicyPasswordlessAcceptableAaguids) {
this.webAuthnPolicyPasswordlessAcceptableAaguids = webAuthnPolicyPasswordlessAcceptableAaguids;
}
public String getBrowserFlow() {
return browserFlow;
}

View file

@ -633,6 +633,18 @@ public class RealmAdapter implements CachedRealmModel {
updated.setWebAuthnPolicy(policy);
}
@Override
public WebAuthnPolicy getWebAuthnPolicyPasswordless() {
if (isUpdated()) return updated.getWebAuthnPolicyPasswordless();
return cached.getWebAuthnPasswordlessPolicy();
}
@Override
public void setWebAuthnPolicyPasswordless(WebAuthnPolicy policy) {
getDelegateForUpdate();
updated.setWebAuthnPolicyPasswordless(policy);
}
@Override
public RoleModel getRoleById(String id) {
if (isUpdated()) return updated.getRoleById(id);

View file

@ -97,6 +97,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected PasswordPolicy passwordPolicy;
protected OTPPolicy otpPolicy;
protected WebAuthnPolicy webAuthnPolicy;
protected WebAuthnPolicy webAuthnPasswordlessPolicy;
protected String loginTheme;
protected String accountTheme;
@ -207,6 +208,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
passwordPolicy = model.getPasswordPolicy();
otpPolicy = model.getOTPPolicy();
webAuthnPolicy = model.getWebAuthnPolicy();
webAuthnPasswordlessPolicy = model.getWebAuthnPolicyPasswordless();
loginTheme = model.getLoginTheme();
accountTheme = model.getAccountTheme();
@ -613,6 +615,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return webAuthnPolicy;
}
public WebAuthnPolicy getWebAuthnPasswordlessPolicy() {
return webAuthnPasswordlessPolicy;
}
public AuthenticationFlowModel getBrowserFlow() {
return browserFlow;
}

View file

@ -941,55 +941,80 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
em.flush();
}
// WebAuthn
@Override
public WebAuthnPolicy getWebAuthnPolicy() {
return getWebAuthnPolicy("");
}
@Override
public void setWebAuthnPolicy(WebAuthnPolicy policy) {
setWebAuthnPolicy(policy, "");
}
@Override
public WebAuthnPolicy getWebAuthnPolicyPasswordless() {
// We will use some prefix for attributes related to passwordless WebAuthn policy
return getWebAuthnPolicy(Constants.WEBAUTHN_PASSWORDLESS_PREFIX);
}
@Override
public void setWebAuthnPolicyPasswordless(WebAuthnPolicy policy) {
// We will use some prefix for attributes related to passwordless WebAuthn policy
setWebAuthnPolicy(policy, Constants.WEBAUTHN_PASSWORDLESS_PREFIX);
}
private WebAuthnPolicy getWebAuthnPolicy(String attributePrefix) {
WebAuthnPolicy policy = new WebAuthnPolicy();
// mandatory parameters
String rpEntityName = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME);
String rpEntityName = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME + attributePrefix);
if (rpEntityName == null || rpEntityName.isEmpty())
rpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
policy.setRpEntityName(rpEntityName);
String signatureAlgorithmsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS);
String signatureAlgorithmsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS + attributePrefix);
if (signatureAlgorithmsString == null || signatureAlgorithmsString.isEmpty())
signatureAlgorithmsString = Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS;
List<String> signatureAlgorithms = Arrays.asList(signatureAlgorithmsString.split(","));
policy.setSignatureAlgorithm(signatureAlgorithms);
// optional parameters
String rpId = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID);
String rpId = getAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID + attributePrefix);
if (rpId == null || rpId.isEmpty()) rpId = "";
policy.setRpId(rpId);
String attestationConveyancePreference = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE);
String attestationConveyancePreference = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE + attributePrefix);
if (attestationConveyancePreference == null || attestationConveyancePreference.isEmpty())
attestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
policy.setAttestationConveyancePreference(attestationConveyancePreference);
String authenticatorAttachment = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT);
String authenticatorAttachment = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT + attributePrefix);
if (authenticatorAttachment == null || authenticatorAttachment.isEmpty())
authenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
policy.setAuthenticatorAttachment(authenticatorAttachment);
String requireResidentKey = getAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY);
String requireResidentKey = getAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY + attributePrefix);
if (requireResidentKey == null || requireResidentKey.isEmpty())
requireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
policy.setRequireResidentKey(requireResidentKey);
String userVerificationRequirement = getAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT);
String userVerificationRequirement = getAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT + attributePrefix);
if (userVerificationRequirement == null || userVerificationRequirement.isEmpty())
userVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
policy.setUserVerificationRequirement(userVerificationRequirement);
String createTime = getAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT);
String createTime = getAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT + attributePrefix);
if (createTime != null) policy.setCreateTimeout(Integer.parseInt(createTime));
else policy.setCreateTimeout(0);
String avoidSameAuthenticatorRegister = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER);
String avoidSameAuthenticatorRegister = getAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER + attributePrefix);
if (avoidSameAuthenticatorRegister != null) policy.setAvoidSameAuthenticatorRegister(Boolean.parseBoolean(avoidSameAuthenticatorRegister));
String acceptableAaguidsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS);
String acceptableAaguidsString = getAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix);
List<String> acceptableAaguids = new ArrayList<>();
if (acceptableAaguidsString != null && !acceptableAaguidsString.isEmpty())
acceptableAaguids = Arrays.asList(acceptableAaguidsString.split(","));
@ -998,44 +1023,45 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
return policy;
}
@Override
public void setWebAuthnPolicy(WebAuthnPolicy policy) {
private void setWebAuthnPolicy(WebAuthnPolicy policy, String attributePrefix) {
// mandatory parameters
String rpEntityName = policy.getRpEntityName();
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME, rpEntityName);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ENTITY_NAME + attributePrefix, rpEntityName);
List<String> signatureAlgorithms = policy.getSignatureAlgorithm();
String signatureAlgorithmsString = String.join(",", signatureAlgorithms);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS, signatureAlgorithmsString);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS + attributePrefix, signatureAlgorithmsString);
// optional parameters
String rpId = policy.getRpId();
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID, rpId);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_RP_ID + attributePrefix, rpId);
String attestationConveyancePreference = policy.getAttestationConveyancePreference();
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE, attestationConveyancePreference);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ATTESTATION_CONVEYANCE_PREFERENCE + attributePrefix, attestationConveyancePreference);
String authenticatorAttachment = policy.getAuthenticatorAttachment();
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT, authenticatorAttachment);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AUTHENTICATOR_ATTACHMENT + attributePrefix, authenticatorAttachment);
String requireResidentKey = policy.getRequireResidentKey();
setAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY, requireResidentKey);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_REQUIRE_RESIDENT_KEY + attributePrefix, requireResidentKey);
String userVerificationRequirement = policy.getUserVerificationRequirement();
setAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT, userVerificationRequirement);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_USER_VERIFICATION_REQUIREMENT + attributePrefix, userVerificationRequirement);
int createTime = policy.getCreateTimeout();
setAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT, Integer.toString(createTime));
setAttribute(RealmAttributes.WEBAUTHN_POLICY_CREATE_TIMEOUT + attributePrefix, Integer.toString(createTime));
boolean avoidSameAuthenticatorRegister = policy.isAvoidSameAuthenticatorRegister();
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER, Boolean.toString(avoidSameAuthenticatorRegister));
setAttribute(RealmAttributes.WEBAUTHN_POLICY_AVOID_SAME_AUTHENTICATOR_REGISTER + attributePrefix, Boolean.toString(avoidSameAuthenticatorRegister));
List<String> acceptableAaguids = policy.getAcceptableAaguids();
if (acceptableAaguids != null && !acceptableAaguids.isEmpty()) {
String acceptableAaguidsString = String.join(",", acceptableAaguids);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS, acceptableAaguidsString);
setAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix, acceptableAaguidsString);
} else {
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS);
removeAttribute(RealmAttributes.WEBAUTHN_POLICY_ACCEPTABLE_AAGUIDS + attributePrefix);
}
}

View file

@ -73,4 +73,6 @@ public interface Details {
String X509_CERTIFICATE_SERIAL_NUMBER = "x509_cert_serial_number";
String X509_CERTIFICATE_SUBJECT_DISTINGUISHED_NAME = "x509_cert_subject_distinguished_name";
String X509_CERTIFICATE_ISSUER_DISTINGUISHED_NAME = "x509_cert_issuer_distinguished_name";
String CREDENTIAL_TYPE = "credential_type";
}

View file

@ -65,6 +65,9 @@ public final class Constants {
// it stands for optional parameter not specified in WebAuthn
public static final String DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED = "not specified";
// Prefix used for the realm attributes and other places
public static final String WEBAUTHN_PASSWORDLESS_PREFIX = "Passwordless";
public static final String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
public static final String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
public static final String EXECUTION = "execution";

View file

@ -343,6 +343,7 @@ public class ModelToRepresentation {
rep.setOtpPolicyType(otpPolicy.getType());
rep.setOtpPolicyLookAheadWindow(otpPolicy.getLookAheadWindow());
rep.setOtpSupportedApplications(otpPolicy.getSupportedApplications());
WebAuthnPolicy webAuthnPolicy = realm.getWebAuthnPolicy();
rep.setWebAuthnPolicyRpEntityName(webAuthnPolicy.getRpEntityName());
rep.setWebAuthnPolicySignatureAlgorithms(webAuthnPolicy.getSignatureAlgorithm());
@ -354,6 +355,19 @@ public class ModelToRepresentation {
rep.setWebAuthnPolicyCreateTimeout(webAuthnPolicy.getCreateTimeout());
rep.setWebAuthnPolicyAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
rep.setWebAuthnPolicyAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
webAuthnPolicy = realm.getWebAuthnPolicyPasswordless();
rep.setWebAuthnPolicyPasswordlessRpEntityName(webAuthnPolicy.getRpEntityName());
rep.setWebAuthnPolicyPasswordlessSignatureAlgorithms(webAuthnPolicy.getSignatureAlgorithm());
rep.setWebAuthnPolicyPasswordlessRpId(webAuthnPolicy.getRpId());
rep.setWebAuthnPolicyPasswordlessAttestationConveyancePreference(webAuthnPolicy.getAttestationConveyancePreference());
rep.setWebAuthnPolicyPasswordlessAuthenticatorAttachment(webAuthnPolicy.getAuthenticatorAttachment());
rep.setWebAuthnPolicyPasswordlessRequireResidentKey(webAuthnPolicy.getRequireResidentKey());
rep.setWebAuthnPolicyPasswordlessUserVerificationRequirement(webAuthnPolicy.getUserVerificationRequirement());
rep.setWebAuthnPolicyPasswordlessCreateTimeout(webAuthnPolicy.getCreateTimeout());
rep.setWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister(webAuthnPolicy.isAvoidSameAuthenticatorRegister());
rep.setWebAuthnPolicyPasswordlessAcceptableAaguids(webAuthnPolicy.getAcceptableAaguids());
if (realm.getBrowserFlow() != null) rep.setBrowserFlow(realm.getBrowserFlow().getAlias());
if (realm.getRegistrationFlow() != null) rep.setRegistrationFlow(realm.getRegistrationFlow().getAlias());
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());

View file

@ -265,55 +265,12 @@ public class RepresentationToModel {
if (rep.getOtpPolicyType() != null) newRealm.setOTPPolicy(toPolicy(rep));
else newRealm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyRpEntityName();
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
String webAuthnPolicyRpId = rep.getWebAuthnPolicyRpId();
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
webAuthnPolicyRpId = "";
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyCreateTimeout();
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
else webAuthnPolicy.setCreateTimeout(0);
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
WebAuthnPolicy webAuthnPolicy = getWebAuthnPolicyTwoFactor(rep);
newRealm.setWebAuthnPolicy(webAuthnPolicy);
webAuthnPolicy = getWebAuthnPolicyPasswordless(rep);
newRealm.setWebAuthnPolicyPasswordless(webAuthnPolicy);
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
if (rep.getRequiredActions() != null) {
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
@ -483,6 +440,110 @@ public class RepresentationToModel {
}
}
private static WebAuthnPolicy getWebAuthnPolicyTwoFactor(RealmRepresentation rep) {
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyRpEntityName();
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
String webAuthnPolicyRpId = rep.getWebAuthnPolicyRpId();
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
webAuthnPolicyRpId = "";
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyCreateTimeout();
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
else webAuthnPolicy.setCreateTimeout(0);
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
return webAuthnPolicy;
}
private static WebAuthnPolicy getWebAuthnPolicyPasswordless(RealmRepresentation rep) {
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyPasswordlessRpEntityName();
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicyPasswordlessSignatureAlgorithms();
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
String webAuthnPolicyRpId = rep.getWebAuthnPolicyPasswordlessRpId();
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
webAuthnPolicyRpId = "";
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyPasswordlessAttestationConveyancePreference();
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyPasswordlessAuthenticatorAttachment();
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyPasswordlessRequireResidentKey();
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyPasswordlessUserVerificationRequirement();
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyPasswordlessCreateTimeout();
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
else webAuthnPolicy.setCreateTimeout(0);
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister();
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyPasswordlessAcceptableAaguids();
if (webAuthnPolicyAcceptableAaguids != null) webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
return webAuthnPolicy;
}
public static void importUserFederationProvidersAndMappers(KeycloakSession session, RealmRepresentation rep, RealmModel newRealm) {
// providers to convert to component model
Set<String> convertSet = new HashSet<>();
@ -1042,55 +1103,12 @@ public class RepresentationToModel {
realm.updateDefaultRoles(rep.getDefaultRoles().toArray(new String[rep.getDefaultRoles().size()]));
}
WebAuthnPolicy webAuthnPolicy = new WebAuthnPolicy();
String webAuthnPolicyRpEntityName = rep.getWebAuthnPolicyRpEntityName();
if (webAuthnPolicyRpEntityName == null || webAuthnPolicyRpEntityName.isEmpty())
webAuthnPolicyRpEntityName = Constants.DEFAULT_WEBAUTHN_POLICY_RP_ENTITY_NAME;
webAuthnPolicy.setRpEntityName(webAuthnPolicyRpEntityName);
List<String> webAuthnPolicySignatureAlgorithms = rep.getWebAuthnPolicySignatureAlgorithms();
if (webAuthnPolicySignatureAlgorithms == null || webAuthnPolicySignatureAlgorithms.isEmpty())
webAuthnPolicySignatureAlgorithms = Arrays.asList(Constants.DEFAULT_WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS.split(","));
webAuthnPolicy.setSignatureAlgorithm(webAuthnPolicySignatureAlgorithms);
String webAuthnPolicyRpId = rep.getWebAuthnPolicyRpId();
if (webAuthnPolicyRpId == null || webAuthnPolicyRpId.isEmpty())
webAuthnPolicyRpId = "";
webAuthnPolicy.setRpId(webAuthnPolicyRpId);
String webAuthnPolicyAttestationConveyancePreference = rep.getWebAuthnPolicyAttestationConveyancePreference();
if (webAuthnPolicyAttestationConveyancePreference == null || webAuthnPolicyAttestationConveyancePreference.isEmpty())
webAuthnPolicyAttestationConveyancePreference = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setAttestationConveyancePreference(webAuthnPolicyAttestationConveyancePreference);
String webAuthnPolicyAuthenticatorAttachment = rep.getWebAuthnPolicyAuthenticatorAttachment();
if (webAuthnPolicyAuthenticatorAttachment == null || webAuthnPolicyAuthenticatorAttachment.isEmpty())
webAuthnPolicyAuthenticatorAttachment = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setAuthenticatorAttachment(webAuthnPolicyAuthenticatorAttachment);
String webAuthnPolicyRequireResidentKey = rep.getWebAuthnPolicyRequireResidentKey();
if (webAuthnPolicyRequireResidentKey == null || webAuthnPolicyRequireResidentKey.isEmpty())
webAuthnPolicyRequireResidentKey = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setRequireResidentKey(webAuthnPolicyRequireResidentKey);
String webAuthnPolicyUserVerificationRequirement = rep.getWebAuthnPolicyUserVerificationRequirement();
if (webAuthnPolicyUserVerificationRequirement == null || webAuthnPolicyUserVerificationRequirement.isEmpty())
webAuthnPolicyUserVerificationRequirement = Constants.DEFAULT_WEBAUTHN_POLICY_NOT_SPECIFIED;
webAuthnPolicy.setUserVerificationRequirement(webAuthnPolicyUserVerificationRequirement);
Integer webAuthnPolicyCreateTimeout = rep.getWebAuthnPolicyCreateTimeout();
if (webAuthnPolicyCreateTimeout != null) webAuthnPolicy.setCreateTimeout(webAuthnPolicyCreateTimeout);
else webAuthnPolicy.setCreateTimeout(0);
Boolean webAuthnPolicyAvoidSameAuthenticatorRegister = rep.isWebAuthnPolicyAvoidSameAuthenticatorRegister();
if (webAuthnPolicyAvoidSameAuthenticatorRegister != null) webAuthnPolicy.setAvoidSameAuthenticatorRegister(webAuthnPolicyAvoidSameAuthenticatorRegister);
List<String> webAuthnPolicyAcceptableAaguids = rep.getWebAuthnPolicyAcceptableAaguids();
webAuthnPolicy.setAcceptableAaguids(webAuthnPolicyAcceptableAaguids);
WebAuthnPolicy webAuthnPolicy = getWebAuthnPolicyTwoFactor(rep);
realm.setWebAuthnPolicy(webAuthnPolicy);
webAuthnPolicy = getWebAuthnPolicyPasswordless(rep);
realm.setWebAuthnPolicyPasswordless(webAuthnPolicy);
if (rep.getSmtpServer() != null) {
Map<String, String> config = new HashMap(rep.getSmtpServer());
if (rep.getSmtpServer().containsKey("password") && ComponentRepresentation.SECRET_VALUE.equals(rep.getSmtpServer().get("password"))) {

View file

@ -240,9 +240,30 @@ public interface RealmModel extends RoleContainerModel {
OTPPolicy getOTPPolicy();
void setOTPPolicy(OTPPolicy policy);
/**
* @return WebAuthn policy for 2-factor authentication
*/
WebAuthnPolicy getWebAuthnPolicy();
/**
* Set WebAuthn policy for 2-factor authentication
*
* @param policy
*/
void setWebAuthnPolicy(WebAuthnPolicy policy);
/**
*
* @return WebAuthn passwordless policy below. This is temporary and will be removed later.
*/
WebAuthnPolicy getWebAuthnPolicyPasswordless();
/**
* Set WebAuthn passwordless policy below. This is temporary and will be removed later.
* @param policy
*/
void setWebAuthnPolicyPasswordless(WebAuthnPolicy policy);
RoleModel getRoleById(String id);
List<GroupModel> getDefaultGroups();

View file

@ -31,22 +31,28 @@ import org.keycloak.util.JsonSerialization;
*/
public class WebAuthnCredentialModel extends CredentialModel {
public static final String TYPE = "webauthn";
// Credential type used for WebAuthn two factor credentials
public static final String TYPE_TWOFACTOR = "webauthn";
// Credential type used for WebAuthn passwordless credentials
public static final String TYPE_PASSWORDLESS = "webauthn-passwordless";
// Either
private final WebAuthnCredentialData credentialData;
private final WebAuthnSecretData secretData;
private WebAuthnCredentialModel(WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
private WebAuthnCredentialModel(String credentialType, WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
this.credentialData = credentialData;
this.secretData = secretData;
setType(credentialType);
}
public static WebAuthnCredentialModel create(String userLabel, String aaguid, String credentialId,
public static WebAuthnCredentialModel create(String credentialType, String userLabel, String aaguid, String credentialId,
String attestationStatement, String credentialPublicKey, long counter) {
WebAuthnCredentialData credentialData = new WebAuthnCredentialData(aaguid, credentialId, counter, attestationStatement, credentialPublicKey);
WebAuthnSecretData secretData = new WebAuthnSecretData();
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialData, secretData);
WebAuthnCredentialModel credentialModel = new WebAuthnCredentialModel(credentialType, credentialData, secretData);
credentialModel.fillCredentialModelFields();
credentialModel.setUserLabel(userLabel);
return credentialModel;
@ -58,10 +64,10 @@ public class WebAuthnCredentialModel extends CredentialModel {
WebAuthnCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), WebAuthnCredentialData.class);
WebAuthnSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), WebAuthnSecretData.class);
WebAuthnCredentialModel webAuthnCredentialModel = new WebAuthnCredentialModel(credentialData, secretData);
WebAuthnCredentialModel webAuthnCredentialModel = new WebAuthnCredentialModel(credentialModel.getType(), credentialData, secretData);
webAuthnCredentialModel.setUserLabel(credentialModel.getUserLabel());
webAuthnCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
webAuthnCredentialModel.setType(TYPE);
webAuthnCredentialModel.setType(credentialModel.getType());
webAuthnCredentialModel.setId(credentialModel.getId());
webAuthnCredentialModel.setSecretData(credentialModel.getSecretData());
webAuthnCredentialModel.setCredentialData(credentialModel.getCredentialData());
@ -95,7 +101,6 @@ public class WebAuthnCredentialModel extends CredentialModel {
try {
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setType(TYPE);
setCreatedDate(Time.currentTimeMillis());
} catch (IOException e) {
throw new RuntimeException(e);
@ -106,7 +111,8 @@ public class WebAuthnCredentialModel extends CredentialModel {
@Override
public String toString() {
return "WebAuthnCredentialModel { " +
credentialData +
getType() +
", " + credentialData +
", " + secretData +
" }";
}

View file

@ -35,16 +35,17 @@ import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.UriUtils;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.OTPCredentialProvider;
import org.keycloak.credential.WebAuthnCredentialModelInput;
import org.keycloak.credential.WebAuthnCredentialProvider;
import org.keycloak.credential.WebAuthnCredentialProviderFactory;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.forms.login.freemarker.model.WebAuthnAuthenticatorsBean;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.WebAuthnPolicy;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import javax.ws.rs.core.MultivaluedMap;
@ -52,6 +53,9 @@ import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
/**
* Authenticator for WebAuthn authentication, which will be typically used when WebAuthn is used as second factor.
*/
public class WebAuthnAuthenticator implements Authenticator, CredentialValidator<WebAuthnCredentialProvider> {
private static final Logger logger = Logger.getLogger(WebAuthnAuthenticator.class);
@ -69,7 +73,8 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
context.getAuthenticationSession().setAuthNote(WebAuthnConstants.AUTH_CHALLENGE_NOTE, challengeValue);
form.setAttribute(WebAuthnConstants.CHALLENGE, challengeValue);
String rpId = context.getRealm().getWebAuthnPolicy().getRpId();
WebAuthnPolicy policy = getWebAuthnPolicy(context);
String rpId = policy.getRpId();
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
form.setAttribute(WebAuthnConstants.RP_ID, rpId);
@ -77,7 +82,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
boolean isUserIdentified = false;
if (user != null) {
// in 2 Factor Scenario where the user has already been identified
WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user);
WebAuthnAuthenticatorsBean authenticators = new WebAuthnAuthenticatorsBean(context.getSession(), context.getRealm(), user, getCredentialType());
if (authenticators.getAuthenticators().isEmpty()) {
// require the user to register webauthn authenticator
return;
@ -91,15 +96,26 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
form.setAttribute(WebAuthnConstants.IS_USER_IDENTIFIED, Boolean.toString(isUserIdentified));
// read options from policy
String userVerificationRequirement = context.getRealm().getWebAuthnPolicy().getUserVerificationRequirement();
String userVerificationRequirement = policy.getUserVerificationRequirement();
form.setAttribute(WebAuthnConstants.USER_VERIFICATION, userVerificationRequirement);
context.challenge(form.createLoginWebAuthn());
}
protected WebAuthnPolicy getWebAuthnPolicy(AuthenticationFlowContext context) {
return context.getRealm().getWebAuthnPolicy();
}
protected String getCredentialType() {
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
}
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
context.getEvent().detail(Details.CREDENTIAL_TYPE, getCredentialType());
// receive error from navigator.credentials.get()
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
@ -121,7 +137,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
String userId = params.getFirst(WebAuthnConstants.USER_HANDLE);
boolean isUVFlagChecked = false;
String userVerificationRequirement = context.getRealm().getWebAuthnPolicy().getUserVerificationRequirement();
String userVerificationRequirement = getWebAuthnPolicy(context).getUserVerificationRequirement();
if (WebAuthnConstants.OPTION_REQUIRED.equals(userVerificationRequirement)) isUVFlagChecked = true;
// existing User Handle means that the authenticator used Resident Key supported public key credential
@ -158,7 +174,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
isUVFlagChecked
);
WebAuthnCredentialModelInput cred = new WebAuthnCredentialModelInput();
WebAuthnCredentialModelInput cred = new WebAuthnCredentialModelInput(getCredentialType());
cred.setAuthenticationContext(authenticationContext);
boolean result = false;
@ -171,7 +187,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
String encodedCredentialID = Base64Url.encode(credentialId);
if (result) {
String isUVChecked = Boolean.toString(isUVFlagChecked);
logger.infov("WebAuthn Authentication successed. isUserVerificationChecked = {0}, PublicKeyCredentialID = {1}", isUVChecked, encodedCredentialID);
logger.debugv("WebAuthn Authentication successed. isUserVerificationChecked = {0}, PublicKeyCredentialID = {1}", isUVChecked, encodedCredentialID);
context.setUser(user);
context.getEvent()
.detail("web_authn_authenticator_user_verification_checked", isUVChecked)
@ -191,7 +207,7 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
}
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return session.userCredentialManager().isConfiguredFor(realm, user, WebAuthnCredentialModel.TYPE);
return session.userCredentialManager().isConfiguredFor(realm, user, getCredentialType());
}
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
@ -219,8 +235,8 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
private static final String ERR_NO_AUTHENTICATORS_REGISTERED = "No WebAuthn Authenticator registered.";
private static final String ERR_WEBAUTHN_API_GET = "Failed to authenticate by the WebAuthn Authenticator";
private static final String ERR_DIFFERENT_USER_AUTHENTICATED = "First authenticated user is not the one authenticated by the WebAuthn authenticator.";
private static final String ERR_WEBAUTHN_VERIFICATION_FAIL = "WetAuthn Authentication result is invalid.";
private static final String ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND = "Unknown user authenticated by the WebAuthen Authenticator";
private static final String ERR_WEBAUTHN_VERIFICATION_FAIL = "WebAuthn Authentication result is invalid.";
private static final String ERR_WEBAUTHN_AUTHENTICATED_USER_NOT_FOUND = "Unknown user authenticated by the WebAuthn Authenticator";
private void setErrorResponse(AuthenticationFlowContext context, final String errorCase, final String errorMessage) {
Response errorResponse = null;

View file

@ -57,7 +57,7 @@ public class WebAuthnAuthenticatorFactory implements AuthenticatorFactory {
@Override
public String getHelpText() {
return "Authenticator for WebAuthn";
return "Authenticator for WebAuthn. Usually used for WebAuthn two-factor authentication";
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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";
}
}

View file

@ -37,6 +37,7 @@ import org.keycloak.credential.WebAuthnCredentialModelInput;
import org.keycloak.credential.WebAuthnCredentialProvider;
import org.keycloak.credential.WebAuthnCredentialProviderFactory;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
@ -67,17 +68,15 @@ import com.webauthn4j.validator.attestation.trustworthiness.ecdaa.DefaultECDAATr
import com.webauthn4j.validator.attestation.trustworthiness.self.DefaultSelfAttestationTrustworthinessValidator;
import org.keycloak.models.credential.WebAuthnCredentialModel;
/**
* Required action for register WebAuthn 2-factor credential for the user
*/
public class WebAuthnRegister implements RequiredActionProvider, CredentialRegistrator {
private static final Logger logger = Logger.getLogger(WebAuthnRegister.class);
private KeycloakSession session;
private CertPathTrustworthinessValidator certPathtrustValidator;
public WebAuthnRegister(KeycloakSession session) {
this.session = session;
this.certPathtrustValidator = new NullCertPathTrustworthinessValidator();
}
public WebAuthnRegister(KeycloakSession session, CertPathTrustworthinessValidator certPathtrustValidator) {
this.session = session;
this.certPathtrustValidator = certPathtrustValidator;
@ -95,7 +94,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
// construct parameters for calling WebAuthn API navigator.credential.create()
// mandatory
WebAuthnPolicy policy = context.getRealm().getWebAuthnPolicy();
WebAuthnPolicy policy = getWebAuthnPolicy(context);
List<String> signatureAlgorithmsList = policy.getSignatureAlgorithm();
String signatureAlgorithms = stringifySignatureAlgorithms(signatureAlgorithmsList);
String rpEntityName = policy.getRpEntityName();
@ -112,7 +111,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
String excludeCredentialIds = "";
if (avoidSameAuthenticatorRegister) {
List<CredentialModel> webAuthnCredentials = session.userCredentialManager().getStoredCredentialsByType(context.getRealm(), userModel, WebAuthnCredentialModel.TYPE);
List<CredentialModel> webAuthnCredentials = session.userCredentialManager().getStoredCredentialsByType(context.getRealm(), userModel, getCredentialType());
List<String> webAuthnCredentialPubKeyIds = webAuthnCredentials.stream().map(credentialModel -> {
WebAuthnCredentialModel credModel = WebAuthnCredentialModel.createFromCredentialModel(credentialModel);
@ -140,11 +139,25 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
context.challenge(form);
}
protected WebAuthnPolicy getWebAuthnPolicy(RequiredActionContext context) {
return context.getRealm().getWebAuthnPolicy();
}
protected String getCredentialType() {
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
}
protected String getCredentialProviderId() {
return WebAuthnCredentialProviderFactory.PROVIDER_ID;
}
@Override
public void processAction(RequiredActionContext context) {
MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();
context.getEvent().detail(Details.CREDENTIAL_TYPE, getCredentialType());
// receive error from navigator.credentials.create()
String errorMsgFromWebAuthnApi = params.getFirst(WebAuthnConstants.ERROR);
if (errorMsgFromWebAuthnApi != null && !errorMsgFromWebAuthnApi.isEmpty()) {
@ -152,7 +165,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
return;
}
WebAuthnPolicy policy = context.getRealm().getWebAuthnPolicy();
WebAuthnPolicy policy = getWebAuthnPolicy(context);
String rpId = policy.getRpId();
if (rpId == null || rpId.isEmpty()) rpId = context.getUriInfo().getBaseUri().getHost();
String label = params.getFirst(WebAuthnConstants.AUTHENTICATOR_LABEL);
@ -176,20 +189,20 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
checkAcceptedAuthenticator(response, policy);
WebAuthnCredentialModelInput credential = new WebAuthnCredentialModelInput();
WebAuthnCredentialModelInput credential = new WebAuthnCredentialModelInput(getCredentialType());
credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
// Save new webAuthn credential
WebAuthnCredentialProvider webAuthnCredProvider = (WebAuthnCredentialProvider) this.session.getProvider(CredentialProvider.class, WebAuthnCredentialProviderFactory.PROVIDER_ID);
WebAuthnCredentialProvider webAuthnCredProvider = (WebAuthnCredentialProvider) this.session.getProvider(CredentialProvider.class, getCredentialProviderId());
WebAuthnCredentialModel newCredentialModel = webAuthnCredProvider.getCredentialModelFromCredentialInput(credential, label);
webAuthnCredProvider.createCredential(context.getRealm(), context.getUser(), newCredentialModel);
String aaguid = newCredentialModel.getWebAuthnCredentialData().getAaguid();
logger.debugv("WebAuthn credential registration success for user {0}. publicKeyCredentialId = {1}, publicKeyCredentialLabel = {2}, publicKeyCredentialAAGUID = {3}",
context.getUser().getUsername(), publicKeyCredentialId, label, aaguid);
logger.debugv("WebAuthn credential registration success for user {0}. credentialType = {1}, publicKeyCredentialId = {2}, publicKeyCredentialLabel = {3}, publicKeyCredentialAAGUID = {4}",
context.getUser().getUsername(), getCredentialType(), publicKeyCredentialId, label, aaguid);
webAuthnCredProvider.dumpCredentialModel(newCredentialModel, credential);
context.getEvent()

View file

@ -16,6 +16,7 @@
package org.keycloak.authentication.requiredactions;
import com.webauthn4j.validator.attestation.trustworthiness.certpath.CertPathTrustworthinessValidator;
import org.keycloak.OAuth2Constants;
import org.keycloak.Config.Scope;
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
@ -39,17 +40,21 @@ public class WebAuthnRegisterFactory implements RequiredActionFactory, DisplayTy
WebAuthnRegister webAuthnRegister = null;
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
webAuthnRegister = new WebAuthnRegister(session, new NullCertPathTrustworthinessValidator());
webAuthnRegister = createProvider(session, new NullCertPathTrustworthinessValidator());
} else {
KeyStoreTrustAnchorsProvider trustAnchorsProvider = new KeyStoreTrustAnchorsProvider();
trustAnchorsProvider.setKeyStore(truststoreProvider.getTruststore());
TrustAnchorsResolverImpl resolverImpl = new TrustAnchorsResolverImpl(trustAnchorsProvider);
TrustAnchorCertPathTrustworthinessValidator trustValidator = new TrustAnchorCertPathTrustworthinessValidator(resolverImpl);
webAuthnRegister = new WebAuthnRegister(session, trustValidator);
webAuthnRegister = createProvider(session, trustValidator);
}
return webAuthnRegister;
}
protected WebAuthnRegister createProvider(KeycloakSession session, CertPathTrustworthinessValidator trustValidator) {
return new WebAuthnRegister(session, trustValidator);
}
@Override
public void init(Scope config) {
// NOP

View file

@ -26,13 +26,16 @@ import org.keycloak.models.credential.WebAuthnCredentialModel;
public class WebAuthnCredentialModelInput implements CredentialInput {
public static final String WEBAUTHN_CREDENTIAL_TYPE = WebAuthnCredentialModel.TYPE;
private AttestedCredentialData attestedCredentialData;
private AttestationStatement attestationStatement;
private WebAuthnAuthenticationContext authenticationContext;
private long count;
private String credentialDBId;
private final String credentialType;
public WebAuthnCredentialModelInput(String credentialType) {
this.credentialType = credentialType;
}
@Override
public String getCredentialId() {
@ -46,12 +49,9 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
@Override
public String getType() {
return WEBAUTHN_CREDENTIAL_TYPE;
return credentialType;
}
public WebAuthnCredentialModelInput() {
}
public AttestedCredentialData getAttestedCredentialData() {
return attestedCredentialData;
@ -93,8 +93,12 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
this.credentialDBId = credentialDBId;
}
public String getCredentialType() {
return credentialType;
}
public String toString() {
StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder("Credential Type = " + credentialType + ",");
if (credentialDBId != null)
sb.append("Credential DB Id = ")
.append(credentialDBId)

View file

@ -40,6 +40,9 @@ import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
/**
* Credential provider for WebAuthn 2-factor credential of the user
*/
public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCredentialModel>, CredentialInputValidator {
private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class);
@ -98,7 +101,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
String credentialPublicKey = credentialPublicKeyConverter.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCOSEKey());
long counter = webAuthnModel.getCount();
WebAuthnCredentialModel model = WebAuthnCredentialModel.create(userLabel, aaguid, credentialId, null, credentialPublicKey, counter);
WebAuthnCredentialModel model = WebAuthnCredentialModel.create(getType(), userLabel, aaguid, credentialId, null, credentialPublicKey, counter);
model.setId(webAuthnModel.getCredentialDBId());
@ -114,7 +117,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
WebAuthnCredentialData credData = webAuthnCredential.getWebAuthnCredentialData();
WebAuthnCredentialModelInput auth = new WebAuthnCredentialModelInput();
WebAuthnCredentialModelInput auth = new WebAuthnCredentialModelInput(getType());
byte[] credentialId = null;
try {
@ -142,7 +145,7 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
@Override
public boolean supportsCredentialType(String credentialType) {
return WebAuthnCredentialModelInput.WEBAUTHN_CREDENTIAL_TYPE.equals(credentialType);
return getType().equals(credentialType);
}
@Override
@ -204,12 +207,12 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
@Override
public String getType() {
return WebAuthnCredentialModel.TYPE;
return WebAuthnCredentialModel.TYPE_TWOFACTOR;
}
private List<WebAuthnCredentialModelInput> getWebAuthnCredentialModelList(RealmModel realm, UserModel user) {
List<CredentialModel> credentialModels = session.userCredentialManager().getStoredCredentialsByType(realm, user, WebAuthnCredentialModel.TYPE);
List<CredentialModel> credentialModels = session.userCredentialManager().getStoredCredentialsByType(realm, user, getType());
return credentialModels.stream()
.map(this::getCredentialInputFromCredentialModel)

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -28,9 +28,9 @@ import org.keycloak.models.credential.WebAuthnCredentialModel;
public class WebAuthnAuthenticatorsBean {
private List<WebAuthnAuthenticatorBean> authenticators = new LinkedList<WebAuthnAuthenticatorBean>();
public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user) {
public WebAuthnAuthenticatorsBean(KeycloakSession session, RealmModel realm, UserModel user, String credentialType) {
// should consider multiple credentials in the future, but only single credential supported now.
for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, WebAuthnCredentialModel.TYPE)) {
for (CredentialModel credential : session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType)) {
WebAuthnCredentialModel webAuthnCredential = WebAuthnCredentialModel.createFromCredentialModel(credential);
String credentialId = Base64Url.encodeBase64ToBase64Url(webAuthnCredential.getWebAuthnCredentialData().getCredentialId());

View file

@ -48,3 +48,4 @@ org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFacto
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory

View file

@ -21,3 +21,4 @@ org.keycloak.authentication.requiredactions.UpdateTotp
org.keycloak.authentication.requiredactions.VerifyEmail
org.keycloak.authentication.requiredactions.TermsAndConditions
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory

View file

@ -1,3 +1,4 @@
org.keycloak.credential.OTPCredentialProviderFactory
org.keycloak.credential.PasswordCredentialProviderFactory
org.keycloak.credential.WebAuthnCredentialProviderFactory
org.keycloak.credential.WebAuthnPasswordlessCredentialProviderFactory

View file

@ -17,174 +17,28 @@
package org.keycloak.testsuite.pages.webauthn;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import static org.keycloak.testsuite.util.UIUtils.clickLink;
/**
* Page shown during WebAuthn login. Page is useful with Chrome testing API
*/
public class WebAuthnLoginPage extends LanguageComboboxAwarePage {
@ArquillianResource
protected OAuthClient oauth;
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "totp")
private WebElement totp;
@FindBy(id = "rememberMe")
private WebElement rememberMe;
@FindBy(name = "login")
private WebElement submitButton;
@FindBy(name = "cancel")
private WebElement cancelButton;
@FindBy(linkText = "Register")
private WebElement registerLink;
@FindBy(linkText = "Forgot Password?")
private WebElement resetPasswordLink;
@FindBy(linkText = "Username")
private WebElement recoverUsernameLink;
@FindBy(className = "alert-error")
private WebElement loginErrorMessage;
@FindBy(className = "alert-warning")
private WebElement loginWarningMessage;
@FindBy(className = "alert-success")
private WebElement loginSuccessMessage;
@FindBy(className = "alert-info")
private WebElement loginInfoMessage;
@FindBy(className = "instruction")
private WebElement instruction;
public void login(String username, String password) {
driver.findElement(By.id("username")).clear();
driver.findElement(By.id("username")).sendKeys(username);
driver.findElement(By.id("password")).clear();
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.name("login")).click();
// After click the button, the "navigator.credentials.get" will be called on the browser side, which should automatically
// login user with the chrome testing API
public void confirmWebAuthnLogin() {
driver.findElement(By.cssSelector("input[type=\"button\"]")).click();
}
public void login(String password) {
driver.findElement(By.id("password")).clear();
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.name("login")).click();
}
public void missingPassword(String username) {
driver.findElement(By.id("username")).clear();
driver.findElement(By.id("username")).sendKeys(username);
driver.findElement(By.id("password")).clear();
driver.findElement(By.name("login")).click();
}
public void missingUsername() {
driver.findElement(By.id("username")).clear();
driver.findElement(By.name("login")).click();
}
public String getUsername() {
return driver.findElement(By.id("username")).getAttribute("value");
}
public boolean isUsernameInputEnabled() {
return driver.findElement(By.id("username")).isEnabled();
}
public String getPassword() {
return driver.findElement(By.id("password")).getAttribute("value");
}
public void cancel() {
driver.findElement(By.name("cancel")).click();
}
public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}
public String getInstruction() {
return instruction != null ? instruction.getText() : null;
}
public String getSuccessMessage() {
return loginSuccessMessage != null ? loginSuccessMessage.getText() : null;
}
public String getInfoMessage() {
return loginInfoMessage != null ? loginInfoMessage.getText() : null;
}
public boolean isCurrent() {
String realm = "test";
return isCurrent(realm);
}
public boolean isCurrent(String realm) {
return driver.getTitle().equals("Log in to " + realm) || driver.getTitle().equals("Anmeldung bei " + realm);
}
public void clickRegister() {
driver.findElement(By.linkText("Register")).click();
}
public void clickSocial(String providerId) {
WebElement socialButton = findSocialButton(providerId);
clickLink(socialButton);
}
public WebElement findSocialButton(String providerId) {
String id = "zocial-" + providerId;
return this.driver.findElement(By.id(id));
}
public void resetPassword() {
resetPasswordLink.click();
}
public void recoverUsername() {
recoverUsernameLink.click();
}
public void setRememberMe(boolean enable) {
boolean current = rememberMe.isSelected();
if (current != enable) {
rememberMe.click();
}
}
public boolean isRememberMeChecked() {
return rememberMe.isSelected();
return driver.getPageSource().contains("navigator.credentials.get");
}
@Override
public void open() {
oauth.openLoginForm();
assertCurrent();
throw new UnsupportedOperationException();
}
}

View file

@ -19,169 +19,37 @@ package org.keycloak.testsuite.pages.webauthn;
import org.junit.Assert;
import org.keycloak.testsuite.pages.AbstractPage;
import org.keycloak.testsuite.pages.PageUtils;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
/**
* WebAuthnRegisterPage, which is displayed when WebAuthnRegister required action is triggered. It is useful with Chrome testing API.
*
* Page will be displayed after successful JS call of "navigator.credentials.create", which will register WebAuthn credential
* with the browser
*/
public class WebAuthnRegisterPage extends AbstractPage {
@FindBy(id = "firstName")
private WebElement firstNameInput;
@FindBy(id = "lastName")
private WebElement lastNameInput;
@FindBy(id = "email")
private WebElement emailInput;
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(id = "password-confirm")
private WebElement passwordConfirmInput;
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
@FindBy(className = "alert-error")
private WebElement loginErrorMessage;
@FindBy(className = "instruction")
private WebElement loginInstructionMessage;
@FindBy(linkText = "« Back to Login")
private WebElement backToLoginLink;
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, String authenticatorLabel) {
driver.findElement(By.id("firstName")).clear();
if (firstName != null) {
driver.findElement(By.id("firstName")).sendKeys(firstName);
}
driver.findElement(By.id("lastName")).clear();
if (lastName != null) {
driver.findElement(By.id("lastName")).sendKeys(lastName);
}
driver.findElement(By.id("email")).clear();
if (email != null) {
driver.findElement(By.id("email")).sendKeys(email);
}
driver.findElement(By.id("username")).clear();
if (username != null) {
driver.findElement(By.id("username")).sendKeys(username);
}
driver.findElement(By.id("password")).clear();
if (password != null) {
driver.findElement(By.id("password")).sendKeys(password);
}
driver.findElement(By.id("password-confirm")).clear();
if (passwordConfirm != null) {
driver.findElement(By.id("password-confirm")).sendKeys(passwordConfirm);
}
driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
public void registerWebAuthnCredential(String authenticatorLabel) {
// label edit after registering authenicator by .create()
WebDriverWait wait = new WebDriverWait(driver, 60);
Alert promptDialog = wait.until(ExpectedConditions.alertIsPresent());
//Alert promptDialog = driver.switchTo().alert();
Assert.assertEquals("Please input your registered authenticator's label", promptDialog.getText());
promptDialog.sendKeys(authenticatorLabel);
promptDialog.accept();
}
public void registerWithEmailAsUsername(String firstName, String lastName, String email, String password, String passwordConfirm) {
driver.findElement(By.id("firstName")).clear();
if (firstName != null) {
driver.findElement(By.id("firstName")).sendKeys(firstName);
}
driver.findElement(By.id("lastName")).clear();
if (lastName != null) {
driver.findElement(By.id("lastName")).sendKeys(lastName);
}
driver.findElement(By.id("email")).clear();
if (email != null) {
driver.findElement(By.id("email")).sendKeys(email);
}
try {
driver.findElement(By.id("username")).clear();
Assert.fail("Form must be without username field");
} catch (NoSuchElementException e) {
// OK
}
driver.findElement(By.id("password")).clear();
if (password != null) {
driver.findElement(By.id("password")).sendKeys(password);
}
driver.findElement(By.id("password-confirm")).clear();
if (passwordConfirm != null) {
driver.findElement(By.id("password-confirm")).sendKeys(passwordConfirm);
}
driver.findElement(By.cssSelector("input[type=\"submit\"]")).click();
}
public void clickBackToLogin() {
backToLoginLink.click();
}
public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}
public String getInstruction() {
try {
return loginInstructionMessage != null ? loginInstructionMessage.getText() : null;
} catch (NoSuchElementException e){
// OK
}
return null;
}
public String getFirstName() {
return driver.findElement(By.id("firstName")).getAttribute("value");
}
public String getLastName() {
return driver.findElement(By.id("lastName")).getAttribute("value");
}
public String getEmail() {
return driver.findElement(By.id("email")).getAttribute("value");
}
public String getUsername() {
return driver.findElement(By.id("username")).getAttribute("value");
}
public String getPassword() {
return driver.findElement(By.id("password")).getAttribute("value");
}
public String getPasswordConfirm() {
return driver.findElement(By.id("password-confirm")).getAttribute("value");
}
public boolean isCurrent() {
return PageUtils.getPageTitle(driver).equals("Register");
// Cant verify the page in case that prompt is shown. Prompt is shown immediately when WebAuthnRegisterPage is displayed
throw new UnsupportedOperationException();
}
@Override
public void open() {
throw new UnsupportedOperationException();

View file

@ -198,7 +198,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
"Testsuite Username authenticator. Username parameter sets username");
addProviderInfo(result, "webauthn-authenticator", "WebAuthn Authenticator", "Authenticator for WebAuthn");
addProviderInfo(result, "webauthn-authenticator", "WebAuthn Authenticator", "Authenticator for WebAuthn. Usually used for WebAuthn two-factor authentication");
addProviderInfo(result, "webauthn-authenticator-passwordless", "WebAuthn Passwordless Authenticator", "Authenticator for Passwordless WebAuthn authentication");
addProviderInfo(result, "auth-username-form", "Username Form",
"Selects a user from his username.");

View file

@ -77,9 +77,9 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
public void testCRUDRequiredAction() {
int lastPriority = authMgmtResource.getRequiredActions().get(authMgmtResource.getRequiredActions().size() - 1).getPriority();
// Just Dummy RequiredAction is not registered in the realm
// Dummy RequiredAction is not registered in the realm and WebAuthn actions
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
Assert.assertEquals(2, result.size());
Assert.assertEquals(3, result.size());
RequiredActionProviderSimpleRepresentation action = result.get(0);
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
Assert.assertEquals("Dummy Action", action.getName());

View file

@ -23,15 +23,21 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.WebAuthnConstants;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.common.util.RandomString;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.webauthn.WebAuthnLoginPage;
import org.keycloak.testsuite.pages.webauthn.WebAuthnRegisterPage;
import org.keycloak.testsuite.WebAuthnAssume;
@ -55,10 +61,16 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
protected AppPage appPage;
@Page
protected WebAuthnLoginPage loginPage;
protected LoginPage loginPage;
@Page
protected WebAuthnRegisterPage registerPage;
protected WebAuthnLoginPage webAuthnLoginPage;
@Page
protected RegisterPage registerPage;
@Page
protected WebAuthnRegisterPage webAuthnRegisterPage;
private static final String ALL_ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
@ -115,7 +127,10 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
registerPage.assertCurrent();
String authenticatorLabel = RandomString.randomCode(24);
registerPage.register("firstName", "lastName", email, username, password, password, authenticatorLabel);
registerPage.register("firstName", "lastName", email, username, password, password);
// User was registered. Now he needs to register WebAuthn credential
webAuthnRegisterPage.registerWebAuthnCredential(authenticatorLabel);
appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@ -153,6 +168,10 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
loginPage.open();
loginPage.login(username, password);
// Confirm login on the WebAuthn login page
webAuthnLoginPage.assertCurrent();
webAuthnLoginPage.confirmWebAuthnLogin();
appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
appPage.openAccount();
@ -174,12 +193,105 @@ public class WebAuthnRegisterAndLoginTest extends AbstractTestRealmKeycloakTest
}
}
@Test
public void testWebAuthnTwoFactorAndWebAuthnPasswordlessTogether() {
// Change binding to browser-webauthn-passwordless. This is flow, which contains both "webauthn" and "webauthn-passwordless" authenticator
RealmRepresentation realmRep = testRealm().toRepresentation();
realmRep.setBrowserFlow("browser-webauthn-passwordless");
testRealm().update(realmRep);
//WaitUtils.pause(10000000);
try {
String userId = ApiUtil.findUserByUsername(testRealm(), "test-user@localhost").getId();
// Login as test-user@localhost with password
loginPage.open();
loginPage.login("test-user@localhost", "password");
// Register first requiredAction is needed. Use label "Label1"
webAuthnRegisterPage.registerWebAuthnCredential("label1");
// Register second requiredAction is needed. Use label "Label2". This will be for passwordless WebAuthn credential
webAuthnRegisterPage.registerWebAuthnCredential("label2");
appPage.assertCurrent();
// Assert user is logged and WebAuthn credentials were registered
EventRepresentation eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
.user(userId)
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnRegisterFactory.PROVIDER_ID)
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "label1")
.assertEvent();
String regPubKeyCredentialId1 = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
eventRep = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION)
.user(userId)
.detail(Details.CUSTOM_REQUIRED_ACTION, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID)
.detail(WebAuthnConstants.PUBKEY_CRED_LABEL_ATTR, "label2")
.assertEvent();
String regPubKeyCredentialId2 = eventRep.getDetails().get(WebAuthnConstants.PUBKEY_CRED_ID_ATTR);
String sessionId = events.expectLogin()
.user(userId)
.assertEvent().getSessionId();
// Logout
appPage.logout();
events.expectLogout(sessionId)
.user(userId)
.assertEvent();
// Assert user has 2 webauthn credentials. One of type "webauthn" and the other of type "webauthn-passwordless".
List<CredentialRepresentation> rep = testRealm().users().get(userId).credentials();
CredentialRepresentation webAuthnCredential1 = rep.stream()
.filter(credential -> WebAuthnCredentialModel.TYPE_TWOFACTOR.equals(credential.getType()))
.findFirst().get();
Assert.assertEquals("label1", webAuthnCredential1.getUserLabel());
CredentialRepresentation webAuthnCredential2 = rep.stream()
.filter(credential -> WebAuthnCredentialModel.TYPE_PASSWORDLESS.equals(credential.getType()))
.findFirst().get();
Assert.assertEquals("label2", webAuthnCredential2.getUserLabel());
// Assert user needs to authenticate first with "webauthn" during login
loginPage.open();
loginPage.login("test-user@localhost", "password");
webAuthnLoginPage.assertCurrent();
Assert.assertTrue(driver.getPageSource().contains(regPubKeyCredentialId1));
webAuthnLoginPage.confirmWebAuthnLogin();
// Assert user needs to authenticate also with "webauthn-passwordless"
webAuthnLoginPage.assertCurrent();
Assert.assertTrue(driver.getPageSource().contains(regPubKeyCredentialId2));
webAuthnLoginPage.confirmWebAuthnLogin();
// Assert user logged now
appPage.assertCurrent();
events.expectLogin()
.user(userId)
.assertEvent();
// Remove webauthn credentials from the user
testRealm().users().get(userId).removeCredential(webAuthnCredential1.getId());
testRealm().users().get(userId).removeCredential(webAuthnCredential2.getId());
} finally {
// Revert binding to browser-webauthn
realmRep.setBrowserFlow("browser-webauthn");
testRealm().update(realmRep);
}
}
private void assertUserRegistered(String userId, String username, String email) {
UserRepresentation user = getUser(userId);
Assert.assertNotNull(user);
Assert.assertNotNull(user.getCreatedTimestamp());
// test that timestamp is current with 10s tollerance
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
// test that timestamp is current with 60s tollerance
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 60000);
// test user info is set from form
assertEquals(username.toLowerCase(), user.getUsername());
assertEquals(email.toLowerCase(), user.getEmail());

View file

@ -26,17 +26,6 @@
"00000000-0000-0000-0000-000000000000",
"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73"
],
"attributes": {
"webAuthnPolicyAcceptableAaguids": "00000000-0000-0000-0000-000000000000,6d44ba9b-f6ec-2e49-b930-0c8fe920cb73",
"webAuthnPolicyAuthenticatorAttachment": "not specified",
"webAuthnPolicyRpEntityName": "keycloak-webauthn-2FA",
"webAuthnPolicySignatureAlgorithms": "ES256,RS256,RS1",
"webAuthnPolicyUserVerificationRequirement": "not specified",
"webAuthnPolicyCreateTimeout": "60",
"webAuthnPolicyRequireResidentKey": "not specified",
"webAuthnPolicyAttestationConveyancePreference": "not specified",
"webAuthnPolicyAvoidSameAuthenticatorRegister": "true"
},
"smtpServer": {
"from": "auto@keycloak.org",
"host": "localhost",
@ -574,7 +563,7 @@
"authenticationFlows": [
{
"alias": "Copy of browser",
"alias": "browser-webauthn",
"description": "browser based authentication",
"providerId": "basic-flow",
"topLevel": true,
@ -604,14 +593,14 @@
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "Copy of browser forms",
"flowAlias": "browser-webauthn-forms",
"userSetupAllowed": false,
"autheticatorFlow": true
}
]
},
{
"alias": "Copy of browser forms",
"alias": "browser-webauthn-forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
"topLevel": false,
@ -640,6 +629,59 @@
}
]
},
{
"alias": "browser-webauthn-passwordless",
"description": "browser based authentication",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": false,
"authenticationExecutions": [
{
"authenticator": "auth-cookie",
"requirement": "ALTERNATIVE",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"requirement": "ALTERNATIVE",
"priority": 30,
"flowAlias": "browser-webauthn-passwordless-forms",
"userSetupAllowed": false,
"autheticatorFlow": true
}
]
},
{
"alias": "browser-webauthn-passwordless-forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
"topLevel": false,
"builtIn": false,
"authenticationExecutions": [
{
"authenticator": "auth-username-password-form",
"requirement": "REQUIRED",
"priority": 10,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "webauthn-authenticator",
"requirement": "REQUIRED",
"priority": 20,
"userSetupAllowed": false,
"autheticatorFlow": false
},
{
"authenticator": "webauthn-authenticator-passwordless",
"requirement": "REQUIRED",
"priority": 30,
"userSetupAllowed": false,
"autheticatorFlow": false
}
]
},
{
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
@ -1083,9 +1125,18 @@
"defaultAction": true,
"priority": 51,
"config": {}
},
{
"alias": "webauthn-register-passwordless",
"name": "Webauthn Register Passwordless",
"providerId": "webauthn-register-passwordless",
"enabled": true,
"defaultAction": false,
"priority": 52,
"config": {}
}
],
"browserFlow": "Copy of browser",
"browserFlow": "browser-webauthn",
"internationalizationEnabled": true,
"supportedLocales": ["en", "de"],

View file

@ -1063,6 +1063,9 @@ add-policy.placeholder=Add policy...
policy-type=Policy Type
policy-value=Policy Value
webauthn-policy=WebAuthn Policy
webauthn-policy.tooltip=Policy for WebAuthn authentication. This one will be used by 'WebAuthn Register' required action and 'WebAuthn Authenticator' authenticator. Typical usage is, when WebAuthn will be used for the two-factor authentication.
webauthn-policy-passwordless=WebAuthn Passwordless Policy
webauthn-policy-passwordless.tooltip=Policy for passwordless WebAuthn authentication. This one will be used by 'Webauthn Register Passwordless' required action and 'WebAuthn Passwordless Authenticator' authenticator. Typical usage is, when WebAuthn will be used as first-factor authentication. Having both 'WebAuthn Policy' and 'WebAuthn Passwordless Policy' allows to use WebAuthn as both first factor and second factor authenticator in the same realm.
webauthn-rp-entity-name=Relying Party Entity Name
webauthn-rp-entity-name.tooltip=Human-readable server name as WebAuthn Relying Party
webauthn-signature-algorithms=Signature Algorithms

View file

@ -2017,6 +2017,18 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmWebAuthnPolicyCtrl'
})
.when('/realms/:realm/authentication/webauthn-policy-passwordless', {
templateUrl : resourceUrl + '/partials/webauthn-policy-passwordless.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
serverInfo : function(ServerInfo) {
return ServerInfo.delay;
}
},
controller : 'RealmWebAuthnPasswordlessPolicyCtrl'
})
.when('/realms/:realm/authentication/flows/:flow/config/:provider/:config', {
templateUrl : resourceUrl + '/partials/authenticator-config.html',
resolve : {

View file

@ -412,6 +412,20 @@ module.controller('RealmWebAuthnPolicyCtrl', function($scope, Current, Realm, re
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy");
});
module.controller('RealmWebAuthnPasswordlessPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
$scope.deleteAcceptableAaguid = function(index) {
$scope.realm.webAuthnPolicyPasswordlessAcceptableAaguids.splice(index, 1);
}
$scope.addAcceptableAaguid = function() {
$scope.realm.webAuthnPolicyPasswordlessAcceptableAaguids.push($scope.newAcceptableAaguid);
$scope.newAcceptableAaguid = "";
}
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/webauthn-policy-passwordless");
});
module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications) {
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $route, Dialog, Notifications, "/realms/" + realm.realm + "/theme-settings");

View file

@ -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>

View file

@ -4,5 +4,12 @@
<li ng-class="{active: path[3] == 'required-actions'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/required-actions">{{:: 'required-actions' | translate}}</a></li>
<li ng-class="{active: path[3] == 'password-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/password-policy">{{:: 'password-policy' | translate}}</a></li>
<li ng-class="{active: path[3] == 'otp-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/otp-policy">{{:: 'otp-policy' | translate}}</a></li>
<li ng-class="{active: path[3] == 'webauthn-policy'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/authentication/webauthn-policy">{{:: 'webauthn-policy' | translate}}</a></li>
<li ng-class="{active: path[3] == 'webauthn-policy'}" data-ng-show="access.viewRealm">
<a href="#/realms/{{realm.realm}}/authentication/webauthn-policy">{{:: 'webauthn-policy' | translate}}</a>
<kc-tooltip>{{:: 'webauthn-policy.tooltip' | translate}}</kc-tooltip>
</li>
<li ng-class="{active: path[3] == 'webauthn-policy-passwordless'}" data-ng-show="access.viewRealm">
<a href="#/realms/{{realm.realm}}/authentication/webauthn-policy-passwordless">{{:: 'webauthn-policy-passwordless' | translate}}</a>
<kc-tooltip>{{:: 'webauthn-policy-passwordless.tooltip' | translate}}</kc-tooltip>
</li>
</ul>

View file

@ -339,5 +339,6 @@ auth-password-form=Password
auth-username-form=Username
auth-username-password-form=Username and password
webauthn-authenticator=WebAuthn
webauthn-authenticator-passwordless=WebAuthn Passwordless
identity-provider-redirector=Connect with another Identity Provider