Radius::getAttributes()   B
last analyzed

Complexity

Conditions 8
Paths 6

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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