Passed
Pull Request — master (#2)
by Tim
02:52
created

YubiKey::authenticate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SimpleSAML\Module\authYubikey\Auth\Source;
4
5
use Exception;
6
use GuzzleHttp\Client as GuzzleClient;
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\Auth;
9
use SimpleSAML\Error;
10
use SimpleSAML\Logger;
11
use SimpleSAML\Module;
12
use SimpleSAML\Utils;
13
use Surfnet\YubikeyApiClient\Crypto\RandomNonceGenerator;
14
use Surfnet\YubikeyApiClient\Crypto\Signer;
15
use Surfnet\YubikeyApiClient\Http\ServerPoolClient;
16
use Surfnet\YubikeyApiClient\Otp;
17
use Surfnet\YubikeyApiClient\Service\OtpVerificationResult;
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 $yubi_id;
60
61
    /** @var string */
62
    private $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
        if (array_key_exists('id', $config)) {
77
            $this->yubi_id = $config['id'];
78
        }
79
80
        if (array_key_exists('key', $config)) {
81
            $this->yubi_key = $config['key'];
82
        }
83
    }
84
85
86
    /**
87
     * Initialize login.
88
     *
89
     * This function saves the information about the login, and redirects to a
90
     * login page.
91
     *
92
     * @param array &$state  Information about the current authentication.
93
     */
94
    public function authenticate(array &$state): void
95
    {
96
        // We are going to need the authId in order to retrieve this authentication source later
97
        $state[self::AUTHID] = $this->authId;
98
99
        $id = Auth\State::saveState($state, self::STAGEID);
100
        $url = Module::getModuleURL('authYubikey/login');
101
        $httpUtils = new Utils\HTTP();
102
        $httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
103
    }
104
105
106
    /**
107
     * Handle login request.
108
     *
109
     * This function is used by the login form (core/www/loginuserpass.php) when the user
110
     * enters a username and password. On success, it will not return. On wrong
111
     * username/password failure, it will return the error code. Other failures will throw an
112
     * exception.
113
     *
114
     * @param string $authStateId  The identifier of the authentication state.
115
     * @param string $otp  The one time password entered-
116
     * @return string|void Error code in the case of an error.
117
     */
118
    public static function handleLogin(string $authStateId, string $otp)
119
    {
120
        /* Retrieve the authentication state. */
121
        $state = Auth\State::loadState($authStateId, self::STAGEID);
122
        if (is_null($state)) {
123
            throw new Error\NoState();
124
        }
125
126
        /* Find authentication source. */
127
        Assert::keyExists($state, self::AUTHID);
128
129
        $source = Auth\Source::getById($state[self::AUTHID]);
130
        Assert::isInstanceOf(
131
            $source,
132
            YubiKey::class,
133
            'Could not find authentication source with id ' . $state[self::AUTHID]
134
        );
135
136
        try {
137
            /**
138
             * Attempt to log in.
139
             *
140
             * @var \SimpleSAML\Module\authYubikey\Auth\Source\YubiKey $source
141
             */
142
            $attributes = $source->login($otp);
143
        } catch (Error\Error $e) {
144
            /* An error occurred during login. Check if it is because of the wrong
145
             * username/password - if it is, we pass that error up to the login form,
146
             * if not, we let the generic error handler deal with it.
147
             */
148
            if ($e->getErrorCode() === 'WRONGUSERPASS') {
149
                return 'WRONGUSERPASS';
150
            }
151
152
            /* Some other error occurred. Rethrow exception and let the generic error
153
             * handler deal with it.
154
             */
155
            throw $e;
156
        }
157
158
        $state['Attributes'] = $attributes;
159
        Auth\Source::completeAuth($state);
160
161
        assert(false);
162
    }
163
164
165
    /**
166
     * Return the user id part of a one time passord
167
     *
168
     * @param string $otp
169
     * @return string
170
     */
171
    public static function getYubiKeyPrefix(string $otp): string
172
    {
173
        $uid = substr($otp, 0, strlen($otp) - self::TOKENSIZE);
174
        return $uid;
175
    }
176
177
178
    /**
179
     * Attempt to log in using the given username and password.
180
     *
181
     * On a successful login, this function should return the users attributes. On failure,
182
     * it should throw an exception. If the error was caused by the user entering the wrong
183
     * username or password, a \SimpleSAML\Error\Error('WRONGUSERPASS') should be thrown.
184
     *
185
     * Note that both the username and the password are UTF-8 encoded.
186
     *
187
     * @param string $otp
188
     * @return array Associative array with the users attributes.
189
     */
190
    protected function login(string $userInputOtp): array
191
    {
192
        $service = new VerificationService(
193
            new ServerPoolClient(new GuzzleClient()),
194
            new RandomNonceGenerator(),
195
            new Signer($this->yubi_key),
196
            $this->yubi_id
197
        );
198
199
        if (!Otp::isValid($userInputOtp)) {
200
            throw new Error\Exception('User-entered OTP string is not valid.');
201
        }
202
203
        $otp = Otp::fromString($userInputOtp);
204
        $result = $service->verify($otp);
205
206
        if ($result->isSuccessful()) {
207
            // Yubico verified OTP.
208
209
            Logger::info(sprintf(
210
                'YubiKey:%s: YubiKey otp %s validated successfully: %s',
211
                $this->authId,
212
                $userInputOtp,
213
                $result::STATUS_OK
214
            ));
215
216
            $uid = self::getYubiKeyPrefix($userInputOtp);
217
            return ['uid' => [$uid]];
218
        }
219
220
        throw new Error\Error($result->getError());
221
    }
222
}
223