webauthn
This commit is contained in:
parent
06d19858e3
commit
7a096cfc94
|
@ -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 (!empty($_SESSION['ldap'])) {
|
||||||
|
$userDn = $_SESSION['ldap']->getUserName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$userDn = $_SESSION['selfService_clientDN'];
|
||||||
|
}
|
||||||
|
$hasTokens = $webauthnManager->isRegistered($userDn);
|
||||||
|
if (!$hasTokens) {
|
||||||
if ($this->config->twoFactorAuthenticationOptional && !$webauthnManager->isRegistered($user) && ($_POST['sig_response'] === 'skip')) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
$response = base64_decode($_POST['sig_response']);
|
$response = base64_decode($_POST['sig_response']);
|
||||||
$registrationObject = PublicKeyCredentialCreationOptions::createFromString($_SESSION['webauthn_registration']);
|
$registrationObject = PublicKeyCredentialCreationOptions::createFromString($_SESSION['webauthn_registration']);
|
||||||
if ($webauthnManager->storeNewRegistration($registrationObject, $response)) {
|
return $webauthnManager->storeNewRegistration($registrationObject, $response);
|
||||||
return true;
|
}
|
||||||
|
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;
|
||||||
|
|
|
@ -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,17 +473,40 @@ 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(
|
||||||
|
':userId' => $userId,
|
||||||
|
':credentialId' => $credentialId
|
||||||
|
));
|
||||||
|
$results = $statement->fetchAll();
|
||||||
|
if (empty($results)) {
|
||||||
|
$statement = $pdo->prepare('insert into ' . self::TABLE_NAME . ' (userId, credentialId, credentialSource, registrationTime, lastUseTime) VALUES (?, ?, ?, ?, ?)');
|
||||||
$statement->execute(array(
|
$statement->execute(array(
|
||||||
$userId,
|
$userId,
|
||||||
$credentialId,
|
$credentialId,
|
||||||
$json,
|
$json,
|
||||||
$registrationTime
|
$currentTime,
|
||||||
|
$currentTime
|
||||||
));
|
));
|
||||||
logNewMessage(LOG_DEBUG, 'Stored new credential for ' . $userId);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the database URL.
|
* Returns the database URL.
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
@ -1463,6 +1466,52 @@ window.lam.webauthn.register = function(publicKey) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an array to a base64 string.
|
* Converts an array to a base64 string.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue