3 * Two authentication factor handling
6 declare(strict_types
=1);
10 use BaconQrCode\Renderer\ImageRenderer
;
11 use CodeLts\U2F\U2FServer\U2FServer
;
12 use PhpMyAdmin\ConfigStorage\Relation
;
13 use PhpMyAdmin\Http\ServerRequest
;
14 use PhpMyAdmin\Plugins\TwoFactor\Application
;
15 use PhpMyAdmin\Plugins\TwoFactor\Invalid
;
16 use PhpMyAdmin\Plugins\TwoFactor\Key
;
17 use PhpMyAdmin\Plugins\TwoFactorPlugin
;
18 use PragmaRX\Google2FAQRCode\Google2FA
;
21 use function array_merge
;
22 use function class_exists
;
23 use function extension_loaded
;
24 use function in_array
;
25 use function is_array
;
27 use function is_string
;
31 * Two factor authentication wrapper class
37 * @psalm-var array{backend: string, settings: mixed[], type?: 'session'|'db'}
41 protected bool $writable;
43 protected TwoFactorPlugin
$backend;
46 protected array $available;
48 private UserPreferences
$userPreferences;
51 * Creates new TwoFactor object
53 * @param string $user User name
55 public function __construct(public string $user)
57 $dbi = DatabaseInterface
::getInstance();
59 $this->userPreferences
= new UserPreferences($dbi, new Relation($dbi), new Template());
60 $this->available
= $this->getAvailableBackends();
61 $this->config
= $this->readConfig();
62 $this->writable
= $this->config
['type'] === 'db';
63 $this->backend
= $this->getBackendForCurrentUser();
67 * Reads the configuration
69 * @psalm-return array{backend: string, settings: mixed[], type: 'session'|'db'}
71 public function readConfig(): array
74 $config = $this->userPreferences
->load();
75 if (isset($config['config_data']['2fa']) && is_array($config['config_data']['2fa'])) {
76 $result = $config['config_data']['2fa'];
80 if (isset($result['backend']) && is_string($result['backend'])) {
81 $backend = $result['backend'];
85 if (isset($result['settings']) && is_array($result['settings'])) {
86 $settings = $result['settings'];
89 return ['backend' => $backend, 'settings' => $settings, 'type' => $config['type']];
92 public function isWritable(): bool
94 return $this->writable
;
97 public function getBackend(): TwoFactorPlugin
99 return $this->backend
;
102 /** @return string[] */
103 public function getAvailable(): array
105 return $this->available
;
108 public function showSubmit(): bool
110 return $this->backend
::$showSubmit;
114 * Returns list of available backends
118 public function getAvailableBackends(): array
121 $config = Config
::getInstance();
122 if ($config->config
->debug
->simple2fa
) {
123 $result[] = 'simple';
127 class_exists(Google2FA
::class)
128 && class_exists(ImageRenderer
::class)
129 && (class_exists(XMLWriter
::class) ||
extension_loaded('imagick'))
131 $result[] = 'application';
134 $result[] = 'WebAuthn';
136 if (class_exists(U2FServer
::class)) {
144 * Returns list of missing dependencies
146 * @return array<int, array{class: string, dep: string}>
148 public function getMissingDeps(): array
151 if (! class_exists(Google2FA
::class)) {
152 $result[] = ['class' => Application
::getName(), 'dep' => 'pragmarx/google2fa-qrcode'];
155 if (! class_exists(ImageRenderer
::class)) {
156 $result[] = ['class' => Application
::getName(), 'dep' => 'bacon/bacon-qr-code'];
159 if (! class_exists(U2FServer
::class)) {
160 $result[] = ['class' => Key
::getName(), 'dep' => 'code-lts/u2f-php-server'];
167 * Returns class name for given name
169 * @param string $name Backend name
171 * @psalm-return class-string<TwoFactorPlugin>
173 public function getBackendClass(string $name): string
175 $result = TwoFactorPlugin
::class;
176 if (in_array($name, $this->available
, true)) {
177 /** @psalm-var class-string<TwoFactorPlugin> $result */
178 $result = 'PhpMyAdmin\\Plugins\\TwoFactor\\' . ucfirst($name);
179 } elseif ($name !== '') {
180 $result = Invalid
::class;
187 * Returns backend for current user
189 public function getBackendForCurrentUser(): TwoFactorPlugin
191 $name = $this->getBackendClass($this->config
['backend']);
193 return new $name($this);
197 * Checks authentication, returns true on success
199 * @param bool $skipSession Skip session cache
201 public function check(ServerRequest
$request, bool $skipSession = false): bool
204 return $this->backend
->check($request);
207 if (! isset($_SESSION['two_factor_check']) ||
! is_bool($_SESSION['two_factor_check'])) {
208 $_SESSION['two_factor_check'] = $this->backend
->check($request);
211 return $_SESSION['two_factor_check'];
215 * Renders user interface to enter two-factor authentication
217 * @return string HTML code
219 public function render(ServerRequest
$request): string
221 return $this->backend
->getError() . $this->backend
->render($request);
225 * Renders user interface to configure two-factor authentication
227 * @return string HTML code
229 public function setup(ServerRequest
$request): string
231 return $this->backend
->getError() . $this->backend
->setup($request);
235 * Saves current configuration.
237 * @return true|Message
239 public function save(): bool|Message
241 return $this->userPreferences
->persistOption('2fa', $this->config
, null);
245 * Changes two-factor authentication settings
247 * The object might stay in partially changed setup
248 * if configuration fails.
250 * @param string $name Backend name
252 public function configure(ServerRequest
$request, string $name): bool
254 $this->config
= ['backend' => $name, 'settings' => []];
256 $cls = $this->getBackendClass($name);
257 $this->backend
= new $cls($this);
259 if (! in_array($name, $this->available
, true)) {
263 $cls = $this->getBackendClass($name);
264 $this->backend
= new $cls($this);
265 if (! $this->backend
->configure($request)) {
270 $result = $this->save();
271 if ($result !== true) {
272 echo $result->getDisplay();
279 * Returns array with all available backends
281 * @return array<int, array{id: mixed, name: mixed, description: mixed}>
283 public function getAllBackends(): array
285 $all = array_merge([''], $this->available
);
287 foreach ($all as $name) {
288 $cls = $this->getBackendClass($name);
289 $backends[] = ['id' => $cls::$id, 'name' => $cls::getName(), 'description' => $cls::getDescription()];