This commit is contained in:
Roland Gruber 2019-12-31 17:01:51 +01:00
parent 06d19858e3
commit 7a096cfc94
4 changed files with 228 additions and 19 deletions

View File

@ -573,13 +573,26 @@ class WebauthnProvider extends BaseProvider {
logNewMessage(LOG_DEBUG, 'WebauthnProvider: Checking 2nd factor for ' . $user); logNewMessage(LOG_DEBUG, 'WebauthnProvider: Checking 2nd factor for ' . $user);
include_once __DIR__ . '/webauthn.inc'; include_once __DIR__ . '/webauthn.inc';
$webauthnManager = new WebauthnManager(); $webauthnManager = new WebauthnManager();
if ($this->config->twoFactorAuthenticationOptional && !$webauthnManager->isRegistered($user) && ($_POST['sig_response'] === 'skip')) { if (!empty($_SESSION['ldap'])) {
return true; $userDn = $_SESSION['ldap']->getUserName();
} }
$response = base64_decode($_POST['sig_response']); else {
$registrationObject = PublicKeyCredentialCreationOptions::createFromString($_SESSION['webauthn_registration']); $userDn = $_SESSION['selfService_clientDN'];
if ($webauthnManager->storeNewRegistration($registrationObject, $response)) { }
return true; $hasTokens = $webauthnManager->isRegistered($userDn);
if (!$hasTokens) {
if ($this->config->twoFactorAuthenticationOptional && !$webauthnManager->isRegistered($user) && ($_POST['sig_response'] === 'skip')) {
logNewMessage(LOG_DEBUG, 'Skipped 2FA for ' . $user . ' as no devices are registered and 2FA is optional.');
return true;
}
$response = base64_decode($_POST['sig_response']);
$registrationObject = PublicKeyCredentialCreationOptions::createFromString($_SESSION['webauthn_registration']);
return $webauthnManager->storeNewRegistration($registrationObject, $response);
}
else {
logNewMessage(LOG_DEBUG, 'Checking webauthn response of ' . $userDn);
$response = base64_decode($_POST['sig_response']);
return $webauthnManager->isValidAuthentication($response, $userDn);
} }
logNewMessage(LOG_ERR, 'Webauthn authentication failed'); logNewMessage(LOG_ERR, 'Webauthn authentication failed');
return false; return false;

View File

@ -19,6 +19,7 @@ use Nyholm\Psr7\Factory\Psr17Factory;
use PDO; use PDO;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Throwable;
use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport; use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport;
use Webauthn\AttestationStatement\AttestationObjectLoader; use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AttestationStatement\AttestationStatementSupportManager; use Webauthn\AttestationStatement\AttestationStatementSupportManager;
@ -27,11 +28,14 @@ use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AttestationStatement\PackedAttestationStatementSupport; use Webauthn\AttestationStatement\PackedAttestationStatementSupport;
use Webauthn\AttestationStatement\TPMAttestationStatementSupport; use Webauthn\AttestationStatement\TPMAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponse; use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator; use Webauthn\AuthenticatorAttestationResponseValidator;
use \Webauthn\PublicKeyCredentialCreationOptions; use \Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialDescriptor; use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialLoader; use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialRequestOptions;
use \Webauthn\PublicKeyCredentialRpEntity; use \Webauthn\PublicKeyCredentialRpEntity;
use \Webauthn\PublicKeyCredentialParameters; use \Webauthn\PublicKeyCredentialParameters;
use Webauthn\PublicKeyCredentialSource; use Webauthn\PublicKeyCredentialSource;
@ -101,10 +105,10 @@ class WebauthnManager {
public function getRegistrationObject($dn, $isSelfService) { public function getRegistrationObject($dn, $isSelfService) {
$rpEntity = $this->createRpEntry($isSelfService); $rpEntity = $this->createRpEntry($isSelfService);
$userEntity = $this->getUserEntity($dn); $userEntity = $this->getUserEntity($dn);
$challenge = generateRandomPassword(32); $challenge = $this->createChallenge();
$credentialParameters = $this->getCredentialParameters(); $credentialParameters = $this->getCredentialParameters();
$excludedKeys = $this->getExcludedKeys($userEntity); $excludedKeys = $this->getExcludedKeys($userEntity);
$timeout = 20000; $timeout = $this->getTimeout();
$registrationObject = new PublicKeyCredentialCreationOptions( $registrationObject = new PublicKeyCredentialCreationOptions(
$rpEntity, $rpEntity,
$userEntity, $userEntity,
@ -240,6 +244,17 @@ class WebauthnManager {
$manager->add(new FidoU2FAttestationStatementSupport()); $manager->add(new FidoU2FAttestationStatementSupport());
$manager->add(new AndroidKeyAttestationStatementSupport($decoder)); $manager->add(new AndroidKeyAttestationStatementSupport($decoder));
$manager->add(new TPMAttestationStatementSupport()); $manager->add(new TPMAttestationStatementSupport());
$coseManager = $this->getAlgorithmManager();
$manager->add(new PackedAttestationStatementSupport($decoder, $coseManager));
return $manager;
}
/**
* Returns the COSE algorithm manager.
*
* @return Manager algorithm manager
*/
private function getAlgorithmManager() {
$coseManager = new Manager(); $coseManager = new Manager();
$coseManager->add(new ES256()); $coseManager->add(new ES256());
$coseManager->add(new ES384()); $coseManager->add(new ES384());
@ -249,8 +264,7 @@ class WebauthnManager {
$coseManager->add(new RS256()); $coseManager->add(new RS256());
$coseManager->add(new RS384); $coseManager->add(new RS384);
$coseManager->add(new RS512()); $coseManager->add(new RS512());
$manager->add(new PackedAttestationStatementSupport($decoder, $coseManager)); return $coseManager;
return $manager;
} }
/** /**
@ -294,6 +308,104 @@ class WebauthnManager {
return new PublicKeyCredentialSourceRepositorySQLite(); return new PublicKeyCredentialSourceRepositorySQLite();
} }
/**
* Returns the timeout for user operations.
*
* @return int timeout in ms
*/
private function getTimeout() {
return 120000;
}
/**
* Creates a new challenge.
*
* @return String challenge
*/
private function createChallenge() {
return generateRandomPassword(32);
}
/**
* Returns the authentication object for a registered user.
*
* @param $userDN user DN
* @param bool $isSelfService self service
* @return PublicKeyCredentialRequestOptions authentication object
*/
public function getAuthenticationObject($userDN, bool $isSelfService) {
$timeout = $this->getTimeout();
$challenge = $this->createChallenge();
$database = $this->getDatabase();
$userEntity = $this->getUserEntity($userDN);
$publicKeyCredentialSources = $database->findAllForUserEntity($userEntity);
$publicKeyDescriptors = array();
foreach ($publicKeyCredentialSources as $publicKeyCredentialSource) {
$publicKeyDescriptors[] = $publicKeyCredentialSource->getPublicKeyCredentialDescriptor();
}
$userVerification = PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
$extensions = new AuthenticationExtensionsClientInputs();
$relyingParty = $this->createRpEntry($isSelfService);
return new PublicKeyCredentialRequestOptions(
$challenge,
$timeout,
$relyingParty->getId(),
$publicKeyDescriptors,
$userVerification,
$extensions
);
}
/**
* Checks if the provided authentication is valid.
*
* @param string $response authentication response
* @param string $userDn user DN
* @return bool true if all is ok
*/
public function isValidAuthentication(string $response, string $userDn) {
$database = $this->getDatabase();
$decoder = $this->getCborDecoder();
$tokenBindingHandler = new IgnoreTokenBindingHandler();
$extensionOutputCheckerHandler = $this->getExtensionOutputChecker();
$algorithmManager = $this->getAlgorithmManager();
$responseValidator = new AuthenticatorAssertionResponseValidator(
$database,
$decoder,
$tokenBindingHandler,
$extensionOutputCheckerHandler,
$algorithmManager
);
$attestationSupportManager = $this->getAttestationSupportManager($decoder);
$attestationObjectLoader = $this->getAttestationObjectLoader($attestationSupportManager, $decoder);
$publicKeyCredentialLoader = $this->getPublicKeyCredentialLoader($attestationObjectLoader, $decoder);
$publicKeyCredential = $publicKeyCredentialLoader->load($response);
$authenticatorAssertionResponse = $publicKeyCredential->getResponse();
if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) {
logNewMessage(LOG_ERR, 'Invalid authenticator assertion response');
return false;
}
try {
$symfonyRequest = Request::createFromGlobals();
$psr17Factory = new Psr17Factory();
$psrFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$psr7Request = $psrFactory->createRequest($symfonyRequest);
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($_SESSION['webauthn_authentication']);
$responseValidator->check(
$publicKeyCredential->getRawId(),
$publicKeyCredential->getResponse(),
$publicKeyCredentialRequestOptions,
$psr7Request,
$userDn
);
return true;
}
catch (Throwable $e) {
logNewMessage(LOG_ERR, 'Error validating webauthn authentication: ' . $e->getMessage());
}
return false;
}
} }
/** /**
@ -361,16 +473,39 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo
$json = json_encode($publicKeyCredentialSource); $json = json_encode($publicKeyCredentialSource);
$credentialId = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId()); $credentialId = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId());
$userId = $publicKeyCredentialSource->getUserHandle(); $userId = $publicKeyCredentialSource->getUserHandle();
$registrationTime = time(); $currentTime = time();
$pdo = $this->getPDO(); $pdo = $this->getPDO();
$statement = $pdo->prepare('insert into ' . self::TABLE_NAME . ' (userId, credentialId, credentialSource, registrationTime) VALUES (?, ?, ?, ?)'); $statement = $pdo->prepare('select * from ' . self::TABLE_NAME . ' where userId = :userId and credentialId = :credentialId');
$statement->execute(array( $statement->execute(array(
$userId, ':userId' => $userId,
$credentialId, ':credentialId' => $credentialId
$json,
$registrationTime
)); ));
logNewMessage(LOG_DEBUG, 'Stored new credential for ' . $userId); $results = $statement->fetchAll();
if (empty($results)) {
$statement = $pdo->prepare('insert into ' . self::TABLE_NAME . ' (userId, credentialId, credentialSource, registrationTime, lastUseTime) VALUES (?, ?, ?, ?, ?)');
$statement->execute(array(
$userId,
$credentialId,
$json,
$currentTime,
$currentTime
));
logNewMessage(LOG_DEBUG, 'Stored new credential for ' . $userId);
}
else {
$statement = $pdo->prepare(
'update ' . self::TABLE_NAME .
' set credentialSource = :credentialSource, lastUseTime = :lastUseTime' .
' WHERE userId = :userId AND credentialId = :credentialId'
);
$statement->execute(array(
':credentialSource' => $json,
':lastUseTime' => $currentTime,
':userId' => $userId,
':credentialId' => $credentialId
));
logNewMessage(LOG_DEBUG, 'Stored updated credential for ' . $userId);
}
} }
/** /**
@ -432,6 +567,7 @@ class PublicKeyCredentialSourceRepositorySQLite implements PublicKeyCredentialSo
. 'credentialId TEXT NOT NULL,' . 'credentialId TEXT NOT NULL,'
. 'credentialSource TEXT NOT NULL,' . 'credentialSource TEXT NOT NULL,'
. 'registrationTime VARCHAR(11) NOT NULL,' . 'registrationTime VARCHAR(11) NOT NULL,'
. 'lastUseTime VARCHAR(11) NOT NULL,'
. 'PRIMARY KEY(userId,credentialId)' . 'PRIMARY KEY(userId,credentialId)'
. ');'; . ');';
$pdo->exec($sql); $pdo->exec($sql);

View File

@ -1409,6 +1409,9 @@ window.lam.webauthn.run = function(prefix) {
if (jsonData.action === 'register') { if (jsonData.action === 'register') {
window.lam.webauthn.register(jsonData.registration); window.lam.webauthn.register(jsonData.registration);
} }
else if (jsonData.action === 'authenticate') {
window.lam.webauthn.authenticate(jsonData.authentication);
}
}) })
.fail(function() { .fail(function() {
console.log('Webauthn failed'); console.log('Webauthn failed');
@ -1425,7 +1428,7 @@ window.lam.webauthn.register = function(publicKey) {
publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), c=>c.charCodeAt(0)); publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), c=>c.charCodeAt(0));
publicKey.rp.icon = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + publicKey.rp.icon; publicKey.rp.icon = window.location.href.substring(0, window.location.href.lastIndexOf("/")) + publicKey.rp.icon;
if (publicKey.excludeCredentials) { if (publicKey.excludeCredentials) {
for (var i = 0; i < publicKey.excludeCredentials.length; i++) { for (let i = 0; i < publicKey.excludeCredentials.length; i++) {
let idOrig = publicKey.excludeCredentials[i]['id']; let idOrig = publicKey.excludeCredentials[i]['id'];
idOrig = idOrig.replace(/-/g, "+").replace(/_/g, "/"); idOrig = idOrig.replace(/-/g, "+").replace(/_/g, "/");
let idOrigDecoded = atob(idOrig); let idOrigDecoded = atob(idOrig);
@ -1449,6 +1452,52 @@ window.lam.webauthn.register = function(publicKey) {
form.append('<input type="hidden" name="sig_response" value="' + response + '"/>'); form.append('<input type="hidden" name="sig_response" value="' + response + '"/>');
form.submit(); form.submit();
}, function (error) { }, function (error) {
console.log(error.message);
let errorDiv = jQuery('#generic-webauthn-error');
let buttonLabel = errorDiv.data('button');
let dialogTitle = errorDiv.data('title');
errorDiv.text(error.message);
window.lam.dialog.showMessage(dialogTitle,
buttonLabel,
'generic-webauthn-error',
function () {
jQuery('#btn_logout').click();
});
});
}
/**
* Performs a webauthn authentication.
*
* @param publicKey authentication object
*/
window.lam.webauthn.authenticate = function(publicKey) {
publicKey.challenge = Uint8Array.from(window.atob(publicKey.challenge), c => c.charCodeAt(0));
for (let i = 0; i < publicKey.allowCredentials.length; i++) {
let idOrig = publicKey.allowCredentials[i]['id'];
idOrig = idOrig.replace(/-/g, "+").replace(/_/g, "/");
let idOrigDecoded = atob(idOrig);
let idArray = Uint8Array.from(idOrigDecoded, c => c.charCodeAt(0))
publicKey.allowCredentials[i]['id'] = idArray;
}
navigator.credentials.get({publicKey})
.then(data => {
let publicKeyCredential = {
id: data.id,
type: data.type,
rawId: window.lam.webauthn.arrayToBase64String(new Uint8Array(data.rawId)),
response: {
authenticatorData: window.lam.webauthn.arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
clientDataJSON: window.lam.webauthn.arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
signature: window.lam.webauthn.arrayToBase64String(new Uint8Array(data.response.signature)),
userHandle: data.response.userHandle ? window.lam.webauthn.arrayToBase64String(new Uint8Array(data.response.userHandle)) : null
}
};
let form = jQuery("#2faform");
let response = btoa(JSON.stringify(publicKeyCredential));
form.append('<input type="hidden" name="sig_response" value="' + response + '"/>');
form.submit();
}, error => {
console.log(error.message); console.log(error.message);
let errorDiv = jQuery('#generic-webauthn-error'); let errorDiv = jQuery('#generic-webauthn-error');
let buttonLabel = errorDiv.data('button'); let buttonLabel = errorDiv.data('button');

View File

@ -204,8 +204,19 @@ class Ajax {
), ),
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
); );
die();
} }
else {
$authenticationObject = $webauthnManager->getAuthenticationObject($userDN, $isSelfService);
$_SESSION['webauthn_authentication'] = json_encode($authenticationObject);
echo json_encode(
array(
'action' => 'authenticate',
'authentication' => $authenticationObject
),
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
}
die();
} }
/** /**