Passed
Pull Request — master (#5)
by Thijs
02:12
created

Radius::getAttributes()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 25
c 2
b 0
f 0
nc 6
nop 1
dl 0
loc 45
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\radius\Auth\Source;
6
7
use Exception;
8
use Dapphp\Radius\Radius as RadiusClient;
9
use SimpleSAML\Assert\Assert;
10
use SimpleSAML\Configuration;
11
use SimpleSAML\Logger;
12
use SimpleSAML\Module\core\Auth\UserPassBase;
13
use SimpleSAML\Utils;
14
15
use function array_key_exists;
16
use function array_merge;
17
use function is_array;
18
use function sprintf;
19
use function strtok;
20
use function var_export;
21
22
/**
23
 * RADIUS authentication source.
24
 *
25
 * This class is based on www/auth/login-radius.php.
26
 *
27
 * @package SimpleSAMLphp
28
 */
29
class Radius extends UserPassBase
30
{
31
    public const RADIUS_USERNAME = 1;
32
    public const RADIUS_VENDOR_SPECIFIC = 26;
33
    public const RADIUS_NAS_IDENTIFIER = 32;
34
35
    /**
36
     * @var array The list of radius servers to use.
37
     */
38
    private array $servers;
39
40
    /**
41
     * @var string The hostname of the radius server.
42
     */
43
    private string $hostname;
44
45
    /**
46
     * @var int The port of the radius server.
47
     */
48
    private int $port;
49
50
    /**
51
     * @var string The secret used when communicating with the radius server.
52
     */
53
    private string $secret;
54
55
    /**
56
     * @var int The timeout for contacting the radius server.
57
     */
58
    private int $timeout;
59
60
    /**
61
     * @var string|null The realm to be added to the entered username.
62
     */
63
    private ?string $realm;
64
65
    /**
66
     * @var string|null The attribute name where the username should be stored.
67
     */
68
    private ?string $usernameAttribute = null;
69
70
    /**
71
     * @var int|null The vendor for the RADIUS attributes we are interrested in.
72
     */
73
    private ?int $vendor = null;
74
75
    /**
76
     * @var int The vendor-specific attribute for the RADIUS attributes we are
77
     *     interrested in.
78
     */
79
    private int $vendorType;
80
81
    /**
82
     * @var string|null The NAS-Identifier that should be set in Access-Request packets.
83
     */
84
    private ?string $nasIdentifier = null;
85
86
    /**
87
     * @var bool Debug modus
88
     */
89
    private bool $debug;
90
91
92
    /**
93
     * Constructor for this authentication source.
94
     *
95
     * @param array $info  Information about this authentication source.
96
     * @param array $config  Configuration.
97
     */
98
    public function __construct(array $info, array $config)
99
    {
100
        // Call the parent constructor first, as required by the interface
101
        parent::__construct($info, $config);
102
103
        // Parse configuration.
104
        $cfg = Configuration::loadFromArray(
105
            $config,
106
            'Authentication source ' . var_export($this->authId, true)
107
        );
108
109
        $this->servers = $cfg->getArray('servers');
110
        // For backwards compatibility
111
        if (empty($this->servers)) {
112
            $this->hostname = $cfg->getString('hostname');
113
            $this->port = $cfg->getOptionalIntegerRange('port', 1, 65535, 1812);
114
            $this->secret = $cfg->getString('secret');
115
            $this->servers[] = [
116
                'hostname' => $this->hostname,
117
                'port' => $this->port,
118
                'secret' => $this->secret
119
            ];
120
        }
121
        $this->debug = $cfg->getOptionalBoolean('debug', false);
122
        $this->timeout = $cfg->getOptionalInteger('timeout', 5);
123
        $this->realm = $cfg->getOptionalString('realm', null);
124
        $this->usernameAttribute = $cfg->getOptionalString('username_attribute', null);
125
        $this->nasIdentifier = $cfg->getOptionalString('nas_identifier', null);
126
127
        $this->vendor = $cfg->getOptionalInteger('attribute_vendor', null);
128
        if ($this->vendor !== null) {
129
            $this->vendorType = $cfg->getInteger('attribute_vendor_type');
130
        }
131
    }
132
133
134
    /**
135
     * Attempt to log in using the given username and password.
136
     *
137
     * @param string $username  The username the user wrote.
138
     * @param string $password  The password the user wrote.
139
     * @return array[] Associative array with the user's attributes.
140
     */
141
    protected function login(string $username, string $password): array
142
    {
143
        $radius = new RadiusClient();
144
        $response = false;
145
146
        // Try to add all radius servers, trigger a failure if no one works
147
        foreach ($this->servers as $server) {
148
            $radius->setServer($server['hostname']);
149
            $radius->setAuthenticationPort($server['port']);
150
            $radius->setSecret($server['secret']);
151
            $radius->setDebug($this->debug);
152
            $radius->setTimeout($this->timeout);
153
154
            $httpUtils = new Utils\HTTP();
155
            $radius->setNasIpAddress($_SERVER['SERVER_ADDR'] ?: $httpUtils->getSelfHost());
156
157
            if ($this->nasIdentifier !== null) {
158
                $radius->setAttribute(self::RADIUS_NAS_IDENTIFIER, $this->nasIdentifier);
159
            }
160
161
            if ($this->realm !== null) {
162
                $radius->setRadiusSuffix('@' . $this->realm);
163
            }
164
            $response = $radius->accessRequest($username, $password);
165
166
            if ($response !== false) {
167
                break;
168
            }
169
        }
170
171
        if ($response === false) {
172
            $errorCode = $radius->getErrorCode();
173
            switch($errorCode) {
174
                case $radius::TYPE_ACCESS_REJECT:
175
                    throw new \SimpleSAML\Error\Error('WRONGUSERPASS');
176
                case $radius::TYPE_ACCESS_CHALLENGE:
177
                    throw new Exception('Radius authentication error: Challenge requested, but not supported.');
178
                default:
179
                    throw new Exception(sprintf(
180
                        'Error during radius authentication; %s (%d)',
181
                            $radius->getErrorMessage(),
182
                            $errorCode
183
                    ));
184
            }
185
        }
186
187
        // If we get this far, we have a valid login
188
189
        $attributes = [];
190
        if ($this->usernameAttribute !== null) {
191
            $attributes[$this->usernameAttribute] = [$username];
192
        }
193
194
        if ($this->vendor === null) {
195
            /*
196
             * We aren't interested in any vendor-specific attributes. We are
197
             * therefore done now.
198
             */
199
            return $attributes;
200
        }
201
202
        return array_merge($attributes, $this->getAttributes($radius));
203
    }
204
205
206
    /**
207
     * @param \Dapphp\Radius\Radius $radius
208
     * @return array
209
     */
210
    private function getAttributes(RadiusClient $radius): array
211
    {
212
        // get AAI attribute sets.
213
        $resa = $radius->getReceivedAttributes();
214
        $attributes = [];
215
216
        // Use the received user name
217
        if ($resa['attr'] === self::RADIUS_USERNAME && $this->usernameAttribute !== null) {
218
            $attributes[$this->usernameAttribute] = [$resa['data']];
219
            return $attributes;
220
        }
221
222
        if ($resa['attr'] !== self::RADIUS_VENDOR_SPECIFIC) {
223
            return $attributes;
224
        }
225
226
        $resv = $resa['data'];
227
        if ($resv === false) {
228
            throw new Exception(sprintf(
229
                'Error getting vendor specific attribute',
230
                $radius->getErrorMessage(),
231
                $radius->getErrorCode()
232
            ));
233
        }
234
235
        $vendor = $resv['vendor'];
236
        $attrv = $resv['attr'];
237
        $datav = $resv['data'];
238
239
        if ($vendor !== $this->vendor || $attrv !== $this->vendorType) {
240
            return $attributes;
241
        }
242
243
        $attrib_name = strtok($datav, '=');
244
        /** @psalm-suppress TooFewArguments */
245
        $attrib_value = strtok('=');
246
247
        // if the attribute name is already in result set, add another value
248
        if (array_key_exists($attrib_name, $attributes)) {
249
            $attributes[$attrib_name][] = $attrib_value;
250
        } else {
251
            $attributes[$attrib_name] = [$attrib_value];
252
        }
253
254
        return $attributes;
255
    }
256
}
257