Merge pull request #13787 from nijel/second-factor
Second authentication factor
This commit is contained in:
commit
45d1924e70
@ -10,6 +10,7 @@ build:
|
||||
dependencies:
|
||||
before:
|
||||
- composer install
|
||||
- composer require tecnickcom/tcpdf pragmarx/google2fa bacon/bacon-qr-code samyoul/u2f-php-server
|
||||
- ./vendor/bin/phpcs --config-set installed_paths `pwd`/vendor/phpmyadmin/coding-standard
|
||||
tests:
|
||||
override:
|
||||
|
||||
@ -57,7 +57,10 @@
|
||||
"symfony/polyfill-mbstring": "^1.3"
|
||||
},
|
||||
"conflict": {
|
||||
"tecnickcom/tcpdf": "<6.2"
|
||||
"tecnickcom/tcpdf": "<6.2",
|
||||
"pragmarx/google2fa": "<2.0",
|
||||
"bacon/bacon-qr-code": "<1.0",
|
||||
"samyoul/u2f-php-server": "<1.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-openssl": "Cookie encryption",
|
||||
@ -68,7 +71,10 @@
|
||||
"ext-zip": "For zip import and export",
|
||||
"ext-gd2": "For image transformations",
|
||||
"ext-mbstring": "For best performance",
|
||||
"tecnickcom/tcpdf": "For PDF support"
|
||||
"tecnickcom/tcpdf": "For PDF support",
|
||||
"pragmarx/google2fa": "For 2FA authentication",
|
||||
"bacon/bacon-qr-code": "For 2FA authentication",
|
||||
"samyoul/u2f-php-server": "For FIDO U2F authentication"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.1",
|
||||
|
||||
@ -3345,6 +3345,13 @@ Developer
|
||||
* The setup script is enabled even with existing configuration.
|
||||
* The setup does not try to connect to the MySQL server.
|
||||
|
||||
.. config:option:: $cfg['DBG']['simple2fa']
|
||||
|
||||
:type: boolean
|
||||
:default: false
|
||||
|
||||
Can be used for testing second authentication factor.
|
||||
|
||||
.. _config-examples:
|
||||
|
||||
Examples
|
||||
|
||||
52
doc/second_factor.rst
Normal file
52
doc/second_factor.rst
Normal file
@ -0,0 +1,52 @@
|
||||
.. _2fa:
|
||||
|
||||
Second authentication factor
|
||||
============================
|
||||
|
||||
.. versionadded:: 4.8.0
|
||||
|
||||
Since phpMyAdmin 4.8.0 you can configure second authentication factor to be
|
||||
used when logging into it. To use this, you first need to configure
|
||||
:ref:`linked-tables`. Once this is done, every user can opt-in for second
|
||||
authentication factor in the :guilabel:`Settings`.
|
||||
|
||||
Authentication Application
|
||||
--------------------------
|
||||
|
||||
Using application for authentication is quite common approach based on HOTP and
|
||||
TOTP. It is based on transmitting private key from phpMyAdmin to the
|
||||
authentication application and the application is then able to generate one
|
||||
time codes based on this key.
|
||||
|
||||
There are dozens of applications available for mobile phones to implement these
|
||||
standards, the most widely used include:
|
||||
|
||||
* `FreeOTP for iOS, Android and Pebble <https://freeotp.github.io/>`_
|
||||
* `Authy for iOS, Android, Chrome, OS X <https://www.authy.com/>`_
|
||||
* `Google Authenticator for iOS <https://itunes.apple.com/us/app/google-authenticator/id388497605>`_
|
||||
* `Google Authenticator for Android <https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2>`_
|
||||
* `LastPass Authenticator for iOS, Android, OS X, Windows <https://lastpass.com/auth/>`_
|
||||
|
||||
Hardware Security Key
|
||||
---------------------
|
||||
|
||||
Using hardware tokens is considered to be more secure than software based
|
||||
solution. phpMyAdmin supports `FIDO U2F <https://en.wikipedia.org/wiki/Universal_2nd_Factor>`_
|
||||
tokens.
|
||||
|
||||
There are several manufacturers of these tokens, for example:
|
||||
|
||||
* `youbico FIDO U2F Security Key <https://www.yubico.com/products/yubikey-hardware/fido-u2f-security-key/>`_
|
||||
* `HyperFIDO <https://www.hypersecu.com/products/hyperfido>`_
|
||||
* `ePass FIDO USB <https://www.ftsafe.com/onlinestore/product?id=21>`_
|
||||
* `TREZOR Bitcoin wallet <https://shop.trezor.io?a=572b241135e1>`_ can `act as an U2F token <http://doc.satoshilabs.com/trezor-user/u2f.html>`_
|
||||
|
||||
Simple Second Factor
|
||||
--------------------
|
||||
|
||||
This authentication is included for testing and demostration purposes only as
|
||||
it really does not provide second factor, it just asks user to confirm login by
|
||||
clicking on the button.
|
||||
|
||||
It should not be used in the production and is disabled unless
|
||||
:config:option:`$cfg['DBG']['simple2fa']` is set.
|
||||
@ -1030,6 +1030,7 @@ are always ways to make your installation more secure:
|
||||
* In case you don't want all MySQL users to be able to access
|
||||
phpMyAdmin, you can use :config:option:`$cfg['Servers'][$i]['AllowDeny']['rules']` to limit them
|
||||
or :config:option:`$cfg['Servers'][$i]['AllowRoot']` to deny root user access.
|
||||
* Enable :ref:`2fa` for your account.
|
||||
* Consider hiding phpMyAdmin behind an authentication proxy, so that
|
||||
users need to authenticate prior to providing MySQL credentials
|
||||
to phpMyAdmin. You can achieve this by configuring your web server to request
|
||||
|
||||
@ -5,6 +5,7 @@ User Guide
|
||||
:maxdepth: 2
|
||||
|
||||
settings
|
||||
second_factor
|
||||
transformations
|
||||
bookmarks
|
||||
privileges
|
||||
|
||||
@ -742,6 +742,10 @@ $js_messages['strWeak'] = __('Weak');
|
||||
$js_messages['strGood'] = __('Good');
|
||||
$js_messages['strStrong'] = __('Strong');
|
||||
|
||||
/* U2F errors */
|
||||
$js_messages['strU2FTimeout'] = __('Timed out waiting for security key activation.');
|
||||
$js_messages['strU2FError'] = __('Failed security key activation (%s).');
|
||||
|
||||
echo "var PMA_messages = new Array();\n";
|
||||
foreach ($js_messages as $name => $js_message) {
|
||||
Sanitize::printJsValue("PMA_messages['" . $name . "']", $js_message);
|
||||
|
||||
55
js/u2f.js
Normal file
55
js/u2f.js
Normal file
@ -0,0 +1,55 @@
|
||||
/** global: AJAX */
|
||||
/** global: PMA_messages */
|
||||
/** global: u2f */
|
||||
AJAX.registerOnload('u2f.js', function () {
|
||||
var $inputReg = $('#u2f_registration_response');
|
||||
if ($inputReg.length > 0) {
|
||||
var $formReg = $inputReg.parents('form');
|
||||
$formReg.find('input[type=submit]').hide();
|
||||
setTimeout(function() {
|
||||
// A magic JS function that talks to the USB device. This function will keep polling for the USB device until it finds one.
|
||||
u2f.register([JSON.parse($inputReg.attr('data-request'))], JSON.parse($inputReg.attr('data-signatures')), function(data) {
|
||||
// Handle returning error data
|
||||
if(data.errorCode && data.errorCode !== 0) {
|
||||
if (data.errorCode === 5) {
|
||||
PMA_ajaxShowMessage(PMA_messages.strU2FTimeout, false);
|
||||
} else {
|
||||
PMA_ajaxShowMessage(
|
||||
PMA_sprintf(PMA_messages.strU2FError, data.errorCode), false
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill and submit form.
|
||||
$inputReg.val(JSON.stringify(data));
|
||||
$formReg.submit();
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
var $inputAuth = $('#u2f_authentication_response');
|
||||
if ($inputAuth.length > 0) {
|
||||
var $formAuth = $inputAuth.parents('form');
|
||||
$formAuth.find('input[type=submit]').hide();
|
||||
setTimeout(function() {
|
||||
// Magic JavaScript talking to your HID
|
||||
u2f.sign(JSON.parse($inputAuth.attr('data-request')), function(data) {
|
||||
// Handle returning error data
|
||||
if(data.errorCode && data.errorCode !== 0) {
|
||||
if (data.errorCode === 5) {
|
||||
PMA_ajaxShowMessage(PMA_messages.strU2FTimeout, false);
|
||||
} else {
|
||||
PMA_ajaxShowMessage(
|
||||
PMA_sprintf(PMA_messages.strU2FError, data.errorCode), false
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill and submit form.
|
||||
$inputAuth.val(JSON.stringify(data));
|
||||
$formAuth.submit();
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
748
js/vendor/u2f-api.js
vendored
Normal file
748
js/vendor/u2f-api.js
vendored
Normal file
@ -0,0 +1,748 @@
|
||||
//Copyright 2014-2015 Google Inc. All rights reserved.
|
||||
|
||||
//Use of this source code is governed by a BSD-style
|
||||
//license that can be found in the LICENSE file or at
|
||||
//https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
/**
|
||||
* @fileoverview The U2F api.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
||||
/**
|
||||
* Namespace for the U2F api.
|
||||
* @type {Object}
|
||||
*/
|
||||
var u2f = u2f || {};
|
||||
|
||||
/**
|
||||
* FIDO U2F Javascript API Version
|
||||
* @number
|
||||
*/
|
||||
var js_api_version;
|
||||
|
||||
/**
|
||||
* The U2F extension id
|
||||
* @const {string}
|
||||
*/
|
||||
// The Chrome packaged app extension ID.
|
||||
// Uncomment this if you want to deploy a server instance that uses
|
||||
// the package Chrome app and does not require installing the U2F Chrome extension.
|
||||
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
|
||||
// The U2F Chrome extension ID.
|
||||
// Uncomment this if you want to deploy a server instance that uses
|
||||
// the U2F Chrome extension to authenticate.
|
||||
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
|
||||
|
||||
|
||||
/**
|
||||
* Message types for messsages to/from the extension
|
||||
* @const
|
||||
* @enum {string}
|
||||
*/
|
||||
u2f.MessageTypes = {
|
||||
'U2F_REGISTER_REQUEST': 'u2f_register_request',
|
||||
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
|
||||
'U2F_SIGN_REQUEST': 'u2f_sign_request',
|
||||
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
|
||||
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
|
||||
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Response status codes
|
||||
* @const
|
||||
* @enum {number}
|
||||
*/
|
||||
u2f.ErrorCodes = {
|
||||
'OK': 0,
|
||||
'OTHER_ERROR': 1,
|
||||
'BAD_REQUEST': 2,
|
||||
'CONFIGURATION_UNSUPPORTED': 3,
|
||||
'DEVICE_INELIGIBLE': 4,
|
||||
'TIMEOUT': 5
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A message for registration requests
|
||||
* @typedef {{
|
||||
* type: u2f.MessageTypes,
|
||||
* appId: ?string,
|
||||
* timeoutSeconds: ?number,
|
||||
* requestId: ?number
|
||||
* }}
|
||||
*/
|
||||
u2f.U2fRequest;
|
||||
|
||||
|
||||
/**
|
||||
* A message for registration responses
|
||||
* @typedef {{
|
||||
* type: u2f.MessageTypes,
|
||||
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
|
||||
* requestId: ?number
|
||||
* }}
|
||||
*/
|
||||
u2f.U2fResponse;
|
||||
|
||||
|
||||
/**
|
||||
* An error object for responses
|
||||
* @typedef {{
|
||||
* errorCode: u2f.ErrorCodes,
|
||||
* errorMessage: ?string
|
||||
* }}
|
||||
*/
|
||||
u2f.Error;
|
||||
|
||||
/**
|
||||
* Data object for a single sign request.
|
||||
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC, USB_INTERNAL}}
|
||||
*/
|
||||
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);
|
||||
});
|
||||
};
|
||||
@ -7,11 +7,16 @@
|
||||
*/
|
||||
namespace PhpMyAdmin\Plugins;
|
||||
|
||||
use PhpMyAdmin\Config;
|
||||
use PhpMyAdmin\Core;
|
||||
use PhpMyAdmin\IpAllowDeny;
|
||||
use PhpMyAdmin\Logging;
|
||||
use PhpMyAdmin\Message;
|
||||
use PhpMyAdmin\Response;
|
||||
use PhpMyAdmin\Sanitize;
|
||||
use PhpMyAdmin\SecondFactor;
|
||||
use PhpMyAdmin\Session;
|
||||
use PhpMyAdmin\Template;
|
||||
use PhpMyAdmin\Url;
|
||||
|
||||
/**
|
||||
@ -297,4 +302,39 @@ abstract class AuthenticationPlugin
|
||||
$this->showFailure('empty-denied');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether two factor authentication is active
|
||||
* for given user and performs it.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function checkSecondFactor()
|
||||
{
|
||||
$second = new SecondFactor($this->user);
|
||||
|
||||
/* Do we need to show the form? */
|
||||
if ($second->check()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = Response::getInstance();
|
||||
if ($response->loginPage()) {
|
||||
if (defined('TESTSUITE')) {
|
||||
return true;
|
||||
} else {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
echo Template::get('login/header')->render(['theme' => $GLOBALS['PMA_Theme']]);
|
||||
Message::rawNotice(
|
||||
__('You have enabled two factor authentication, please confirm your login.')
|
||||
)->display();
|
||||
echo Template::get('login/second')->render(['form' => $second->render()]);
|
||||
echo Template::get('login/footer')->render();
|
||||
echo Config::renderFooter();
|
||||
if (! defined('TESTSUITE')) {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
142
libraries/classes/Plugins/SecondFactor/Application.php
Normal file
142
libraries/classes/Plugins/SecondFactor/Application.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* Second authentication factor handling
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
namespace PhpMyAdmin\Plugins\SecondFactor;
|
||||
|
||||
use PhpMyAdmin\SecondFactor;
|
||||
use PhpMyAdmin\Template;
|
||||
use PhpMyAdmin\Plugins\SecondFactorPlugin;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
|
||||
/**
|
||||
* HOTP and TOTP based second factor
|
||||
*
|
||||
* Also known as Google, Authy, or OTP
|
||||
*/
|
||||
class Application extends SecondFactorPlugin
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $id = 'application';
|
||||
|
||||
protected $_google2fa;
|
||||
|
||||
/**
|
||||
* Creates object
|
||||
*
|
||||
* @param SecondFactor $second SecondFactor instance
|
||||
*/
|
||||
public function __construct(SecondFactor $second)
|
||||
{
|
||||
parent::__construct($second);
|
||||
$this->_google2fa = new Google2FA();
|
||||
$this->_google2fa->setWindow(8);
|
||||
if (!isset($this->_second->config['settings']['secret'])) {
|
||||
$this->_second->config['settings']['secret'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any property of this class
|
||||
*
|
||||
* @param string $property name of the property
|
||||
*
|
||||
* @return mixed|void if property exist, value of the relevant property
|
||||
*/
|
||||
public function __get($property)
|
||||
{
|
||||
switch ($property) {
|
||||
case 'google2fa':
|
||||
return $this->_google2fa;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks authentication, returns true on success
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
$this->_provided = false;
|
||||
if (!isset($_POST['2fa_code'])) {
|
||||
return false;
|
||||
}
|
||||
$this->_provided = true;
|
||||
return $this->_google2fa->verifyKey(
|
||||
$this->_second->config['settings']['secret'], $_POST['2fa_code']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to enter second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return Template::get('login/second/application')->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to configure second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function setup()
|
||||
{
|
||||
$inlineUrl = $this->_google2fa->getQRCodeInline(
|
||||
'phpMyAdmin (' . $this->getAppId(false) . ')',
|
||||
$this->_second->user,
|
||||
$this->_second->config['settings']['secret']
|
||||
);
|
||||
return Template::get('login/second/application_configure')->render([
|
||||
'image' => $inlineUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs backend configuration
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function configure()
|
||||
{
|
||||
if (! isset($_SESSION['2fa_application_key'])) {
|
||||
$_SESSION['2fa_application_key'] = $this->_google2fa->generateSecretKey();
|
||||
}
|
||||
$this->_second->config['settings']['secret'] = $_SESSION['2fa_application_key'];
|
||||
|
||||
$result = $this->check();
|
||||
if ($result) {
|
||||
unset($_SESSION['2fa_application_key']);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user visible name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getName()
|
||||
{
|
||||
return __('Authentication application');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user visible description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getDescription()
|
||||
{
|
||||
return __('Provides authentication using HOTP and TOTP applications such as FreeOTP, Google Authenticator or Authy.');
|
||||
}
|
||||
}
|
||||
|
||||
196
libraries/classes/Plugins/SecondFactor/Key.php
Normal file
196
libraries/classes/Plugins/SecondFactor/Key.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* Second authentication factor handling
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
namespace PhpMyAdmin\Plugins\SecondFactor;
|
||||
|
||||
use PhpMyAdmin\Response;
|
||||
use PhpMyAdmin\SecondFactor;
|
||||
use PhpMyAdmin\Template;
|
||||
use PhpMyAdmin\Plugins\SecondFactorPlugin;
|
||||
use Samyoul\U2F\U2FServer\U2FServer;
|
||||
use Samyoul\U2F\U2FServer\U2FException;
|
||||
|
||||
/**
|
||||
* Hardware key based second factor
|
||||
*
|
||||
* Supports FIDO U2F tokens
|
||||
*/
|
||||
class Key extends SecondFactorPlugin
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $id = 'key';
|
||||
|
||||
/**
|
||||
* Creates object
|
||||
*
|
||||
* @param SecondFactor $second SecondFactor instance
|
||||
*/
|
||||
public function __construct(SecondFactor $second)
|
||||
{
|
||||
parent::__construct($second);
|
||||
if (!isset($this->_second->config['settings']['registrations'])) {
|
||||
$this->_second->config['settings']['registrations'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array of U2F registration objects
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRegistrations()
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->_second->config['settings']['registrations'] as $index => $data) {
|
||||
$reg = new \StdClass;
|
||||
$reg->keyHandle = $data['keyHandle'];
|
||||
$reg->publicKey = $data['publicKey'];
|
||||
$reg->certificate = $data['certificate'];
|
||||
$reg->counter = $data['counter'];
|
||||
$reg->index = $index;
|
||||
$result[] = $reg;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks authentication, returns true on success
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
$this->_provided = false;
|
||||
if (!isset($_POST['u2f_authentication_response']) || !isset($_SESSION['authenticationRequest'])) {
|
||||
return false;
|
||||
}
|
||||
$this->_provided = true;
|
||||
try {
|
||||
$response = json_decode($_POST['u2f_authentication_response']);
|
||||
if (is_null($response)) {
|
||||
return false;
|
||||
}
|
||||
$authentication = U2FServer::authenticate(
|
||||
$_SESSION['authenticationRequest'],
|
||||
$this->getRegistrations(),
|
||||
$response
|
||||
);
|
||||
$this->_second->config['settings']['registrations'][$authentication->index]['counter'] = $authentication->counter;
|
||||
$this->_second->save();
|
||||
return true;
|
||||
} catch (U2FException $e) {
|
||||
$this->_message = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads needed javascripts into the page
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loadScripts()
|
||||
{
|
||||
$response = Response::getInstance();
|
||||
$scripts = $response->getHeader()->getScripts();
|
||||
$scripts->addFile('vendor/u2f-api.js');
|
||||
$scripts->addFile('u2f.js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to enter second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$request = U2FServer::makeAuthentication(
|
||||
$this->getRegistrations(),
|
||||
$this->getAppId(true)
|
||||
);
|
||||
$_SESSION['authenticationRequest'] = $request;
|
||||
$this->loadScripts();
|
||||
return Template::get('login/second/key')->render([
|
||||
'request' => json_encode($request),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to configure second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function setup()
|
||||
{
|
||||
$registrationData = U2FServer::makeRegistration(
|
||||
$this->getAppId(true),
|
||||
$this->getRegistrations()
|
||||
);
|
||||
$_SESSION['registrationRequest'] = $registrationData['request'];
|
||||
|
||||
$this->loadScripts();
|
||||
return Template::get('login/second/key_configure')->render([
|
||||
'request' => json_encode($registrationData['request']),
|
||||
'signatures' => json_encode($registrationData['signatures']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs backend configuration
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function configure()
|
||||
{
|
||||
$this->_provided = false;
|
||||
if (! isset($_POST['u2f_registration_response']) || ! isset($_SESSION['registrationRequest'])) {
|
||||
return false;
|
||||
}
|
||||
$this->_provided = true;
|
||||
try {
|
||||
$response = json_decode($_POST['u2f_registration_response']);
|
||||
if (is_null($response)) {
|
||||
return false;
|
||||
}
|
||||
$registration = U2FServer::register(
|
||||
$_SESSION['registrationRequest'], $response
|
||||
);
|
||||
$this->_second->config['settings']['registrations'][] = [
|
||||
'keyHandle' => $registration->getKeyHandle(),
|
||||
'publicKey' => $registration->getPublicKey(),
|
||||
'certificate' => $registration->getCertificate(),
|
||||
'counter' => $registration->getCounter(),
|
||||
];
|
||||
return true;
|
||||
} catch (U2FException $e) {
|
||||
$this->_message = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user visible name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getName()
|
||||
{
|
||||
return __('Security key');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user visible description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getDescription()
|
||||
{
|
||||
return __('Provides authentication using hardware security tokens supporting FIDO U2F.');
|
||||
}
|
||||
}
|
||||
64
libraries/classes/Plugins/SecondFactor/Simple.php
Normal file
64
libraries/classes/Plugins/SecondFactor/Simple.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* Second authentication factor handling
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
namespace PhpMyAdmin\Plugins\SecondFactor;
|
||||
|
||||
use PhpMyAdmin\Plugins\SecondFactorPlugin;
|
||||
use PhpMyAdmin\Template;
|
||||
|
||||
/**
|
||||
* Simple second factor auth asking just for confirmation.
|
||||
*
|
||||
* This has no practical use, but can be used for testing.
|
||||
*/
|
||||
class Simple extends SecondFactorPlugin
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $id = 'simple';
|
||||
|
||||
/**
|
||||
* Checks authentication, returns true on success
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
return isset($_POST['2fa_confirm']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to enter second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return Template::get('login/second/simple')->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user visible name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getName()
|
||||
{
|
||||
return __('Simple second factor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user visible description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getDescription()
|
||||
{
|
||||
return __('For testing purposes only!');
|
||||
}
|
||||
}
|
||||
165
libraries/classes/Plugins/SecondFactorPlugin.php
Normal file
165
libraries/classes/Plugins/SecondFactorPlugin.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* Second authentication factor handling
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
namespace PhpMyAdmin\Plugins;
|
||||
|
||||
use PhpMyAdmin\Core;
|
||||
use PhpMyAdmin\Message;
|
||||
use PhpMyAdmin\SecondFactor;
|
||||
|
||||
/**
|
||||
* Second factor authentication plugin class
|
||||
*
|
||||
* This is basic implementation which does no
|
||||
* additional authentication, subclasses are expected
|
||||
* to implement this.
|
||||
*/
|
||||
class SecondFactorPlugin
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $id = '';
|
||||
|
||||
/**
|
||||
* @var SecondFactor
|
||||
*/
|
||||
protected $_second;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $_provided;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $_message;
|
||||
|
||||
/**
|
||||
* Creates object
|
||||
*
|
||||
* @param SecondFactor $second SecondFactor instance
|
||||
*/
|
||||
public function __construct(SecondFactor $second)
|
||||
{
|
||||
$this->_second = $second;
|
||||
$this->_provided = false;
|
||||
$this->_message = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns authentication error message
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getError()
|
||||
{
|
||||
if ($this->_provided) {
|
||||
if (!empty($this->_message)) {
|
||||
return Message::rawError(
|
||||
sprintf(__('Two-factor authentication failed: %s'), $this->_message)
|
||||
)->getDisplay();
|
||||
}
|
||||
return Message::rawError(
|
||||
__('Two-factor authentication failed.')
|
||||
)->getDisplay();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks authentication, returns true on success
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to enter second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to configure second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function setup()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs backend configuration
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function configure()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user visible name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getName()
|
||||
{
|
||||
return __('None two-factor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user visible description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getDescription()
|
||||
{
|
||||
return __('Login using password only.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an applicaiton ID
|
||||
*
|
||||
* Either hostname or hostname with scheme.
|
||||
*
|
||||
* @param boolean $return_url Whether to generate URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAppId($return_url)
|
||||
{
|
||||
global $PMA_Config;
|
||||
|
||||
$url = $PMA_Config->get('PmaAbsoluteUri');
|
||||
$parsed = [];
|
||||
if (!empty($url)) {
|
||||
$parsed = parse_url($url);
|
||||
}
|
||||
if (empty($parsed['scheme'])) {
|
||||
$parsed['scheme'] = $PMA_Config->isHttps() ? 'https' : 'http';
|
||||
}
|
||||
if (empty($parsed['host'])) {
|
||||
$parsed['host'] = Core::getenv('HTTP_HOST');
|
||||
}
|
||||
if ($return_url) {
|
||||
return $parsed['scheme'] . '://' . $parsed['host'] . (!empty($parsed['port']) ? ':' . $parsed['port'] : '');
|
||||
} else {
|
||||
return $parsed['host'];
|
||||
}
|
||||
}
|
||||
}
|
||||
248
libraries/classes/SecondFactor.php
Normal file
248
libraries/classes/SecondFactor.php
Normal file
@ -0,0 +1,248 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* Second authentication factor handling
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
namespace PhpMyAdmin;
|
||||
|
||||
use PhpMyAdmin\UserPreferences;
|
||||
|
||||
/**
|
||||
* Second factor authentication wrapper class
|
||||
*/
|
||||
class SecondFactor
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $_writable;
|
||||
|
||||
/**
|
||||
* @var PhpMyAdmin\Plugins\SecondFactorPlugin
|
||||
*/
|
||||
protected $_backend;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $_available;
|
||||
|
||||
/**
|
||||
* Creates new SecondFactor object
|
||||
*
|
||||
* @param string $user User name
|
||||
*/
|
||||
public function __construct($user)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->_available = $this->getAvailable();
|
||||
$this->config = $this->readConfig();
|
||||
$this->_writable = ($this->config['type'] == 'db');
|
||||
$this->_backend = $this->getBackend();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the configuration
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function readConfig()
|
||||
{
|
||||
$result = [];
|
||||
$config = UserPreferences::load();
|
||||
if (isset($config['config_data']['2fa'])) {
|
||||
$result = $config['config_data']['2fa'];
|
||||
}
|
||||
$result['type'] = $config['type'];
|
||||
if (! isset($result['backend'])) {
|
||||
$result['backend'] = '';
|
||||
}
|
||||
if (! isset($result['settings'])) {
|
||||
$result['settings'] = [];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any property of this class
|
||||
*
|
||||
* @param string $property name of the property
|
||||
*
|
||||
* @return mixed|void if property exist, value of the relevant property
|
||||
*/
|
||||
public function __get($property)
|
||||
{
|
||||
switch ($property) {
|
||||
case 'backend':
|
||||
return $this->_backend;
|
||||
case 'available':
|
||||
return $this->_available;
|
||||
case 'writable':
|
||||
return $this->_writable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of available backends
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailable()
|
||||
{
|
||||
$result = [];
|
||||
if ($GLOBALS['cfg']['DBG']['simple2fa']) {
|
||||
$result[] = 'simple';
|
||||
}
|
||||
if (class_exists('PragmaRX\Google2FA\Google2FA') && class_exists('BaconQrCode\Renderer\Image\Png')) {
|
||||
$result[] = 'application';
|
||||
}
|
||||
if (class_exists('Samyoul\U2F\U2FServer\U2FServer')) {
|
||||
$result[] = 'key';
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns class name for given name
|
||||
*
|
||||
* @param string $name Backend name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBackendClass($name)
|
||||
{
|
||||
$result = 'PhpMyAdmin\\Plugins\\SecondFactorPlugin';
|
||||
if (in_array($name, $this->_available)) {
|
||||
$result = 'PhpMyAdmin\\Plugins\\SecondFactor\\' . ucfirst($name);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns backend for current user
|
||||
*
|
||||
* @return PhpMyAdmin\Plugins\SecondFactorPlugin
|
||||
*/
|
||||
public function getBackend()
|
||||
{
|
||||
$name = $this->getBackendClass($this->config['backend']);
|
||||
return new $name($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks authentication, returns true on success
|
||||
*
|
||||
* @param boolean $skip_session Skip session cache
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function check($skip_session = false)
|
||||
{
|
||||
if ($skip_session) {
|
||||
return $this->_backend->check();
|
||||
}
|
||||
if (empty($_SESSION['second_factor_check'])) {
|
||||
$_SESSION['second_factor_check'] = $this->_backend->check();
|
||||
}
|
||||
return $_SESSION['second_factor_check'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to enter second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return $this->_backend->getError() . $this->_backend->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders user interface to configure second factor
|
||||
*
|
||||
* @return string HTML code
|
||||
*/
|
||||
public function setup()
|
||||
{
|
||||
return $this->_backend->getError() . $this->_backend->setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves current configuration.
|
||||
*
|
||||
* @return true|PhpMyAdmin\Message
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
return UserPreferences::persistOption('2fa', $this->config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes second factor settings
|
||||
*
|
||||
* The object might stay in partialy changed setup
|
||||
* if configuration fails.
|
||||
*
|
||||
* @param string $name Backend name
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function configure($name)
|
||||
{
|
||||
$this->config = [
|
||||
'backend' => $name
|
||||
];
|
||||
if ($name === '') {
|
||||
$cls = $this->getBackendClass($name);
|
||||
$this->config['settings'] = [];
|
||||
$this->_backend = new $cls($this);
|
||||
} else {
|
||||
if (! in_array($name, $this->_available)) {
|
||||
return false;
|
||||
}
|
||||
$cls = $this->getBackendClass($name);
|
||||
$this->config['settings'] = [];
|
||||
$this->_backend = new $cls($this);
|
||||
if (! $this->_backend->configure()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$result = $this->save();
|
||||
if ($result !== true) {
|
||||
$result->display();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array with all available backends
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAllBackends()
|
||||
{
|
||||
$all = array_merge([''], $this->available);
|
||||
$backends = [];
|
||||
foreach ($all as $name) {
|
||||
$cls = $this->getBackendClass($name);
|
||||
$backends[] = [
|
||||
'id' => $cls::$id,
|
||||
'name' => $cls::getName(),
|
||||
'description' => $cls::getDescription(),
|
||||
];
|
||||
}
|
||||
return $backends;
|
||||
}
|
||||
}
|
||||
@ -173,6 +173,7 @@ class UserPreferences
|
||||
$whitelist['collation_connection'] = true;
|
||||
$whitelist['Server/hide_db'] = true;
|
||||
$whitelist['Server/only_db'] = true;
|
||||
$whitelist['2fa'] = true;
|
||||
foreach ($config_data as $path => $value) {
|
||||
if (! isset($whitelist[$path]) || isset($blacklist[$path])) {
|
||||
continue;
|
||||
|
||||
@ -544,6 +544,8 @@ if (! defined('PMA_MINIMUM_COMMON')) {
|
||||
|
||||
$auth_plugin->rememberCredentials();
|
||||
|
||||
$auth_plugin->checkSecondFactor();
|
||||
|
||||
/* Log success */
|
||||
Logging::logUser($cfg['Server']['user']);
|
||||
|
||||
|
||||
@ -3058,6 +3058,13 @@ $cfg['DBG']['sqllog'] = false;
|
||||
*/
|
||||
$cfg['DBG']['demo'] = false;
|
||||
|
||||
/**
|
||||
* Enable Simple second factor
|
||||
*
|
||||
* @global boolean $cfg['DBG']['simple2fa']
|
||||
*/
|
||||
$cfg['DBG']['simple2fa'] = false;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* MySQL settings
|
||||
|
||||
@ -9,6 +9,7 @@ use PhpMyAdmin\Config\Forms\User\UserFormList;
|
||||
use PhpMyAdmin\Message;
|
||||
use PhpMyAdmin\Relation;
|
||||
use PhpMyAdmin\Sanitize;
|
||||
use PhpMyAdmin\SecondFactor;
|
||||
|
||||
if (!defined('PHPMYADMIN')) {
|
||||
exit;
|
||||
@ -30,6 +31,13 @@ $content = PhpMyAdmin\Util::getHtmlTab(
|
||||
'text' => __('Manage your settings')
|
||||
)
|
||||
) . "\n";
|
||||
/* Second authentication factor */
|
||||
$content .= PhpMyAdmin\Util::getHtmlTab(
|
||||
array(
|
||||
'link' => 'prefs_second.php',
|
||||
'text' => __('Two-factor authentication')
|
||||
)
|
||||
) . "\n";
|
||||
$script_name = basename($GLOBALS['PMA_PHP_SELF']);
|
||||
foreach (UserFormList::getAll() as $formset) {
|
||||
$formset_class = UserFormList::get($formset);
|
||||
|
||||
51
prefs_second.php
Normal file
51
prefs_second.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* User preferences management page
|
||||
*
|
||||
* @package PhpMyAdmin
|
||||
*/
|
||||
use PhpMyAdmin\Message;
|
||||
use PhpMyAdmin\SecondFactor;
|
||||
use PhpMyAdmin\Template;
|
||||
|
||||
/**
|
||||
* Gets some core libraries and displays a top message if required
|
||||
*/
|
||||
require_once 'libraries/common.inc.php';
|
||||
|
||||
require 'libraries/user_preferences.inc.php';
|
||||
|
||||
$second_factor = new SecondFactor($GLOBALS['cfg']['Server']['user']);
|
||||
|
||||
if (isset($_POST['2fa_remove'])) {
|
||||
if (! $second_factor->check(true)) {
|
||||
echo Template::get('prefs_second_confirm')->render([
|
||||
'form' => $second_factor->render(),
|
||||
]);
|
||||
exit;
|
||||
} else {
|
||||
$second_factor->configure('');
|
||||
Message::rawNotice(__('Two-factor authentication has been removed.'))->display();
|
||||
}
|
||||
} elseif (isset($_POST['2fa_configure'])) {
|
||||
if (! $second_factor->configure($_POST['2fa_configure'])) {
|
||||
echo Template::get('prefs_second_configure')->render([
|
||||
'form' => $second_factor->setup(),
|
||||
'configure' => $_POST['2fa_configure'],
|
||||
]);
|
||||
exit;
|
||||
} else {
|
||||
Message::rawNotice(__('Two-factor authentication has been configured.'))->display();
|
||||
}
|
||||
}
|
||||
|
||||
$backend = $second_factor->backend;
|
||||
echo Template::get('prefs_second')->render([
|
||||
'enabled' => $second_factor->writable,
|
||||
'num_backends' => count($second_factor->available),
|
||||
'backend_id' => $backend::$id,
|
||||
'backend_name' => $backend::getName(),
|
||||
'backend_description' => $backend::getDescription(),
|
||||
'backends' => $second_factor->getAllBackends(),
|
||||
]);
|
||||
@ -228,7 +228,7 @@ if [ ! -d libraries/tcpdf ] ; then
|
||||
# suggested package. Let's require it and then revert
|
||||
# composer.json to original state.
|
||||
cp composer.json composer.json.backup
|
||||
composer require --update-no-dev tecnickcom/tcpdf
|
||||
composer require --update-no-dev tecnickcom/tcpdf pragmarx/google2fa bacon/bacon-qr-code samyoul/u2f-php-server
|
||||
mv composer.json.backup composer.json
|
||||
echo "* Cleanup of composer packages"
|
||||
rm -rf \
|
||||
|
||||
5
templates/login/second.twig
Normal file
5
templates/login/second.twig
Normal file
@ -0,0 +1,5 @@
|
||||
<form method="POST" class="disableAjax">
|
||||
{{ Url_getHiddenInputs() }}
|
||||
{{ form|raw }}
|
||||
<input type="submit" value="{% trans "Verify" %}" />
|
||||
</form>
|
||||
4
templates/login/second/application.twig
Normal file
4
templates/login/second/application.twig
Normal file
@ -0,0 +1,4 @@
|
||||
<p>
|
||||
<label>{% trans "Authentication code:" %} <input type="text" name="2fa_code" /></label>
|
||||
</p>
|
||||
<p>{% trans "Open the two-factor authentication app on your device to view your authentication code and verify your identity." %}</p>
|
||||
10
templates/login/second/application_configure.twig
Normal file
10
templates/login/second/application_configure.twig
Normal file
@ -0,0 +1,10 @@
|
||||
{{ Url_getHiddenInputs() }}
|
||||
<p>
|
||||
{% trans "Please scan following QR code into the two-factor authentication app on your device and enter authentication code it generates." %}
|
||||
</p>
|
||||
<p>
|
||||
<img src="{{ image }}" />
|
||||
</p>
|
||||
<p>
|
||||
<label>{% trans "Authentication code:" %} <input type="text" name="2fa_code" /></label>
|
||||
</p>
|
||||
4
templates/login/second/key.twig
Normal file
4
templates/login/second/key.twig
Normal file
@ -0,0 +1,4 @@
|
||||
<p>
|
||||
{% trans "Please connect your FIDO U2F device into your computer's USB port. Then confirm login on the device." %}
|
||||
</p>
|
||||
<input id="u2f_authentication_response" name="u2f_authentication_response" value="" type="hidden" data-request="{{ request }}"/>
|
||||
4
templates/login/second/key_configure.twig
Normal file
4
templates/login/second/key_configure.twig
Normal file
@ -0,0 +1,4 @@
|
||||
<p>
|
||||
{% trans "Please connect your FIDO U2F device into your computer's USB port. Then confirm registration on the device." %}
|
||||
</p>
|
||||
<input id="u2f_registration_response" name="u2f_registration_response" value="" type="hidden" data-request="{{ request }}" data-signatures="{{ signatures }}"/>
|
||||
1
templates/login/second/simple.twig
Normal file
1
templates/login/second/simple.twig
Normal file
@ -0,0 +1 @@
|
||||
<input type="hidden" name="2fa_confirm" value="1" />
|
||||
53
templates/prefs_second.twig
Normal file
53
templates/prefs_second.twig
Normal file
@ -0,0 +1,53 @@
|
||||
<div class="group">
|
||||
<h2>
|
||||
{% trans "Two-factor authentication status" %}
|
||||
{{ Util_showDocu('second_factor') }}
|
||||
</h2>
|
||||
<div class="group-cnt">
|
||||
{% if enabled %}
|
||||
{% if num_backends == 0 %}
|
||||
<p>{% trans "Two-factor authentication is not available, please install optional dependencies to enable authentication backends." %}</p>
|
||||
{% else %}
|
||||
{% if backend_id %}
|
||||
<p>{% trans "Two-factor authentication is available and configured for this account." %}</p>
|
||||
{% else %}
|
||||
<p>{% trans "Two-factor authentication is available, but not configured for this account." %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>{% trans "Two-factor authentication is not available, enable phpMyAdmin configuration storage to use it." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if backend_id %}
|
||||
<div class="group">
|
||||
<h2>{{ backend_name }}</h2>
|
||||
<div class="group-cnt">
|
||||
<p>{% trans "You have enabled two factor authentication." %}</p>
|
||||
<p>{{ backend_description }}</p>
|
||||
<form method="POST" action="prefs_second.php">
|
||||
{{ Url_getHiddenInputs() }}
|
||||
<input type="submit" name="2fa_remove" value="{% trans "Disable two-factor authentication" %}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="group">
|
||||
<h2>{% trans "Configure two-factor authentication" %}</h2>
|
||||
<div class="group-cnt">
|
||||
<form method="POST" action="prefs_second.php">
|
||||
{{ Url_getHiddenInputs() }}
|
||||
<option name="2fa_configure">
|
||||
{% for backend in backends %}
|
||||
<label>
|
||||
<input type="radio" name="2fa_configure" {% if backend["id"] == "" %}checked="checked"{% endif %} value="{{ backend["id"] }}"/>
|
||||
<strong>{{ backend["name"] }}</strong>
|
||||
<p>{{ backend["description"] }}</p>
|
||||
</label>
|
||||
{% endfor %}
|
||||
<input type="submit" value="{% trans "Configure two-factor authentication" %}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
13
templates/prefs_second_configure.twig
Normal file
13
templates/prefs_second_configure.twig
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="group">
|
||||
<h2>{% trans "Configure two-factor authentication" %}</h2>
|
||||
<div class="group-cnt">
|
||||
<form method="POST" action="prefs_second.php">
|
||||
{{ Url_getHiddenInputs() }}
|
||||
<input type="hidden" name="2fa_configure" value="{{ configure }}" />
|
||||
{{ form|raw }}
|
||||
<input type="submit" value="{% trans "Enable two-factor authentication" %}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
12
templates/prefs_second_confirm.twig
Normal file
12
templates/prefs_second_confirm.twig
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="group">
|
||||
<h2>{% trans "Confirm disabling two-factor authentication" %}</h2>
|
||||
<div class="group-cnt">
|
||||
<form method="POST" action="prefs_second.php">
|
||||
{{ Message_notice("By disabling two factor authentication you will be again able to login using password only."|trans) }}
|
||||
{{ Url_getHiddenInputs() }}
|
||||
{{ form|raw }}
|
||||
<input type="hidden" name="2fa_remove" value="1" />
|
||||
<input type="submit" value="{% trans "Disable two-factor authentication" %}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -2,3 +2,5 @@
|
||||
|
||||
./test/install-runkit
|
||||
composer install --no-interaction
|
||||
# Install optional deps
|
||||
composer require tecnickcom/tcpdf pragmarx/google2fa bacon/bacon-qr-code samyoul/u2f-php-server
|
||||
|
||||
256
test/classes/SecondFactorTest.php
Normal file
256
test/classes/SecondFactorTest.php
Normal file
@ -0,0 +1,256 @@
|
||||
<?php
|
||||
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
||||
/**
|
||||
* tests for SecondFactor class
|
||||
*
|
||||
* @package PhpMyAdmin-test
|
||||
*/
|
||||
namespace PhpMyAdmin\Tests;
|
||||
|
||||
use PhpMyAdmin\SecondFactor;
|
||||
use Samyoul\U2F\U2FServer\RegistrationRequest;
|
||||
use Samyoul\U2F\U2FServer\SignRequest;
|
||||
|
||||
/**
|
||||
* Tests behaviour of SecondFactor class
|
||||
*
|
||||
* @package PhpMyAdmin-test
|
||||
*/
|
||||
class SecondFactorTest extends PmaTestCase
|
||||
{
|
||||
public function setUp()
|
||||
{
|
||||
$GLOBALS['server'] = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates SecondFactor mock with custom configuration
|
||||
*
|
||||
* @param string $user Username
|
||||
* @param array $config Second factor authentication configuraiton
|
||||
*
|
||||
* @return SecondFactor
|
||||
*/
|
||||
public function getSecondFactorMock($user, $config)
|
||||
{
|
||||
if (! isset($config['backend'])) {
|
||||
$config['backend'] = '';
|
||||
}
|
||||
if (! isset($config['settings'])) {
|
||||
$config['settings'] = [];
|
||||
}
|
||||
$result = $this->getMockbuilder('PhpMyAdmin\SecondFactor')
|
||||
->setMethods(['readConfig'])
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$result->method('readConfig')->willReturn($config);
|
||||
$result->__construct($user);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function testNone()
|
||||
{
|
||||
$object = $this->getSecondFactorMock('user', ['type' => 'db']);
|
||||
$backend = $object->backend;
|
||||
$this->assertEquals('', $backend::$id);
|
||||
// Is always valid
|
||||
$this->assertTrue($object->check(true));
|
||||
// Test session persistence
|
||||
$this->assertTrue($object->check());
|
||||
$this->assertTrue($object->check());
|
||||
$this->assertEquals('', $object->render());
|
||||
$this->assertTrue($object->configure(''));
|
||||
$this->assertEquals('', $object->setup());
|
||||
}
|
||||
|
||||
public function testSimple()
|
||||
{
|
||||
$GLOBALS['cfg']['DBG']['simple2fa'] = true;
|
||||
$object = $this->getSecondFactorMock('user', ['type' => 'db', 'backend' => 'simple']);
|
||||
$backend = $object->backend;
|
||||
$this->assertEquals('simple', $backend::$id);
|
||||
$GLOBALS['cfg']['DBG']['simple2fa'] = false;
|
||||
|
||||
unset($_POST['2fa_confirm']);
|
||||
$this->assertFalse($object->check(true));
|
||||
|
||||
$_POST['2fa_confirm'] = 1;
|
||||
$this->assertTrue($object->check(true));
|
||||
unset($_POST['2fa_confirm']);
|
||||
|
||||
/* Test rendering */
|
||||
$this->assertNotEquals('', $object->render());
|
||||
$this->assertEquals('', $object->setup());
|
||||
}
|
||||
|
||||
public function testLoad()
|
||||
{
|
||||
$object = new SecondFactor('user');
|
||||
$backend = $object->backend;
|
||||
$this->assertEquals('', $backend::$id);
|
||||
}
|
||||
|
||||
public function testConfigureSimple()
|
||||
{
|
||||
$GLOBALS['cfg']['DBG']['simple2fa'] = true;
|
||||
$object = new SecondFactor('user');
|
||||
$this->assertTrue($object->configure('simple'));
|
||||
$backend = $object->backend;
|
||||
$this->assertEquals('simple', $backend::$id);
|
||||
$this->assertTrue($object->configure(''));
|
||||
$backend = $object->backend;
|
||||
$this->assertEquals('', $backend::$id);
|
||||
$GLOBALS['cfg']['DBG']['simple2fa'] = false;
|
||||
$object = new SecondFactor('user');
|
||||
$this->assertFalse($object->configure('simple'));
|
||||
}
|
||||
|
||||
public function testApplication()
|
||||
{
|
||||
$object = new SecondFactor('user');
|
||||
if (! in_array('application', $object->available)) {
|
||||
$this->markTestSkipped('google2fa not available');
|
||||
}
|
||||
/* Without providing code this should fail */
|
||||
unset($_POST['2fa_code']);
|
||||
$this->assertFalse($object->configure('application'));
|
||||
|
||||
/* Invalid code */
|
||||
$_POST['2fa_code'] = 'invalid';
|
||||
$this->assertFalse($object->configure('application'));
|
||||
|
||||
/* Generate valid code */
|
||||
$google2fa = $object->backend->google2fa;
|
||||
$_POST['2fa_code'] = $google2fa->oathHotp(
|
||||
$object->config['settings']['secret'],
|
||||
$google2fa->getTimestamp()
|
||||
);
|
||||
$this->assertTrue($object->configure('application'));
|
||||
unset($_POST['2fa_code']);
|
||||
|
||||
/* Check code */
|
||||
unset($_POST['2fa_code']);
|
||||
$this->assertFalse($object->check(true));
|
||||
$_POST['2fa_code'] = 'invalid';
|
||||
$this->assertFalse($object->check(true));
|
||||
$_POST['2fa_code'] = $google2fa->oathHotp(
|
||||
$object->config['settings']['secret'],
|
||||
$google2fa->getTimestamp()
|
||||
);
|
||||
$this->assertTrue($object->check(true));
|
||||
unset($_POST['2fa_code']);
|
||||
|
||||
/* Test rendering */
|
||||
$this->assertNotEquals('', $object->render());
|
||||
$this->assertNotEquals('', $object->setup());
|
||||
}
|
||||
|
||||
public function testKey()
|
||||
{
|
||||
$object = new SecondFactor('user');
|
||||
if (! in_array('key', $object->available)) {
|
||||
$this->markTestSkipped('u2f-php-server not available');
|
||||
}
|
||||
$_SESSION['registrationRequest'] = null;
|
||||
/* Without providing code this should fail */
|
||||
unset($_POST['u2f_registration_response']);
|
||||
$this->assertFalse($object->configure('key'));
|
||||
|
||||
/* Invalid code */
|
||||
$_POST['u2f_registration_response'] = 'invalid';
|
||||
$this->assertFalse($object->configure('key'));
|
||||
|
||||
/* Invalid code */
|
||||
$_POST['u2f_registration_response'] = '[]';
|
||||
$this->assertFalse($object->configure('key'));
|
||||
|
||||
/* Without providing code this should fail */
|
||||
unset($_POST['u2f_authentication_response']);
|
||||
$this->assertFalse($object->check(true));
|
||||
|
||||
/* Invalid code */
|
||||
$_POST['u2f_authentication_response'] = 'invalid';
|
||||
$this->assertFalse($object->check(true));
|
||||
|
||||
/* Invalid code */
|
||||
$_POST['u2f_authentication_response'] = '[]';
|
||||
$this->assertFalse($object->check(true));
|
||||
|
||||
/* Test rendering */
|
||||
$this->assertNotEquals('', $object->render());
|
||||
$this->assertNotEquals('', $object->setup());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting AppId
|
||||
*/
|
||||
public function testKeyAppId()
|
||||
{
|
||||
$object = new SecondFactor('user');
|
||||
$GLOBALS['PMA_Config']->set('PmaAbsoluteUri', 'http://demo.example.com');
|
||||
$this->assertEquals('http://demo.example.com', $object->backend->getAppId(true));
|
||||
$this->assertEquals('demo.example.com', $object->backend->getAppId(false));
|
||||
$GLOBALS['PMA_Config']->set('PmaAbsoluteUri', 'https://demo.example.com:123');
|
||||
$this->assertEquals('https://demo.example.com:123', $object->backend->getAppId(true));
|
||||
$this->assertEquals('demo.example.com', $object->backend->getAppId(false));
|
||||
$GLOBALS['PMA_Config']->set('PmaAbsoluteUri', '');
|
||||
$GLOBALS['PMA_Config']->set('is_https', true);
|
||||
$_SERVER['HTTP_HOST'] = 'pma.example.com';
|
||||
$this->assertEquals('https://pma.example.com', $object->backend->getAppId(true));
|
||||
$this->assertEquals('pma.example.com', $object->backend->getAppId(false));
|
||||
$GLOBALS['PMA_Config']->set('is_https', false);
|
||||
$this->assertEquals('http://pma.example.com', $object->backend->getAppId(true));
|
||||
$this->assertEquals('pma.example.com', $object->backend->getAppId(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test based on upstream test data:
|
||||
* https://github.com/Yubico/php-u2flib-server
|
||||
*/
|
||||
public function testKeyAuthentication()
|
||||
{
|
||||
$object = new SecondFactor('user');
|
||||
if (! in_array('key', $object->available)) {
|
||||
$this->markTestSkipped('u2f-php-server not available');
|
||||
}
|
||||
$_SESSION['registrationRequest'] = new RegistrationRequest('yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8', 'http://demo.example.com');
|
||||
unset($_POST['u2f_registration_response']);
|
||||
$this->assertFalse($object->configure('key'));
|
||||
|
||||
$_POST['u2f_registration_response'] = '';
|
||||
$this->assertFalse($object->configure('key'));
|
||||
|
||||
$_POST['u2f_registration_response'] = '{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9", "errorCode": 0 }';
|
||||
$this->assertTrue($object->configure('key'));
|
||||
|
||||
unset($_POST['u2f_authentication_response']);
|
||||
$this->assertFalse($object->check(true));
|
||||
|
||||
$_POST['u2f_authentication_response'] = '';
|
||||
$this->assertFalse($object->check(true));
|
||||
|
||||
$_SESSION['authenticationRequest'] = [new SignRequest([
|
||||
'challenge' => 'fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g',
|
||||
'keyHandle' => 'CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w',
|
||||
'appId' => 'http://demo.example.com'
|
||||
])];
|
||||
$this->assertFalse($object->check(true));
|
||||
$_POST['u2f_authentication_response'] = '{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w", "errorCode": 0 }';
|
||||
$this->assertTrue($object->check(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test listing of available backends.
|
||||
*/
|
||||
public function testBackends()
|
||||
{
|
||||
$GLOBALS['cfg']['DBG']['simple2fa'] = true;
|
||||
$object = new SecondFactor('user');
|
||||
$backends = $object->getAllBackends();
|
||||
$this->assertEquals(
|
||||
count($object->available) + 1,
|
||||
count($backends)
|
||||
);
|
||||
$GLOBALS['cfg']['DBG']['simple2fa'] = false;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user