Completed
Pull Request — master (#461)
by Daniel
15:02
created

MultiFactorAuth::generateRecoveryCodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 0
cts 0
cp 0
rs 9.8666
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Idp\Hook;
13
14
use Balloon\App\Idp\Exception;
15
use Balloon\Hook\AbstractHook;
16
use Balloon\Server\RoleInterface;
17
use Balloon\Server\User;
18
use Dolondro\GoogleAuthenticator\GoogleAuthenticator;
19
use Dolondro\GoogleAuthenticator\Secret;
20
use Dolondro\GoogleAuthenticator\SecretFactory;
21
use Micro\Auth\Adapter\Basic\BasicInterface;
22
use Micro\Auth\Identity;
23
use Psr\Log\LoggerInterface;
24
25
class MultiFactorAuth extends AbstractHook
26
{
27
    /**
28
     * Issuer.
29
     */
30
    public const ISSUER = 'balloon';
31
32
    /**
33
     * Logger.
34
     *
35
     * @var LoggerInterface
36
     */
37
    protected $logger;
38
39
    /**
40
     * Secret factory.
41
     *
42
     * @var SecretFactory
43
     */
44
    protected $secret_factory;
45
46
    /**
47
     * Secret.
48
     *
49
     * @var Secret
50
     */
51
    protected $secret;
52
53
    /**
54
     * Google authenticator.
55
     *
56
     * @var GoogleAuthenticator
57
     */
58
    protected $authenticator;
59
60
    /**
61
     * Recovery codes.
62
     *
63
     * @var array
64
     */
65
    protected $recovery_codes = [];
66
67
    /**
68
     * Constructor.
69
     */
70
    public function __construct(LoggerInterface $logger, SecretFactory $secret_factory, GoogleAuthenticator $authenticator)
71
    {
72
        $this->logger = $logger;
73
        $this->secret_factory = $secret_factory;
74
        $this->authenticator = $authenticator;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function preUpdateUser(User $user, array &$attributes = []): void
81
    {
82
        $existing = $user->getAttributes();
83
84
        if (isset($attributes['multi_factor_auth'])) {
85
            if ($attributes['multi_factor_auth'] === true) {
86
                $attributes['multi_factor_auth'] = false;
87
88
                if (isset($attributes['multi_factor_validate']) && $this->authenticator->authenticate($existing['google_auth_secret'], $attributes['multi_factor_validate'])) {
89
                    $attributes['multi_factor_auth'] = true;
90
                    unset($attributes['multi_factor_validate']);
91
92
                    list($codes, $hash) = $this->generateRecoveryCodes();
93
                    $this->recovery_codes = $codes;
94
                    $attributes['multi_factor_recovery'] = $hash;
95
96
                    return;
97
                }
98
99
                if (!isset($attributes['multi_factor_validate'])) {
100
                    $secret = $this->secret_factory->create(self::ISSUER, $user->getUsername());
101
                    $attributes['google_auth_secret'] = $secret->getSecretKey();
102
                    $this->secret = $secret;
103
                }
104
105
                unset($attributes['multi_factor_validate']);
106
            } else {
107
                $attributes['google_auth_secret'] = null;
108
                $attributes['multi_factor_recovery'] = [];
109
            }
110
        }
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function preDecorateRole(RoleInterface $role, array &$attributes = []): void
117
    {
118
        if (!($role instanceof User)) {
119
            return;
120
        }
121
122
        $mfa = $role->getAttributes()['multi_factor_auth'];
123
        $attributes['multi_factor_auth'] = $mfa;
124
125
        if ($this->secret !== null) {
126
            $attributes['multi_factor_uri'] = $this->secret->getUri();
127
        }
128
129
        if ($this->recovery_codes != []) {
130
            $attributes['multi_factor_recovery'] = $this->recovery_codes;
131
        }
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function preServerIdentity(Identity $identity, ?User &$user): void
138
    {
139
        if (null === $user || (preg_match('#^/index.php/api/v2/tokens#', $_SERVER['ORIG_SCRIPT_NAME']) && isset($_POST['grant_type']) && (preg_match('#_mfa$#', $_POST['grant_type']) || $_POST['grant_type'] === 'refresh_token'))) {
140
            return;
141
        }
142
143
        if (($identity->getAdapter() instanceof BasicInterface || preg_match('#^/index.php/api/v2/tokens#', $_SERVER['ORIG_SCRIPT_NAME'])) && $user->getAttributes()['multi_factor_auth'] === true) {
144
            throw new Exception\MultiFactorAuthenticationRequired('multi-factor authentication required');
145
        }
146
147
        $this->logger->debug('multi-factor authentication is not required for user ['.$user->getId().']', [
148
            'category' => get_class($this),
149
        ]);
150
    }
151
152
    /**
153
     * Generate one time recovery codes.
154
     */
155
    protected function generateRecoveryCodes(): array
156
    {
157
        $list = [];
158
        $hash = [];
159
160
        for ($i = 0; $i < 10; ++$i) {
161
            $list[$i] = bin2hex(random_bytes(6));
162
            $hash[$i] = hash('sha512', (string) $list[$i]);
163
        }
164
165
        return [$list, $hash];
166
    }
167
}
168