KEYCLOAK-12696 Upgrade to webauthn4j 0.10.2.RELEASE

This commit is contained in:
Takashi Norimatsu 2020-02-19 09:05:58 +09:00 committed by Marek Posolda
parent 86089d40b8
commit fc58af1365
12 changed files with 111 additions and 75 deletions

View file

@ -521,22 +521,22 @@
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthnj4-core</artifactId>
<version>0.9.14.RELEASE</version>
<version>0.10.2.RELEASE</version>
<licenses>
<license>
<name>Apache Software License 2.0</name>
<url>https://raw.githubusercontent.com/webauthn4j/webauthn4j/0.9.14.RELEASE/LICENSE.txt</url>
<url>https://raw.githubusercontent.com/webauthn4j/webauthn4j/0.10.2.RELEASE/LICENSE.txt</url>
</license>
</licenses>
</dependency>
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthnj4-util</artifactId>
<version>0.9.14.RELEASE</version>
<version>0.10.2.RELEASE</version>
<licenses>
<license>
<name>Apache Software License 2.0</name>
<url>https://raw.githubusercontent.com/webauthn4j/webauthn4j/0.9.14.RELEASE/LICENSE.txt</url>
<url>https://raw.githubusercontent.com/webauthn4j/webauthn4j/0.10.2.RELEASE/LICENSE.txt</url>
</license>
</licenses>
</dependency>

View file

@ -521,22 +521,22 @@
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthnj4-core</artifactId>
<version>0.9.14.RELEASE</version>
<version>0.10.2.RELEASE</version>
<licenses>
<license>
<name>Apache Software License 2.0</name>
<url>https://raw.githubusercontent.com/webauthn4j/webauthn4j/0.9.14.RELEASE/LICENSE.txt</url>
<url>https://raw.githubusercontent.com/webauthn4j/webauthn4j/0.10.2.RELEASE/LICENSE.txt</url>
</license>
</licenses>
</dependency>
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthnj4-util</artifactId>
<version>0.9.14.RELEASE</version>
<version>0.10.2.RELEASE</version>
<licenses>
<license>
<name>Apache Software License 2.0</name>
<url>https://raw.githubusercontent.com/webauthn4j/webauthn4j/0.9.14.RELEASE/LICENSE.txt</url>
<url>https://raw.githubusercontent.com/webauthn4j/webauthn4j/0.10.2.RELEASE/LICENSE.txt</url>
</license>
</licenses>
</dependency>

View file

@ -169,7 +169,7 @@
<spring-boot22.version>2.2.0.RELEASE</spring-boot22.version>
<!-- webauthn support -->
<webauthn4j.version>0.9.14.RELEASE</webauthn4j.version>
<webauthn4j.version>0.10.2.RELEASE</webauthn4j.version>
<org.apache.kerby.kerby-asn1.version>2.0.0</org.apache.kerby.kerby-asn1.version>
</properties>

View file

