diff --git a/class.two-factor-core.php b/class.two-factor-core.php index 58d179b6..4b7add52 100644 --- a/class.two-factor-core.php +++ b/class.two-factor-core.php @@ -71,10 +71,7 @@ public static function load_textdomain() { public static function get_providers() { $providers = array( 'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class.two-factor-email.php', - 'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class.two-factor-totp.php', - 'Two_Factor_FIDO_U2F' => TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f.php', 'Two_Factor_Backup_Codes' => TWO_FACTOR_DIR . 'providers/class.two-factor-backup-codes.php', - 'Two_Factor_Dummy' => TWO_FACTOR_DIR . 'providers/class.two-factor-dummy.php', ); /** @@ -88,16 +85,6 @@ public static function get_providers() { */ $providers = apply_filters( 'two_factor_providers', $providers ); - // FIDO U2F is PHP 5.3+ only. - if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) { - unset( $providers['Two_Factor_FIDO_U2F'] ); - trigger_error( sprintf( // WPCS: XSS OK. - /* translators: %s: version number */ - __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), - PHP_VERSION - ) ); - } - /** * For each filtered provider, */ diff --git a/composer.json b/composer.json index 7b1d4675..e5d8cd92 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "type": "wordpress-plugin", - "name": "georgestephanis/two-factor", + "name": "wordpress/two-factor", "description": "Two-Factor Authentication for WordPress.", "require": { "php": ">=5.4" diff --git a/includes/Google/u2f-api.js b/includes/Google/u2f-api.js deleted file mode 100644 index 9244d14e..00000000 --- a/includes/Google/u2f-api.js +++ /dev/null @@ -1,748 +0,0 @@ -//Copyright 2014-2015 Google Inc. All rights reserved. - -//Use of this source code is governed by a BSD-style -//license that can be found in the LICENSE file or at -//https://developers.google.com/open-source/licenses/bsd - -/** - * @fileoverview The U2F api. - */ -'use strict'; - - -/** - * Namespace for the U2F api. - * @type {Object} - */ -var u2f = u2f || {}; - -/** - * FIDO U2F Javascript API Version - * @number - */ -var js_api_version; - -/** - * The U2F extension id - * @const {string} - */ -// The Chrome packaged app extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the package Chrome app and does not require installing the U2F Chrome extension. - u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; -// The U2F Chrome extension ID. -// Uncomment this if you want to deploy a server instance that uses -// the U2F Chrome extension to authenticate. -// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; - - -/** - * Message types for messsages to/from the extension - * @const - * @enum {string} - */ -u2f.MessageTypes = { - 'U2F_REGISTER_REQUEST': 'u2f_register_request', - 'U2F_REGISTER_RESPONSE': 'u2f_register_response', - 'U2F_SIGN_REQUEST': 'u2f_sign_request', - 'U2F_SIGN_RESPONSE': 'u2f_sign_response', - 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', - 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' -}; - - -/** - * Response status codes - * @const - * @enum {number} - */ -u2f.ErrorCodes = { - 'OK': 0, - 'OTHER_ERROR': 1, - 'BAD_REQUEST': 2, - 'CONFIGURATION_UNSUPPORTED': 3, - 'DEVICE_INELIGIBLE': 4, - 'TIMEOUT': 5 -}; - - -/** - * A message for registration requests - * @typedef {{ - * type: u2f.MessageTypes, - * appId: ?string, - * timeoutSeconds: ?number, - * requestId: ?number - * }} - */ -u2f.U2fRequest; - - -/** - * A message for registration responses - * @typedef {{ - * type: u2f.MessageTypes, - * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), - * requestId: ?number - * }} - */ -u2f.U2fResponse; - - -/** - * An error object for responses - * @typedef {{ - * errorCode: u2f.ErrorCodes, - * errorMessage: ?string - * }} - */ -u2f.Error; - -/** - * Data object for a single sign request. - * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} - */ -u2f.Transport; - - -/** - * Data object for a single sign request. - * @typedef {Array} - */ -u2f.Transports; - -/** - * Data object for a single sign request. - * @typedef {{ - * version: string, - * challenge: string, - * keyHandle: string, - * appId: string - * }} - */ -u2f.SignRequest; - - -/** - * Data object for a sign response. - * @typedef {{ - * keyHandle: string, - * signatureData: string, - * clientData: string - * }} - */ -u2f.SignResponse; - - -/** - * Data object for a registration request. - * @typedef {{ - * version: string, - * challenge: string - * }} - */ -u2f.RegisterRequest; - - -/** - * Data object for a registration response. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: Transports, - * appId: string - * }} - */ -u2f.RegisterResponse; - - -/** - * Data object for a registered key. - * @typedef {{ - * version: string, - * keyHandle: string, - * transports: ?Transports, - * appId: ?string - * }} - */ -u2f.RegisteredKey; - - -/** - * Data object for a get API register response. - * @typedef {{ - * js_api_version: number - * }} - */ -u2f.GetJsApiVersionResponse; - - -//Low level MessagePort API support - -/** - * Sets up a MessagePort to the U2F extension using the - * available mechanisms. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - */ -u2f.getMessagePort = function(callback) { - if (typeof chrome != 'undefined' && chrome.runtime) { - // The actual message here does not matter, but we need to get a reply - // for the callback to run. Thus, send an empty signature request - // in order to get a failure response. - var msg = { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: [] - }; - chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { - if (!chrome.runtime.lastError) { - // We are on a whitelisted origin and can talk directly - // with the extension. - u2f.getChromeRuntimePort_(callback); - } else { - // chrome.runtime was available, but we couldn't message - // the extension directly, use iframe - u2f.getIframePort_(callback); - } - }); - } else if (u2f.isAndroidChrome_()) { - u2f.getAuthenticatorPort_(callback); - } else if (u2f.isIosChrome_()) { - u2f.getIosPort_(callback); - } else { - // chrome.runtime was not available at all, which is normal - // when this origin doesn't have access to any extensions. - u2f.getIframePort_(callback); - } -}; - -/** - * Detect chrome running on android based on the browser's useragent. - * @private - */ -u2f.isAndroidChrome_ = function() { - var userAgent = navigator.userAgent; - return userAgent.indexOf('Chrome') != -1 && - userAgent.indexOf('Android') != -1; -}; - -/** - * Detect chrome running on iOS based on the browser's platform. - * @private - */ -u2f.isIosChrome_ = function() { - return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; -}; - -/** - * Connects directly to the extension via chrome.runtime.connect. - * @param {function(u2f.WrappedChromeRuntimePort_)} callback - * @private - */ -u2f.getChromeRuntimePort_ = function(callback) { - var port = chrome.runtime.connect(u2f.EXTENSION_ID, - {'includeTlsChannelId': true}); - setTimeout(function() { - callback(new u2f.WrappedChromeRuntimePort_(port)); - }, 0); -}; - -/** - * Return a 'port' abstraction to the Authenticator app. - * @param {function(u2f.WrappedAuthenticatorPort_)} callback - * @private - */ -u2f.getAuthenticatorPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedAuthenticatorPort_()); - }, 0); -}; - -/** - * Return a 'port' abstraction to the iOS client app. - * @param {function(u2f.WrappedIosPort_)} callback - * @private - */ -u2f.getIosPort_ = function(callback) { - setTimeout(function() { - callback(new u2f.WrappedIosPort_()); - }, 0); -}; - -/** - * A wrapper for chrome.runtime.Port that is compatible with MessagePort. - * @param {Port} port - * @constructor - * @private - */ -u2f.WrappedChromeRuntimePort_ = function(port) { - this.port_ = port; -}; - -/** - * Format and return a sign request compliant with the JS API version supported by the extension. - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatSignRequest_ = - function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: challenge, - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - signRequests: signRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_SIGN_REQUEST, - appId: appId, - challenge: challenge, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - -/** - * Format and return a register request compliant with the JS API version supported by the extension.. - * @param {Array} signRequests - * @param {Array} signRequests - * @param {number} timeoutSeconds - * @param {number} reqId - * @return {Object} - */ -u2f.formatRegisterRequest_ = - function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { - if (js_api_version === undefined || js_api_version < 1.1) { - // Adapt request to the 1.0 JS API - for (var i = 0; i < registerRequests.length; i++) { - registerRequests[i].appId = appId; - } - var signRequests = []; - for (var i = 0; i < registeredKeys.length; i++) { - signRequests[i] = { - version: registeredKeys[i].version, - challenge: registerRequests[0], - keyHandle: registeredKeys[i].keyHandle, - appId: appId - }; - } - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - signRequests: signRequests, - registerRequests: registerRequests, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; - } - // JS 1.1 API - return { - type: u2f.MessageTypes.U2F_REGISTER_REQUEST, - appId: appId, - registerRequests: registerRequests, - registeredKeys: registeredKeys, - timeoutSeconds: timeoutSeconds, - requestId: reqId - }; -}; - - -/** - * Posts a message on the underlying channel. - * @param {Object} message - */ -u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { - this.port_.postMessage(message); -}; - - -/** - * Emulates the HTML 5 addEventListener interface. Works only for the - * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedChromeRuntimePort_.prototype.addEventListener = - function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message' || name == 'onmessage') { - this.port_.onMessage.addListener(function(message) { - // Emulate a minimal MessageEvent object - handler({'data': message}); - }); - } else { - console.error('WrappedChromeRuntimePort only supports onMessage'); - } -}; - -/** - * Wrap the Authenticator app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedAuthenticatorPort_ = function() { - this.requestId_ = -1; - this.requestObject_ = null; -} - -/** - * Launch the Authenticator intent. - * @param {Object} message - */ -u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { - var intentUrl = - u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + - ';S.request=' + encodeURIComponent(JSON.stringify(message)) + - ';end'; - document.location = intentUrl; -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { - return "WrappedAuthenticatorPort_"; -}; - - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name == 'message') { - var self = this; - /* Register a callback to that executes when - * chrome injects the response. */ - window.addEventListener( - 'message', self.onRequestUpdate_.bind(self, handler), false); - } else { - console.error('WrappedAuthenticatorPort only supports message'); - } -}; - -/** - * Callback invoked when a response is received from the Authenticator. - * @param function({data: Object}) callback - * @param {Object} message message Object - */ -u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = - function(callback, message) { - var messageObject = JSON.parse(message.data); - var intentUrl = messageObject['intentURL']; - - var errorCode = messageObject['errorCode']; - var responseObject = null; - if (messageObject.hasOwnProperty('data')) { - responseObject = /** @type {Object} */ ( - JSON.parse(messageObject['data'])); - } - - callback({'data': responseObject}); -}; - -/** - * Base URL for intents to Authenticator. - * @const - * @private - */ -u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = - 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; - -/** - * Wrap the iOS client app with a MessagePort interface. - * @constructor - * @private - */ -u2f.WrappedIosPort_ = function() {}; - -/** - * Launch the iOS client app request - * @param {Object} message - */ -u2f.WrappedIosPort_.prototype.postMessage = function(message) { - var str = JSON.stringify(message); - var url = "u2f://auth?" + encodeURI(str); - location.replace(url); -}; - -/** - * Tells what type of port this is. - * @return {String} port type - */ -u2f.WrappedIosPort_.prototype.getPortType = function() { - return "WrappedIosPort_"; -}; - -/** - * Emulates the HTML 5 addEventListener interface. - * @param {string} eventName - * @param {function({data: Object})} handler - */ -u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { - var name = eventName.toLowerCase(); - if (name !== 'message') { - console.error('WrappedIosPort only supports message'); - } -}; - -/** - * Sets up an embedded trampoline iframe, sourced from the extension. - * @param {function(MessagePort)} callback - * @private - */ -u2f.getIframePort_ = function(callback) { - // Create the iframe - var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; - var iframe = document.createElement('iframe'); - iframe.src = iframeOrigin + '/u2f-comms.html'; - iframe.setAttribute('style', 'display:none'); - document.body.appendChild(iframe); - - var channel = new MessageChannel(); - var ready = function(message) { - if (message.data == 'ready') { - channel.port1.removeEventListener('message', ready); - callback(channel.port1); - } else { - console.error('First event on iframe port was not "ready"'); - } - }; - channel.port1.addEventListener('message', ready); - channel.port1.start(); - - iframe.addEventListener('load', function() { - // Deliver the port to the iframe and initialize - iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); - }); -}; - - -//High-level JS API - -/** - * Default extension response timeout in seconds. - * @const - */ -u2f.EXTENSION_TIMEOUT_SEC = 30; - -/** - * A singleton instance for a MessagePort to the extension. - * @type {MessagePort|u2f.WrappedChromeRuntimePort_} - * @private - */ -u2f.port_ = null; - -/** - * Callbacks waiting for a port - * @type {Array} - * @private - */ -u2f.waitingForPort_ = []; - -/** - * A counter for requestIds. - * @type {number} - * @private - */ -u2f.reqCounter_ = 0; - -/** - * A map from requestIds to client callbacks - * @type {Object.} - * @private - */ -u2f.callbackMap_ = {}; - -/** - * Creates or retrieves the MessagePort singleton to use. - * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback - * @private - */ -u2f.getPortSingleton_ = function(callback) { - if (u2f.port_) { - callback(u2f.port_); - } else { - if (u2f.waitingForPort_.length == 0) { - u2f.getMessagePort(function(port) { - u2f.port_ = port; - u2f.port_.addEventListener('message', - /** @type {function(Event)} */ (u2f.responseHandler_)); - - // Careful, here be async callbacks. Maybe. - while (u2f.waitingForPort_.length) - u2f.waitingForPort_.shift()(u2f.port_); - }); - } - u2f.waitingForPort_.push(callback); - } -}; - -/** - * Handles response messages from the extension. - * @param {MessageEvent.} message - * @private - */ -u2f.responseHandler_ = function(message) { - var response = message.data; - var reqId = response['requestId']; - if (!reqId || !u2f.callbackMap_[reqId]) { - console.error('Unknown or missing requestId in response.'); - return; - } - var cb = u2f.callbackMap_[reqId]; - delete u2f.callbackMap_[reqId]; - cb(response['responseData']); -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the sign request. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual sign request. - u2f.getApiVersion( - function (response) { - js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); - }); - } else { - // We know the JS API version. Send the actual sign request in the supported API version. - u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); - } -}; - -/** - * Dispatches an array of sign requests to available U2F tokens. - * @param {string=} appId - * @param {string=} challenge - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.SignResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); - port.postMessage(req); - }); -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * If the JS API version supported by the extension is unknown, it first sends a - * message to the extension to find out the supported API version and then it sends - * the register request. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { - if (js_api_version === undefined) { - // Send a message to get the extension to JS API version, then send the actual register request. - u2f.getApiVersion( - function (response) { - js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; - console.log("Extension JS API Version: ", js_api_version); - u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, - callback, opt_timeoutSeconds); - }); - } else { - // We know the JS API version. Send the actual register request in the supported API version. - u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, - callback, opt_timeoutSeconds); - } -}; - -/** - * Dispatches register requests to available U2F tokens. An array of sign - * requests identifies already registered tokens. - * @param {string=} appId - * @param {Array} registerRequests - * @param {Array} registeredKeys - * @param {function((u2f.Error|u2f.RegisterResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); - var req = u2f.formatRegisterRequest_( - appId, registeredKeys, registerRequests, timeoutSeconds, reqId); - port.postMessage(req); - }); -}; - - -/** - * Dispatches a message to the extension to find out the supported - * JS API version. - * If the user is on a mobile phone and is thus using Google Authenticator instead - * of the Chrome extension, don't send the request and simply return 0. - * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback - * @param {number=} opt_timeoutSeconds - */ -u2f.getApiVersion = function(callback, opt_timeoutSeconds) { - u2f.getPortSingleton_(function(port) { - // If we are using Android Google Authenticator or iOS client app, - // do not fire an intent to ask which JS API version to use. - if (port.getPortType) { - var apiVersion; - switch (port.getPortType()) { - case 'WrappedIosPort_': - case 'WrappedAuthenticatorPort_': - apiVersion = 1.1; - break; - - default: - apiVersion = 0; - break; - } - callback({ 'js_api_version': apiVersion }); - return; - } - var reqId = ++u2f.reqCounter_; - u2f.callbackMap_[reqId] = callback; - var req = { - type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, - timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? - opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), - requestId: reqId - }; - port.postMessage(req); - }); -}; diff --git a/includes/Yubico/U2F.php b/includes/Yubico/U2F.php deleted file mode 100644 index a11c78fb..00000000 --- a/includes/Yubico/U2F.php +++ /dev/null @@ -1,507 +0,0 @@ -appId = $appId; - $this->attestDir = $attestDir; - } - - /** - * Called to get a registration request to send to a user. - * Returns an array of one registration request and a array of sign requests. - * - * @param array $registrations List of current registrations for this - * user, to prevent the user from registering the same authenticator several - * times. - * @return array An array of two elements, the first containing a - * RegisterRequest the second being an array of SignRequest - * @throws Error - */ - public function getRegisterData(array $registrations = array()) - { - $challenge = $this->createChallenge(); - $request = new RegisterRequest($challenge, $this->appId); - $signs = $this->getAuthenticateData($registrations); - return array($request, $signs); - } - - /** - * Called to verify and unpack a registration message. - * - * @param RegisterRequest $request this is a reply to - * @param object $response response from a user - * @param bool $includeCert set to true if the attestation certificate should be - * included in the returned Registration object - * @return Registration - * @throws Error - */ - public function doRegister($request, $response, $includeCert = true) - { - if( !is_object( $request ) ) { - throw new \InvalidArgumentException('$request of doRegister() method only accepts object.'); - } - - if( !is_object( $response ) ) { - throw new \InvalidArgumentException('$response of doRegister() method only accepts object.'); - } - - if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { - throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); - } - - if( !is_bool( $includeCert ) ) { - throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.'); - } - - $rawReg = $this->base64u_decode($response->registrationData); - $regData = array_values(unpack('C*', $rawReg)); - $clientData = $this->base64u_decode($response->clientData); - $cli = json_decode($clientData); - - if($cli->challenge !== $request->challenge) { - throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE ); - } - - $registration = new Registration(); - $offs = 1; - $pubKey = substr($rawReg, $offs, PUBKEY_LEN); - $offs += PUBKEY_LEN; - // decode the pubKey to make sure it's good - $tmpKey = $this->pubkey_to_pem($pubKey); - if($tmpKey === null) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - $registration->publicKey = base64_encode($pubKey); - $khLen = $regData[$offs++]; - $kh = substr($rawReg, $offs, $khLen); - $offs += $khLen; - $registration->keyHandle = $this->base64u_encode($kh); - - // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes) - $certLen = 4; - $certLen += ($regData[$offs + 2] << 8); - $certLen += $regData[$offs + 3]; - - $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen)); - $offs += $certLen; - $pemCert = "-----BEGIN CERTIFICATE-----\r\n"; - $pemCert .= chunk_split(base64_encode($rawCert), 64); - $pemCert .= "-----END CERTIFICATE-----"; - if($includeCert) { - $registration->certificate = base64_encode($rawCert); - } - if($this->attestDir) { - if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) { - throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION ); - } - } - - if(!openssl_pkey_get_public($pemCert)) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - $signature = substr($rawReg, $offs); - - $dataToVerify = chr(0); - $dataToVerify .= hash('sha256', $request->appId, true); - $dataToVerify .= hash('sha256', $clientData, true); - $dataToVerify .= $kh; - $dataToVerify .= $pubKey; - - if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) { - return $registration; - } else { - throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE ); - } - } - - /** - * Called to get an authentication request. - * - * @param array $registrations An array of the registrations to create authentication requests for. - * @return array An array of SignRequest - * @throws Error - */ - public function getAuthenticateData(array $registrations) - { - $sigs = array(); - $challenge = $this->createChallenge(); - foreach ($registrations as $reg) { - if( !is_object( $reg ) ) { - throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.'); - } - - $sig = new SignRequest(); - $sig->appId = $this->appId; - $sig->keyHandle = $reg->keyHandle; - $sig->challenge = $challenge; - $sigs[] = $sig; - } - return $sigs; - } - - /** - * Called to verify an authentication response - * - * @param array $requests An array of outstanding authentication requests - * @param array $registrations An array of current registrations - * @param object $response A response from the authenticator - * @return Registration - * @throws Error - * - * The Registration object returned on success contains an updated counter - * that should be saved for future authentications. - * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of - * token cloning or similar and appropriate action should be taken. - */ - public function doAuthenticate(array $requests, array $registrations, $response) - { - if( !is_object( $response ) ) { - throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.'); - } - - if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { - throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); - } - - /** @var object|null $req */ - $req = null; - - /** @var object|null $reg */ - $reg = null; - - $clientData = $this->base64u_decode($response->clientData); - $decodedClient = json_decode($clientData); - foreach ($requests as $req) { - if( !is_object( $req ) ) { - throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.'); - } - - if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) { - break; - } - - $req = null; - } - if($req === null) { - throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST ); - } - foreach ($registrations as $reg) { - if( !is_object( $reg ) ) { - throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.'); - } - - if($reg->keyHandle === $response->keyHandle) { - break; - } - $reg = null; - } - if($reg === null) { - throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION ); - } - $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey)); - if($pemKey === null) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - - $signData = $this->base64u_decode($response->signatureData); - $dataToVerify = hash('sha256', $req->appId, true); - $dataToVerify .= substr($signData, 0, 5); - $dataToVerify .= hash('sha256', $clientData, true); - $signature = substr($signData, 5); - - if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) { - $ctr = unpack("Nctr", substr($signData, 1, 4)); - $counter = $ctr['ctr']; - /* TODO: wrap-around should be handled somehow.. */ - if($counter > $reg->counter) { - $reg->counter = $counter; - return $reg; - } else { - throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW ); - } - } else { - throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE ); - } - } - - /** - * @return array - */ - private function get_certs() - { - $files = array(); - $dir = $this->attestDir; - if($dir && $handle = opendir($dir)) { - while(false !== ($entry = readdir($handle))) { - if(is_file("$dir/$entry")) { - $files[] = "$dir/$entry"; - } - } - closedir($handle); - } - return $files; - } - - /** - * @param string $data - * @return string - */ - private function base64u_encode($data) - { - return trim(strtr(base64_encode($data), '+/', '-_'), '='); - } - - /** - * @param string $data - * @return string - */ - private function base64u_decode($data) - { - return base64_decode(strtr($data, '-_', '+/')); - } - - /** - * @param string $key - * @return null|string - */ - private function pubkey_to_pem($key) - { - if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") { - return null; - } - - /* - * Convert the public key to binary DER format first - * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480 - * - * SEQUENCE(2 elem) 30 59 - * SEQUENCE(2 elem) 30 13 - * OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01 - * OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07 - * BIT STRING(520 bit) 03 42 ..key.. - */ - $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01"; - $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42"; - $der .= "\0".$key; - - $pem = "-----BEGIN PUBLIC KEY-----\r\n"; - $pem .= chunk_split(base64_encode($der), 64); - $pem .= "-----END PUBLIC KEY-----"; - - return $pem; - } - - /** - * @return string - * @throws Error - */ - private function createChallenge() - { - $challenge = openssl_random_pseudo_bytes(32, $crypto_strong ); - if( $crypto_strong !== true ) { - throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM); - } - - $challenge = $this->base64u_encode( $challenge ); - - return $challenge; - } - - /** - * Fixes a certificate where the signature contains unused bits. - * - * @param string $cert - * @return mixed - */ - private function fixSignatureUnusedBits($cert) - { - if(in_array(hash('sha256', $cert), $this->FIXCERTS)) { - $cert[strlen($cert) - 257] = "\0"; - } - return $cert; - } -} - -/** - * Class for building a registration request - * - * @package u2flib_server - */ -class RegisterRequest -{ - /** Protocol version */ - public $version = U2F_VERSION; - - /** Registration challenge */ - public $challenge; - - /** Application id */ - public $appId; - - /** - * @param string $challenge - * @param string $appId - * @internal - */ - public function __construct($challenge, $appId) - { - $this->challenge = $challenge; - $this->appId = $appId; - } -} - -/** - * Class for building up an authentication request - * - * @package u2flib_server - */ -class SignRequest -{ - /** Protocol version */ - public $version = U2F_VERSION; - - /** Authentication challenge */ - public $challenge; - - /** Key handle of a registered authenticator */ - public $keyHandle; - - /** Application id */ - public $appId; -} - -/** - * Class returned for successful registrations - * - * @package u2flib_server - */ -class Registration -{ - /** The key handle of the registered authenticator */ - public $keyHandle; - - /** The public key of the registered authenticator */ - public $publicKey; - - /** The attestation certificate of the registered authenticator */ - public $certificate; - - /** The counter associated with this registration */ - public $counter = -1; -} - -/** - * Error class, returned on errors - * - * @package u2flib_server - */ -class Error extends \Exception -{ - /** - * Override constructor and make message and code mandatory - * @param string $message - * @param int $code - * @param \Exception|null $previous - */ - public function __construct($message, $code, \Exception $previous = null) { - parent::__construct($message, $code, $previous); - } -} diff --git a/providers/class.two-factor-dummy.php b/providers/class.two-factor-dummy.php deleted file mode 100644 index c28ccca7..00000000 --- a/providers/class.two-factor-dummy.php +++ /dev/null @@ -1,93 +0,0 @@ - -

- wp_strip_all_tags( __( 'Name', 'two-factor' ) ), - 'added' => wp_strip_all_tags( __( 'Added', 'two-factor' ) ), - 'last_used' => wp_strip_all_tags( __( 'Last Used', 'two-factor' ) ), - ); - } - - /** - * Prepares the list of items for displaying. - * - * @since 0.1-dev - */ - public function prepare_items() { - $columns = $this->get_columns(); - $hidden = array(); - $sortable = array(); - $primary = 'name'; - $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); - } - - /** - * Generates content for a single row of the table - * - * @since 0.1-dev - * @access protected - * - * @param object $item The current item. - * @param string $column_name The current column name. - * @return string - */ - protected function column_default( $item, $column_name ) { - switch ( $column_name ) { - case 'name': - $out = ''; - - $actions = array( - 'rename hide-if-no-js' => Two_Factor_FIDO_U2F_Admin::rename_link( $item ), - 'delete' => Two_Factor_FIDO_U2F_Admin::delete_link( $item ), - ); - - return esc_html( $item->name ) . $out . self::row_actions( $actions ); - case 'added': - return date( get_option( 'date_format', 'r' ), $item->added ); - case 'last_used': - return date( get_option( 'date_format', 'r' ), $item->last_used ); - default: - return 'WTF^^?'; - } - } - - /** - * Generates custom table navigation to prevent conflicting nonces. - * - * @since 0.1-dev - * @access protected - * - * @param string $which The location of the bulk actions: 'top' or 'bottom'. - */ - protected function display_tablenav( $which ) { - // Not used for the Security key list. - } - - /** - * Generates content for a single row of the table - * - * @since 0.1-dev - * @access public - * - * @param object $item The current item. - */ - public function single_row( $item ) { - ?> - - single_row_columns( $item ); ?> - - - - - - - - -
- getRegisterData( $security_keys ); - list( $req,$sigs ) = $data; - - update_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, $req ); - } catch ( Exception $e ) { - return false; - } - - wp_enqueue_style( - 'fido-u2f-admin', - plugins_url( 'css/fido-u2f-admin.css', __FILE__ ), - null, - self::asset_version() - ); - - wp_enqueue_script( - 'fido-u2f-admin', - plugins_url( 'js/fido-u2f-admin.js', __FILE__ ), - array( 'jquery', 'fido-u2f-api' ), - self::asset_version(), - true - ); - - /** - * Pass a U2F challenge and user data to our scripts - */ - - $translation_array = array( - 'register' => array( - 'request' => $req, - 'sigs' => $sigs, - ), - 'text' => array( - 'insert' => esc_html__( 'Now insert (and tap) your Security Key.', 'two-factor' ), - 'error' => esc_html__( 'U2F request failed.', 'two-factor' ), - 'error_codes' => array( - // Map u2f.ErrorCodes to error messages. - 0 => esc_html__( 'Request OK.', 'two-factor' ), - 1 => esc_html__( 'Other U2F error.', 'two-factor' ), - 2 => esc_html__( 'Bad U2F request.', 'two-factor' ), - 3 => esc_html__( 'Unsupported U2F configuration.', 'two-factor' ), - 4 => esc_html__( 'U2F device ineligible.', 'two-factor' ), - 5 => esc_html__( 'U2F request timeout reached.', 'two-factor' ), - ), - 'u2f_not_supported' => esc_html__( 'FIDO U2F appears to be not supported by your web browser. Try using Google Chrome or Firefox.', 'two-factor' ), - ), - ); - - wp_localize_script( - 'fido-u2f-admin', - 'u2fL10n', - $translation_array - ); - - /** - * Script for admin UI - */ - - wp_enqueue_script( - 'inline-edit-key', - plugins_url( 'js/fido-u2f-admin-inline-edit.js', __FILE__ ), - array( 'jquery' ), - self::asset_version(), - true - ); - - wp_localize_script( - 'inline-edit-key', - 'inlineEditL10n', - array( - 'error' => esc_html__( 'Error while saving the changes.', 'two-factor' ), - ) - ); - } - - /** - * Return the current asset version number. - * - * Added as own helper to allow swapping the implementation once we inject - * it as a dependency. - * - * @return string - */ - protected static function asset_version() { - return Two_Factor_FIDO_U2F::asset_version(); - } - - /** - * Display the security key section in a users profile. - * - * This executes during the `show_user_security_settings` action. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param WP_User $user WP_User object of the logged-in user. - */ - public static function show_user_profile( $user ) { - wp_nonce_field( "user_security_keys-{$user->ID}", '_nonce_user_security_keys' ); - $new_key = false; - - $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user->ID ); - if ( $security_keys ) { - foreach ( $security_keys as &$security_key ) { - if ( property_exists( $security_key, 'new' ) ) { - $new_key = true; - unset( $security_key->new ); - - // If we've got a new one, update the db record to not save it there any longer. - Two_Factor_FIDO_U2F::update_security_key( $user->ID, $security_key ); - } - } - unset( $security_key ); - } - - ?> -
-

- - -

- -

- - -
- - - - - -
- - -
-

-
- - -

- - items = $security_keys; - $u2f_list_table->prepare_items(); - $u2f_list_table->display(); - $u2f_list_table->inline_edit(); - ?> -
- doRegister( get_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, true ), $response ); - $reg->new = true; - - Two_Factor_FIDO_U2F::add_security_key( $user_id, $reg ); - } catch ( Exception $e ) { - return false; - } - - delete_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY ); - - wp_safe_redirect( add_query_arg( array( - 'new_app_pass' => 1, - ), wp_get_referer() ) . '#security-keys-section' ); - exit; - } - } - - /** - * Catch the delete security key request. - * - * This executes during the `load-profile.php` & `load-user-edit.php` actions. - * - * @since 0.1-dev - * - * @access public - * @static - */ - public static function catch_delete_security_key() { - $user_id = get_current_user_id(); - if ( ! empty( $_REQUEST['delete_security_key'] ) ) { - $slug = $_REQUEST['delete_security_key']; - check_admin_referer( "delete_security_key-{$slug}", '_nonce_delete_security_key' ); - - Two_Factor_FIDO_U2F::delete_security_key( $user_id, $slug ); - - wp_safe_redirect( remove_query_arg( 'new_app_pass', wp_get_referer() ) . '#security-keys-section' ); - } - } - - /** - * Generate a link to rename a specified security key. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param array $item The current item. - * @return string - */ - public static function rename_link( $item ) { - return sprintf( '%s', esc_html__( 'Rename', 'two-factor' ) ); - } - - /** - * Generate a link to delete a specified security key. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param array $item The current item. - * @return string - */ - public static function delete_link( $item ) { - $delete_link = add_query_arg( 'delete_security_key', $item->keyHandle ); - $delete_link = wp_nonce_url( $delete_link, "delete_security_key-{$item->keyHandle}", '_nonce_delete_security_key' ); - return sprintf( '%2$s', esc_url( $delete_link ), esc_html__( 'Delete', 'two-factor' ) ); - } - - /** - * Ajax handler for quick edit saving for a security key. - * - * @since 0.1-dev - * - * @access public - * @static - */ - public static function wp_ajax_inline_save() { - check_ajax_referer( 'keyinlineeditnonce', '_inline_edit' ); - - require( TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f-admin-list-table.php' ); - $wp_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table(); - - if ( ! isset( $_POST['keyHandle'] ) ) { - wp_die(); - } - - $user_id = get_current_user_id(); - - $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id ); - if ( ! $security_keys ) { - wp_die(); - } - - foreach ( $security_keys as &$key ) { - if ( $key->keyHandle === $_POST['keyHandle'] ) { - break; - } - } - - $key->name = $_POST['name']; - - $updated = Two_Factor_FIDO_U2F::update_security_key( $user_id, $key ); - if ( ! $updated ) { - wp_die( esc_html__( 'Item not updated.', 'two-factor' ) ); - } - $wp_list_table->single_row( $key ); - wp_die(); - } -} diff --git a/providers/class.two-factor-fido-u2f.php b/providers/class.two-factor-fido-u2f.php deleted file mode 100644 index 54644251..00000000 --- a/providers/class.two-factor-fido-u2f.php +++ /dev/null @@ -1,384 +0,0 @@ - -

- ID ); - $data = self::$u2f->getAuthenticateData( $keys ); - update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data ); - } catch ( Exception $e ) { - ?> -

- $data, - ) - ); - - wp_enqueue_script( 'fido-u2f-login' ); - - ?> -

