**Summary**: Very Simple RADIUS Client

**Description**: Very Simple RADIUS Client that supports only PAP.

**License**: MIT

# Introduction

We wrote a very simple RADIUS client that supports only PAP for password 
authentication. We needed this in order to support RADIUS authentication with
various software projects written in PHP.

It aims at implementing just the part of the specification that is required in
order to make RADIUS authentication work for most use case.

We tested this library with:

* OpenBSD [radiusd](https://man.openbsd.org/radiusd.8) (see 
  [here](#openbsd-server-configuration) for how it was configured)
* [FreeRADIUS](https://freeradius.org/)
* [Radiator](https://radiatorsoftware.com/products/radiator/)

# Why

Due to lack of (proper) support for PHP 8 (and OpenSSL 3.0) we were not 
satisfied with the available options. We investigated:

* [php-pecl-radius](https://pecl.php.net/package/radius)
* [dapphp/radius](https://github.com/dapphp/radius)

We wanted something simpler that would just be used to verify credentials and
(optionally) return some attributes.

# Features

* Supports multiple RADIUS servers
* Always adds and verifies the 
  [Message-Authenticator](https://www.rfc-editor.org/rfc/rfc2869#section-5.14) 
  attribute
* Enforces use of `Message-Authenticator` by default to mitigate 
  [Blast-RADIUS](https://www.blastradius.fail/)
* Supports connections to RADIUS servers over IPv4 and IPv6
* Supports "Vendor-Specific" attributes
* Simple API
* Supports RADIUS Challenge/Response

# API

```php
<?php

require_once 'example/autoload.php';

use fkooman\Radius\Exception\AccessRejectException;
use fkooman\Radius\RadiusClient;
use fkooman\Radius\ServerInfo;

$userName = 'foo';
$userPass = 'bar';

try {
    $radiusClient = new RadiusClient();
    $radiusClient->addServer(new ServerInfo('udp://[fd43::5]:1812', 's3cr3t'));
    $radiusClient->addServer(new ServerInfo('udp://10.43.43.5:1812', 's3cr3t'));
    $accessResponse = $radiusClient->accessRequest($userName, $userPass);
    echo '==> ' . $accessResponse->packetType() . PHP_EOL;
    echo $accessResponse->attributeCollection() . PHP_EOL;
} catch (AccessRejectException $e) {
    $accessResponse = $e->radiusPacket();
    echo '==> ' . $accessResponse->packetType() . PHP_EOL;
    echo $accessResponse->attributeCollection() . PHP_EOL;

    exit(1);
} catch (Exception $e) {
    echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
    exit(1);
}
```

# Challenge/Response

The client also supports handling `Access-Challenge` responses from the RADIUS 
server. In that case, you MUST catch the `AccessChallengeException` and handle
the "challenge" step.

If you do not care about Challenge/Response you can ignore everything below 
as the `AccessChallengeException` extends the `AccessRejectException` which 
would then just result in a failed authentication if the server all of a 
sudden decided to implement a Challenge/Response mechanism.

The below is for a simple CLI client, which does not need to store the state 
anywhere. For applications where you need to (temporary) store the state, you
can use the `toData()` and `fromData()` methods of `RadiusPacket`. The data is
binary, so you MAY need to encode it using e.g. `bin2hex` before storing it. A
good place to store it would be the user's session data. When calling 
`RadiusClient::accessRequest` again, you MUST then provide the `RadiusPacket` 
as a parameter as well and use the reponse to the challenge as the 
"User-Password".

```php
require_once 'example/autoload.php';

use fkooman\Radius\Exception\AccessChallengeException;
use fkooman\Radius\Exception\AccessRejectException;
use fkooman\Radius\RadiusClient;
use fkooman\Radius\RadiusPacket;
use fkooman\Radius\ServerInfo;

$userName = 'foo';
$userPassword = 'bar';

function accessRequest(RadiusClient $radiusClient, string $userName, string $userPassword, ?RadiusPacket $accessChallenge = null): RadiusPacket
{
    try {
        return $radiusClient->accessRequest($userName, $userPassword, $accessChallenge);
    } catch (AccessChallengeException $e) {
        $challengeResponse = $e->radiusPacket();
        echo $challengeResponse->attributeCollection() . PHP_EOL;
        echo 'Answer to Challenge: ';
        $userPassword = trim(fgets(STDIN));

        return accessRequest($radiusClient, $userName, $userPassword, $challengeResponse);
    }
}

try {
    $radiusClient = new RadiusClient();
    $radiusClient->addServer(new ServerInfo('udp://[fd43::5]:1812', 's3cr3t'));
    $radiusClient->addServer(new ServerInfo('udp://10.43.43.5:1812', 's3cr3t'));
    $accessResponse = accessRequest($radiusClient, $userName, $userPassword);
    echo '==> ' . $accessResponse->packetType() . PHP_EOL;
    echo $accessResponse->attributeCollection() . PHP_EOL;
} catch (AccessRejectException $e) {
    $accessResponse = $e->radiusPacket();
    echo '==> ' . $accessResponse->packetType() . PHP_EOL;
    echo $accessResponse->attributeCollection() . PHP_EOL;

    exit(1);
} catch (Exception $e) {
    echo 'ERROR: ' . $e->getMessage() . PHP_EOL;
    exit(1);
}
```

# Logging

The `RadiusClient` class takes a second parameter with a `LoggerInterface` 
implementation (`src/LoggerInterface.php`). See the `ErrorLogger` class 
embedded in `example/client.php` to get an idea.

```php
$radiusClient = new fkooman\Radius\RadiusClient(
    new fkooman\Radius\ClientConfig('my-nas-id'), 
    new MyLogImplementation()
);
```

**NOTE**: your implementation MUST NOT write `LoggerInterface::debug` calls to 
the (normal) log as it contains the user's credentials!

You SHOULD use your own implementation that integrates with your own 
application's logging mechanism!

# Testing

If you'd like to test this library against your own RADIUS infrastructure, you
can using the included example client. Make sure you have PHP installed, more
specifically, that you can run `php` from the command line.

```bash
$ git clone https://codeberg.org/fkooman/php-radius
$ cd php-radius
```

You can run the `example/client.php` script to talk to your own RADIUS server. 
You can set the following environment variables if needed:

| Variable           | Description                                 | Default                |
| ------------------ | ------------------------------------------- | ---------------------- |
| `SERVER_URI`       | The URI for the server                      | `udp://127.0.0.1:1812` |
| `SHARED_SECRET`    | The shared secret                           | `s3cr3t`               |
| `SERVER_TIMEOUT`   | Time to wait for a server response          | `3`                    |
| `SERVER_MAX_TRIES` | Number of times to try to connect to server | `3`                    | 
| `REQUIRE_MA`       | Require server to use Message Authenticator | `1`                    |
| `NAS_ID`           | The NAS Identifier                          | `My NAS`               |
         
In order to run the tests you need to at least set the environment variables 
`SERVER_URI` and `SHARED_SECRET` to your server's values, e.g.:

```bash
$ SERVER_URI=udp://127.0.0.1:1812 SHARED_SECRET=s3cr3t php example/client.php alice alic3
(LOG) I Access-Request for User-Name "alice"
(LOG) D Access-Request for User-Password "alic3"
(LOG) D RADIUS PACKET: TYPE=Access-Request ID=0 AUTHENTICATOR=0x55fb5fda8e340b72cb26febbbf59d228
(LOG) D Server: udp://127.0.0.1:1812
(LOG) D --> 0100004755fb5fda8e340b72cb26febbbf59d2280107616c69636502120212dab2f5e47db5580a445f7006d30320084d79204e415350120daf965aeadda63b66a69f57d8b30763
(LOG) D <-- 0200003f8affdeb44d247452ff178909c256d0c8121957656c636f6d65203e3e3e20616c696365203c3c3c20215012225b79f263ae27268723e31546d85758
(LOG) D RADIUS PACKET: TYPE=Access-Accept ID=0 AUTHENTICATOR=0x8affdeb44d247452ff178909c256d0c8
******* OK *******
Reply-Message:
	Welcome >>> alice <<< !
Message-Authenticator:
	0x225b79f263ae27268723e31546d85758
```

When the credentials are not correct:

```bash
$ SERVER_URI=udp://127.0.0.1:1812 SHARED_SECRET=s3cr3t php example/client.php alice inval1d
(LOG) I Access-Request for User-Name "alice"
(LOG) D Access-Request for User-Password "inval1d"
(LOG) D RADIUS PACKET: TYPE=Access-Request ID=0 AUTHENTICATOR=0x490d9f5d5b185056ee4622bbf688c3af
(LOG) D Server: udp://127.0.0.1:1812
(LOG) D --> 01000047490d9f5d5b185056ee4622bbf688c3af0107616c6963650212b2cadde32837686d1278658ffa76016d20084d79204e415350120551c96c2cbb69dfd83200d979f589aa
(LOG) D <-- 03000038d8c1dd540633a1c848e355acccc99e6c1212496e76616c69642050617373776f72645012dbf33af7fac383198c3aae4cf4be2998
(LOG) D RADIUS PACKET: TYPE=Access-Reject ID=0 AUTHENTICATOR=0xd8c1dd540633a1c848e355acccc99e6c
******* FAIL *******
Reply-Message:
	Invalid Password
Message-Authenticator:
	0xdbf33af7fac383198c3aae4cf4be2998
```

If the account has "2FA" enabled, which triggers a challenge:

```bash
$ SERVER_URI=udp://127.0.0.1:1812 SHARED_SECRET=s3cr3t php example/client.php bob b0b
(LOG) I Access-Request for User-Name "bob"
(LOG) D Access-Request for User-Password "b0b"
(LOG) D RADIUS PACKET: TYPE=Access-Request ID=0 AUTHENTICATOR=0x1b1e064753358ad1badfddf71a28a545
(LOG) D Server: udp://127.0.0.1:1812
(LOG) D --> 010000451b1e064753358ad1badfddf71a28a5450105626f620212d6f7713cd36f563ad1c7b7f1c91726d120084d79204e41535012f758341cf3f7836ed9aebf3265e037c9
(LOG) D <-- 0b00004a03905892421ddac2c6965fbadaea3e1f121250726f7669646520796f7572204f54501812140c059b009292b29d50920d85a4f734501201b4f1edeabb9035dcff600f6f400745
(LOG) D RADIUS PACKET: TYPE=Access-Challenge ID=0 AUTHENTICATOR=0x03905892421ddac2c6965fbadaea3e1f
Reply-Message:
	Provide your OTP
State:
	0x140c059b009292b29d50920d85a4f734
Message-Authenticator:
	0x01b4f1edeabb9035dcff600f6f400745
Answer to Challenge: 123456
(LOG) I Access-Request for User-Name "bob"
(LOG) D Access-Request for User-Password "123456"
(LOG) D RADIUS PACKET: TYPE=Access-Request ID=1 AUTHENTICATOR=0x77165f2813d8d2859e388dd2a2757878
(LOG) D Server: udp://127.0.0.1:1812
(LOG) D --> 0101005777165f2813d8d2859e388dd2a27578780105626f620212e36807f154e2e580521ffd95bdf6828320084d79204e41531812140c059b009292b29d50920d85a4f73450123f11fb8314d7a51fa0101762ed052d03
(LOG) D <-- 0201004cc77aed0663bbfb2d80795d18295c673e121757656c636f6d65203e3e3e20626f62203c3c3c2021120f4f545020416363657074656421501215a3492cdcb65733db5bed78c7750e3a
(LOG) D RADIUS PACKET: TYPE=Access-Accept ID=1 AUTHENTICATOR=0xc77aed0663bbfb2d80795d18295c673e
******* OK *******
Reply-Message:
	Welcome >>> bob <<< !
	OTP Accepted!
Message-Authenticator:
	0x15a3492cdcb65733db5bed78c7750e3a
```

**NOTE**: in the output above, attribute values that are NOT of type `text` are
hex encoded to avoid displaying binary data in the terminal. See the list of 
types per attribute 
[here](https://www.iana.org/assignments/radius-types/radius-types.xhtml).

**NOTE**: do NOT use REAL credentials when providing us with the debug output, 
but create a test account and a (temporary) test shared secret!

**NOTE**: use `REQUIRE_MA=0` to disable the need for the RADIUS server to sign
the RADIUS messages with `Message-Authenticator`.

**NOTE**: please [report](https://codeberg.org/fkooman/php-radius/issues/new) 
any issues you find when testing with your RADIUS server!

# Test Server

Included is also a test server that was created to easily "mock" a RADIUS 
server and modify its configuration easily. See `example/server.php`. It offers
support for `Message-Authenticator` and "challenge/response". It has some users
configured, one of which requires responding to a challenge, a "dummy" OTP. 

To start the server:

```bash
$ php example/server.php
```

It has the `-n` option to NOT add the `Message-Authenticator` attribute to 
responses, and the `-r` option to not require the client to use 
`Message-Authenticator`. If you modify the server code, do not forget to 
"restart" it.

# OpenBSD Server Configuration

For the RADIUS server configuration we use for testing with OpenBSD, we used 
the most simple configuration in `/etc/radiusd.conf`:

```
listen on 0.0.0.0
listen on ::

client fd99:9:9:9::/64 {
	secret "s3cr3t"
}

client 10.9.9.0/24 {
	secret "s3cr3t"
}

module load bsdauth "/usr/libexec/radiusd/radiusd_bsdauth"

authenticate * {
	authenticate-by bsdauth
}
```

Then we enabled the `radiusd` service using `rcctl`:

```bash
$ doas rcctl enable radiusd
$ doas rcctl start radiusd
```