@ -16,13 +16,16 @@
package org.keycloak.authentication.authenticators.browser;
import com.webauthn4j.data.WebAuthnAuthenticationContext;
import com.webauthn4j.data.AuthenticationParameters;
import com.webauthn4j.data.AuthenticationRequest;
import com.webauthn4j.data.client.Origin;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.server.ServerProperty;
import com.webauthn4j.util.exception.WebAuthnException;
import org.jboss.logging.Logger;
import org.keycloak.WebAuthnConstants;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
@ -177,17 +180,24 @@ public class WebAuthnAuthenticator implements Authenticator, CredentialValidator
if (WebAuthnConstants.OPTION_REQUIRED.equals(userVerificationRequirement)) isUVFlagChecked = true;
UserModel user = session.users().getUserById(userId, context.getRealm());
WebAuthnAuthenticationContext authenticationContext = new WebAuthnAuthenticationContext(
AuthenticationRequest authenticationRequest = new AuthenticationRequest(
credentialId,
clientDataJSON,
authenticatorData,
signature,
clientDataJSON,
signature
);
AuthenticationParameters authenticationParameters = new AuthenticationParameters(
server,
null, // here authenticator cannot be fetched, set it afterwards in WebAuthnCredentialProvider.isValid()
isUVFlagChecked
);
);
WebAuthnCredentialModelInput cred = new WebAuthnCredentialModelInput(getCredentialType());
cred.setAuthenticationContext(authenticationContext);
cred.setAuthenticationRequest(authenticationRequest);
cred.setAuthenticationParameters(authenticationParameters);
boolean result = false;
try {

View file

@ -19,6 +19,7 @@ package org.keycloak.authentication.requiredactions;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -47,19 +48,19 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.models.WebAuthnPolicy;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.JsonConverter;
import com.webauthn4j.data.WebAuthnRegistrationContext;
import com.webauthn4j.WebAuthnManager;
import com.webauthn4j.converter.util.ObjectConverter;
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
import com.webauthn4j.data.attestation.statement.AttestationStatement;
import com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier;
import com.webauthn4j.data.client.Origin;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.data.RegistrationRequest;
import com.webauthn4j.data.RegistrationData;
import com.webauthn4j.data.RegistrationParameters;
import com.webauthn4j.server.ServerProperty;
import com.webauthn4j.util.exception.WebAuthnException;
import com.webauthn4j.validator.WebAuthnRegistrationContextValidationResponse;
import com.webauthn4j.validator.WebAuthnRegistrationContextValidator;
import com.webauthn4j.validator.attestation.statement.androidkey.AndroidKeyAttestationStatementValidator;
import com.webauthn4j.validator.attestation.statement.androidsafetynet.AndroidSafetyNetAttestationStatementValidator;
import com.webauthn4j.validator.attestation.statement.none.NoneAttestationStatementValidator;
@ -204,19 +205,24 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
// check User Verification by considering a malicious user might modify the result of calling WebAuthn API
boolean isUserVerificationRequired = policy.getUserVerificationRequirement().equals(WebAuthnConstants.OPTION_REQUIRED);
RegistrationRequest registrationRequest = new RegistrationRequest(attestationObject, clientDataJSON);
RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, isUserVerificationRequired);
WebAuthnManager webAuthnManager = createWebAuthnManager();
try {
WebAuthnRegistrationContext registrationContext = new WebAuthnRegistrationContext(clientDataJSON, attestationObject, serverProperty, isUserVerificationRequired);
WebAuthnRegistrationContextValidator webAuthnRegistrationContextValidator = createWebAuthnRegistrationContextValidator();
WebAuthnRegistrationContextValidationResponse response = webAuthnRegistrationContextValidator.validate(registrationContext);
// parse
RegistrationData registrationData = webAuthnManager.parse(registrationRequest);
// validate
webAuthnManager.validate(registrationData, registrationParameters);
showInfoAfterWebAuthnApiCreate(response);
showInfoAfterWebAuthnApiCreate(registrationData);
checkAcceptedAuthenticator(response, policy);
checkAcceptedAuthenticator(registrationData, policy);
WebAuthnCredentialModelInput credential = new WebAuthnCredentialModelInput(getCredentialType());
credential.setAttestedCredentialData(response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
credential.setCount(response.getAttestationObject().getAuthenticatorData().getSignCount());
credential.setAttestedCredentialData(registrationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData());
credential.setCount(registrationData.getAttestationObject().getAuthenticatorData().getSignCount());
// Save new webAuthn credential
WebAuthnCredentialProvider webAuthnCredProvider = (WebAuthnCredentialProvider) this.session.getProvider(CredentialProvider.class, getCredentialProviderId());
@ -245,8 +251,8 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
}
}
private WebAuthnRegistrationContextValidator createWebAuthnRegistrationContextValidator() {
return new WebAuthnRegistrationContextValidator(
private WebAuthnManager createWebAuthnManager() {
return new WebAuthnManager(
Arrays.asList(
new NoneAttestationStatementValidator(),
new PackedAttestationStatementValidator(),
@ -257,8 +263,10 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
), this.certPathtrustValidator,
new DefaultECDAATrustworthinessValidator(),
new DefaultSelfAttestationTrustworthinessValidator(),
new JsonConverter(),
new CborConverter());
Collections.emptyList(), // Custom Registration Validator is not supported
Collections.emptyList(), // Custom Authentication Validator is not supported
new ObjectConverter()
);
}
private String stringifySignatureAlgorithms(List<String> signatureAlgorithmsList) {
@ -304,7 +312,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
return sb.toString();
}
private void showInfoAfterWebAuthnApiCreate(WebAuthnRegistrationContextValidationResponse response) {
private void showInfoAfterWebAuthnApiCreate(RegistrationData response) {
AttestedCredentialData attestedCredentialData = response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData();
AttestationStatement attestationStatement = response.getAttestationObject().getAttestationStatement();
logger.debugv("createad key's algorithm = {0}", String.valueOf(attestedCredentialData.getCOSEKey().getAlgorithm().getValue()));
@ -312,7 +320,7 @@ public class WebAuthnRegister implements RequiredActionProvider, CredentialRegis
logger.debugv("attestation format = {0}", attestationStatement.getFormat());
}
private void checkAcceptedAuthenticator(WebAuthnRegistrationContextValidationResponse response, WebAuthnPolicy policy) throws Exception {
private void checkAcceptedAuthenticator(RegistrationData response, WebAuthnPolicy policy) throws Exception {
String aaguid = response.getAttestationObject().getAuthenticatorData().getAttestedCredentialData().getAaguid().toString();
List<String> acceptableAaguids = policy.getAcceptableAaguids();
boolean isAcceptedAuthenticator = false;

View file

@ -16,6 +16,7 @@
package org.keycloak.credential;
import com.webauthn4j.converter.util.ObjectConverter;
import org.keycloak.common.util.Base64Url;
import com.webauthn4j.converter.util.CborConverter;
@ -23,20 +24,20 @@ import com.webauthn4j.data.attestation.statement.AttestationStatement;
public class AttestationStatementConverter {
private CborConverter converter;
private CborConverter cborConverter;
public AttestationStatementConverter(CborConverter converter) {
this.converter = converter;
public AttestationStatementConverter(ObjectConverter objectConverter) {
this.cborConverter = objectConverter.getCborConverter();
}
public String convertToDatabaseColumn(AttestationStatement attribute) {
AttestationStatementSerializationContainer container = new AttestationStatementSerializationContainer(attribute);
return Base64Url.encode(converter.writeValueAsBytes(container));
return Base64Url.encode(cborConverter.writeValueAsBytes(container));
}
public AttestationStatement convertToEntityAttribute(String dbData) {
byte[] data = Base64Url.decode(dbData);
AttestationStatementSerializationContainer container = converter.readValue(data, AttestationStatementSerializationContainer.class);
AttestationStatementSerializationContainer container = cborConverter.readValue(data, AttestationStatementSerializationContainer.class);
return container.getAttestationStatement();
}
}

View file

@ -16,6 +16,7 @@
package org.keycloak.credential;
import com.webauthn4j.converter.util.ObjectConverter;
import org.keycloak.common.util.Base64Url;
import com.webauthn4j.converter.util.CborConverter;
@ -23,17 +24,17 @@ import com.webauthn4j.data.attestation.authenticator.COSEKey;
public class CredentialPublicKeyConverter {
private CborConverter converter;
private CborConverter cborConverter;
public CredentialPublicKeyConverter(CborConverter converter) {
this.converter = converter;
public CredentialPublicKeyConverter(ObjectConverter objectConverter) {
this.cborConverter = objectConverter.getCborConverter();
}
public String convertToDatabaseColumn(COSEKey credentialPublicKey) {
return Base64Url.encode(converter.writeValueAsBytes(credentialPublicKey));
return Base64Url.encode(cborConverter.writeValueAsBytes(credentialPublicKey));
}
public COSEKey convertToEntityAttribute(String s) {
return converter.readValue(Base64Url.decode(s), COSEKey.class);
return cborConverter.readValue(Base64Url.decode(s), COSEKey.class);
}
}

View file

@ -18,17 +18,18 @@ package org.keycloak.credential;
import org.keycloak.common.util.Base64;
import com.webauthn4j.data.WebAuthnAuthenticationContext;
import com.webauthn4j.data.AuthenticationParameters;
import com.webauthn4j.data.AuthenticationRequest;
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
import com.webauthn4j.data.attestation.authenticator.COSEKey;
import com.webauthn4j.data.attestation.statement.AttestationStatement;
import org.keycloak.models.credential.WebAuthnCredentialModel;
public class WebAuthnCredentialModelInput implements CredentialInput {
private AttestedCredentialData attestedCredentialData;
private AttestationStatement attestationStatement;
private WebAuthnAuthenticationContext authenticationContext;
private AuthenticationParameters authenticationParameters; // not persisted because it can only be used on authentication operation.
private AuthenticationRequest authenticationRequest; // not persisted because it can only be used on authentication operation.
private long count;
private String credentialDBId;
private final String credentialType;
@ -65,12 +66,20 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
return count;
}
public WebAuthnAuthenticationContext getAuthenticationContext() {
return authenticationContext;
public AuthenticationParameters getAuthenticationParameters() {
return authenticationParameters;
}
public void setAuthenticationContext(WebAuthnAuthenticationContext authenticationContext) {
this.authenticationContext = authenticationContext;
public void setAuthenticationParameters(AuthenticationParameters authenticationParameters) {
this.authenticationParameters = authenticationParameters;
}
public AuthenticationRequest getAuthenticationRequest() {
return authenticationRequest;
}
public void setAuthenticationRequest(AuthenticationRequest authenticationRequest) {
this.authenticationRequest = authenticationRequest;
}
public void setAttestedCredentialData(AttestedCredentialData attestedCredentialData) {
@ -127,10 +136,10 @@ public class WebAuthnCredentialModelInput implements CredentialInput {
.append(credPubKey.getKeyType().name())
.append(",");
}
if (authenticationContext != null) {
if (authenticationRequest != null) {
// only set on Authentication
sb.append("Credential Id = ")
.append(Base64.encodeBytes(authenticationContext.getCredentialId()))
.append(Base64.encodeBytes(authenticationRequest.getCredentialId()))
.append(",");
}
if (sb.length() > 0)

View file

@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.webauthn4j.converter.util.ObjectConverter;
import org.jboss.logging.Logger;
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
import org.keycloak.common.util.Base64;
@ -29,15 +30,15 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import com.webauthn4j.WebAuthnManager;
import com.webauthn4j.authenticator.Authenticator;
import com.webauthn4j.authenticator.AuthenticatorImpl;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.data.AuthenticationData;
import com.webauthn4j.data.AuthenticationParameters;
import com.webauthn4j.data.attestation.authenticator.AAGUID;
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
import com.webauthn4j.data.attestation.authenticator.COSEKey;
import com.webauthn4j.util.exception.WebAuthnException;
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidationResponse;
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
@ -53,12 +54,12 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
private CredentialPublicKeyConverter credentialPublicKeyConverter;
private AttestationStatementConverter attestationStatementConverter;
public WebAuthnCredentialProvider(KeycloakSession session, CborConverter converter) {
public WebAuthnCredentialProvider(KeycloakSession session, ObjectConverter objectConverter) {
this.session = session;
if (credentialPublicKeyConverter == null)
credentialPublicKeyConverter = new CredentialPublicKeyConverter(converter);
credentialPublicKeyConverter = new CredentialPublicKeyConverter(objectConverter);
if (attestationStatementConverter == null)
attestationStatementConverter = new AttestationStatementConverter(converter);
attestationStatementConverter = new AttestationStatementConverter(objectConverter);
}
private UserCredentialStore getCredentialStore() {
@ -163,26 +164,32 @@ public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCr
WebAuthnCredentialModelInput context = WebAuthnCredentialModelInput.class.cast(input);
List<WebAuthnCredentialModelInput> auths = getWebAuthnCredentialModelList(realm, user);
WebAuthnAuthenticationContextValidator webAuthnAuthenticationContextValidator =
new WebAuthnAuthenticationContextValidator();
WebAuthnManager webAuthnManager = WebAuthnManager.createNonStrictWebAuthnManager(); // not special setting is needed for authentication's validation.
AuthenticationData authenticationData = null;
try {
for (WebAuthnCredentialModelInput auth : auths) {
byte[] credentialId = auth.getAttestedCredentialData().getCredentialId();
if (Arrays.equals(credentialId, context.getAuthenticationContext().getCredentialId())) {
if (Arrays.equals(credentialId, context.getAuthenticationRequest().getCredentialId())) {
Authenticator authenticator = new AuthenticatorImpl(
auth.getAttestedCredentialData(),
auth.getAttestationStatement(),
auth.getCount()
);
// WebAuthnException is thrown if validation fails
WebAuthnAuthenticationContextValidationResponse response =
webAuthnAuthenticationContextValidator.validate(
context.getAuthenticationContext(),
authenticator);
// parse
authenticationData = webAuthnManager.parse(context.getAuthenticationRequest());
// validate
AuthenticationParameters authenticationParameters = new AuthenticationParameters(
context.getAuthenticationParameters().getServerProperty(),
authenticator,
context.getAuthenticationParameters().isUserVerificationRequired()
);
webAuthnManager.validate(authenticationData, authenticationParameters);
logger.debugv("response.getAuthenticatorData().getFlags() = {0}", response.getAuthenticatorData().getFlags());
logger.debugv("response.getAuthenticatorData().getFlags() = {0}", authenticationData.getAuthenticatorData().getFlags());
// update authenticator counter
long count = auth.getCount();

View file

@ -19,14 +19,14 @@ package org.keycloak.credential;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.ObjectConverter;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
public class WebAuthnCredentialProviderFactory implements CredentialProviderFactory<WebAuthnCredentialProvider>, EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "keycloak-webauthn";
private static CborConverter converter = new CborConverter();
private static ObjectConverter converter = new ObjectConverter();
@Override
public CredentialProvider create(KeycloakSession session) {

View file

@ -18,7 +18,7 @@
package org.keycloak.credential;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.ObjectConverter;
import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.credential.WebAuthnCredentialModel;
@ -30,8 +30,8 @@ import org.keycloak.models.credential.WebAuthnCredentialModel;
*/
public class WebAuthnPasswordlessCredentialProvider extends WebAuthnCredentialProvider {
public WebAuthnPasswordlessCredentialProvider(KeycloakSession session, CborConverter converter) {
super(session, converter);
public WebAuthnPasswordlessCredentialProvider(KeycloakSession session, ObjectConverter objectConverter) {
super(session, objectConverter);
}
@Override

View file

@ -18,7 +18,7 @@
package org.keycloak.credential;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.ObjectConverter;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
@ -30,11 +30,11 @@ public class WebAuthnPasswordlessCredentialProviderFactory implements Credential
public static final String PROVIDER_ID = "keycloak-webauthn-passwordless";
private static CborConverter converter = new CborConverter();
private static ObjectConverter objectConverter = new ObjectConverter();
@Override
public CredentialProvider create(KeycloakSession session) {
return new WebAuthnPasswordlessCredentialProvider(session, converter);
return new WebAuthnPasswordlessCredentialProvider(session, objectConverter);
}
@Override