YubiKey   A
last analyzed

Complexity

Total Complexity 10

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 54
c 2
b 0
f 0
dl 0
loc 185
rs 10
wmc 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A authenticate() 0 9 1
A getYubiKeyPrefix() 0 4 1
A handleLogin() 0 47 4
A login() 0 31 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\authYubiKey\Auth\Source;
6
7
use GuzzleHttp\Client as GuzzleClient;
8
use SimpleSAML\Assert\Assert;
9
use SimpleSAML\Auth;
10
use SimpleSAML\Error;
11
use SimpleSAML\Logger;
12
use SimpleSAML\Module;
13
use SimpleSAML\Utils;
14
use Surfnet\YubikeyApiClient\Crypto\RandomNonceGenerator;
15
use Surfnet\YubikeyApiClient\Crypto\Signer;
16
use Surfnet\YubikeyApiClient\Http\ServerPoolClient;
17
use Surfnet\YubikeyApiClient\Otp;
18
use Surfnet\YubikeyApiClient\Service\VerificationService;
19
20
/**
21
 * YubiKey authentication module, see http://www.yubico.com/developers/intro/
22
 *
23
 * Configure it by adding an entry to config/authsources.php such as this:
24
 *
25
 *    'yubikey' => [
26
 *        'authYubiKey:YubiKey',
27
 *        'id' => 997,
28
 *        'key' => 'b64hmackey',
29
 *    ],
30
 *
31
 * To generate your own client id/key you will need one YubiKey, and then
32
 * go to http://yubico.com/developers/api/
33
 *
34
 * @package simplesamlphp/simplesamlphp-module-authyubikey
35
 */
36
37
class YubiKey extends Auth\Source
38
{
39
    /**
40
     * The string used to identify our states.
41
     */
42
    public const STAGEID = '\SimpleSAML\Module\authYubiKey\Auth\Source\YubiKey.state';
43
44
    /**
45
     * The number of characters of the OTP that is the secure token.
46
     * The rest is the user id.
47
     */
48
    public const TOKENSIZE = 32;
49
50
    /**
51
     * The key of the AuthId field in the state.
52
     */
53
    public const AUTHID = '\SimpleSAML\Module\authYubiKey\Auth\Source\YubiKey.AuthId';
54
55
    /**
56
     * The client id/key for use with the Auth_Yubico PHP module.
57
     * @var string
58
     */
59
    private string $yubi_id;
60
61
    /** @var string */
62
    private string $yubi_key;
63
64
65
    /**
66
     * Constructor for this authentication source.
67
     *
68
     * @param array $info  Information about this authentication source.
69
     * @param array $config  Configuration.
70
     */
71
    public function __construct(array $info, array $config)
72
    {
73
        // Call the parent constructor first, as required by the interface
74
        parent::__construct($info, $config);
75
76
        Assert::keyExists($config, 'id', Error\ConfigurationError::class);
77
        Assert::keyExists($config, 'key', Error\ConfigurationError::class);
78
79
        $this->yubi_id = $config['id'];
80
        $this->yubi_key = $config['key'];
81
    }
82
83
84
    /**
85
     * Initialize login.
86
     *
87
     * This function saves the information about the login, and redirects to a
88
     * login page.
89
     *
90
     * @param array &$state  Information about the current authentication.
91
     */
92
    public function authenticate(array &$state): void
93
    {
94
        // We are going to need the authId in order to retrieve this authentication source later
95
        $state[self::AUTHID] = $this->authId;
96
97
        $id = Auth\State::saveState($state, self::STAGEID);
98
        $url = Module::getModuleURL('authYubiKey/login');
99
        $httpUtils = new Utils\HTTP();
100
        $httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
101
    }
102
103
104
    /**
105
     * Handle login request.
106
     *
107
     * This function is used by the login form (core/www/loginuserpass.php) when the user
108
     * enters a username and password. On success, it will not return. On wrong
109
     * username/password failure, it will return the error code. Other failures will throw an
110
     * exception.
111
     *
112
     * @param string $authStateId  The identifier of the authentication state.
113
     * @param string $otp  The one time password entered-
114
     * @return string|void Error code in the case of an error.
115
     */
116
    public static function handleLogin(
117
        string $authStateId,
118
        #[\SensitiveParameter]
119
        string $otp,
120
    ) {
121
        /* Retrieve the authentication state. */
122
        $state = Auth\State::loadState($authStateId, self::STAGEID);
123
        if (is_null($state)) {
124
            throw new Error\NoState();
125
        }
126
127
        /* Find authentication source. */
128
        Assert::keyExists($state, self::AUTHID);
129
130
        $source = Auth\Source::getById($state[self::AUTHID]);
131
        Assert::isInstanceOf(
132
            $source,
133
            YubiKey::class,
134
            'Could not find authentication source with id ' . $state[self::AUTHID],
135
        );
136
137
        try {
138
            /**
139
             * Attempt to log in.
140
             *
141
             * @var \SimpleSAML\Module\authYubiKey\Auth\Source\YubiKey $source
142
             */
143
            $attributes = $source->login($otp);
144
        } catch (Error\Error $e) {
145
            /* An error occurred during login. Check if it is because of the wrong
146
             * username/password - if it is, we pass that error up to the login form,
147
             * if not, we let the generic error handler deal with it.
148
             */
149
            if ($e->getErrorCode() === 'WRONGUSERPASS') {
150
                return 'WRONGUSERPASS';
151
            }
152
153
            /* Some other error occurred. Rethrow exception and let the generic error
154
             * handler deal with it.
155
             */
156
            throw $e;
157
        }
158
159
        $state['Attributes'] = $attributes;
160
        Auth\Source::completeAuth($state);
161
162
        assert(false);
163
    }
164
165
166
    /**
167
     * Return the user id part of a one time passord
168
     *
169
     * @param string $otp
170
     * @return string
171
     */
172
    public static function getYubiKeyPrefix(#[\SensitiveParameter] string $otp): string
173
    {
174
        $uid = substr($otp, 0, strlen($otp) - self::TOKENSIZE);
175
        return $uid;
176
    }
177
178
179
    /**
180
     * Attempt to log in using the given username and password.
181
     *
182
     * On a successful login, this function should return the users attributes. On failure,
183
     * it should throw an exception. If the error was caused by the user entering the wrong
184
     * username or password, a \SimpleSAML\Error\Error('WRONGUSERPASS') should be thrown.
185
     *
186
     * Note that both the username and the password are UTF-8 encoded.
187
     *
188
     * @param string $otp
189
     * @return array Associative array with the users attributes.
190
     */
191
    protected function login(#[\SensitiveParameter] string $userInputOtp): array
192
    {
193
        $service = new VerificationService(
194
            new ServerPoolClient(new GuzzleClient()),
195
            new RandomNonceGenerator(),
196
            new Signer($this->yubi_key),
197
            $this->yubi_id,
198
        );
199
200
        if (!Otp::isValid($userInputOtp)) {
201
            throw new Error\Exception('User-entered OTP string is not valid.');
202
        }
203
204
        $otp = Otp::fromString($userInputOtp);
205
        $result = $service->verify($otp);
206
207
        if ($result->isSuccessful()) {
208
            // Yubico verified OTP.
209
210
            Logger::info(sprintf(
211
                'YubiKey:%s: YubiKey otp %s validated successfully: %s',
212
                $this->authId,
213
                $userInputOtp,
214
                $result::STATUS_OK,
215
            ));
216
217
            $uid = self::getYubiKeyPrefix($userInputOtp);
218
            return ['uid' => [$uid]];
219
        }
220
221
        throw new Error\Error($result->getError());
222
    }
223
}
224