- - ID, self::AUTH_DATA_USER_META_KEY, true ); - - $response = json_decode( stripslashes( $_REQUEST['u2f_response'] ) ); - - $keys = self::get_security_keys( $user->ID ); - - try { - $reg = self::$u2f->doAuthenticate( $requests, $keys, $response ); - - $reg->last_used = current_time( 'timestamp' ); - - self::update_security_key( $user->ID, $reg ); - - return true; - } catch ( Exception $e ) { - return false; - } - } - - /** - * Whether this Two Factor provider is configured and available for the user specified. - * - * @since 0.1-dev - * - * @param WP_User $user WP_User object of the logged-in user. - * @return boolean - */ - public function is_available_for_user( $user ) { - return (bool) self::get_security_keys( $user->ID ); - } - - /** - * Inserts markup at the end of the user profile field for this provider. - * - * @since 0.1-dev - * - * @param WP_User $user WP_User object of the logged-in user. - */ - public function user_options( $user ) { - ?> -

- -

- keyHandle ) - || ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey ) - || ! property_exists( $register, 'certificate' ) || empty( $register->certificate ) - || ! property_exists( $register, 'counter' ) || ( -1 > $register->counter ) - ) { - return false; - } - - $register = array( - 'keyHandle' => $register->keyHandle, - 'publicKey' => $register->publicKey, - 'certificate' => $register->certificate, - 'counter' => $register->counter, - ); - - $register['name'] = __( 'New Security Key', 'two-factor' ); - $register['added'] = current_time( 'timestamp' ); - $register['last_used'] = $register['added']; - - return add_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, $register ); - } - - /** - * Retrieve registered security keys for a user. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @return array|bool Array of keys on success, false on failure. - */ - public static function get_security_keys( $user_id ) { - if ( ! is_numeric( $user_id ) ) { - return false; - } - - $keys = get_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY ); - if ( $keys ) { - foreach ( $keys as &$key ) { - $key = (object) $key; - } - unset( $key ); - } - - return $keys; - } - - /** - * Update registered security key. - * - * Use the $prev_value parameter to differentiate between meta fields with the - * same key and user ID. - * - * If the meta field for the user does not exist, it will be added. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @param object $data The data of registered security key. - * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure. - */ - public static function update_security_key( $user_id, $data ) { - if ( ! is_numeric( $user_id ) ) { - return false; - } - - if ( - ! is_object( $data ) - || ! property_exists( $data, 'keyHandle' ) || empty( $data->keyHandle ) - || ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey ) - || ! property_exists( $data, 'certificate' ) || empty( $data->certificate ) - || ! property_exists( $data, 'counter' ) || ( -1 > $data->counter ) - ) { - return false; - } - - $keys = self::get_security_keys( $user_id ); - if ( $keys ) { - foreach ( $keys as $key ) { - if ( $key->keyHandle === $data->keyHandle ) { - return update_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, (array) $data, (array) $key ); - } - } - } - - return self::add_security_key( $user_id, $data ); - } - - /** - * Remove registered security key matching criteria from a user. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @param string $keyHandle Optional. Key handle. - * @return bool True on success, false on failure. - */ - public static function delete_security_key( $user_id, $keyHandle = null ) { - global $wpdb; - - if ( ! is_numeric( $user_id ) ) { - return false; - } - - $user_id = absint( $user_id ); - if ( ! $user_id ) { - return false; - } - - $table = $wpdb->usermeta; - - $keyHandle = wp_unslash( $keyHandle ); - $keyHandle = maybe_serialize( $keyHandle ); - - $query = $wpdb->prepare( "SELECT umeta_id FROM $table WHERE meta_key = '%s' AND user_id = %d", self::REGISTERED_KEY_USER_META_KEY, $user_id ); - - if ( $keyHandle ) { - $query .= $wpdb->prepare( ' AND meta_value LIKE %s', '%:"' . $keyHandle . '";s:%' ); - } - - $meta_ids = $wpdb->get_col( $query ); - if ( ! count( $meta_ids ) ) { - return false; - } - - foreach ( $meta_ids as $meta_id ) { - delete_metadata_by_mid( 'user', $meta_id ); - } - - return true; - } -} diff --git a/providers/class.two-factor-totp.php b/providers/class.two-factor-totp.php deleted file mode 100644 index d9a56781..00000000 --- a/providers/class.two-factor-totp.php +++ /dev/null @@ -1,509 +0,0 @@ -ID ) ) { - return false; - } - - wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false ); - - $key = $this->get_user_totp_key( $user->ID ); - $this->admin_notices(); - - ?> -
- generate_key(); - $site_name = get_bloginfo( 'name', 'display' ); - $totp_title = apply_filters( 'two_factor_totp_title', $site_name . ':' . $user->user_login, $user ); - ?> -

