updated plugin Two Factor version 0.16.0

This commit is contained in:
2026-06-03 21:29:19 +00:00
committed by Gitium
parent bc89bee944
commit 57bccfdbd1
33 changed files with 1920 additions and 2942 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
<g fill="none" fill-rule="evenodd">
<path fill="#CCC" d="M98 150a60 60 0 1 1 60 0v60a8 8 0 0 1-8 8h-44a8 8 0 0 1-8-8v-60z"/>
<path fill="#0073AA" d="M116 132a36 36 0 1 1 24 0v64.7a4 4 0 0 1-4 4h-16a4 4 0 0 1-4-4v-64-.7z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -11,11 +11,15 @@
* Should be used with care because ideally we wouldn't need
* any integration specific code for this plugin. Everything should
* be handled through clever use of hooks and best practices.
*
* @since 0.5.0
*/
class Two_Factor_Compat {
/**
* Initialize all the custom hooks as necessary.
*
* @since 0.5.0
*
* @return void
*/
public function init() {
@ -30,6 +34,8 @@ class Two_Factor_Compat {
/**
* Jetpack single sign-on wants long-lived sessions for users.
*
* @since 0.5.0
*
* @param boolean $rememberme Current state of the "remember me" toggle.
*
* @return boolean
@ -47,6 +53,8 @@ class Two_Factor_Compat {
/**
* Helper to detect the presence of the active SSO module.
*
* @since 0.5.0
*
* @return boolean
*/
public function jetpack_is_sso_active() {

File diff suppressed because it is too large Load Diff

View File

@ -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 messages 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.Transport>}
*/
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<u2f.SignRequest>} 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<u2f.SignRequest>} signRequests
* @param {Array<u2f.RegisterRequest>} 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<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
* @private
*/
u2f.waitingForPort_ = [];
/**
* A counter for requestIds.
* @type {number}
* @private
*/
u2f.reqCounter_ = 0;
/**
* A map from requestIds to client callbacks
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
* |function((u2f.Error|u2f.SignResponse)))>}
* @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.<u2f.Response>} 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<u2f.RegisteredKey>} 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<u2f.RegisteredKey>} 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<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} 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<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} 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);
});
};

View File

@ -1,507 +0,0 @@
<?php
/* Copyright (c) 2014 Yubico AB
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace u2flib_server;
/** Constant for the version of the u2f protocol */
const U2F_VERSION = "U2F_V2";
/** Error for the authentication message not matching any outstanding
* authentication request */
const ERR_NO_MATCHING_REQUEST = 1;
/** Error for the authentication message not matching any registration */
const ERR_NO_MATCHING_REGISTRATION = 2;
/** Error for the signature on the authentication message not verifying with
* the correct key */
const ERR_AUTHENTICATION_FAILURE = 3;
/** Error for the challenge in the registration message not matching the
* registration challenge */
const ERR_UNMATCHED_CHALLENGE = 4;
/** Error for the attestation signature on the registration message not
* verifying */
const ERR_ATTESTATION_SIGNATURE = 5;
/** Error for the attestation verification not verifying */
const ERR_ATTESTATION_VERIFICATION = 6;
/** Error for not getting good random from the system */
const ERR_BAD_RANDOM = 7;
/** Error when the counter is lower than expected */
const ERR_COUNTER_TOO_LOW = 8;
/** Error decoding public key */
const ERR_PUBKEY_DECODE = 9;
/** Error user-agent returned error */
const ERR_BAD_UA_RETURNING = 10;
/** Error old OpenSSL version */
const ERR_OLD_OPENSSL = 11;
/** @internal */
const PUBKEY_LEN = 65;
class U2F
{
/** @var string */
private $appId;
/** @var null|string */
private $attestDir;
/** @internal */
private $FIXCERTS = array(
'349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
'1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
'6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
);
/**
* @param string $appId Application id for the running application
* @param string|null $attestDir Directory where trusted attestation roots may be found
* @throws Error If OpenSSL older than 1.0.0 is used
*/
public function __construct($appId, $attestDir = null)
{
if(OPENSSL_VERSION_NUMBER < 0x10000000) {
throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL);
}
$this->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);
}
}

View File

