diff --git a/out/out.SetupWebauthn.php b/out/out.SetupWebauthn.php new file mode 100644 index 000000000..641e01c49 --- /dev/null +++ b/out/out.SetupWebauthn.php @@ -0,0 +1,39 @@ + + * @copyright Copyright (C) 2016 Uwe Steinmann + * @version Release: @package_version@ + */ + +if(!isset($settings)) + require_once("../inc/inc.Settings.php"); +require_once("inc/inc.Language.php"); +require_once("inc/inc.Init.php"); +require_once("inc/inc.Extension.php"); +require_once("inc/inc.DBInit.php"); +require_once("inc/inc.ClassUI.php"); +require_once("inc/inc.WebAuthn.php"); +require_once("inc/inc.Authentication.php"); + +$tmp = explode('.', basename($_SERVER['SCRIPT_FILENAME'])); +$view = UI::factory($theme, $tmp[1], array('dms'=>$dms, 'user'=>$user)); +$accessop = new SeedDMS_AccessOperation($dms, $user, $settings); + +if ($user->isGuest()) { + UI::exitError(getMLText("webauthn"),getMLText("access_denied")); +} + +if($view) { + $view->setParam('sitename', $settings->_siteName); + $view->setParam('enable2factauth', $settings->_enable2FactorAuthentication); + $view->setParam('accessobject', $accessop); + $view($_REQUEST); + exit; +} + diff --git a/views/bootstrap/class.SetupWebauthn.php b/views/bootstrap/class.SetupWebauthn.php new file mode 100644 index 000000000..a71d11ee5 --- /dev/null +++ b/views/bootstrap/class.SetupWebauthn.php @@ -0,0 +1,414 @@ + + * @copyright Copyright (C) 2016 Uwe Steinmann + * @version Release: @package_version@ + */ + +/** + * Include parent class + */ +require_once("class.Bootstrap.php"); + +/** + * Include classes for 2-factor authentication + */ +require "vendor/autoload.php"; + +/** + * Class which outputs the html page for ForcePasswordChange view + * + * @category DMS + * @package SeedDMS + * @author Markus Westphal, Malcolm Cowe, Uwe Steinmann + * @copyright Copyright (C) 2016 Uwe Steinmann + * @version Release: @package_version@ + */ +class SeedDMS_View_SetupWebauthn extends SeedDMS_Bootstrap_Style { + + function css() { /* {{{ */ + header('Content-Type: text/css; charset=UTF-8'); +?> +.cdokey { + display: none; + background-color: orange; + color: white; + font-weight: bold; + margin: 10px 0; + padding: 10px; +} + +function webauthnRegister(key, callback){ + key = JSON.parse(key); + key.publicKey.attestation = undefined; + key.publicKey.challenge = new Uint8Array(key.publicKey.challenge); // convert type for use by key + key.publicKey.user.id = new Uint8Array(key.publicKey.user.id); + + navigator.credentials.create({publicKey: key.publicKey}) + .then(function (aNewCredentialInfo) { + console.log("Credentials.Create response: ", aNewCredentialInfo); + var cd = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(aNewCredentialInfo.response.clientDataJSON))); + if (key.b64challenge != cd.challenge) { + callback(false, 'key returned something unexpected (1)'); + } + if ('http://'+key.publicKey.rp.name != cd.origin) { + console.log(key.publicKey.rp.name); + console.log(cd.origin); + return callback(false, 'key returned something unexpected (2)'); + } + if (! ('type' in cd)) { + return callback(false, 'key returned something unexpected (3)'); + } + if (cd.type != 'webauthn.create') { + return callback(false, 'key returned something unexpected (4)'); + } + + var ao = []; + (new Uint8Array(aNewCredentialInfo.response.attestationObject)).forEach(function(v){ + ao.push(v); + }); + var rawId = []; + (new Uint8Array(aNewCredentialInfo.rawId)).forEach(function(v){ + rawId.push(v); + }); + var info = { + rawId: rawId, + id: aNewCredentialInfo.id, + type: aNewCredentialInfo.type, + response: { + attestationObject: ao, + clientDataJSON: + JSON.parse(String.fromCharCode.apply(null, new Uint8Array(aNewCredentialInfo.response.clientDataJSON))) + } + }; + console.log("Info: ", info); + callback(true, JSON.stringify(info)); + }) + .catch(function (aErr) { + if ( + ("name" in aErr) && (aErr.name == "AbortError" || aErr.name == "NS_ERROR_ABORT") + || aErr.name == 'NotAllowedError' + ) { + callback(false, 'abort'); + } else { + callback(false, aErr.toString()); + } + }); +} +function webauthnAuthenticate(key, cb){ + var pk = JSON.parse(key); + var originalChallenge = pk.challenge; + pk.challenge = new Uint8Array(pk.challenge); + pk.allowCredentials.forEach(function(k, idx){ + pk.allowCredentials[idx].id = new Uint8Array(k.id); + }); + /* ask the browser to prompt the user */ + navigator.credentials.get({publicKey: pk}) + .then(function(aAssertion) { + // console.log("Credentials.Get response: ", aAssertion); + var ida = []; + (new Uint8Array(aAssertion.rawId)).forEach(function(v){ ida.push(v); }); + var cd = JSON.parse(String.fromCharCode.apply(null, + new Uint8Array(aAssertion.response.clientDataJSON))); + var cda = []; + (new Uint8Array(aAssertion.response.clientDataJSON)).forEach(function(v){ cda.push(v); }); + var ad = []; + (new Uint8Array(aAssertion.response.authenticatorData)).forEach(function(v){ ad.push(v); }); + var sig = []; + (new Uint8Array(aAssertion.response.signature)).forEach(function(v){ sig.push(v); }); + var info = { + type: aAssertion.type, + originalChallenge: originalChallenge, + rawId: ida, + response: { + authenticatorData: ad, + clientData: cd, + clientDataJSONarray: cda, + signature: sig + } + }; + cb(true, JSON.stringify(info)); + }) + .catch(function (aErr) { + if (("name" in aErr) && (aErr.name == "AbortError" || aErr.name == "NS_ERROR_ABORT" || + aErr.name == "NotAllowedError")) { + cb(false, 'abort'); + } else { + cb(false, aErr.toString()); + } + }); +} + + $(function(){ + + $('#iregisterform').submit(function(ev){ + var self = $(this); + ev.preventDefault(); + var cp = $('select[name=cp]').val(); + if (cp == "") { + $('.cerror').show().text("Please choose cross-platform setting - see note below about what this means"); + return; + } + + $('.cerror').empty().hide(); + + $.ajax({url: '../out/out.SetupWebauthn.php', + method: 'POST', + data: {action: 'registeruser', registerusername: self.find('[name=registerusername]').val(), crossplatform: cp}, + dataType: 'json', + success: function(j){ + /* activate the key and get the response */ + webauthnRegister(j.challenge, function(success, info){ + if (success) { + $.ajax({url: '../out/out.SetupWebauthn.php', + method: 'POST', + data: {action: 'register', register: info}, + dataType: 'json', + success: function(j){ + noty({ + text: 'registration completed successfully', + type: 'success', + dismissQueue: true, + layout: 'topRight', + theme: 'defaultTheme', + timeout: 1500, + }); + }, + error: function(xhr, status, error){ + noty({ + text: 'registration failed: '+error+": "+xhr.responseText, + type: 'error', + dismissQueue: true, + layout: 'topRight', + theme: 'defaultTheme', + timeout: 1500, + }); +// $('.cerror').text("registration failed: "+error+": "+xhr.responseText).show(); + } + }); + } else { + noty({ + text: info, + type: 'error', + dismissQueue: true, + layout: 'topRight', + theme: 'defaultTheme', + timeout: 1500, + }); +// $('.cerror').text(info).show(); + } + }); + }, + + error: function(xhr, status, error){ + noty({ + text: "couldn't initiate registration: "+error+": "+xhr.responseText, + type: 'error', + dismissQueue: true, + layout: 'topRight', + theme: 'defaultTheme', + timeout: 1500, + }); + } + }); + }); + + $('#iloginform').submit(function(ev){ + var self = $(this); + ev.preventDefault(); + $('.cerror').empty().hide(); + + $.ajax({url: '../out/out.SetupWebauthn.php', + method: 'POST', + data: {action: 'preparelogin', loginusername: self.find('[name=loginusername]').val()}, + dataType: 'json', + success: function(j){ + /* activate the key and get the response */ + webauthnAuthenticate(j.challenge, function(success, info){ + if (success) { + $.ajax({url: '../out/out.SetupWebauthn.php', + method: 'POST', + data: {action: 'login', login: info}, + dataType: 'json', + success: function(j){ + noty({ + text: 'login completed successfully', + type: 'success', + dismissQueue: true, + layout: 'topRight', + theme: 'defaultTheme', + timeout: 1500, + }); + }, + error: function(xhr, status, error){ + noty({ + text: 'login failed: '+error+": "+xhr.responseText, + type: 'error', + dismissQueue: true, + layout: 'topRight', + theme: 'defaultTheme', + timeout: 1500, + }); +// $('.cerror').text("login failed: "+error+": "+xhr.responseText).show(); + } + }); + } else { + noty({ + text: info, + type: 'error', + dismissQueue: true, + layout: 'topRight', + theme: 'defaultTheme', + timeout: 1500, + }); +// $('.cerror').text(info).show(); + } + }); + }, + + error: function(xhr, status, error){ + noty({ + text: "couldn't initiate login: "+error+": "+xhr.responseText, + type: 'error', + dismissQueue: true, + layout: 'topRight', + theme: 'defaultTheme', + timeout: 1500, + }); + } + }); + }); + }); +params['dms']; + $user = $this->params['user']; + + $webauthn = new \Davidearl\WebAuthn\WebAuthn($_SERVER['HTTP_HOST']); + + /* initiate the registration */ + $username = $user->getLogin(); + $userid = $user->getId(); + $crossplatform = ! empty($_POST['crossplatform']) && $_POST['crossplatform'] == 'Yes'; + +// $user->setWebauthn($webauthn->cancel()); + + $j = ['challenge' => $webauthn->prepareChallengeForRegistration($username, $userid, $crossplatform)]; + header('Content-type: application/json'); + echo json_encode($j); + } /* }}} */ + + function register() { /* {{{ */ + $dms = $this->params['dms']; + $user = $this->params['user']; + + $webauthn = new \Davidearl\WebAuthn\WebAuthn($_SERVER['HTTP_HOST']); + + /* The heart of the matter */ + $user->setWebauthn($webauthn->register($_POST['register'], $user->getWebauthn())); + + $j = 'ok'; + + header('Content-type: application/json'); + echo json_encode($j); + } /* }}} */ + + function preparelogin() { /* {{{ */ + $dms = $this->params['dms']; + $user = $this->params['user']; + + $webauthn = new \Davidearl\WebAuthn\WebAuthn($_SERVER['HTTP_HOST']); + + $j['challenge'] = $webauthn->prepareForLogin($user->getWebauthn()); + header('Content-type: application/json'); + echo json_encode($j); + } /* }}} */ + + function login() { /* {{{ */ + $dms = $this->params['dms']; + $user = $this->params['user']; + + $webauthn = new \Davidearl\WebAuthn\WebAuthn($_SERVER['HTTP_HOST']); + + if (! $webauthn->authenticate($_POST['login'], $user->getWebauthn())) { + http_response_code(401); + echo 'failed to authenticate with that key'; + exit; + } + $j = 'ok'; + header('Content-type: application/json'); + echo json_encode($j); + } /* }}} */ + + function show() { /* {{{ */ + $dms = $this->params['dms']; + $user = $this->params['user']; + $sitename = $this->params['sitename']; + + $this->htmlStartPage(getMLText("webauthn"), "forcepasswordchange"); + $this->globalNavigation(); + $this->contentStart(); + $this->pageNavigation(getMLText("my_account"), "my_account"); + $this->contentHeading(getMLText('webauthn_auth')); + echo "
".getMLText('webauthn_info')."
"; + echo '
'; + echo '
'; + $this->contentHeading('webauthn_registration'); + $this->contentContainerStart(); +?> +
+
+ +
+
+ +
+ +
+formSubmit(getMLText('submit_webauthn_register')); +?> +
+contentContainerEnd(); + + echo $this->infoMsg(getMLText('webauthn_crossplatform_info')); + echo "
"; + if($user->getWebauthn()) { + echo '
'; + $this->contentHeading('webauthn_login_test'); + $this->contentContainerStart(); +?> +
+ +formSubmit(getMLText('submit_webauthn_login')); +?> +
+contentContainerEnd(); + } + + echo '
'; + echo '
'; + $this->contentEnd(); + $this->htmlEndPage(); + } /* }}} */ +} +