- -

-

- -

-

- -

-

- - - -

- -

- -

-

- - - - -

- -
- get_user_totp_key( $user_id ); - - if ( isset( $_POST['_nonce_user_two_factor_totp_options'] ) ) { - check_admin_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' ); - - // Delete the secret key. - if ( ! empty( $current_key ) && isset( $_POST['two-factor-totp-delete'] ) ) { - $this->delete_user_totp_key( $user_id ); - } - - // Validate and store a new secret key. - if ( ! empty( $_POST['two-factor-totp-authcode'] ) && ! empty( $_POST['two-factor-totp-key'] ) ) { - if ( $this->is_valid_key( $_POST['two-factor-totp-key'] ) ) { - if ( $this->is_valid_authcode( $_POST['two-factor-totp-key'], $_POST['two-factor-totp-authcode'] ) ) { - if ( ! $this->set_user_totp_key( $user_id, $_POST['two-factor-totp-key'] ) ) { - $errors[] = __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' ); - } - } else { - $errors[] = __( 'Invalid Two Factor Authentication code.', 'two-factor' ); - } - } else { - $errors[] = __( 'Invalid Two Factor Authentication secret key.', 'two-factor' ); - } - } - - if ( ! empty( $errors ) ) { - $notices['error'] = $errors; - } - - if ( ! empty( $notices ) ) { - update_user_meta( $user_id, self::NOTICES_META_KEY, $notices ); - } - } - } - - /** - * Get the TOTP secret key for a user. - * - * @param int $user_id User ID. - * - * @return string - */ - public function get_user_totp_key( $user_id ) { - return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true ); - } - - /** - * Set the TOTP secret key for a user. - * - * @param int $user_id User ID. - * @param string $key TOTP secret key. - * - * @return boolean If the key was stored successfully. - */ - public function set_user_totp_key( $user_id, $key ) { - return update_user_meta( $user_id, self::SECRET_META_KEY, $key ); - } - - /** - * Delete the TOTP secret key for a user. - * - * @param int $user_id User ID. - * - * @return boolean If the key was deleted successfully. - */ - public function delete_user_totp_key( $user_id ) { - return delete_user_meta( $user_id, self::SECRET_META_KEY ); - } - - /** - * Check if the TOTP secret key has a proper format. - * - * @param string $key TOTP secret key. - * - * @return boolean - */ - public function is_valid_key( $key ) { - $check = sprintf( '/^[%s]+$/', self::$_base_32_chars ); - - if ( 1 === preg_match( $check, $key ) ) { - return true; - } - - return false; - } - - /** - * Display any available admin notices. - */ - public function admin_notices() { - $notices = get_user_meta( get_current_user_id(), self::NOTICES_META_KEY, true ); - - if ( ! empty( $notices ) ) { - delete_user_meta( get_current_user_id(), self::NOTICES_META_KEY ); - foreach ( $notices as $class => $messages ) { - ?> -
- -

- -

- -
- is_valid_authcode( - $this->get_user_totp_key( $user->ID ), - sanitize_text_field( $_REQUEST['authcode'] ) // WPCS: CSRF ok, nonce verified by login_form_validate_2fa(). - ); - } - - return false; - } - - /** - * Checks if a given code is valid for a given key, allowing for a certain amount of time drift - * - * @param string $key The share secret key to use. - * @param string $authcode The code to test. - * - * @return bool Whether the code is valid within the time frame - */ - public static function is_valid_authcode( $key, $authcode ) { - /** - * Filter the maximum ticks to allow when checking valid codes. - * - * Ticks are the allowed offset from the correct time in 30 second increments, - * so the default of 4 allows codes that are two minutes to either side of server time - * - * @param int $max_ticks Max ticks of time correction to allow. Default 4. - */ - $max_ticks = apply_filters( 'two-factor-totp-time-step-allowance', self::DEFAULT_TIME_STEP_ALLOWANCE ); - - // Array of all ticks to allow, sorted using absolute value to test closest match first. - $ticks = range( - $max_ticks, $max_ticks ); - usort( $ticks, array( __CLASS__, 'abssort' ) ); - - $time = time() / self::DEFAULT_TIME_STEP_SEC; - - foreach ( $ticks as $offset ) { - $log_time = $time + $offset; - if ( self::calc_totp( $key, $log_time ) === $authcode ) { - return true; - } - } - return false; - } - - /** - * Generates key - * - * @param int $bitsize Nume of bits to use for key. - * - * @return string $bitsize long string composed of available base32 chars. - */ - public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) { - $bytes = ceil( $bitsize / 8 ); - $secret = wp_generate_password( $bytes, true, true ); - - return self::base32_encode( $secret ); - } - - /** - * Pack stuff - * - * @param string $value The value to be packed. - * - * @return string Binary packed string. - */ - public static function pack64( $value ) { - // 64bit mode (PHP_INT_SIZE == 8). - if ( PHP_INT_SIZE >= 8 ) { - // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality. - if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) { - return pack( 'J', $value ); - } - $highmap = 0xffffffff << 32; - $higher = ( $value & $highmap ) >> 32; - } else { - /* - * 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher - * and not pack anything beyond it's limits. - */ - $higher = 0; - } - - $lowmap = 0xffffffff; - $lower = $value & $lowmap; - - return pack( 'NN', $higher, $lower ); - } - - /** - * Calculate a valid code given the shared secret key - * - * @param string $key The shared secret key to use for calculating code. - * @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size. - * @param int $digits The number of digits in the returned code. - * @param string $hash The hash used to calculate the code. - * @param int $time_step The size of the time step. - * - * @return string The totp code - */ - public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) { - $secret = self::base32_decode( $key ); - - if ( false === $step_count ) { - $step_count = floor( time() / $time_step ); - } - - $timestamp = self::pack64( $step_count ); - - $hash = hash_hmac( $hash, $timestamp, $secret, true ); - - $offset = ord( $hash[19] ) & 0xf; - - $code = ( - ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) | - ( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) | - ( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) | - ( ord( $hash[ $offset + 3 ] ) & 0xff ) - ) % pow( 10, $digits ); - - return str_pad( $code, $digits, '0', STR_PAD_LEFT ); - } - - /** - * Uses the Google Charts API to build a QR Code for use with an otpauth url - * - * @param string $name The name to display in the Authentication app. - * @param string $key The secret key to share with the Authentication app. - * @param string $title The title to display in the Authentication app. - * - * @return string A URL to use as an img src to display the QR code - */ - public static function get_google_qr_code( $name, $key, $title = null ) { - // Encode to support spaces, question marks and other characters. - $name = rawurlencode( $name ); - $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key ); - if ( isset( $title ) ) { - $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) ); - } - return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_url; - } - - /** - * Whether this Two Factor provider is configured and available for the user specified. - * - * @param WP_User $user WP_User object of the logged-in user. - * - * @return boolean - */ - public function is_available_for_user( $user ) { - // Only available if the secret key has been saved for the user. - $key = $this->get_user_totp_key( $user->ID ); - - return ! empty( $key ); - } - - /** - * Prints the form that prompts the user to authenticate. - * - * @param WP_User $user WP_User object of the logged-in user. - */ - public function authentication_page( $user ) { - require_once( ABSPATH . '/wp-admin/includes/template.php' ); - ?> -

- - -

- - = 8 ) { - $j -= 8; - $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j ); - } - } - - return $binary; - } - - /** - * Used with usort to sort an array by distance from 0 - * - * @param int $a First array element. - * @param int $b Second array element. - * - * @return int -1, 0, or 1 as needed by usort - */ - private static function abssort( $a, $b ) { - $a = abs( $a ); - $b = abs( $b ); - if ( $a === $b ) { - return 0; - } - return ($a < $b) ? -1 : 1; - } -} diff --git a/providers/css/fido-u2f-admin.css b/providers/css/fido-u2f-admin.css deleted file mode 100644 index 947dbf43..00000000 --- a/providers/css/fido-u2f-admin.css +++ /dev/null @@ -1,10 +0,0 @@ -#security-keys-section .wp-list-table { - margin-bottom: 2em; -} -#security-keys-section .register-security-key .spinner { - float: none; -} -#security-keys-section .security-key-status { - vertical-align: middle; - font-style: italic; -} diff --git a/providers/js/fido-u2f-admin-inline-edit.js b/providers/js/fido-u2f-admin-inline-edit.js deleted file mode 100644 index 8f40372b..00000000 --- a/providers/js/fido-u2f-admin-inline-edit.js +++ /dev/null @@ -1,145 +0,0 @@ -/* global inlineEditL10n, ajaxurl */ -var inlineEditKey; - -( function( $ ) { - inlineEditKey = { - - init: function() { - var t = this, - row = $( '#security-keys-section #inline-edit' ); - - t.what = '#key-'; - - $( '#security-keys-section #the-list' ).on( 'click', 'a.editinline', function() { - inlineEditKey.edit( this ); - return false; - } ); - - // Prepare the edit row. - row.keyup( function( event ) { - if ( 27 === event.which ) { - return inlineEditKey.revert(); - } - } ); - - $( 'a.cancel', row ).click( function() { - return inlineEditKey.revert(); - } ); - - $( 'a.save', row ).click( function() { - return inlineEditKey.save( this ); - } ); - - $( 'input, select', row ).keydown( function( event ) { - if ( 13 === event.which ) { - return inlineEditKey.save( this ); - } - } ); - }, - - toggle: function( el ) { - var t = this; - 'none' === $( t.what + t.getId( el ) ).css( 'display' ) ? t.revert() : t.edit( el ); - }, - - edit: function( id ) { - var editRow, rowData, val, - t = this; - t.revert(); - - if ( 'object' === typeof id ) { - id = t.getId( id ); - } - - editRow = $( '#inline-edit' ).clone( true ), rowData = $( '#inline_' + id ); - $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '#security-keys-section .widefat thead' ).length ); - - $( t.what + id ).hide().after( editRow ).after( '' ); - - val = $( '.name', rowData ); - val.find( 'img' ).replaceWith( function() { - return this.alt; - } ); - val = val.text(); - $( ':input[name="name"]', editRow ).val( val ); - - $( editRow ).attr( 'id', 'edit-' + id ).addClass( 'inline-editor' ).show(); - $( '.ptitle', editRow ).eq( 0 ).focus(); - - return false; - }, - - save: function( id ) { - var params, fields; - - if ( 'object' === typeof id ) { - id = this.getId( id ); - } - - $( '#security-keys-section table.widefat .spinner' ).addClass( 'is-active' ); - - params = { - action: 'inline-save-key', - keyHandle: id - }; - - fields = $( '#edit-' + id ).find( ':input' ).serialize(); - params = fields + '&' + $.param( params ); - - // Make ajax request. - $.post( ajaxurl, params, - function( r ) { - var row, newID, optionValue; - $( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' ); - - if ( r ) { - if ( -1 !== r.indexOf( '' )[0].submit.call( $( '#your-profile' )[0] ); - } ); - } ); -} )( jQuery ); diff --git a/providers/js/fido-u2f-login.js b/providers/js/fido-u2f-login.js deleted file mode 100644 index 0fe4cb1a..00000000 --- a/providers/js/fido-u2f-login.js +++ /dev/null @@ -1,16 +0,0 @@ -/* global u2f, u2fL10n */ -( function( $ ) { - if ( ! window.u2fL10n ) { - window.console.error( 'u2fL10n is not defined' ); - return; - } - - u2f.sign( u2fL10n.request[0].appId, u2fL10n.request[0].challenge, u2fL10n.request, function( data ) { - if ( data.errorCode ) { - window.console.error( 'Registration Failed', data.errorCode ); - } else { - $( '#u2f_response' ).val( JSON.stringify( data ) ); - $( '#loginform' ).submit(); - } - } ); -} )( jQuery ); diff --git a/readme.txt b/readme.txt index 6baa4caa..87f469b8 100644 --- a/readme.txt +++ b/readme.txt @@ -12,10 +12,7 @@ Enable Two-Factor Authentication using time-based one-time passwords (OTP, Googl Use the "Two-Factor Options" section under "Users" → "Your Profile" to enable and configure one or multiple two-factor authentication providers for your account: - Email codes -- Time Based One-Time Passwords (TOTP) -- FIDO Universal 2nd Factor (U2F) - Backup Codes -- Dummy Method (only for testing purposes) For more history, see [this post](https://stephanis.info/2013/08/14/two-cents-on-two-factor/). @@ -28,11 +25,11 @@ For more history, see [this post](https://stephanis.info/2013/08/14/two-cents-on == Get Involved == -Development happens [on GitHub](https://github.com/georgestephanis/two-factor/). Join the `#core-passwords` channel [on WordPress Slack](http://wordpress.slack.com) ([sign up here](http://chat.wordpress.org)). +Development happens [on GitHub](https://github.com/WordPress/two-factor/). Join the `#core-passwords` channel [on WordPress Slack](http://wordpress.slack.com) ([sign up here](http://chat.wordpress.org)). Here is how to get started: - $ git clone https://github.com/georgestephanis/two-factor.git + $ git clone https://github.com/WordPress/two-factor.git $ npm install Then open [a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) with the suggested changes. diff --git a/tests/providers/class.two-factor-dummy.php b/tests/providers/class.two-factor-dummy.php deleted file mode 100644 index eb758c9c..00000000 --- a/tests/providers/class.two-factor-dummy.php +++ /dev/null @@ -1,88 +0,0 @@ -provider = Two_Factor_Dummy::get_instance(); - } - - /** - * Verify an instance exists. - * @covers Two_Factor_Dummy::get_instance - */ - function test_get_instance() { - - $this->assertNotNull( $this->provider->get_instance() ); - - } - - /** - * Verify the label value. - * @covers Two_Factor_Dummy::get_label - */ - function test_get_label() { - - $this->assertContains( 'Dummy Method', $this->provider->get_label() ); - - } - - /** - * Verify the contents of the authentication page. - * @covers Two_Factor_Dummy::authentication_page - */ - function test_authentication_page() { - - ob_start(); - $this->provider->authentication_page( false ); - $contents = ob_get_clean(); - - $this->assertContains( 'Are you really you?', $contents ); - $this->assertContains( '