@ -39,13 +39,36 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
parent::__construct();
}
/**
* Enqueue scripts for backup codes.
*
* @since 0.10.0
*
* @codeCoverageIgnore
*
* @param string $hook_suffix Optional. The current admin page hook suffix.
*/
public function enqueue_assets( $hook_suffix = '' ) {
wp_register_script(
'two-factor-backup-codes-admin',
plugins_url( 'js/backup-codes-admin.js', __FILE__ ),
array( 'jquery', 'wp-api-request' ),
TWO_FACTOR_VERSION,
true
);
}
/**
* Register the rest-api endpoints required for this provider.
*
* @since 0.8.0
*
* @codeCoverageIgnore
*/
public function register_rest_routes() {
@ -55,11 +78,11 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'rest_generate_codes' ),
'permission_callback' => function( $request ) {
'permission_callback' => function ( $request ) {
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
},
'args' => array(
'user_id' => array(
'user_id' => array(
'required' => true,
'type' => 'integer',
),
@ -106,7 +129,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
array( 'a' => array( 'href' => true ) )
);
?>
<span>
</span>
</p>
</div>
<?php
@ -154,12 +177,19 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
* @param WP_User $user WP_User object of the logged-in user.
*/
public function user_options( $user ) {
wp_enqueue_script( 'wp-api-request' );
wp_enqueue_script( 'jquery' );
wp_localize_script(
'two-factor-backup-codes-admin',
'twoFactorBackupCodes',
array(
'restPath' => Two_Factor_Core::REST_NAMESPACE . '/generate-backup-codes',
'userId' => $user->ID,
)
);
wp_enqueue_script( 'two-factor-backup-codes-admin' );
$count = self::codes_remaining_for_user( $user );
?>
<p id="two-factor-backup-codes">
<div id="two-factor-backup-codes">
<p class="two-factor-backup-codes-count">
<?php
echo esc_html(
@ -175,58 +205,40 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
<button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
<?php esc_html_e( 'Generate new recovery codes', 'two-factor' ); ?>
</button>
<em><?php esc_html_e( 'This invalidates all currently stored codes.', 'two-factor' ); ?></em>
</p>
</p>
</div>
<div class="two-factor-backup-codes-wrapper" style="display:none;">
<ol class="two-factor-backup-codes-unused-codes"></ol>
<div class="two-factor-backup-codes-list-wrap">
<ol class="two-factor-backup-codes-unused-codes"></ol>
</div>
<p class="description"><?php esc_html_e( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'two-factor' ); ?></p>
<p>
<a class="button button-two-factor-backup-codes-copy button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-copy-link"><?php esc_html_e( 'Copy Codes', 'two-factor' ); ?></a>
<a class="button button-two-factor-backup-codes-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'two-factor' ); ?></a>
<p>
</p>
</div>
<script type="text/javascript">
( function( $ ) {
$( '.button-two-factor-backup-codes-generate' ).click( function() {
wp.apiRequest( {
method: 'POST',
path: <?php echo wp_json_encode( Two_Factor_Core::REST_NAMESPACE . '/generate-backup-codes' ); ?>,
data: {
user_id: <?php echo wp_json_encode( $user->ID ); ?>
}
} ).then( function( response ) {
var $codesList = $( '.two-factor-backup-codes-unused-codes' );
$( '.two-factor-backup-codes-wrapper' ).show();
$codesList.html( '' );
// Append the codes.
for ( i = 0; i < response.codes.length; i++ ) {
$codesList.append( '<li>' + response.codes[ i ] + '</li>' );
}
// Update counter.
$( '.two-factor-backup-codes-count' ).html( response.i18n.count );
$( '#two-factor-backup-codes-download-link' ).attr( 'href', response.download_link );
} );
} );
} )( jQuery );
</script>
<?php
}
/**
* Get the backup code length for a user.
*
* @since 0.11.0
*
* @param WP_User $user User object.
*
* @return int Number of characters.
*/
private function get_backup_code_length( $user ) {
/**
* Customize the character count of the backup codes.
* Filters the character count of the backup codes.
*
* @var int $code_length Length of the backup code.
* @var WP_User $user User object.
* @since 0.11.0
*
* @param int $code_length Length of the backup code. Default 8.
* @param WP_User $user User object.
*/
$code_length = (int) apply_filters( 'two_factor_backup_code_length', 8, $user );
@ -242,7 +254,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
* @param array $args Optional arguments for assigning new codes.
* @return array
*/
public function generate_codes( $user, $args = '' ) {
public function generate_codes( $user, $args = array() ) {
$codes = array();
$codes_hashed = array();
@ -277,13 +289,15 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
* Generates Backup Codes for returning through the WordPress Rest API.
*
* @since 0.8.0
* @param WP_REST_Request $request Request object.
* @return array|WP_Error
*/
public function rest_generate_codes( $request ) {
$user_id = $request['user_id'];
$user = get_user_by( 'id', $user_id );
// Hardcode these, the user shouldn't be able to choose them.
$args = array(
$args = array(
'number' => self::NUMBER_OF_CODES,
'method' => 'replace',
);
@ -304,7 +318,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
$i = 1;
foreach ( $codes as $code ) {
$download_link .= rawurlencode( "{$i}. {$code}\r\n" );
$i++;
++$i;
}
$i18n = array(
@ -327,6 +341,8 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
/**
* Returns the number of unused codes for the specified user
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @return int $int The number of unused codes remaining
*/
@ -348,17 +364,47 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
$code_length = $this->get_backup_code_length( $user );
$code_length = $this->get_backup_code_length( $user );
$code_placeholder = str_repeat( 'X', $code_length );
?>
<?php
/**
* Fires before the two-factor authentication prompt text.
*
* @since 0.15.0
*
* @param Two_Factor_Provider $provider The two-factor provider instance.
*/
do_action( 'two_factor_before_authentication_prompt', $this );
?>
<p class="two-factor-prompt"><?php esc_html_e( 'Enter a recovery code.', 'two-factor' ); ?></p>
<?php
/**
* Fires after the two-factor authentication prompt text.
*
* @since 0.15.0
*
* @param Two_Factor_Provider $provider The two-factor provider instance.
*/
do_action( 'two_factor_after_authentication_prompt', $this );
?>
<p>
<label for="authcode"><?php esc_html_e( 'Recovery Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" name="two-factor-backup-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $code_placeholder ); ?>" data-digits="<?php echo esc_attr( $code_length ); ?>" />
</p>
<?php
submit_button( __( 'Submit', 'two-factor' ) );
/**
* Fires after the two-factor authentication input field.
*
* @since 0.15.0
*
* @param Two_Factor_Provider $provider The two-factor provider instance.
*/
do_action( 'two_factor_after_authentication_input', $this );
?>
<?php
submit_button( __( 'Verify', 'two-factor' ) );
}
/**
@ -428,6 +474,8 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {

View File

@ -43,8 +43,20 @@ class Two_Factor_Dummy extends Two_Factor_Provider {
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_before_authentication_prompt', $this );
?>
<p><?php esc_html_e( 'Are you really you?', 'two-factor' ); ?></p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_prompt', $this );
?>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_input', $this );
?>
<?php
submit_button( __( 'Yup.', 'two-factor' ) );
}

View File

@ -39,6 +39,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
* Class constructor.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
protected function __construct() {
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
@ -66,13 +68,17 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Get the email token length.
*
* @since 0.11.0
*
* @return int Email token string length.
*/
private function get_token_length() {
/**
* Number of characters in the email token.
* Filters the number of characters in the email token.
*
* @param int $token_length Number of characters in the email token.
* @since 0.11.0
*
* @param int $token_length Number of characters in the email token. Default 8.
*/
$token_length = (int) apply_filters( 'two_factor_email_token_length', 8 );
@ -99,6 +105,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Check if user has a valid token already.
*
* @since 0.2.0
*
* @param int $user_id User ID.
* @return boolean If user has a valid email token.
*/
@ -115,6 +123,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Has the user token validity timestamp expired.
*
* @since 0.6.0
*
* @param integer $user_id User ID.
*
* @return boolean
@ -134,6 +144,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Get the lifetime of a user token in seconds.
*
* @since 0.6.0
*
* @param integer $user_id User ID.
*
* @return integer|null Return `null` if the lifetime can't be measured.
@ -151,6 +163,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Return the token time-to-live for a user.
*
* @since 0.6.0
*
* @param integer $user_id User ID.
*
* @return integer
@ -159,22 +173,23 @@ class Two_Factor_Email extends Two_Factor_Provider {
$token_ttl = 15 * MINUTE_IN_SECONDS;
/**
* Number of seconds the token is considered valid
* after the generation.
* Filters the number of seconds the email token is considered valid after generation.
*
* @since 0.6.0
* @deprecated 0.11.0 Use {@see 'two_factor_email_token_ttl'} instead.
*
* @param integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
* @param int $token_ttl Token time-to-live in seconds.
* @param int $user_id User ID.
*/
$token_ttl = (int) apply_filters_deprecated( 'two_factor_token_ttl', array( $token_ttl, $user_id ), '0.11.0', 'two_factor_email_token_ttl' );
/**
* Number of seconds the token is considered valid
* after the generation.
* Filters the number of seconds the email token is considered valid after generation.
*
* @param integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
* @since 0.11.0
*
* @param int $token_ttl Token time-to-live in seconds.
* @param int $user_id User ID.
*/
return (int) apply_filters( 'two_factor_email_token_ttl', $token_ttl, $user_id );
}
@ -182,6 +197,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Get the authentication token for the user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
*
* @return string|boolean User token or `false` if no token found.
@ -234,6 +251,24 @@ class Two_Factor_Email extends Two_Factor_Provider {
delete_user_meta( $user_id, self::TOKEN_META_KEY );
}
/**
* Get the client IP address for the current request.
*
* @since 0.15.0
*
* Note that the IP address is used only for information purposes
* and is expected to be configured correctly, if behind proxy.
*
* @return string|null
*/
private function get_client_ip() {
if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders -- don't have more reliable option for now.
return preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__REMOTE_ADDR__ -- we're limit the allowed characters.
}
return null;
}
/**
* Generate and email the user token.
*
@ -243,15 +278,40 @@ class Two_Factor_Email extends Two_Factor_Provider {
* @return bool Whether the email contents were sent successfully.
*/
public function generate_and_email_token( $user ) {
$token = $this->generate_token( $user->ID );
$token = $this->generate_token( $user->ID );
$remote_ip = $this->get_client_ip();
$ttl_minutes = (int) ceil( $this->user_token_ttl( $user->ID ) / MINUTE_IN_SECONDS );
/* translators: %s: site name */
$subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'two-factor' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) );
/* translators: %s: token */
$message = wp_strip_all_tags( sprintf( __( 'Enter %s to log in.', 'two-factor' ), $token ) );
$subject = wp_strip_all_tags(
sprintf(
/* translators: %s: site name */
__( '[%s] Login confirmation code', 'two-factor' ),
wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES )
)
);
$message_parts = array(
__( 'Please complete the login by entering the verification code below:', 'two-factor' ),
$token,
sprintf(
/* translators: %d: number of minutes */
__( 'This code will expire in %d minutes.', 'two-factor' ),
$ttl_minutes
),
sprintf(
/* translators: %1$s: IP address of user, %2$s: user login */
__( 'A user from IP address %1$s has successfully authenticated as %2$s. If this wasn\'t you, please change your password.', 'two-factor' ),
$remote_ip,
$user->user_login
),
);
$message = wp_strip_all_tags( implode( "\n\n", $message_parts ) );
/**
* Filter the token email subject.
* Filters the token email subject.
*
* @since 0.5.2
*
* @param string $subject The email subject line.
* @param int $user_id The ID of the user.
@ -259,7 +319,9 @@ class Two_Factor_Email extends Two_Factor_Provider {
$subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID );
/**
* Filter the token email message.
* Filters the token email message.
*
* @since 0.5.2
*
* @param string $message The email message.
* @param string $token The token.
@ -286,30 +348,33 @@ class Two_Factor_Email extends Two_Factor_Provider {
$this->generate_and_email_token( $user );
}
$token_length = $this->get_token_length();
$token_length = $this->get_token_length();
$token_placeholder = str_repeat( 'X', $token_length );
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_before_authentication_prompt', $this );
?>
<p class="two-factor-prompt"><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_prompt', $this );
?>
<p>
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" name="two-factor-email-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" autocomplete="one-time-code" placeholder="<?php echo esc_attr( $token_placeholder ); ?>" data-digits="<?php echo esc_attr( $token_length ); ?>" />
<?php submit_button( __( 'Log In', 'two-factor' ) ); ?>
</p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_input', $this );
?>
<?php submit_button( __( 'Verify', 'two-factor' ) ); ?>
<p class="two-factor-email-resend">
<input type="submit" class="button" name="<?php echo esc_attr( self::INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'two-factor' ); ?>" />
</p>
<script type="text/javascript">
setTimeout( function(){
var d;
try{
d = document.getElementById('authcode');
d.value = '';
d.focus();
} catch(e){}
}, 200);
</script>
<?php wp_enqueue_script( 'two-factor-login' ); ?>
<?php
}
@ -317,11 +382,13 @@ class Two_Factor_Email extends Two_Factor_Provider {
* Send the email code if missing or requested. Stop the authentication
* validation if a new token has been generated and sent.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function pre_process_authentication( $user ) {
if ( isset( $user->ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) {
if ( isset( $user->ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- non-distructive option that relies on user state.
$this->generate_and_email_token( $user );
return true;
}
@ -368,7 +435,7 @@ class Two_Factor_Email extends Two_Factor_Provider {
public function user_options( $user ) {
$email = $user->user_email;
?>
<div>
<p>
<?php
echo esc_html(
sprintf(
@ -378,13 +445,15 @@ class Two_Factor_Email extends Two_Factor_Provider {
)
);
?>
</div>
</p>
<?php
}
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {

View File

@ -1,160 +0,0 @@
<?php
/**
* Class for displaying the list of security key items.
*
* @package Two_Factor
*/
// Load the parent class if it doesn't exist.
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Class for displaying the list of security key items.
*
* @since 0.1-dev
* @access private
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
/**
* Get a list of columns.
*
* @since 0.1-dev
*
* @return array
*/
public function get_columns() {
return array(
'name' => 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 = '<div class="hidden" id="inline_' . esc_attr( $item->keyHandle ) . '">'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$out .= '<div class="name">' . esc_html( $item->name ) . '</div>';
$out .= '</div>';
$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 gmdate( get_option( 'date_format', 'r' ), $item->added );
case 'last_used':
return gmdate( 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 ) {
?>
<tr id="key-<?php echo esc_attr( $item->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ?>">
<?php $this->single_row_columns( $item ); ?>
</tr>
<?php
}
/**
* Outputs the hidden row displayed when inline editing
*
* @since 0.1-dev
*/
public function inline_edit() {
?>
<table style="display: none">
<tbody id="inlineedit">
<tr id="inline-edit" class="inline-edit-row" style="display: none">
<td colspan="<?php echo esc_attr( $this->get_column_count() ); ?>" class="colspanchange">
<fieldset>
<div class="inline-edit-col">
<label>
<span class="title"><?php esc_html_e( 'Name', 'two-factor' ); ?></span>
<span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" /></span>
</label>
</div>
</fieldset>
<?php
$core_columns = array(
'name' => true,
'added' => true,
'last_used' => true,
);
list( $columns ) = $this->get_column_info();
foreach ( $columns as $column_name => $column_display_name ) {
if ( isset( $core_columns[ $column_name ] ) ) {
continue;
}
/** This action is documented in wp-admin/includes/class-wp-posts-list-table.php */
do_action( 'quick_edit_custom_box', $column_name, 'edit-security-keys' );
}
?>
<p class="inline-edit-save submit">
<a href="#inline-edit" class="cancel button-secondary alignleft"><?php esc_html_e( 'Cancel', 'two-factor' ); ?></a>
<a href="#inline-edit" class="save button-primary alignright"><?php esc_html_e( 'Update', 'two-factor' ); ?></a>
<span class="spinner"></span>
<span class="error" style="display:none;"></span>
<?php wp_nonce_field( 'keyinlineeditnonce', '_inline_edit', false ); ?>
<br class="clear" />
</p>
</td>
</tr>
</tbody>
</table>
<?php
}
}

View File

@ -1,363 +0,0 @@
<?php
/**
* Class for registering & modifying FIDO U2F security keys.
*
* @package Two_Factor
*/
/**
* Class for registering & modifying FIDO U2F security keys.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F_Admin {
/**
* The user meta register data.
*
* @type string
*/
const REGISTER_DATA_USER_META_KEY = '_two_factor_fido_u2f_register_request';
/**
* Add various hooks.
*
* @since 0.1-dev
*
* @access public
* @static
*/
public static function add_hooks() {
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
add_action( 'show_user_security_settings', array( __CLASS__, 'show_user_profile' ) );
add_action( 'personal_options_update', array( __CLASS__, 'catch_submission' ), 0 );
add_action( 'edit_user_profile_update', array( __CLASS__, 'catch_submission' ), 0 );
add_action( 'load-profile.php', array( __CLASS__, 'catch_delete_security_key' ) );
add_action( 'load-user-edit.php', array( __CLASS__, 'catch_delete_security_key' ) );
add_action( 'wp_ajax_inline-save-key', array( __CLASS__, 'wp_ajax_inline_save' ) );
}
/**
* Enqueue assets.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param string $hook Current page.
*/
public static function enqueue_assets( $hook ) {
if ( ! in_array( $hook, array( 'user-edit.php', 'profile.php' ), true ) ) {
return;
}
$user_id = Two_Factor_Core::current_user_being_edited();
if ( ! $user_id ) {
return;
}
$security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id );
// @todo Ensure that scripts don't fail because of missing u2fL10n.
try {
$data = Two_Factor_FIDO_U2F::$u2f->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(
'user_id' => $user_id,
'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 ) {
if ( ! Two_Factor_FIDO_U2F::is_supported_for_user( $user ) ) {
return;
}
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 );
}
?>
<div class="security-keys" id="security-keys-section">
<h3><?php esc_html_e( 'Security Keys', 'two-factor' ); ?></h3>
<?php if ( ! is_ssl() ) : ?>
<p class="u2f-error-https">
<em><?php esc_html_e( 'U2F requires an HTTPS connection. You won\'t be able to add new security keys over HTTP.', 'two-factor' ); ?></em>
</p>
<?php endif; ?>
<div class="register-security-key">
<input type="hidden" name="do_new_security_key" id="do_new_security_key" />
<input type="hidden" name="u2f_response" id="u2f_response" />
<button type="button" class="button button-secondary" id="register_security_key"><?php echo esc_html( _x( 'Register New Key', 'security key', 'two-factor' ) ); ?></button>
<span class="spinner"></span>
<span class="security-key-status"></span>
</div>
<?php if ( $new_key ) : ?>
<div class="notice notice-success is-dismissible">
<p class="new-security-key"><?php esc_html_e( 'Your new security key registered.', 'two-factor' ); ?></p>
</div>
<?php endif; ?>
<p><a href="https://support.google.com/accounts/answer/6103523"><?php esc_html_e( 'You can find FIDO U2F Security Key devices for sale from here.', 'two-factor' ); ?></a></p>
<?php
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin-list-table.php';
$u2f_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table();
$u2f_list_table->items = $security_keys;
$u2f_list_table->prepare_items();
$u2f_list_table->display();
$u2f_list_table->inline_edit();
?>
</div>
<?php
}
/**
* Catch the non-ajax submission from the new form.
*
* This executes during the `personal_options_update` & `edit_user_profile_update` actions.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param int $user_id User ID.
* @return void|never
*/
public static function catch_submission( $user_id ) {
if ( ! empty( $_REQUEST['do_new_security_key'] ) ) {
check_admin_referer( "user_security_keys-{$user_id}", '_nonce_user_security_keys' );
try {
$response = json_decode( stripslashes( $_POST['u2f_response'] ) );
$reg = Two_Factor_FIDO_U2F::$u2f->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;
}
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 = Two_Factor_Core::current_user_being_edited();
if ( ! empty( $user_id ) && ! 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' );
exit;
}
}
/**
* 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( '<a href="#" class="editinline">%s</a>', 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 ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$delete_link = wp_nonce_url( $delete_link, "delete_security_key-{$item->keyHandle}", '_nonce_delete_security_key' );
return sprintf( '<a href="%1$s">%2$s</a>', 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_once 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 = Two_Factor_Core::current_user_being_edited();
$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'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
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();
}
}

View File

@ -1,404 +0,0 @@
<?php
/**
* Class for creating a FIDO Universal 2nd Factor provider.
*
* @package Two_Factor
*/
/**
* Class for creating a FIDO Universal 2nd Factor provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
/**
* U2F Library
*
* @var u2flib_server\U2F
*/
public static $u2f;
/**
* The user meta registered key.
*
* @type string
*/
const REGISTERED_KEY_USER_META_KEY = '_two_factor_fido_u2f_registered_key';
/**
* The user meta authenticate data.
*
* @type string
*/
const AUTH_DATA_USER_META_KEY = '_two_factor_fido_u2f_login_request';
/**
* Version number for the bundled assets.
*
* @var string
*/
const U2F_ASSET_VERSION = '0.2.1';
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
if ( version_compare( PHP_VERSION, '5.3.0', '<' ) ) {
return;
}
require_once TWO_FACTOR_DIR . 'includes/Yubico/U2F.php';
self::$u2f = new u2flib_server\U2F( self::get_u2f_app_id() );
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin.php';
Two_Factor_FIDO_U2F_Admin::add_hooks();
// Ensure the script dependencies have been registered before they're enqueued at a later priority.
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'login_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
parent::__construct();
}
/**
* Get the asset version number.
*
* TODO: There should be a plugin-level helper for getting the current plugin version.
*
* @return string
*/
public static function asset_version() {
return self::U2F_ASSET_VERSION;
}
/**
* Return the U2F AppId. U2F requires the AppID to use HTTPS
* and a top-level domain.
*
* @return string AppID URI
*/
public static function get_u2f_app_id() {
$url_parts = wp_parse_url( home_url() );
if ( ! empty( $url_parts['port'] ) ) {
return sprintf( 'https://%s:%d', $url_parts['host'], $url_parts['port'] );
} else {
return sprintf( 'https://%s', $url_parts['host'] );
}
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'FIDO U2F Security Keys', 'Provider Label', 'two-factor' );
}
/**
* Returns the "continue with" text provider for the login screen.
*
* @since 0.9.0
*/
public function get_alternative_provider_label() {
return __( 'Use your security key', 'two-factor' );
}
/**
* Register script dependencies used during login and when
* registering keys in the WP admin.
*
* @return void
*/
public static function enqueue_scripts() {
wp_register_script(
'fido-u2f-api',
plugins_url( 'includes/Google/u2f-api.js', dirname( __FILE__ ) ),
null,
self::asset_version(),
true
);
wp_register_script(
'fido-u2f-login',
plugins_url( 'js/fido-u2f-login.js', __FILE__ ),
array( 'jquery', 'fido-u2f-api' ),
self::asset_version(),
true
);
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return void
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
// U2F doesn't work without HTTPS.
if ( ! is_ssl() ) {
?>
<p><?php esc_html_e( 'U2F requires an HTTPS connection. Please use an alternative 2nd factor method.', 'two-factor' ); ?></p>
<?php
return;
}
try {
$keys = self::get_security_keys( $user->ID );
$data = self::$u2f->getAuthenticateData( $keys );
update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data );
} catch ( Exception $e ) {
?>
<p><?php esc_html_e( 'An error occurred while creating authentication data.', 'two-factor' ); ?></p>
<?php
return;
}
wp_localize_script(
'fido-u2f-login',
'u2fL10n',
array(
'request' => $data,
)
);
wp_enqueue_script( 'fido-u2f-login' );
?>
<p><?php esc_html_e( 'Now insert (and tap) your Security Key.', 'two-factor' ); ?></p>
<input type="hidden" name="u2f_response" id="u2f_response" />
<?php
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
$requests = get_user_meta( $user->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 = time();
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 ) {
?>
<p>
<?php esc_html_e( 'Requires an HTTPS connection. Configure your security keys in the "Security Keys" section below.', 'two-factor' ); ?>
</p>
<?php
}
/**
* Add registered security key to a user.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param object $register The data of registered security key.
* @return int|bool Meta ID on success, false on failure.
*/
public static function add_security_key( $user_id, $register ) {
if ( ! is_numeric( $user_id ) ) {
return false;
}
if (
! is_object( $register )
|| ! property_exists( $register, 'keyHandle' ) || empty( $register->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $register, 'certificate' ) || empty( $register->certificate )
|| ! property_exists( $register, 'counter' ) || ( -1 > $register->counter )
) {
return false;
}
$register = array(
'keyHandle' => $register->keyHandle, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'publicKey' => $register->publicKey, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'certificate' => $register->certificate,
'counter' => $register->counter,
);
$register['name'] = __( 'New Security Key', 'two-factor' );
$register['added'] = time();
$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 ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! 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 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
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 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
global $wpdb;
if ( ! is_numeric( $user_id ) ) {
return false;
}
$user_id = absint( $user_id );
if ( ! $user_id ) {
return false;
}
$keyHandle = wp_unslash( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$keyHandle = maybe_serialize( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$query = $wpdb->prepare( "SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id = %d", self::REGISTERED_KEY_USER_META_KEY, $user_id );
if ( $keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$key_handle_lookup = sprintf( ':"%s";s:', $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$query .= $wpdb->prepare(
' AND meta_value LIKE %s',
'%' . $wpdb->esc_like( $key_handle_lookup ) . '%'
);
}
$meta_ids = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( ! count( $meta_ids ) ) {
return false;
}
foreach ( $meta_ids as $meta_id ) {
delete_metadata_by_mid( 'user', $meta_id );
}
return true;
}
/**
* Return user meta keys to delete during plugin uninstall.
*
* @return array
*/
public static function uninstall_user_meta_keys() {
return array(
self::REGISTERED_KEY_USER_META_KEY,
self::AUTH_DATA_USER_META_KEY,
'_two_factor_fido_u2f_register_request', // From Two_Factor_FIDO_U2F_Admin which is not loaded during uninstall.
);
}
}

View File

@ -25,7 +25,7 @@ abstract class Two_Factor_Provider {
$class_name = static::class;
if ( ! isset( $instances[ $class_name ] ) ) {
$instances[ $class_name ] = new $class_name;
$instances[ $class_name ] = new $class_name();
}
return $instances[ $class_name ];
@ -37,7 +37,6 @@ abstract class Two_Factor_Provider {
* @since 0.1-dev
*/
protected function __construct() {
return $this;
}
/**
@ -68,6 +67,8 @@ abstract class Two_Factor_Provider {
* Prints the name of the provider.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
public function print_label() {
echo esc_html( $this->get_label() );
@ -98,6 +99,8 @@ abstract class Two_Factor_Provider {
* Return `true` to prevent the authentication and render the
* authentication page.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
@ -118,6 +121,8 @@ abstract class Two_Factor_Provider {
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @since 0.7.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
@ -126,6 +131,8 @@ abstract class Two_Factor_Provider {
/**
* If this provider should be available for the user.
*
* @since 0.13.0
*
* @param WP_User|int $user WP_User object, user ID or null to resolve the current user.
*
* @return bool
@ -159,6 +166,8 @@ abstract class Two_Factor_Provider {
/**
* Sanitizes a numeric code to be used as an auth code.
*
* @since 0.8.0
*
* @param string $field The _REQUEST field to check for the code.
* @param int $length The valid expected length of the field.
* @return false|string Auth code on success, false if the field is not set or not expected length.
@ -182,6 +191,8 @@ abstract class Two_Factor_Provider {
/**
* Return the user meta keys that need to be deletated on plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {
@ -191,6 +202,8 @@ abstract class Two_Factor_Provider {
/**
* Return the option keys that need to be deleted on plugin uninstall.
*
* @since 0.10.0
*
* Note: this method doesn't have access to the instantiated provider object.
*
* @return array

View File

@ -7,6 +7,8 @@
/**
* Class Two_Factor_Totp
*
* @since 0.2.0
*/
class Two_Factor_Totp extends Two_Factor_Provider {
@ -40,6 +42,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Class constructor. Sets up hooks, etc.
*
* @since 0.2.0
*
* @codeCoverageIgnore
*/
protected function __construct() {
@ -51,9 +55,40 @@ class Two_Factor_Totp extends Two_Factor_Provider {
parent::__construct();
}
/**
* Timestamp returned by time()
*
* @var int $now
*/
private static $now;
/**
* Override time() in the current object for testing.
*
* @since 0.15.0
*
* @return int
*/
private static function time() {
return self::$now ? self::$now : time();
}
/**
* Set up the internal state of time() invocations for deterministic generation.
*
* @since 0.15.0
*
* @param int $now Timestamp to use when overriding time().
*/
public static function set_time( $now ) {
self::$now = $now;
}
/**
* Register the rest-api endpoints required for this provider.
*
* @since 0.8.0
*
* @codeCoverageIgnore
*/
public function register_rest_routes() {
@ -64,7 +99,7 @@ class Two_Factor_Totp extends Two_Factor_Provider {
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'rest_delete_totp' ),
'permission_callback' => function( $request ) {
'permission_callback' => function ( $request ) {
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
},
'args' => array(
@ -77,20 +112,20 @@ class Two_Factor_Totp extends Two_Factor_Provider {
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'rest_setup_totp' ),
'permission_callback' => function( $request ) {
'permission_callback' => function ( $request ) {
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
},
'args' => array(
'user_id' => array(
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'key' => array(
'key' => array(
'type' => 'string',
'default' => '',
'validate_callback' => null, // Note: validation handled in ::rest_setup_totp().
),
'code' => array(
'code' => array(
'type' => 'string',
'default' => '',
'validate_callback' => null, // Note: validation handled in ::rest_setup_totp().
@ -108,6 +143,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Returns the name of the provider.
*
* @since 0.2.0
*/
public function get_label() {
return _x( 'Authenticator App', 'Provider Label', 'two-factor' );
@ -125,7 +162,10 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Enqueue scripts
*
* @since 0.8.0
*
* @codeCoverageIgnore
* @param string $hook_suffix Hook suffix.
*/
public function enqueue_assets( $hook_suffix ) {
$environment_prefix = file_exists( TWO_FACTOR_DIR . '/dist' ) ? '/dist' : '';
@ -137,37 +177,57 @@ class Two_Factor_Totp extends Two_Factor_Provider {
TWO_FACTOR_VERSION,
true
);
wp_register_script(
'two-factor-totp-qrcode',
plugins_url( 'js/totp-admin-qrcode.js', __FILE__ ),
array( 'two-factor-qr-code-generator' ),
TWO_FACTOR_VERSION,
true
);
wp_register_script(
'two-factor-totp-admin',
plugins_url( 'js/totp-admin.js', __FILE__ ),
array( 'jquery', 'wp-api-request', 'two-factor-qr-code-generator' ),
TWO_FACTOR_VERSION,
true
);
}
/**
* Rest API endpoint for handling deactivation of TOTP.
*
* @since 0.8.0
*
* @param WP_REST_Request $request The Rest Request object.
* @return array Success array.
* @return WP_Error|array Array of data on success, WP_Error on error.
*/
public function rest_delete_totp( $request ) {
$user_id = $request['user_id'];
$user = get_user_by( 'id', $user_id );
$this->delete_user_totp_key( $user_id );
if ( ! Two_Factor_Core::disable_provider_for_user( $user_id, 'Two_Factor_Totp' ) ) {
return new WP_Error( 'db_error', __( 'Unable to disable TOTP provider for this user.', 'two-factor' ), array( 'status' => 500 ) );
}
$this->delete_user_totp_key( $user_id );
ob_start();
$this->user_two_factor_options( $user );
$html = ob_get_clean();
return [
return array(
'success' => true,
'html' => $html,
];
);
}
/**
* REST API endpoint for setting up TOTP.
*
* @since 0.8.0
*
* @param WP_REST_Request $request The Rest Request object.
* @return WP_Error|array Array of data on success, WP_Error on error.
*/
@ -198,15 +258,17 @@ class Two_Factor_Totp extends Two_Factor_Provider {
$this->user_two_factor_options( $user );
$html = ob_get_clean();
return [
return array(
'success' => true,
'html' => $html,
];
);
}
/**
* Generates a URL that can be used to create a QR code.
*
* @since 0.8.0
*
* @param WP_User $user The user to generate a URL for.
* @param string $secret_key The secret key.
*
@ -216,21 +278,27 @@ class Two_Factor_Totp extends Two_Factor_Provider {
$issuer = get_bloginfo( 'name', 'display' );
/**
* Filter the Issuer for the TOTP.
* Filters the Issuer for the TOTP.
*
* Must follow the TOTP format for a "issuer". Do not URL Encode.
*
* @since 0.8.0
*
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format#issuer
*
* @param string $issuer The issuer for TOTP.
*/
$issuer = apply_filters( 'two_factor_totp_issuer', $issuer );
/**
* Filter the Label for the TOTP.
* Filters the Label for the TOTP.
*
* Must follow the TOTP format for a "label". Do not URL Encode.
*
* @since 0.4.7
*
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label
*
* @param string $totp_title The label for the TOTP.
* @param WP_User $user The User object.
* @param string $issuer The issuer of the TOTP. This should be the prefix of the result.
@ -246,16 +314,19 @@ class Two_Factor_Totp extends Two_Factor_Provider {
);
/**
* Filter the TOTP generated URL.
* Filters the TOTP generated URL.
*
* Must follow the TOTP format. Do not URL Encode.
*
* @since 0.8.0
*
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
*
* @param string $totp_url The TOTP URL.
* @param WP_User $user The user object.
*/
$totp_url = apply_filters( 'two_factor_totp_url', $totp_url, $user );
$totp_url = esc_url( $totp_url, array( 'otpauth' ) );
$totp_url = esc_url_raw( $totp_url, array( 'otpauth' ) );
return $totp_url;
}
@ -263,6 +334,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Display TOTP options on the user settings page.
*
* @since 0.2.0
*
* @param WP_User $user The current user being edited.
* @return void
*
@ -275,132 +348,92 @@ class Two_Factor_Totp extends Two_Factor_Provider {
$key = $this->get_user_totp_key( $user->ID );
wp_enqueue_script( 'two-factor-qr-code-generator' );
wp_enqueue_script( 'wp-api-request' );
wp_enqueue_script( 'jquery' );
wp_localize_script(
'two-factor-totp-admin',
'twoFactorTotpAdmin',
array(
'restPath' => Two_Factor_Core::REST_NAMESPACE . '/totp',
'userId' => $user->ID,
'qrCodeAriaLabel' => __( 'Authenticator App QR Code', 'two-factor' ),
)
);
wp_enqueue_script( 'two-factor-totp-admin' );
?>
<div id="two-factor-totp-options">
<?php
if ( empty( $key ) ) :
$key = $this->generate_key();
$totp_url = $this->generate_qr_code_url( $user, $key );
?>
<p>
<?php esc_html_e( 'Please scan the QR code or manually enter the key, then enter an authentication code from your app in order to complete setup.', 'two-factor' ); ?>
<?php esc_html_e( 'Please follow these steps in order to complete setup:', 'two-factor' ); ?>
</p>
<p id="two-factor-qr-code">
<a href="<?php echo $totp_url; ?>">
Loading...
<img src="<?php echo esc_url( admin_url( 'images/spinner.gif' ) ); ?>" alt="" />
</a>
</p>
<style>
#two-factor-qr-code {
/* The size of the image will change based on the length of the URL inside it. */
min-width: 205px;
min-height: 205px;
}
</style>
<script>
(function(){
var qr_generator = function() {
/*
* 0 = Automatically select the version, to avoid going over the limit of URL
* length.
* L = Least amount of error correction, because it's not needed when scanning
* on a monitor, and it lowers the image size.
*/
var qr = qrcode( 0, 'L' );
qr.addData( <?php echo wp_json_encode( $totp_url ); ?> );
qr.make();
document.querySelector( '#two-factor-qr-code a' ).innerHTML = qr.createSvgTag( 5 );
// For accessibility, markup the SVG with a title and role.
var svg = document.querySelector( '#two-factor-qr-code a svg' ),
title = document.createElement( 'title' );
svg.role = 'image';
svg.ariaLabel = <?php echo wp_json_encode( __( 'Authenticator App QR Code', 'two-factor' ) ); ?>;
title.innerText = svg.ariaLabel;
svg.appendChild( title );
};
// Run now if the document is loaded, otherwise on DOMContentLoaded.
if ( document.readyState === 'complete' ) {
qr_generator();
} else {
window.addEventListener( 'DOMContentLoaded', qr_generator );
}
})();
</script>
<p>
<code><?php echo esc_html( $key ); ?></code>
</p>
<p>
<input type="hidden" id="two-factor-totp-key" name="two-factor-totp-key" value="<?php echo esc_attr( $key ); ?>" />
<label for="two-factor-totp-authcode">
<?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?>
<?php
/* translators: Example auth code. */
$placeholder = sprintf( __( 'eg. %s', 'two-factor' ), '123456' );
?>
<input type="text" inputmode="numeric" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $placeholder ); ?>" autocomplete="off" />
</label>
<input type="submit" class="button totp-submit" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Submit', 'two-factor' ); ?>" />
</p>
<script>
(function($){
// Focus the auth code input when the checkbox is clicked.
document.getElementById('enabled-Two_Factor_Totp').addEventListener('click', function(e) {
if ( e.target.checked ) {
document.getElementById('two-factor-totp-authcode').focus();
}
});
$('.totp-submit').click( function( e ) {
e.preventDefault();
var key = $('#two-factor-totp-key').val(),
code = $('#two-factor-totp-authcode').val();
wp.apiRequest( {
method: 'POST',
path: <?php echo wp_json_encode( Two_Factor_Core::REST_NAMESPACE . '/totp' ); ?>,
data: {
user_id: <?php echo wp_json_encode( $user->ID ); ?>,
key: key,
code: code,
enable_provider: true,
}
} ).fail( function( response, status ) {
var errorMessage = response.responseJSON.message || status,
$error = $( '#totp-setup-error' );
if ( ! $error.length ) {
$error = $('<div class="error" id="totp-setup-error"><p></p></div>').insertAfter( $('.totp-submit') );
}
$error.find('p').text( errorMessage );
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false );
$('#two-factor-totp-authcode').val('');
} ).then( function( response ) {
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', true );
$( '#two-factor-totp-options' ).html( response.html );
} );
} );
})(jQuery);
</script>
<ol class="totp-steps">
<li>
<?php esc_html_e( 'Install an authenticator app on your desktop/laptop and/or phone. Popular examples are Microsoft Authenticator, Google Authenticator and Authy.', 'two-factor' ); ?>
</li>
<li>
<?php esc_html_e( 'Scan this QR code using the app you installed:', 'two-factor' ); ?>
<p id="two-factor-qr-code">
<a href="<?php echo esc_url( $totp_url, array( 'otpauth' ) ); ?>">
<?php esc_html_e( 'Loading…', 'two-factor' ); ?>
<img src="<?php echo esc_url( admin_url( 'images/spinner.gif' ) ); ?>" alt="" />
</a>
</p>
<p>
<?php
esc_html_e(
'If scanning isn\'t possible or doesn\'t work, click on the QR code or use the secret key shown below to add the account to your chosen app:',
'two-factor'
);
?>
</p>
<p>
<code><?php echo esc_html( $key ); ?></code>
</p>
</li>
<li>
<p><?php esc_html_e( 'Enter the code generated by the Authenticator app to complete the setup:', 'two-factor' ); ?></p>
<p>
<input type="hidden" id="two-factor-totp-key" name="two-factor-totp-key" value="<?php echo esc_attr( $key ); ?>" />
<label for="two-factor-totp-authcode">
<?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?>
<?php
/* translators: Example auth code. */
$placeholder = sprintf( __( 'eg. %s', 'two-factor' ), '123456' );
?>
<input type="text" inputmode="numeric" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $placeholder ); ?>" autocomplete="off" />
</label>
<input type="submit" class="button totp-submit" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Verify', 'two-factor' ); ?>" />
</p>
<p class="description">
<?php
printf(
/* translators: 1: server date and time */
esc_html__( 'If the code is rejected, check that your web server time is accurate: %1$s. Your device and server times must match.', 'two-factor' ),
sprintf(
'<time class="two-factor-server-datetime-epoch" datetime="%1$s">%2$s (%3$s)</time>',
esc_attr( wp_date( 'c' ) ),
esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ),
esc_html( wp_timezone_string() )
)
);
?>
</p>
</li>
</ol>
<?php
wp_localize_script(
'two-factor-totp-qrcode',
'twoFactorTotpQrcode',
array(
'totpUrl' => $totp_url,
'qrCodeLabel' => __( 'Authenticator App QR Code', 'two-factor' ),
)
);
wp_enqueue_script( 'two-factor-totp-qrcode' );
?>
<?php else : ?>
<p class="success">
<?php esc_html_e( 'An authenticator app is currently configured. You will need to re-scan the QR code on all devices if reset.', 'two-factor' ); ?>
@ -409,24 +442,6 @@ class Two_Factor_Totp extends Two_Factor_Provider {
<button type="button" class="button button-secondary reset-totp-key hide-if-no-js">
<?php esc_html_e( 'Reset authenticator app', 'two-factor' ); ?>
</button>
<script>
( function( $ ) {
$( '.button.reset-totp-key' ).click( function( e ) {
e.preventDefault();
wp.apiRequest( {
method: 'DELETE',
path: <?php echo wp_json_encode( Two_Factor_Core::REST_NAMESPACE . '/totp' ); ?>,
data: {
user_id: <?php echo wp_json_encode( $user->ID ); ?>,
}
} ).then( function( response ) {
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false );
$( '#two-factor-totp-options' ).html( response.html );
} );
} );
} )( jQuery );
</script>
</p>
<?php endif; ?>
</div>
@ -436,6 +451,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Get the TOTP secret key for a user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
*
* @return string
@ -447,6 +464,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Set the TOTP secret key for a user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
* @param string $key TOTP secret key.
*
@ -459,6 +478,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Delete the TOTP secret key for a user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
*
* @return boolean If the key was deleted successfully.
@ -471,6 +492,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Check if the TOTP secret key has a proper format.
*
* @since 0.2.0
*
* @param string $key TOTP secret key.
*
* @return boolean
@ -488,6 +511,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Validates authentication.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @return bool Whether the user gave a valid code
@ -504,6 +529,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Validates an authentication code for a given user, preventing re-use and older TOTP keys.
*
* @since 0.8.0
*
* @param WP_User $user WP_User object of the logged-in user.
* @param int $code The TOTP token to validate.
*
@ -535,48 +562,70 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift.
*
* @since 0.15.0
*
* @param string $key The share secret key to use.
* @param string $authcode The code to test.
* @param string $hash The hash used to calculate the code.
* @param int $time_step The size of the time step.
*
* @return bool Whether the code is valid within the time frame.
*/
public static function is_valid_authcode( $key, $authcode ) {
return (bool) self::get_authcode_valid_ticktime( $key, $authcode );
public static function is_valid_authcode( $key, $authcode, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
return (bool) self::get_authcode_valid_ticktime( $key, $authcode, $hash, $time_step );
}
/**
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift.
*
* @since 0.15.0
*
* @param string $key The share secret key to use.
* @param string $authcode The code to test.
* @param string $hash The hash used to calculate the code.
* @param int $time_step The size of the time step.
*
* @return false|int Returns the timestamp of the authcode on success, False otherwise.
*/
public static function get_authcode_valid_ticktime( $key, $authcode ) {
public static function get_authcode_valid_ticktime( $key, $authcode, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
/**
* 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
* so the default of 4 allows codes that are two minutes to either side of server time.
*
* @since 0.2.0
* @deprecated 0.7.0 Use {@see 'two_factor_totp_time_step_allowance'} instead.
*
* @param int $max_ticks Max ticks of time correction to allow. Default 4.
*/
$max_ticks = apply_filters_deprecated( 'two-factor-totp-time-step-allowance', array( self::DEFAULT_TIME_STEP_ALLOWANCE ), '0.7.0', 'two_factor_totp_time_step_allowance' );
/**
* Filters 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.
*
* @since 0.7.0
*
* @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 = floor( time() / self::DEFAULT_TIME_STEP_SEC );
$time = (int) floor( self::time() / $time_step );
$digits = strlen( $authcode );
foreach ( $ticks as $offset ) {
$log_time = $time + $offset;
if ( hash_equals( self::calc_totp( $key, $log_time ), $authcode ) ) {
$log_time = (int) ( $time + $offset );
if ( hash_equals( self::calc_totp( $key, $log_time, $digits, $hash, $time_step ), $authcode ) ) {
// Return the tick timestamp.
return $log_time * self::DEFAULT_TIME_STEP_SEC;
return (int) ( $log_time * self::DEFAULT_TIME_STEP_SEC );
}
}
@ -586,6 +635,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Generates key
*
* @since 0.2.0
*
* @param int $bitsize Nume of bits to use for key.
*
* @return string $bitsize long string composed of available base32 chars.
@ -598,58 +649,94 @@ class Two_Factor_Totp extends Two_Factor_Provider {
}
/**
* Pack stuff
* Pack stuff. We're currently only using this to pack integers, however the generic `pack` method can handle mixed.
*
* @param string $value The value to be packed.
* @since 0.2.0
*
* @param int $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 ); // phpcs:ignore PHPCompatibility.ParameterValues.NewPackFormat.NewFormatFound
}
$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;
public static function pack64( int $value ): string {
// Native 64-bit support (modern PHP on 64-bit builds).
if ( 8 === PHP_INT_SIZE ) {
return pack( 'J', $value );
}
$lowmap = 0xffffffff;
$lower = $value & $lowmap;
// 32-bit PHP fallback
$higher = ( $value >> 32 ) & 0xFFFFFFFF;
$lower = $value & 0xFFFFFFFF;
return pack( 'NN', $higher, $lower );
}
/**
* Pad a short secret with bytes from the same until it's the correct length
* for hashing.
*
* @since 0.15.0
*
* @param string $secret Secret key to pad.
* @param int $length Byte length of the desired padded secret.
*
* @throws InvalidArgumentException If the secret or length are invalid.
*
* @return string
*/
protected static function pad_secret( $secret, $length ) {
if ( empty( $secret ) ) {
throw new InvalidArgumentException( 'Secret must be non-empty!' );
}
$length = intval( $length );
if ( $length <= 0 ) {
throw new InvalidArgumentException( 'Padding length must be non-zero' );
}
return str_pad( $secret, $length, $secret, STR_PAD_RIGHT );
}
/**
* Calculate a valid code given the shared secret key
*
* @since 0.2.0
*
* @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.
*
* @throws InvalidArgumentException If the hash type is invalid.
*
* @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 );
switch ( $hash ) {
case 'sha1':
$secret = self::pad_secret( $secret, 20 );
break;
case 'sha256':
$secret = self::pad_secret( $secret, 32 );
break;
case 'sha512':
$secret = self::pad_secret( $secret, 64 );
break;
default:
throw new InvalidArgumentException( 'Invalid hash type specified!' );
}
if ( false === $step_count ) {
$step_count = floor( time() / $time_step );
$step_count = floor( self::time() / $time_step );
}
$timestamp = self::pack64( $step_count );
$hash = hash_hmac( $hash, $timestamp, $secret, true );
$offset = ord( $hash[19] ) & 0xf;
$offset = ord( $hash[ strlen( $hash ) - 1 ] ) & 0xf;
$code = (
( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
@ -664,6 +751,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @return boolean
@ -678,6 +767,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.2.0
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @codeCoverageIgnore
@ -685,41 +776,48 @@ class Two_Factor_Totp extends Two_Factor_Provider {
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_before_authentication_prompt', $this );
?>
<p class="two-factor-prompt">
<?php esc_html_e( 'Enter the code generated by your authenticator app.', 'two-factor' ); ?>
</p>
<?php
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_prompt', $this );
?>
<p>
<label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" autocomplete="one-time-code" name="authcode" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="123 456" autocomplete="one-time-code" data-digits="<?php echo esc_attr( self::DEFAULT_DIGIT_COUNT ); ?>" />
<input type="text" inputmode="numeric" name="authcode" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="123 456" autocomplete="one-time-code" data-digits="<?php echo esc_attr( self::DEFAULT_DIGIT_COUNT ); ?>" />
</p>
<script type="text/javascript">
setTimeout( function(){
var d;
try{
d = document.getElementById('authcode');
d.focus();
} catch(e){}
}, 200);
</script>
<?php
submit_button( __( 'Authenticate', 'two-factor' ) );
/** This action is documented in providers/class-two-factor-backup-codes.php */
do_action( 'two_factor_after_authentication_input', $this );
?>
<?php
wp_enqueue_script( 'two-factor-login' );
submit_button( __( 'Verify', 'two-factor' ) );
}
/**
* Returns a base32 encoded string.
*
* @param string $string String to be encoded using base32.
* @since 0.2.0
*
* @param string $input String to be encoded using base32.
*
* @return string base32 encoded string without padding.
*/
public static function base32_encode( $string ) {
if ( empty( $string ) ) {
public static function base32_encode( $input ) {
if ( empty( $input ) ) {
return '';
}
$binary_string = '';
foreach ( str_split( $string ) as $character ) {
foreach ( str_split( $input ) as $character ) {
$binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
}
@ -736,6 +834,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Decode a base32 string and return a binary representation
*
* @since 0.2.0
*
* @param string $base32_string The base 32 string to decode.
*
* @throws Exception If string contains non-base32 characters.
@ -773,6 +873,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Used with usort to sort an array by distance from 0
*
* @since 0.2.0
*
* @param int $a First array element.
* @param int $b Second array element.
*
@ -790,6 +892,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {

View File

@ -1,12 +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;
}

View File

@ -0,0 +1,49 @@
/* global twoFactorBackupCodes, wp, navigator, document, jQuery */
( function( $ ) {
$( '.button-two-factor-backup-codes-copy' ).click( function() {
var csvCodes = $( '.two-factor-backup-codes-wrapper' ).data( 'codesCsv' ),
$temp;
if ( ! csvCodes ) {
return;
}
if ( navigator.clipboard && navigator.clipboard.writeText ) {
navigator.clipboard.writeText( csvCodes );
return;
}
$temp = $( '<textarea>' ).val( csvCodes ).css( { position: 'absolute', left: '-9999px' } );
$( 'body' ).append( $temp );
$temp[0].select();
document.execCommand( 'copy' );
$temp.remove();
} );
$( '.button-two-factor-backup-codes-generate' ).click( function() {
wp.apiRequest( {
method: 'POST',
path: twoFactorBackupCodes.restPath,
data: {
user_id: parseInt( twoFactorBackupCodes.userId, 10 )
}
} ).then( function( response ) {
var $codesList = $( '.two-factor-backup-codes-unused-codes' ),
i;
$( '.two-factor-backup-codes-wrapper' ).show();
$codesList.html( '' );
$codesList.css( { 'column-count': 2, 'column-gap': '80px', 'max-width': '420px' } );
$( '.two-factor-backup-codes-wrapper' ).data( 'codesCsv', response.codes.join( ',' ) );
// Append the codes.
for ( i = 0; i < response.codes.length; i++ ) {
$codesList.append( '<li class="two-factor-backup-codes-token">' + response.codes[ i ] + '</li>' );
}
// Update counter.
$( '.two-factor-backup-codes-count' ).html( response.i18n.count );
$( '#two-factor-backup-codes-download-link' ).attr( 'href', response.download_link );
} );
} );
}( jQuery ) );

View File

@ -1,150 +0,0 @@
/* global window, document, jQuery, 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;
if ( 'none' === $( t.what + t.getId( el ) ).css( 'display' ) ) {
t.revert();
} else {
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( '<tr class="hidden"></tr>' );
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,
user_id: window.u2fL10n.user_id
};
fields = $( '#edit-' + id ).find( ':input' ).serialize();
params = fields + '&' + $.param( params );
// Make ajax request.
$.post( ajaxurl, params,
function( r ) {
var row, newID;
$( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' );
if ( r ) {
if ( -1 !== r.indexOf( '<tr' ) ) {
$( inlineEditKey.what + id ).siblings( 'tr.hidden' ).addBack().remove();
newID = $( r ).attr( 'id' );
$( '#edit-' + id ).before( r ).remove();
if ( newID ) {
row = $( '#' + newID );
} else {
row = $( inlineEditKey.what + id );
}
row.hide().fadeIn();
} else {
$( '#edit-' + id + ' .inline-edit-save .error' ).html( r ).show();
}
} else {
$( '#edit-' + id + ' .inline-edit-save .error' ).html( inlineEditL10n.error ).show();
}
}
);
return false;
},
revert: function() {
var id = $( '#security-keys-section table.widefat tr.inline-editor' ).attr( 'id' );
if ( id ) {
$( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' );
$( '#' + id ).siblings( 'tr.hidden' ).addBack().remove();
id = id.replace( /\w+\-/, '' );
$( this.what + id ).show();
}
return false;
},
getId: function( o ) {
var id = 'TR' === o.tagName ? o.id : $( o ).parents( 'tr' ).attr( 'id' );
return id.replace( /\w+\-/, '' );
}
};
$( document ).ready( function() {
inlineEditKey.init();
} );
}( jQuery ) );

View File

@ -1,48 +0,0 @@
/* global window, u2fL10n, jQuery */
( function( $ ) {
var $button = $( '#register_security_key' );
var $statusNotice = $( '#security-keys-section .security-key-status' );
var u2fSupported = ( window.u2f && 'register' in window.u2f );
if ( ! u2fSupported ) {
$statusNotice.text( u2fL10n.text.u2f_not_supported );
}
$button.click( function() {
var registerRequest;
if ( $( this ).prop( 'disabled' ) ) {
return false;
}
$( this ).prop( 'disabled', true );
$( '.register-security-key .spinner' ).addClass( 'is-active' );
$statusNotice.text( '' );
registerRequest = {
version: u2fL10n.register.request.version,
challenge: u2fL10n.register.request.challenge
};
window.u2f.register( u2fL10n.register.request.appId, [ registerRequest ], u2fL10n.register.sigs, function( data ) {
$( '.register-security-key .spinner' ).removeClass( 'is-active' );
$button.prop( 'disabled', false );
if ( data.errorCode ) {
if ( u2fL10n.text.error_codes[ data.errorCode ] ) {
$statusNotice.text( u2fL10n.text.error_codes[ data.errorCode ] );
} else {
$statusNotice.text( u2fL10n.text.error_codes[ u2fL10n.text.error ] );
}
return false;
}
$( '#do_new_security_key' ).val( 'true' );
$( '#u2f_response' ).val( JSON.stringify( data ) );
// See: http://stackoverflow.com/questions/833032/submit-is-not-a-function-error-in-javascript
$( '<form>' )[0].submit.call( $( '#your-profile' )[0] );
} );
} );
}( jQuery ) );

View File

@ -1,16 +0,0 @@
/* global window, u2f, u2fL10n, jQuery */
( 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 ) );

View File

@ -0,0 +1,35 @@
/* global twoFactorTotpQrcode, qrcode, document, window */
( function() {
var qrGenerator = function() {
/*
* 0 = Automatically select the version, to avoid going over the limit of URL
* length.
* L = Least amount of error correction, because it's not needed when scanning
* on a monitor, and it lowers the image size.
*/
var qr = qrcode( 0, 'L' ),
svg,
title;
qr.addData( twoFactorTotpQrcode.totpUrl );
qr.make();
document.querySelector( '#two-factor-qr-code a' ).innerHTML = qr.createSvgTag( 5 );
// For accessibility, markup the SVG with a title and role.
svg = document.querySelector( '#two-factor-qr-code a svg' );
title = document.createElement( 'title' );
svg.setAttribute( 'role', 'img' );
svg.setAttribute( 'aria-label', twoFactorTotpQrcode.qrCodeLabel );
title.innerText = twoFactorTotpQrcode.qrCodeLabel;
svg.appendChild( title );
};
// Run now if the document is loaded, otherwise on DOMContentLoaded.
if ( document.readyState === 'complete' ) {
qrGenerator();
} else {
window.addEventListener( 'DOMContentLoaded', qrGenerator );
}
}() );

View File

@ -0,0 +1,95 @@
/* global twoFactorTotpAdmin, qrcode, wp, document, jQuery */
( function( $ ) {
var generateQrCode = function( totpUrl ) {
var $qrLink = $( '#two-factor-qr-code a' ),
qr,
svg,
title;
if ( ! $qrLink.length || typeof qrcode === 'undefined' ) {
return;
}
qr = qrcode( 0, 'L' );
qr.addData( totpUrl );
qr.make();
$qrLink.html( qr.createSvgTag( 5 ) );
svg = $qrLink.find( 'svg' )[ 0 ];
if ( svg ) {
var ariaLabel = ( typeof twoFactorTotpAdmin !== 'undefined' && twoFactorTotpAdmin && twoFactorTotpAdmin.qrCodeAriaLabel ) ? twoFactorTotpAdmin.qrCodeAriaLabel : 'Authenticator App QR Code';
title = document.createElement( 'title' );
svg.setAttribute( 'role', 'img' );
svg.setAttribute( 'aria-label', ariaLabel );
title.innerText = ariaLabel;
svg.appendChild( title );
}
};
var checkbox = document.getElementById( 'enabled-Two_Factor_Totp' );
// Focus the auth code input when the checkbox is clicked.
if ( checkbox ) {
checkbox.addEventListener( 'click', function( e ) {
if ( e.target.checked ) {
document.getElementById( 'two-factor-totp-authcode' ).focus();
}
} );
}
$( '.totp-submit' ).click( function( e ) {
var key = $( '#two-factor-totp-key' ).val(),
code = $( '#two-factor-totp-authcode' ).val();
e.preventDefault();
wp.apiRequest( {
method: 'POST',
path: twoFactorTotpAdmin.restPath,
data: {
user_id: parseInt( twoFactorTotpAdmin.userId, 10 ),
key: key,
code: code,
enable_provider: true
}
} ).fail( function( response, status ) {
var errorMessage = ( response && response.responseJSON && response.responseJSON.message ) || ( response && response.statusText ) || status || '',
$error = $( '#totp-setup-error' );
if ( ! $error.length ) {
$error = $( '<div class="error" id="totp-setup-error"><p></p></div>' ).insertAfter( $( '.totp-submit' ) );
}
$error.find( 'p' ).text( errorMessage );
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ).trigger( 'change' );
$( '#two-factor-totp-authcode' ).val( '' );
} ).then( function( response ) {
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', true ).trigger( 'change' );
$( '#two-factor-totp-options' ).html( response.html );
} );
} );
$( '.button.reset-totp-key' ).click( function( e ) {
e.preventDefault();
wp.apiRequest( {
method: 'DELETE',
path: twoFactorTotpAdmin.restPath,
data: {
user_id: parseInt( twoFactorTotpAdmin.userId, 10 )
}
} ).then( function( response ) {
var totpUrl;
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false );
$( '#two-factor-totp-options' ).html( response.html );
totpUrl = $( '#two-factor-qr-code a' ).attr( 'href' );
if ( totpUrl ) {
generateQrCode( totpUrl );
}
} );
} );
}( jQuery ) );

View File

@ -0,0 +1,38 @@
/* global document */
( function() {
// Enforce numeric-only input for numeric inputmode elements.
var form = document.querySelector( '#loginform' ),
inputEl = document.querySelector( 'input.authcode[inputmode="numeric"]' ),
expectedLength = ( inputEl && inputEl.dataset ) ? inputEl.dataset.digits : 0,
spaceInserted = false;
if ( inputEl ) {
inputEl.addEventListener(
'input',
function() {
var value = this.value.replace( /[^0-9 ]/g, '' ).replace( /^\s+/, '' ),
submitControl;
if ( ! spaceInserted && expectedLength && value.length === Math.floor( expectedLength / 2 ) ) {
value += ' ';
spaceInserted = true;
} else if ( spaceInserted && ! this.value ) {
spaceInserted = false;
}
this.value = value;
// Auto-submit if it's the expected length.
if ( expectedLength && value.replace( / /g, '' ).length === parseInt( expectedLength, 10 ) ) {
if ( form && typeof form.requestSubmit === 'function' ) {
form.requestSubmit();
submitControl = form.querySelector( '[type="submit"]' );
if ( submitControl ) {
submitControl.disabled = true;
}
}
}
}
);
}
}() );

View File

@ -0,0 +1,11 @@
/* global document, setTimeout */
( function() {
setTimeout( function() {
var d;
try {
d = document.getElementById( 'authcode' );
d.value = '';
d.focus();
} catch ( e ) {}
}, 200 );
}() );

View File

@ -1,22 +1,87 @@
=== Two-Factor ===
Contributors: georgestephanis, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, kasparsd, alihusnainarshad, passoniate
=== Two Factor ===
Contributors: georgestephanis, kasparsd, masteradhoc, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, alihusnainarshad, passoniate
Tags: 2fa, mfa, totp, authentication, security
Tested up to: 6.7
Stable tag: 0.13.0
Tested up to: 6.9
Stable tag: 0.16.0
License: GPL-2.0-or-later
License URI: https://spdx.org/licenses/GPL-2.0-or-later.html
Enable Two-Factor Authentication (2FA) using time-based one-time passwords (TOTP), Universal 2nd Factor (U2F), email, and backup verification codes.
Enable Two-Factor Authentication (2FA) using time-based one-time passwords (TOTP), email, and backup verification codes.
== Description ==
Use the "Two-Factor Options" section under "Users" → "Your Profile" to enable and configure one or multiple two-factor authentication providers for your account:
The Two-Factor plugin adds an extra layer of security to your WordPress login by requiring users to provide a second form of authentication in addition to their password. This helps protect against unauthorized access even if passwords are compromised.
- Email codes
- Time Based One-Time Passwords (TOTP)
- FIDO Universal 2nd Factor (U2F)
- Backup Codes
- Dummy Method (only for testing purposes)
## Setup Instructions
**Important**: Each user must individually configure their two-factor authentication settings.
### For Individual Users
1. **Navigate to your profile**: Go to "Users" → "Your Profile" in the WordPress admin
2. **Find Two-Factor Options**: Scroll down to the "Two-Factor Options" section
3. **Choose your methods**: Enable one or more authentication providers (noting a site admin may have hidden one or more so what is available could vary):
- **Authenticator App (TOTP)** - Use apps like Google Authenticator, Authy, or 1Password
- **Email Codes** - Receive one-time codes via email
- **Backup Codes** - Generate one-time backup codes for emergencies
- **Dummy Method** - For testing purposes only (requires WP_DEBUG)
4. **Configure each method**: Follow the setup instructions for each enabled provider
5. **Set primary method**: Choose which method to use as your default authentication
6. **Save changes**: Click "Update Profile" to save your settings
### For Site Administrators
- **Plugin settings**: The plugin provides a settings page under "Settings → Two-Factor" to configure which providers should be disabled site-wide.
- **User management**: Administrators can configure 2FA for other users by editing their profiles
- **Security recommendations**: Encourage users to enable backup methods to prevent account lockouts
## Available Authentication Methods
### Authenticator App (TOTP) - Recommended
- **Security**: High - Time-based one-time passwords
- **Setup**: Scan QR code with authenticator app
- **Compatibility**: Works with Google Authenticator, Authy, 1Password, and other TOTP apps
- **Best for**: Most users, provides excellent security with good usability
### Backup Codes - Recommended
- **Security**: Medium - One-time use codes
- **Setup**: Generate 10 backup codes for emergency access
- **Compatibility**: Works everywhere, no special hardware needed
- **Best for**: Emergency access when other methods are unavailable
### Email Codes
- **Security**: Medium - One-time codes sent via email
- **Setup**: Automatic - uses your WordPress email address
- **Compatibility**: Works with any email-capable device
- **Best for**: Users who prefer email-based authentication
### FIDO U2F Security Keys
- Deprecated and removed due to loss of browser support.
### Dummy Method
- **Security**: None - Always succeeds
- **Setup**: Only available when WP_DEBUG is enabled
- **Purpose**: Testing and development only
- **Best for**: Developers testing the plugin
## Important Notes
### HTTPS Requirement
- All methods work on both HTTP and HTTPS sites
### Browser Compatibility
- TOTP and email methods work on all devices and browsers
### Account Recovery
- Always enable backup codes to prevent being locked out of your account
- If you lose access to all authentication methods, contact your site administrator
### Security Best Practices
- Use multiple authentication methods when possible
- Keep backup codes in a secure location
- Regularly review and update your authentication settings
For more information about two-factor authentication in WordPress, see the [WordPress Advanced Administration Security Guide](https://developer.wordpress.org/advanced-administration/security/mfa/).
For more history, see [this post](https://georgestephanis.wordpress.com/2013/08/14/two-cents-on-two-factor/).
@ -28,12 +93,30 @@ Here is a list of action and filter hooks provided by the plugin:
- `two_factor_providers_for_user` filter overrides the available two-factor providers for a specific user. Array values are instances of provider classes and the user object `WP_User` is available as the second argument.
- `two_factor_enabled_providers_for_user` filter overrides the list of two-factor providers enabled for a user. First argument is an array of enabled provider classnames as values, the second argument is the user ID.
- `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow.
- `two_factor_user_api_login_enable` filter restricts authentication for REST API and XML-RPC to application passwords only. Provides the user ID as the second argument.
- `two_factor_email_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated.
- `two_factor_email_token_length` filter overrides the default 8 character count for email tokens.
- `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Providers the `WP_User` of the associated user as the second argument.
- `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Provides the `WP_User` of the associated user as the second argument.
- `two_factor_rest_api_can_edit_user` filter overrides whether a users Two-Factor settings can be edited via the REST API. First argument is the current `$can_edit` boolean, the second argument is the user ID.
- `two_factor_before_authentication_prompt` action which receives the provider object and fires prior to the prompt shown on the authentication input form.
- `two_factor_after_authentication_prompt` action which receives the provider object and fires after the prompt shown on the authentication input form.
- `two_factor_after_authentication_input` action which receives the provider object and fires after the input shown on the authentication input form (if form contains no input, action fires immediately after `two_factor_after_authentication_prompt`).
- `two_factor_login_backup_links` filters the backup links displayed on the two-factor login form.
== Redirect After the Two-Factor Challenge ==
To redirect users to a specific URL after completing the two-factor challenge, use WordPress Core built-in login_redirect filter. The filter works the same way as in a standard WordPress login flow:
add_filter( 'login_redirect', function( $redirect_to, $requested_redirect_to, $user ) {
return home_url( '/dashboard/' );
}, 10, 3 );
== Frequently Asked Questions ==
= What PHP and WordPress versions does the Two-Factor plugin support? =
This plugin supports the last two major versions of WordPress and <a href="https://make.wordpress.org/core/handbook/references/php-compatibility-and-wordpress-versions/">the minimum PHP version</a> supported by those WordPress versions.
= How can I send feedback or get help with a bug? =
The best place to report bugs, feature suggestions, or any other (non-security) feedback is at <a href="https://github.com/WordPress/two-factor/issues">the Two Factor GitHub issues page</a>. Before submitting a new issue, please search the existing issues to check if someone else has reported the same feedback.
@ -44,12 +127,133 @@ The plugin contributors and WordPress community take security bugs seriously. We
To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program.
= What if I lose access to all my authentication methods? =
If you have backup codes enabled, you can use one of those to regain access. If you don't have backup codes or have used them all, you'll need to contact your site administrator to reset your account. This is why it's important to always enable backup codes and keep them in a secure location.
= Can I use this plugin with WebAuthn? =
The plugin previously supported FIDO U2F, which was a predecessor to WebAuthn. There is an open issue to add WebAuthn support here: https://github.com/WordPress/two-factor/pull/427
= Is there a recommended way to use passkeys or hardware security keys with Two-Factor? =
Yes. For passkeys and hardware security keys, you can install the Two-Factor Provider: WebAuthn plugin: https://wordpress.org/plugins/two-factor-provider-webauthn/
. It integrates directly with Two-Factor and adds WebAuthn-based authentication as an additional two-factor option for users.
== Screenshots ==
1. Two-factor options under User Profile.
2. U2F Security Keys section under User Profile.
3. Email Code Authentication during WordPress Login.
1. Two-factor options under User Profile - Shows the main configuration area where users can enable different authentication methods.
2. Email Code Authentication during WordPress Login - Shows the email verification screen that appears during login.
3. Authenticator App (TOTP) setup with QR code - Demonstrates the QR code generation and manual key entry for TOTP setup.
4. Backup codes generation and management - Shows the backup codes interface for generating and managing emergency access codes.
== Changelog ==
See the [release history](https://github.com/wordpress/two-factor/releases).
= 0.16.0 - 2026-03-27 =
* **Breaking Changes:** Remove legacy FIDO U2F provider support by [#439](https://github.com/WordPress/two-factor/pull/439).
* **New Features:** Add a dedicated settings page for plugin configuration in wp-admin by [#764](https://github.com/WordPress/two-factor/pull/764).
* **New Features:** Add a support links filter so consumers can customize contextual recovery/help links by [#615](https://github.com/WordPress/two-factor/pull/615).
* **New Features:** Refresh backup codes UI styling and behavior by [#804](https://github.com/WordPress/two-factor/pull/804).
* **Bug Fixes:** Delete stored TOTP secrets when the TOTP provider is disabled by [#802](https://github.com/WordPress/two-factor/pull/802).
* **Bug Fixes:** Harden provider handling so login/settings checks do not fail open when expected providers disappear by [#586](https://github.com/WordPress/two-factor/pull/586).
* **Bug Fixes:** Ensure only configured providers are saved and enabled in user settings by [#798](https://github.com/WordPress/two-factor/pull/798).
* **Bug Fixes:** Improve settings-page accessibility and fix profile settings link behavior by [#828](https://github.com/WordPress/two-factor/pull/828) and [#830](https://github.com/WordPress/two-factor/pull/830).
* **Bug Fixes:** Resolve PHPCS violations in provider files by [#851](https://github.com/WordPress/two-factor/pull/851).
* **Development Updates:** Move login styles and provider scripts from inline output to enqueued/external assets by [#807](https://github.com/WordPress/two-factor/pull/807) and [#814](https://github.com/WordPress/two-factor/pull/814).
* **Development Updates:** Improve inline docs and static-analysis compatibility (WPCS/phpstan) by [#810](https://github.com/WordPress/two-factor/pull/810), [#815](https://github.com/WordPress/two-factor/pull/815), and [#817](https://github.com/WordPress/two-factor/pull/817).
* **Development Updates:** Improve unit test reliability and integrate CI code coverage reporting by [#825](https://github.com/WordPress/two-factor/pull/825), [#841](https://github.com/WordPress/two-factor/pull/841), and [#842](https://github.com/WordPress/two-factor/pull/842).
* **Development Updates:** Update readme docs and modernize CI workflow infrastructure by [#835](https://github.com/WordPress/two-factor/pull/835), [#837](https://github.com/WordPress/two-factor/pull/837), [#843](https://github.com/WordPress/two-factor/pull/843), and [#849](https://github.com/WordPress/two-factor/pull/849).
* **Dependency Updates:** Bump `qs` from 6.14.1 to 6.14.2 by [#794](https://github.com/WordPress/two-factor/pull/794).
* **Dependency Updates:** Bump `basic-ftp` from 5.0.5 to 5.2.0 by [#816](https://github.com/WordPress/two-factor/pull/816).
* **Dependency Updates:** Apply automatic lint/format updates and associated Composer package refreshes by [#799](https://github.com/WordPress/two-factor/pull/799).
= 0.15.0 - 2026-02-13 =
* **Breaking Changes:** Trigger two-factor flow only when expected by @kasparsd in [#660](https://github.com/WordPress/two-factor/pull/660) and [#793](https://github.com/WordPress/two-factor/pull/793).
* **New Features:** Include user IP address and contextual warning in two-factor code emails by @todeveni in [#728](https://github.com/WordPress/two-factor/pull/728)
* **New Features:** Optimize email text for TOTP by @masteradhoc in [#789](https://github.com/WordPress/two-factor/pull/789)
* **New Features:** Add "Settings" action link to plugin list for quick access to profile by @hardikRathi in [#740](https://github.com/WordPress/two-factor/pull/740)
* **New Features:** Additional form hooks by @eric-michel in [#742](https://github.com/WordPress/two-factor/pull/742)
* **New Features:** Full RFC6238 Compatibility by @ericmann in [#656](https://github.com/WordPress/two-factor/pull/656)
* **New Features:** Consistent user experience for TOTP setup by @kasparsd in [#792](https://github.com/WordPress/two-factor/pull/792)
* **Documentation:** `@since` docs by @masteradhoc in [#781](https://github.com/WordPress/two-factor/pull/781)
* **Documentation:** Update user and admin docs, prepare for more screenshots by @jeffpaul in [#701](https://github.com/WordPress/two-factor/pull/701)
* **Documentation:** Add changelog & credits, update release notes by @jeffpaul in [#696](https://github.com/WordPress/two-factor/pull/696)
* **Documentation:** Clear readme.txt by @masteradhoc in [#785](https://github.com/WordPress/two-factor/pull/785)
* **Documentation:** Add date and time information above TOTP setup instructions by @masteradhoc in [#772](https://github.com/WordPress/two-factor/pull/772)
* **Documentation:** Clarify TOTP setup instructions by @masteradhoc in [#763](https://github.com/WordPress/two-factor/pull/763)
* **Documentation:** Update RELEASING.md by @jeffpaul in [#787](https://github.com/WordPress/two-factor/pull/787)
* **Development Updates:** Pause deploys to SVN trunk for merges to `master` by @kasparsd in [#738](https://github.com/WordPress/two-factor/pull/738)
* **Development Updates:** Fix CI checks for PHP compatability by @kasparsd in [#739](https://github.com/WordPress/two-factor/pull/739)
* **Development Updates:** Fix Playground refs by @kasparsd in [#744](https://github.com/WordPress/two-factor/pull/744)
* **Development Updates:** Persist existing translations when introducing new helper text in emails by @kasparsd in [#745](https://github.com/WordPress/two-factor/pull/745)
* **Development Updates:** Fix `missing_direct_file_access_protection` by @masteradhoc in [#760](https://github.com/WordPress/two-factor/pull/760)
* **Development Updates:** Fix `mismatched_plugin_name` by @masteradhoc in [#754](https://github.com/WordPress/two-factor/pull/754)
* **Development Updates:** Introduce Props Bot workflow by @jeffpaul in [#749](https://github.com/WordPress/two-factor/pull/749)
* **Development Updates:** Plugin Check: Fix Missing $domain parameter by @masteradhoc in [#753](https://github.com/WordPress/two-factor/pull/753)
* **Development Updates:** Tests: Update to supported WP version 6.8 by @masteradhoc in [#770](https://github.com/WordPress/two-factor/pull/770)
* **Development Updates:** Fix PHP 8.5 deprecated message by @masteradhoc in [#762](https://github.com/WordPress/two-factor/pull/762)
* **Development Updates:** Exclude 7.2 and 7.3 checks against trunk by @masteradhoc in [#769](https://github.com/WordPress/two-factor/pull/769)
* **Development Updates:** Fix Plugin Check errors: `MissingTranslatorsComment` & `MissingSingularPlaceholder` by @masteradhoc in [#758](https://github.com/WordPress/two-factor/pull/758)
* **Development Updates:** Add PHP 8.5 tests for latest and trunk version of WP by @masteradhoc in [#771](https://github.com/WordPress/two-factor/pull/771)
* **Development Updates:** Add `phpcs:ignore` for falsepositives by @masteradhoc in [#777](https://github.com/WordPress/two-factor/pull/777)
* **Development Updates:** Fix(totp): `otpauth` link in QR code URL by @sjinks in [#784](https://github.com/WordPress/two-factor/pull/784)
* **Development Updates:** Update deploy.yml by @masteradhoc in [#773](https://github.com/WordPress/two-factor/pull/773)
* **Development Updates:** Update required WordPress Version by @masteradhoc in [#765](https://github.com/WordPress/two-factor/pull/765)
* **Development Updates:** Fix: ensure execution stops after redirects by @sjinks in [#786](https://github.com/WordPress/two-factor/pull/786)
* **Development Updates:** Fix `WordPress.Security.EscapeOutput.OutputNotEscaped` errors by @masteradhoc in [#776](https://github.com/WordPress/two-factor/pull/776)
* **Dependency Updates:** Bump qs and express by @dependabot[bot] in [#746](https://github.com/WordPress/two-factor/pull/746)
* **Dependency Updates:** Bump lodash from 4.17.21 to 4.17.23 by @dependabot[bot] in [#750](https://github.com/WordPress/two-factor/pull/750)
* **Dependency Updates:** Bump lodash-es from 4.17.21 to 4.17.23 by @dependabot[bot] in [#748](https://github.com/WordPress/two-factor/pull/748)
* **Dependency Updates:** Bump phpunit/phpunit from 8.5.44 to 8.5.52 by @dependabot[bot] in [#755](https://github.com/WordPress/two-factor/pull/755)
* **Dependency Updates:** Bump symfony/process from 5.4.47 to 5.4.51 by @dependabot[bot] in [#756](https://github.com/WordPress/two-factor/pull/756)
* **Dependency Updates:** Bump qs and body-parser by @dependabot[bot] in [#782](https://github.com/WordPress/two-factor/pull/782)
* **Dependency Updates:** Bump webpack from 5.101.3 to 5.105.0 by @dependabot[bot] in [#780](https://github.com/WordPress/two-factor/pull/780)
= 0.14.2 - 2025-12-11 =
* **New Features:** Add filter for rest_api_can_edit_user_and_update_two_factor_options by @gutobenn in [#689](https://github.com/WordPress/two-factor/pull/689)
* **Development Updates:** Remove Coveralls tooling and add inline coverage report by @kasparsd in [#717](https://github.com/WordPress/two-factor/pull/717)
* **Development Updates:** Update blueprint path to pull from main branch instead of a deleted f… by @georgestephanis in [#719](https://github.com/WordPress/two-factor/pull/719)
* **Development Updates:** Fix blueprint and wporg asset deploys by @kasparsd in [#734](https://github.com/WordPress/two-factor/pull/734)
* **Development Updates:** Upload release only on tag releases by @kasparsd in [#735](https://github.com/WordPress/two-factor/pull/735)
* **Development Updates:** Bump playwright and @playwright/test by @dependabot[bot] in [#721](https://github.com/WordPress/two-factor/pull/721)
* **Development Updates:** Bump tar-fs from 3.1.0 to 3.1.1 by @dependabot[bot] in [#720](https://github.com/WordPress/two-factor/pull/720)
* **Development Updates:** Bump node-forge from 1.3.1 to 1.3.2 by @dependabot[bot] in [#724](https://github.com/WordPress/two-factor/pull/724)
* **Development Updates:** Bump js-yaml by @dependabot[bot] in [#725](https://github.com/WordPress/two-factor/pull/725)
* **Development Updates:** Mark as tested with the latest WP core version by @kasparsd in [#730](https://github.com/WordPress/two-factor/pull/730)
= 0.14.1 - 2025-09-05 =
- Don't URI encode the TOTP url for display. by @dd32 in [#711](https://github.com/WordPress/two-factor/pull/711)
- Removed the duplicate Security.md by @slvignesh05 in [#712](https://github.com/WordPress/two-factor/pull/712)
- Fixed linting issues by @sudar in [#707](https://github.com/WordPress/two-factor/pull/707)
- Update development dependencies and fix failing QR unit test by @kasparsd in [#714](https://github.com/WordPress/two-factor/pull/714)
- Trigger checkbox js change event by @gedeminas in [#688](https://github.com/WordPress/two-factor/pull/688)
= 0.14.0 - 2025-07-03 =
* **Features:** Enable Application Passwords for REST API and XML-RPC authentication (by default) by @joostdekeijzer in [#697](https://github.com/WordPress/two-factor/pull/697) and [#698](https://github.com/WordPress/two-factor/pull/698). Previously this required two_factor_user_api_login_enable filter to be set to true which is now the default during application password auth. XML-RPC login is still disabled for regular user passwords.
* **Features:** Label recommended methods to simplify the configuration by @kasparsd in [#676](https://github.com/WordPress/two-factor/pull/676) and [#675](https://github.com/WordPress/two-factor/pull/675)
* **Documentation:** Add WP.org plugin demo by @kasparsd in [#667](https://github.com/WordPress/two-factor/pull/667)
* **Documentation:** Document supported versions of WP core and PHP by @jeffpaul in [#695](https://github.com/WordPress/two-factor/pull/695)
* **Documentation:** Document the release process by @jeffpaul in [#684](https://github.com/WordPress/two-factor/pull/684)
* **Tooling:** Remove duplicate WP.org screenshots and graphics from SVN trunk by @jeffpaul in [#683](https://github.com/WordPress/two-factor/pull/683)
= 0.13.0 - 2025-04-02 =
- Add two_factor_providers_for_user filter to limit two-factor providers available to each user by @kasparsd in [#669](https://github.com/WordPress/two-factor/pull/669)
- Update automated testing to cover PHP 8.4 and default to PHP 8.3 by @BrookeDot in [#665](https://github.com/WordPress/two-factor/pull/665)
[View the complete changelog details here](https://github.com/wordpress/two-factor/blob/master/CHANGELOG.md).
== Upgrade Notice ==
= 0.10.0 =
Bumps WordPress minimum supported version to 6.3 and PHP minimum to 7.2.
= 0.9.0 =
Users are now asked to re-authenticate with their two-factor before making changes to their two-factor settings. This associates each login session with the two-factor login meta data for improved handling of that session.

View File

@ -0,0 +1,97 @@
<?php
/**
* Admin settings UI for the Two-Factor plugin.
* Provides a site-wide settings screen for disabling individual Two-Factor providers.
*
* @since 0.16
*
* @package Two_Factor
*/
/**
* Settings screen renderer for Two-Factor.
*
* @since 0.16
*/
class Two_Factor_Settings {
/**
* Render the settings page.
* Also handles saving of settings when the form is submitted.
*
* @since 0.16
*
* @return void
*/
public static function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Handle save.
if ( isset( $_POST['two_factor_settings_submit'] ) ) {
check_admin_referer( 'two_factor_save_settings', 'two_factor_settings_nonce' );
$posted = isset( $_POST['two_factor_enabled_providers'] ) && is_array( $_POST['two_factor_enabled_providers'] ) ? wp_unslash( $_POST['two_factor_enabled_providers'] ) : array();
// Sanitize posted values immediately.
$posted = array_map( 'sanitize_text_field', (array) $posted );
// Remove empty values.
$enabled = array_values( array_filter( $posted, 'strlen' ) );
update_option( 'two_factor_enabled_providers', array_values( array_unique( $enabled ) ) );
echo '<div class="updated"><p>' . esc_html__( 'Settings saved.', 'two-factor' ) . '</p></div>';
}
// Build provider list for display using public core API.
$provider_instances = array();
if ( class_exists( 'Two_Factor_Core' ) && method_exists( 'Two_Factor_Core', 'get_providers' ) ) {
$provider_instances = Two_Factor_Core::get_providers();
if ( ! is_array( $provider_instances ) ) {
$provider_instances = array();
}
}
// Default to all providers enabled when the option has never been saved.
$all_provider_keys = array_keys( $provider_instances );
$saved_enabled = get_option( 'two_factor_enabled_providers', $all_provider_keys );
echo '<div class="wrap two-factor-settings">';
echo '<h1>' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '</h1>';
echo '<h2>' . esc_html__( 'Enabled Providers', 'two-factor' ) . '</h2>';
echo '<p class="description">' . esc_html__( 'Choose which Two-Factor providers are available on this site. All providers are enabled by default.', 'two-factor' ) . '</p>';
echo '<form method="post" action="">';
wp_nonce_field( 'two_factor_save_settings', 'two_factor_settings_nonce' );
echo '<fieldset class="two-factor-providers"><legend class="screen-reader-text">' . esc_html__( 'Providers', 'two-factor' ) . '</legend>';
echo '<table class="form-table"><tbody>';
if ( empty( $provider_instances ) ) {
echo '<tr><td>' . esc_html__( 'No providers found.', 'two-factor' ) . '</td></tr>';
} else {
// Render a compact stacked list of provider checkboxes below the title/description.
echo '<tr>';
echo '<td>';
foreach ( $provider_instances as $provider_key => $instance ) {
$label = method_exists( $instance, 'get_label' ) ? $instance->get_label() : $provider_key;
echo '<p class="provider-item"><label for="provider_' . esc_attr( $provider_key ) . '">';
echo '<input type="checkbox" name="two_factor_enabled_providers[]" id="provider_' . esc_attr( $provider_key ) . '" value="' . esc_attr( $provider_key ) . '" ' . checked( in_array( $provider_key, (array) $saved_enabled, true ), true, false ) . ' /> ';
echo esc_html( $label );
echo '</label></p>';
}
echo '</td>';
echo '</tr>';
}
echo '</tbody></table>';
echo '</fieldset>';
submit_button( __( 'Save Settings', 'two-factor' ), 'primary', 'two_factor_settings_submit' );
echo '</form>';
echo '</div>';
}
}

View File

@ -10,9 +10,9 @@
* @wordpress-plugin
* Plugin Name: Two Factor
* Plugin URI: https://wordpress.org/plugins/two-factor/
* Description: Enable Two-Factor Authentication using time-based one-time passwords, Universal 2nd Factor (FIDO U2F, YubiKey), email, and backup verification codes.
* Version: 0.13.0
* Requires at least: 6.3
* Description: Enable Two-Factor Authentication using time-based one-time passwords, email, and backup verification codes.
* Requires at least: 6.8
* Version: 0.16.0
* Requires PHP: 7.2
* Author: WordPress.org Contributors
* Author URI: https://github.com/wordpress/two-factor/graphs/contributors
@ -22,6 +22,10 @@
* Network: True
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Shortcut constant to the path of this file.
*/
@ -30,7 +34,7 @@ define( 'TWO_FACTOR_DIR', plugin_dir_path( __FILE__ ) );
/**
* Version of the plugin.
*/
define( 'TWO_FACTOR_VERSION', '0.13.0' );
define( 'TWO_FACTOR_VERSION', '0.16.0' );
/**
* Include the base class here, so that other plugins can also extend it.
@ -47,9 +51,140 @@ require_once TWO_FACTOR_DIR . 'class-two-factor-core.php';
*/
require_once TWO_FACTOR_DIR . 'class-two-factor-compat.php';
// Load settings UI class so the settings page can be rendered.
require_once TWO_FACTOR_DIR . 'settings/class-two-factor-settings.php';
$two_factor_compat = new Two_Factor_Compat();
Two_Factor_Core::add_hooks( $two_factor_compat );
// Delete our options and user meta during uninstall.
register_uninstall_hook( __FILE__, array( Two_Factor_Core::class, 'uninstall' ) );
/**
* Register admin menu and plugin action links.
*
* @since 0.16
*/
function two_factor_register_admin_hooks() {
if ( is_admin() ) {
add_action( 'admin_menu', 'two_factor_add_settings_page' );
}
// Load settings page assets when in admin.
// Settings assets handled inline via standard markup; no extra CSS enqueued.
/* Enforcement filters: restrict providers based on saved enabled-providers option. */
add_filter( 'two_factor_providers', 'two_factor_filter_enabled_providers' );
add_filter( 'two_factor_enabled_providers_for_user', 'two_factor_filter_enabled_providers_for_user', 10, 2 );
}
add_action( 'init', 'two_factor_register_admin_hooks' );
/**
* Add the Two Factor settings page under Settings.
*
* @since 0.16
*/
function two_factor_add_settings_page() {
add_options_page(
__( 'Two-Factor Settings', 'two-factor' ),
__( 'Two-Factor', 'two-factor' ),
'manage_options',
'two-factor-settings',
'two_factor_render_settings_page'
);
}
/**
* Render the settings page via the settings class if available.
*
* @since 0.16
*/
function two_factor_render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Prefer new settings class (keeps main file small).
if ( class_exists( 'Two_Factor_Settings' ) && is_callable( array( 'Two_Factor_Settings', 'render_settings_page' ) ) ) {
Two_Factor_Settings::render_settings_page();
return;
}
// Fallback: no UI available.
echo '<div class="wrap"><h1>' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '</h1>';
echo '<p>' . esc_html__( 'Settings not available.', 'two-factor' ) . '</p></div>';
}
/**
* Helper: retrieve the site-enabled providers option.
* Returns null when the option has never been saved (meaning all providers are allowed).
* Returns an array (possibly empty) when the admin has explicitly saved a selection.
*
* @since 0.16
*
* @return array|null
*/
function two_factor_get_enabled_providers_option() {
$enabled = get_option( 'two_factor_enabled_providers', null );
if ( null === $enabled ) {
return null; // Never saved — allow everything.
}
return is_array( $enabled ) ? $enabled : array();
}
/**
* Filter the registered providers to only those in the site-enabled list.
* This filter receives providers in core format: classname => path.
*
* @since 0.16
*
* @param array $providers Registered providers in classname => path format.
* @return array Filtered list of enabled providers.
*/
function two_factor_filter_enabled_providers( $providers ) {
$site_enabled = two_factor_get_enabled_providers_option();
// null means the option was never saved — allow all providers.
if ( null === $site_enabled ) {
return $providers;
}
// On the settings page itself, show all providers so admins can change the selection.
if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) {
return $providers;
}
foreach ( $providers as $key => $path ) {
if ( ! in_array( $key, $site_enabled, true ) ) {
unset( $providers[ $key ] );
}
}
return $providers;
}
/**
* Filter enabled providers for a user (classnames array) to enforce the site-enabled list.
*
* @since 0.16
*
* @param array $enabled Enabled provider classnames for the user.
* @param int $user_id ID of the user being filtered.
* @return array Filtered list of provider classnames allowed by the site.
*/
function two_factor_filter_enabled_providers_for_user( $enabled, $user_id ) {
$site_enabled = two_factor_get_enabled_providers_option();
// null means the option was never saved — allow all.
if ( null === $site_enabled ) {
return $enabled;
}
return array_values( array_intersect( (array) $enabled, $site_enabled ) );
}

View File

@ -7,3 +7,70 @@
display: block;
font-weight: 700;
}
.two-factor-methods-table .two-factor-method-recommended {
font-size: 0.8rem;
line-height: 1;
font-weight: 400;
border: 1px dotted;
border-radius: 0.15rem;
padding: 0.1rem 0.25rem;
margin: 0 0.15rem;
}
#login .backup-methods-wrap {
margin-top: 16px;
padding: 0 24px;
}
#login .backup-methods-wrap a {
text-decoration: none;
}
#login .backup-methods-wrap ul {
list-style-position: inside;
}
/* Prevent Jetpack from hiding our controls, see https://github.com/Automattic/jetpack/issues/3747 */
.jetpack-sso-form-display #loginform > p,
.jetpack-sso-form-display #loginform > div {
display: block;
}
#login form p.two-factor-prompt {
margin-bottom: 1em;
}
#loginform .input.authcode {
letter-spacing: 0.3em;
}
#loginform .input.authcode::placeholder {
opacity: 0.5;
}
.two-factor-backup-codes-wrapper > :not(:last-child) {
margin-bottom: 1em;
}
.two-factor-backup-codes-list-wrap {
background-color: #ddd;
display: inline-block;
margin-top: 24px;
padding: 20px;
}
.two-factor-backup-codes-list-wrap .two-factor-backup-codes-unused-codes {
margin: 0;
padding: revert;
}
.two-factor-backup-codes-list-wrap .two-factor-backup-codes-token {
letter-spacing: 0.3em;
font-family: monospace;
}
#two-factor-qr-code {
min-width: 205px;
min-height: 205px;
}