2019-10-01 13:17:38 +00:00
|
|
|
/*
|
|
|
|
* Copyright 2002-2019 the original author or authors.
|
|
|
|
*
|
|
|
|
* 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 java.io.IOException;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.List;
|
2019-11-14 13:45:05 +00:00
|
|
|
import java.util.stream.Collectors;
|
2019-10-01 13:17:38 +00:00
|
|
|
|
|
|
|
import org.jboss.logging.Logger;
|
|
|
|
import org.keycloak.common.util.Base64;
|
|
|
|
import org.keycloak.common.util.Time;
|
|
|
|
import org.keycloak.models.KeycloakSession;
|
|
|
|
import org.keycloak.models.RealmModel;
|
|
|
|
import org.keycloak.models.UserModel;
|
|
|
|
|
|
|
|
import com.webauthn4j.authenticator.Authenticator;
|
|
|
|
import com.webauthn4j.authenticator.AuthenticatorImpl;
|
|
|
|
import com.webauthn4j.converter.util.CborConverter;
|
|
|
|
import com.webauthn4j.data.attestation.authenticator.AAGUID;
|
|
|
|
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
|
2019-11-05 08:23:09 +00:00
|
|
|
import com.webauthn4j.data.attestation.authenticator.COSEKey;
|
2019-10-01 13:17:38 +00:00
|
|
|
import com.webauthn4j.util.exception.WebAuthnException;
|
|
|
|
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidationResponse;
|
|
|
|
import com.webauthn4j.validator.WebAuthnAuthenticationContextValidator;
|
2019-11-14 13:45:05 +00:00
|
|
|
import org.keycloak.models.credential.WebAuthnCredentialModel;
|
|
|
|
import org.keycloak.models.credential.dto.WebAuthnCredentialData;
|
2019-10-01 13:17:38 +00:00
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCredentialModel>, CredentialInputValidator {
|
2019-10-01 13:17:38 +00:00
|
|
|
|
|
|
|
private static final Logger logger = Logger.getLogger(WebAuthnCredentialProvider.class);
|
|
|
|
|
|
|
|
private KeycloakSession session;
|
|
|
|
|
|
|
|
private CredentialPublicKeyConverter credentialPublicKeyConverter;
|
|
|
|
private AttestationStatementConverter attestationStatementConverter;
|
|
|
|
|
|
|
|
public WebAuthnCredentialProvider(KeycloakSession session, CborConverter converter) {
|
|
|
|
this.session = session;
|
|
|
|
if (credentialPublicKeyConverter == null)
|
|
|
|
credentialPublicKeyConverter = new CredentialPublicKeyConverter(converter);
|
|
|
|
if (attestationStatementConverter == null)
|
|
|
|
attestationStatementConverter = new AttestationStatementConverter(converter);
|
|
|
|
}
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
private UserCredentialStore getCredentialStore() {
|
|
|
|
return session.userCredentialManager();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public CredentialModel createCredential(RealmModel realm, UserModel user, WebAuthnCredentialModel credentialModel) {
|
|
|
|
if (credentialModel.getCreatedDate() == null) {
|
|
|
|
credentialModel.setCreatedDate(Time.currentTimeMillis());
|
|
|
|
}
|
|
|
|
|
|
|
|
return getCredentialStore().createCredential(realm, user, credentialModel);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void deleteCredential(RealmModel realm, UserModel user, String credentialId) {
|
|
|
|
logger.debugv("Delete WebAuthn credential. username = {0}, credentialId = {1}", user.getUsername(), credentialId);
|
|
|
|
getCredentialStore().removeStoredCredential(realm, user, credentialId);
|
|
|
|
}
|
|
|
|
|
2019-10-01 13:17:38 +00:00
|
|
|
@Override
|
2019-11-14 13:45:05 +00:00
|
|
|
public WebAuthnCredentialModel getCredentialFromModel(CredentialModel model) {
|
|
|
|
return WebAuthnCredentialModel.createFromCredentialModel(model);
|
2019-10-01 13:17:38 +00:00
|
|
|
}
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert WebAuthn credential input to the model, which can be saved in the persistent storage (DB)
|
|
|
|
*
|
|
|
|
* @param input should be typically WebAuthnCredentialModelInput
|
|
|
|
* @param userLabel label for the credential
|
|
|
|
*/
|
|
|
|
public WebAuthnCredentialModel getCredentialModelFromCredentialInput(CredentialInput input, String userLabel) {
|
2019-10-01 13:17:38 +00:00
|
|
|
if (!supportsCredentialType(input.getType())) return null;
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
WebAuthnCredentialModelInput webAuthnModel = (WebAuthnCredentialModelInput) input;
|
2019-10-01 13:17:38 +00:00
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
String aaguid = webAuthnModel.getAttestedCredentialData().getAaguid().toString();
|
|
|
|
String credentialId = Base64.encodeBytes(webAuthnModel.getAttestedCredentialData().getCredentialId());
|
|
|
|
String credentialPublicKey = credentialPublicKeyConverter.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCOSEKey());
|
|
|
|
long counter = webAuthnModel.getCount();
|
2019-10-01 13:17:38 +00:00
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
WebAuthnCredentialModel model = WebAuthnCredentialModel.create(userLabel, aaguid, credentialId, null, credentialPublicKey, counter);
|
2019-10-01 13:17:38 +00:00
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
model.setId(webAuthnModel.getCredentialDBId());
|
2019-10-01 13:17:38 +00:00
|
|
|
|
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert WebAuthnCredentialModel, which was usually retrieved from DB, to the CredentialInput, which contains data in the webauthn4j specific format
|
|
|
|
*/
|
|
|
|
private WebAuthnCredentialModelInput getCredentialInputFromCredentialModel(CredentialModel credential) {
|
|
|
|
WebAuthnCredentialModel webAuthnCredential = getCredentialFromModel(credential);
|
|
|
|
|
|
|
|
WebAuthnCredentialData credData = webAuthnCredential.getWebAuthnCredentialData();
|
|
|
|
|
|
|
|
WebAuthnCredentialModelInput auth = new WebAuthnCredentialModelInput();
|
|
|
|
|
|
|
|
byte[] credentialId = null;
|
|
|
|
try {
|
|
|
|
credentialId = Base64.decode(credData.getCredentialId());
|
|
|
|
} catch (IOException ioe) {
|
|
|
|
// NOP
|
2019-10-01 13:17:38 +00:00
|
|
|
}
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
AAGUID aaguid = new AAGUID(credData.getAaguid());
|
|
|
|
|
|
|
|
COSEKey pubKey = credentialPublicKeyConverter.convertToEntityAttribute(credData.getCredentialPublicKey());
|
|
|
|
|
|
|
|
AttestedCredentialData attrCredData = new AttestedCredentialData(aaguid, credentialId, pubKey);
|
|
|
|
|
|
|
|
auth.setAttestedCredentialData(attrCredData);
|
|
|
|
|
|
|
|
long count = credData.getCounter();
|
|
|
|
auth.setCount(count);
|
|
|
|
|
|
|
|
auth.setCredentialDBId(credential.getId());
|
|
|
|
|
|
|
|
return auth;
|
2019-10-01 13:17:38 +00:00
|
|
|
}
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
|
2019-10-01 13:17:38 +00:00
|
|
|
@Override
|
|
|
|
public boolean supportsCredentialType(String credentialType) {
|
2019-11-14 13:45:05 +00:00
|
|
|
return WebAuthnCredentialModelInput.WEBAUTHN_CREDENTIAL_TYPE.equals(credentialType);
|
2019-10-01 13:17:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
|
|
|
|
if (!supportsCredentialType(credentialType)) return false;
|
|
|
|
return !session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
|
|
|
|
}
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
|
2019-10-01 13:17:38 +00:00
|
|
|
@Override
|
|
|
|
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
2019-11-14 13:45:05 +00:00
|
|
|
if (!WebAuthnCredentialModelInput.class.isInstance(input)) return false;
|
2019-10-01 13:17:38 +00:00
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
WebAuthnCredentialModelInput context = WebAuthnCredentialModelInput.class.cast(input);
|
|
|
|
List<WebAuthnCredentialModelInput> auths = getWebAuthnCredentialModelList(realm, user);
|
2019-10-01 13:17:38 +00:00
|
|
|
|
|
|
|
WebAuthnAuthenticationContextValidator webAuthnAuthenticationContextValidator =
|
|
|
|
new WebAuthnAuthenticationContextValidator();
|
|
|
|
try {
|
2019-11-14 13:45:05 +00:00
|
|
|
for (WebAuthnCredentialModelInput auth : auths) {
|
2019-10-01 13:17:38 +00:00
|
|
|
|
|
|
|
byte[] credentialId = auth.getAttestedCredentialData().getCredentialId();
|
|
|
|
if (Arrays.equals(credentialId, context.getAuthenticationContext().getCredentialId())) {
|
|
|
|
Authenticator authenticator = new AuthenticatorImpl(
|
|
|
|
auth.getAttestedCredentialData(),
|
|
|
|
auth.getAttestationStatement(),
|
|
|
|
auth.getCount()
|
|
|
|
);
|
|
|
|
|
|
|
|
// WebAuthnException is thrown if validation fails
|
|
|
|
WebAuthnAuthenticationContextValidationResponse response =
|
|
|
|
webAuthnAuthenticationContextValidator.validate(
|
|
|
|
context.getAuthenticationContext(),
|
|
|
|
authenticator);
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
logger.debugv("response.getAuthenticatorData().getFlags() = {0}", response.getAuthenticatorData().getFlags());
|
2019-10-01 13:17:38 +00:00
|
|
|
|
|
|
|
// update authenticator counter
|
|
|
|
long count = auth.getCount();
|
2019-11-14 13:45:05 +00:00
|
|
|
CredentialModel credModel = getCredentialStore().getStoredCredentialById(realm, user, auth.getCredentialDBId());
|
|
|
|
WebAuthnCredentialModel webAuthnCredModel = getCredentialFromModel(credModel);
|
|
|
|
webAuthnCredModel.updateCounter(count + 1);
|
|
|
|
getCredentialStore().updateCredential(realm, user, webAuthnCredModel);
|
2019-10-01 13:17:38 +00:00
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
logger.debugf("Successfully validated WebAuthn credential for user %s", user.getUsername());
|
|
|
|
dumpCredentialModel(webAuthnCredModel, auth);
|
2019-10-01 13:17:38 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (WebAuthnException wae) {
|
|
|
|
wae.printStackTrace();
|
|
|
|
throw(wae);
|
|
|
|
}
|
|
|
|
// no authenticator matched
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
@Override
|
|
|
|
public String getType() {
|
|
|
|
return WebAuthnCredentialModel.TYPE;
|
|
|
|
}
|
2019-10-01 13:17:38 +00:00
|
|
|
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
private List<WebAuthnCredentialModelInput> getWebAuthnCredentialModelList(RealmModel realm, UserModel user) {
|
|
|
|
List<CredentialModel> credentialModels = session.userCredentialManager().getStoredCredentialsByType(realm, user, WebAuthnCredentialModel.TYPE);
|
2019-10-01 13:17:38 +00:00
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
return credentialModels.stream()
|
|
|
|
.map(this::getCredentialInputFromCredentialModel)
|
|
|
|
.collect(Collectors.toList());
|
2019-10-01 13:17:38 +00:00
|
|
|
}
|
|
|
|
|
2019-11-14 13:45:05 +00:00
|
|
|
public void dumpCredentialModel(WebAuthnCredentialModel credential, WebAuthnCredentialModelInput auth) {
|
|
|
|
if(logger.isDebugEnabled()) {
|
|
|
|
logger.debug(" Persisted Credential Info::");
|
|
|
|
logger.debug(credential);
|
|
|
|
logger.debug(" Context Credential Info::");
|
|
|
|
logger.debug(auth);
|
|
|
|
}
|
2019-10-01 13:17:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|