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