keycloak-scim/services/src/main/java/org/keycloak/credential/WebAuthnCredentialProvider.java
AlistairDoswald 4553234f64 KEYCLOAK-11745 Multi-factor authentication (#6459)
Co-authored-by: Christophe Frattino <christophe.frattino@elca.ch>
Co-authored-by: Francis PEROT <francis.perot@elca.ch>
Co-authored-by: rpo <harture414@gmail.com>
Co-authored-by: mposolda <mposolda@gmail.com>
Co-authored-by: Jan Lieskovsky <jlieskov@redhat.com>
Co-authored-by: Denis <drichtar@redhat.com>
Co-authored-by: Tomas Kyjovsky <tkyjovsk@redhat.com>
2019-11-14 14:45:05 +01:00

228 lines
9.4 KiB
Java

/*
* 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;
import java.util.stream.Collectors;
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;
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;
public class WebAuthnCredentialProvider implements CredentialProvider<WebAuthnCredentialModel>, CredentialInputValidator {
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);
}
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);
}
@Override
public WebAuthnCredentialModel getCredentialFromModel(CredentialModel model) {
return WebAuthnCredentialModel.createFromCredentialModel(model);
}
/**
* 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) {
if (!supportsCredentialType(input.getType())) return null;
WebAuthnCredentialModelInput webAuthnModel = (WebAuthnCredentialModelInput) input;
String aaguid = webAuthnModel.getAttestedCredentialData().getAaguid().toString();
String credentialId = Base64.encodeBytes(webAuthnModel.getAttestedCredentialData().getCredentialId());
String credentialPublicKey = credentialPublicKeyConverter.convertToDatabaseColumn(webAuthnModel.getAttestedCredentialData().getCOSEKey());
long counter = webAuthnModel.getCount();
WebAuthnCredentialModel model = WebAuthnCredentialModel.create(userLabel, aaguid, credentialId, null, credentialPublicKey, counter);
model.setId(webAuthnModel.getCredentialDBId());
return model;
}
/**
* 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
}
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;
}
@Override
public boolean supportsCredentialType(String credentialType) {
return WebAuthnCredentialModelInput.WEBAUTHN_CREDENTIAL_TYPE.equals(credentialType);
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
if (!supportsCredentialType(credentialType)) return false;
return !session.userCredentialManager().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!WebAuthnCredentialModelInput.class.isInstance(input)) return false;
WebAuthnCredentialModelInput context = WebAuthnCredentialModelInput.class.cast(input);
List<WebAuthnCredentialModelInput> auths = getWebAuthnCredentialModelList(realm, user);
WebAuthnAuthenticationContextValidator webAuthnAuthenticationContextValidator =
new WebAuthnAuthenticationContextValidator();
try {
for (WebAuthnCredentialModelInput auth : auths) {
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);
logger.debugv("response.getAuthenticatorData().getFlags() = {0}", response.getAuthenticatorData().getFlags());
// update authenticator counter
long count = auth.getCount();
CredentialModel credModel = getCredentialStore().getStoredCredentialById(realm, user, auth.getCredentialDBId());
WebAuthnCredentialModel webAuthnCredModel = getCredentialFromModel(credModel);
webAuthnCredModel.updateCounter(count + 1);
getCredentialStore().updateCredential(realm, user, webAuthnCredModel);
logger.debugf("Successfully validated WebAuthn credential for user %s", user.getUsername());
dumpCredentialModel(webAuthnCredModel, auth);
return true;
}
}
} catch (WebAuthnException wae) {
wae.printStackTrace();
throw(wae);
}
// no authenticator matched
return false;
}
@Override
public String getType() {
return WebAuthnCredentialModel.TYPE;
}
private List<WebAuthnCredentialModelInput> getWebAuthnCredentialModelList(RealmModel realm, UserModel user) {
List<CredentialModel> credentialModels = session.userCredentialManager().getStoredCredentialsByType(realm, user, WebAuthnCredentialModel.TYPE);
return credentialModels.stream()
.map(this::getCredentialInputFromCredentialModel)
.collect(Collectors.toList());
}
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);
}
}
}