', $contents ); - $this->assertContains( 'Yup', $contents ); - - } - - /** - * Verify that dummy validation returns true. - * @covers Two_Factor_Dummy::validate_authentication - */ - function test_validate_authentication() { - - $this->assertTrue( $this->provider->validate_authentication( false ) ); - - } - - /** - * Verify that dummy availability returns true. - * @covers Two_Factor_Dummy::is_available_for_user - */ - function test_is_available_for_user() { - - $this->assertTrue( $this->provider->is_available_for_user( false ) ); - - } - -} diff --git a/tests/providers/class.two-factor-fido-u2f.php b/tests/providers/class.two-factor-fido-u2f.php deleted file mode 100644 index 2119a438..00000000 --- a/tests/providers/class.two-factor-fido-u2f.php +++ /dev/null @@ -1,198 +0,0 @@ -u2f = new u2flib_server\U2F( "http://demo.example.com" ); - - $this->provider = Two_Factor_FIDO_U2F::get_instance(); - } catch ( Exception $e ) { - $this->markTestSkipped( 'Could not create U2F provider!' ); - } - } - - /** - * Verify the label value. - */ - function test_get_label() { - $this->assertContains( 'FIDO Universal 2nd Factor (U2F)', $this->provider->get_label() ); - } - - function test_add_security_key() { - $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); - $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); - $reg = $this->u2f->doRegister($req, $resp); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible(true); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible(true); - - $user_id = $this->factory->user->create(); - $this->assertInternalType( "int", $add_method->invoke($this->provider, $user_id, $reg ) ); - - $delete_method->invoke($this->provider, $user_id ); - } - - function test_add_security_key2() { - $reg = json_decode('{"keyHandle":"j_k-SThUkpALxdWPS8PjBzxaNUMj3WQIGz1rHIDMRnZCMr26C7xWItTRgiRqeqPVMPgSC6WvBJcKgqNNcReDAw","publicKey":"BBLL5S5hWjGRZzpw4kZnDMbAVTdWtlBFaCt5Fsn7sPWAXdInZpVdk\/bGNVfm43+uUmB2w6u90lf57PXQghhbF4I=","certificate":"MIICLjCCARigAwIBAgIECmML\/zALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCkxJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDE3NDI2MzI5NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKQjZF26iyPtbNnl5IuTKs\/fRWTHVzHxz1IHRRBrSbqWD60PCqUJPe4zkIRFqBa4NnzdhVcS80nlZuY3ANQm0J+jJjAkMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4yMAsGCSqGSIb3DQEBCwOCAQEAZTmwMqHPxEjSB64Umwq2tGDKplAcEzrwmg6kgS8KPkJKXKSu9T1H6XBM9+LAE9cN48oUirFFmDIlTbZRXU2Vm2qO9OdrSVFY+qdbF9oti8CKAmPHuJZSW6ii7qNE59dHKUaP4lDYpnhRDqttWSUalh2LPDJQUpO9bsJPkgNZAhBUQMYZXL\/MQZLRYkX+ld7llTNOX5u7n\/4Y5EMr+lqOyVVC9lQ6JP6xoa9q6Zp9+Y9ZmLCecrrcuH6+pLDgAzPcc8qxhC2OR1B0ZSpI9RBgcT0KqnVE0tq1KEDeokPqF3MgmDRkJ++\/a2pV0wAYfPC3tC57BtBdH\/UXEB8xZVFhtA==","counter":-1}'); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible(true); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible(true); - - $user_id = $this->factory->user->create(); - $this->assertInternalType( "int", $add_method->invoke($this->provider, $user_id, $reg ) ); - - $delete_method->invoke($this->provider, $user_id ); - } - - function test_get_security_keys() { - $reg = json_decode('{"keyHandle":"j_k-SThUkpALxdWPS8PjBzxaNUMj3WQIGz1rHIDMRnZCMr26C7xWItTRgiRqeqPVMPgSC6WvBJcKgqNNcReDAw","publicKey":"BBLL5S5hWjGRZzpw4kZnDMbAVTdWtlBFaCt5Fsn7sPWAXdInZpVdk\/bGNVfm43+uUmB2w6u90lf57PXQghhbF4I=","certificate":"MIICLjCCARigAwIBAgIECmML\/zALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCkxJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDE3NDI2MzI5NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKQjZF26iyPtbNnl5IuTKs\/fRWTHVzHxz1IHRRBrSbqWD60PCqUJPe4zkIRFqBa4NnzdhVcS80nlZuY3ANQm0J+jJjAkMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4yMAsGCSqGSIb3DQEBCwOCAQEAZTmwMqHPxEjSB64Umwq2tGDKplAcEzrwmg6kgS8KPkJKXKSu9T1H6XBM9+LAE9cN48oUirFFmDIlTbZRXU2Vm2qO9OdrSVFY+qdbF9oti8CKAmPHuJZSW6ii7qNE59dHKUaP4lDYpnhRDqttWSUalh2LPDJQUpO9bsJPkgNZAhBUQMYZXL\/MQZLRYkX+ld7llTNOX5u7n\/4Y5EMr+lqOyVVC9lQ6JP6xoa9q6Zp9+Y9ZmLCecrrcuH6+pLDgAzPcc8qxhC2OR1B0ZSpI9RBgcT0KqnVE0tq1KEDeokPqF3MgmDRkJ++\/a2pV0wAYfPC3tC57BtBdH\/UXEB8xZVFhtA==","counter":-1}'); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible(true); - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible(true); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible(true); - - $user_id = $this->factory->user->create(); - - $add_method->invoke($this->provider, $user_id, $reg ); - $actual = $get_method->invoke($this->provider, $user_id ); - - $this->assertEquals($reg->keyHandle, $actual[0]->keyHandle); - $this->assertEquals($reg->publicKey, $actual[0]->publicKey); - $this->assertEquals($reg->certificate, $actual[0]->certificate); - $this->assertEquals($reg->counter, $actual[0]->counter); - - $delete_method->invoke($this->provider, $user_id ); - } - - function test_update_security_key() { - $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); - $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); - $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible(true); - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible(true); - $update_method = new ReflectionMethod( $this->provider, 'update_security_key' ); - $update_method->setAccessible(true); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible(true); - - $user_id = $this->factory->user->create(); - $add_method->invoke($this->provider, $user_id, $regs[0] ); - - $data = $this->u2f->doAuthenticate($reqs, $regs, $resp); - - $this->assertEquals(true, $update_method->invoke($this->provider, $user_id, $data ) ); - - $meta = $get_method->invoke($this->provider, $user_id ); - $this->assertEquals($data->keyHandle, $meta[0]->keyHandle); - $this->assertEquals($data->publicKey, $meta[0]->publicKey); - $this->assertEquals($data->certificate, $meta[0]->certificate); - $this->assertEquals($data->counter, $meta[0]->counter); - $this->assertEquals(4, $meta[0]->counter); - - $delete_method->invoke($this->provider, $user_id ); - } - - function test_update_security_key2() { - $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); - $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); - $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible(true); - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible(true); - $update_method = new ReflectionMethod( $this->provider, 'update_security_key' ); - $update_method->setAccessible(true); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible(true); - - $user_id = $this->factory->user->create(); - - $data = $this->u2f->doAuthenticate($reqs, $regs, $resp); - - $this->assertInternalType( "int", $update_method->invoke($this->provider, $user_id, $data ) ); - - $meta = $get_method->invoke($this->provider, $user_id ); - $this->assertEquals($data->keyHandle, $meta[0]->keyHandle); - $this->assertEquals($data->publicKey, $meta[0]->publicKey); - $this->assertEquals($data->certificate, $meta[0]->certificate); - $this->assertEquals($data->counter, $meta[0]->counter); - $this->assertEquals(4, $meta[0]->counter); - - $delete_method->invoke($this->provider, $user_id ); - } - - function test_delete_security_key() { - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible(true); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible(true); - - $user_id = $this->factory->user->create(); - $delete_method->invoke($this->provider, $user_id ); - - $this->assertEquals(array(), $get_method->invoke($this->provider, $user_id )); - } - - function test_delete_security_key2() { - $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); - $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); - $reg = $this->u2f->doRegister($req, $resp); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible(true); - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible(true); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible(true); - - $user_id = $this->factory->user->create(); - $add_method->invoke($this->provider, $user_id, $reg ); - $delete_method->invoke($this->provider, $user_id ); - - $this->assertEquals(array(), $get_method->invoke($this->provider, $user_id )); - } -} diff --git a/tests/providers/class.two-factor-totp.php b/tests/providers/class.two-factor-totp.php deleted file mode 100644 index a9ec0bb3..00000000 --- a/tests/providers/class.two-factor-totp.php +++ /dev/null @@ -1,267 +0,0 @@ -provider = Two_Factor_Totp::get_instance(); - } - - /** - * Clean up after tests. - * - * @see WP_UnitTestCase::tearDown() - */ - public function tearDown() { - unset( $this->provider ); - - parent::tearDown(); - } - - /** - * @covers Two_Factor_Totp::get_instance - */ - public function test_get_instance() { - $this->assertNotNull( $this->provider->get_instance() ); - } - - /** - * @covers Two_Factor_Totp::get_label - */ - public function test_get_label() { - $this->assertContains( 'Google Authenticator', $this->provider->get_label() ); - } - - /** - * @covers Two_Factor_Totp::user_two_factor_options - */ - public function test_user_two_factor_options_empty() { - $this->assertFalse( $this->provider->user_two_factor_options( get_current_user() ) ); - } - - /** - * @covers Two_Factor_Totp::user_two_factor_options - * @covers Two_Factor_Totp::is_available_for_user - */ - public function test_user_two_factor_options_generates_key() { - $user = new WP_User( $this->factory->user->create() ); - - ob_start(); - $this->provider->user_two_factor_options( $user ); - $content = ob_get_clean(); - - $this->assertContains( __( 'Authentication Code:', 'two-factor' ), $content ); - } - - /** - * @covers Two_Factor_Totp::user_two_factor_options_update - * @covers Two_Factor_Totp::is_available_for_user - */ - public function test_user_two_factor_options_update_set_key_no_authcode() { - $user = new WP_User( $this->factory->user->create() ); - - $request_key = '_nonce_user_two_factor_totp_options'; - $_POST[$request_key] = wp_create_nonce( 'user_two_factor_totp_options' ); - $_REQUEST[$request_key] = $_POST[$request_key]; - - $_POST['two-factor-totp-key'] = $this->provider->generate_key(); - - ob_start(); - $this->provider->user_two_factor_options_update( $user->ID ); - $content = ob_get_clean(); - - unset( $_POST['two-factor-totp-key'] ); - - unset( $_REQUEST[$request_key] ); - unset( $_POST[$request_key] ); - - $this->assertFalse( $this->provider->is_available_for_user( $user ) ); - } - - /** - * @covers Two_Factor_Totp::user_two_factor_options_update - * @covers Two_Factor_Totp::is_available_for_user - */ - public function test_user_two_factor_options_update_set_key_bad_auth_code() { - $user = new WP_User( $this->factory->user->create() ); - - $request_key = '_nonce_user_two_factor_totp_options'; - $_POST[$request_key] = wp_create_nonce( 'user_two_factor_totp_options' ); - $_REQUEST[$request_key] = $_POST[$request_key]; - - $_POST['two-factor-totp-key'] = $this->provider->generate_key(); - $_POST['two-factor-totp-authcode'] = 'bad_test_authcode'; - - ob_start(); - $this->provider->user_two_factor_options_update( $user->ID ); - $content = ob_get_clean(); - - unset( $_POST['two-factor-totp-authcode'] ); - unset( $_POST['two-factor-totp-key'] ); - - unset( $_REQUEST[$request_key] ); - unset( $_POST[$request_key] ); - - $this->assertFalse( $this->provider->is_available_for_user( $user ) ); - } - - /** - * @covers Two_Factor_Totp::user_two_factor_options_update - * @covers Two_Factor_Totp::is_available_for_user - */ - public function test_user_two_factor_options_update_set_key() { - $user = new WP_User( $this->factory->user->create() ); - - $request_key = '_nonce_user_two_factor_totp_options'; - $_POST[$request_key] = wp_create_nonce( 'user_two_factor_totp_options' ); - $_REQUEST[$request_key] = $_POST[$request_key]; - - $key = $this->provider->generate_key(); - $_POST['two-factor-totp-key'] = $key; - $_POST['two-factor-totp-authcode'] = $this->provider->calc_totp( $key ); - - ob_start(); - $this->provider->user_two_factor_options_update( $user->ID ); - $content = ob_get_clean(); - - unset( $_POST['two-factor-totp-authcode'] ); - unset( $_POST['two-factor-totp-key'] ); - - unset( $_REQUEST[$request_key] ); - unset( $_POST[$request_key] ); - - $this->assertTrue( $this->provider->is_available_for_user( $user ) ); - } - - /** - * @covers Two_Factor_Totp::base32_encode - */ - public function test_base32_encode() { - $string = 'EV5XW7TOL4QHIKBIGVEU23KAFRND66LY'; - $string_base32 = 'IVLDKWCXG5KE6TBUKFEESS2CJFDVMRKVGIZUWQKGKJHEINRWJRMQ'; - - $this->assertEquals( $string_base32, $this->provider->base32_encode( $string ) ); - } - - /** - * @covers Two_Factor_Totp::base32_encode - */ - public function test_base32_decode() { - $string = 'EV5XW7TOL4QHIKBIGVEU23KAFRND66LY'; - $string_base32 = 'IVLDKWCXG5KE6TBUKFEESS2CJFDVMRKVGIZUWQKGKJHEINRWJRMQ'; - - $this->assertEquals( $string, $this->provider->base32_decode( $string_base32 ) ); - } - - /** - * @covers Two_Factor_Totp::is_valid_authcode - * @covers Two_Factor_Totp::generate_key - * @covers Two_Factor_Totp::calc_totp - */ - public function test_is_valid_authcode() { - $key = $this->provider->generate_key(); - $authcode = $this->provider->calc_totp( $key ); - - $this->assertTrue( $this->provider->is_valid_authcode( $key, $authcode ) ); - } - - /** - * Check secret key CRUD operations. - * - * @covers Two_Factor_Totp::get_user_totp_key - * @covers Two_Factor_Totp::set_user_totp_key - * @covers Two_Factor_Totp::delete_user_totp_key - */ - public function test_user_totp_key() { - $user = new WP_User( $this->factory->user->create() ); - - $this->assertEquals( - '', - $this->provider->get_user_totp_key( $user->ID ), - 'User does not have TOTP secret configured by default' - ); - - $this->provider->set_user_totp_key( $user->ID, '1234' ); - - $this->assertEquals( - '1234', - $this->provider->get_user_totp_key( $user->ID ), - 'User has a secret key' - ); - - $this->provider->delete_user_totp_key( $user->ID ); - - $this->assertEquals( - '', - $this->provider->get_user_totp_key( $user->ID ), - 'User no longer has a secret key stored' - ); - } - - /** - * @covers Two_Factor_Totp::is_valid_key - */ - public function test_is_valid_key() { - $this->assertTrue( $this->provider->is_valid_key( 'ABC234' ), 'Base32 chars are valid' ); - $this->assertFalse( $this->provider->is_valid_key( '' ), 'Empty string is invalid' ); - $this->assertFalse( $this->provider->is_valid_key( 'abc233' ), 'Lowercase chars are invalid' ); - $this->assertFalse( $this->provider->is_valid_key( 'has a space' ), 'Spaces not allowed' ); - } - - /** - * @covers Two_Factor_Totp::user_two_factor_options_update - */ - public function test_user_can_delete_secret() { - $user = new WP_User( $this->factory->user->create() ); - $key = $this->provider->generate_key(); - - // Configure secret for the user. - $this->provider->set_user_totp_key( $user->ID, $key ); - - $this->assertEquals( - $key, - $this->provider->get_user_totp_key( $user->ID ), - 'Secret was stored and can be fetched' - ); - - // Configure the request and the nonce. - $nonce = wp_create_nonce( 'user_two_factor_totp_options' ); - $_POST['_nonce_user_two_factor_totp_options'] = $nonce; - $_REQUEST['_nonce_user_two_factor_totp_options'] = $nonce; // Required for check_admin_referer(). - - // Set the request to delete things. - $_POST['two-factor-totp-delete'] = 1; - - // Process the request. - $this->provider->user_two_factor_options_update( $user->ID ); - - $this->assertEquals( - '', - $this->provider->get_user_totp_key( $user->ID ), - 'Secret has been deleted' - ); - } - -} diff --git a/two-factor.php b/two-factor.php index 636e29cd..f4220aaf 100644 --- a/two-factor.php +++ b/two-factor.php @@ -4,8 +4,8 @@ * Plugin URI: https://wordpress.org/plugins/two-factor/ * Description: A prototype extensible core to enable Two-Factor Authentication. * Author: Plugin Contributors - * Version: 0.4.7 - * Author URI: https://github.com/georgestephanis/two-factor/graphs/contributors + * Version: 0.5.0-alpha + * Author URI: https://github.com/WordPress/two-factor/graphs/contributors * Network: True * Text Domain: two-factor */